Tìm và khắc phục các sự cố rò rỉ bộ nhớ khó xử do các cửa sổ tách rời gây ra.
Lỗi rò rỉ bộ nhớ trong JavaScript là gì?
Sự cố rò rỉ bộ nhớ là tình trạng ứng dụng vô tình tăng mức sử dụng bộ nhớ theo thời gian. Trong JavaScript, tình trạng rò rỉ bộ nhớ xảy ra khi các đối tượng không còn cần thiết nhưng vẫn được các hàm hoặc các đối tượng khác tham chiếu. Các tệp tham chiếu này ngăn trình thu gom rác thu hồi các đối tượng không cần thiết.
Công việc của trình thu gom rác là xác định và thu hồi các đối tượng không thể truy cập được từ ứng dụng. Điều này hoạt động ngay cả khi các đối tượng tham chiếu chính mình hoặc tham chiếu lẫn nhau theo chu kỳ – khi không còn tham chiếu nào để ứng dụng có thể truy cập vào một nhóm đối tượng, thì nhóm đối tượng đó có thể được thu gom rác.
let A = {};
console.log(A); // local variable reference
let B = {A}; // B.A is a second reference to A
A = null; // unset local variable reference
console.log(B.A); // A can still be referenced by B
B.A = null; // unset B's reference to A
// No references to A are left. It can be garbage collected.
Một loại rò rỉ bộ nhớ đặc biệt khó xử lý xảy ra khi một ứng dụng tham chiếu các đối tượng có vòng đời riêng, chẳng hạn như các phần tử DOM hoặc cửa sổ bật lên. Các loại đối tượng này có thể không được sử dụng mà ứng dụng không biết, nghĩa là mã ứng dụng có thể chỉ còn tham chiếu đến một đối tượng có thể được thu gom rác.
Cửa sổ tách là gì?
Trong ví dụ sau, ứng dụng trình xem trình chiếu có các nút để mở và đóng cửa sổ bật lên ghi chú của người trình bày. Hãy tưởng tượng người dùng nhấp vào Show Notes (Hiện ghi chú), sau đó đóng trực tiếp cửa sổ bật lên thay vì nhấp vào nút Hide Notes (Ẩn ghi chú) – biến notesWindow
vẫn giữ một tệp tham chiếu đến cửa sổ bật lên có thể truy cập được, mặc dù cửa sổ bật lên không còn được sử dụng nữa.
<button id="show">Show Notes</button>
<button id="hide">Hide Notes</button>
<script type="module">
let notesWindow;
document.getElementById('show').onclick = () => {
notesWindow = window.open('/presenter-notes.html');
};
document.getElementById('hide').onclick = () => {
if (notesWindow) notesWindow.close();
};
</script>
Đây là ví dụ về một cửa sổ tách rời. Cửa sổ bật lên đã đóng, nhưng mã của chúng ta có một tệp tham chiếu đến cửa sổ đó, ngăn trình duyệt huỷ bỏ cửa sổ và thu hồi bộ nhớ đó.
Khi một trang gọi window.open()
để tạo một cửa sổ hoặc thẻ trình duyệt mới, một đối tượng Window
sẽ được trả về để đại diện cho cửa sổ hoặc thẻ đó. Ngay cả sau khi cửa sổ đó đã đóng hoặc người dùng đã di chuyển cửa sổ đó, đối tượng Window
được trả về từ window.open()
vẫn có thể được dùng để truy cập thông tin về cửa sổ đó. Đây là một loại cửa sổ tách rời: vì mã JavaScript vẫn có thể truy cập vào các thuộc tính trên đối tượng Window
đã đóng, nên mã này phải được lưu giữ trong bộ nhớ. Nếu cửa sổ chứa nhiều đối tượng hoặc iframe JavaScript, thì bạn không thể thu hồi bộ nhớ đó cho đến khi không còn tham chiếu JavaScript nào đến các thuộc tính của cửa sổ.
Vấn đề tương tự cũng có thể xảy ra khi sử dụng các phần tử <iframe>
. Iframe hoạt động như các cửa sổ lồng nhau chứa tài liệu và thuộc tính contentWindow
của chúng cung cấp quyền truy cập vào đối tượng Window
được chứa, giống như giá trị do window.open()
trả về. Mã JavaScript có thể giữ lại tham chiếu đến contentWindow
hoặc contentDocument
của iframe ngay cả khi iframe bị xoá khỏi DOM hoặc URL của iframe thay đổi, điều này ngăn tài liệu bị thu thập rác vì các thuộc tính của tài liệu vẫn có thể được truy cập.
Trong trường hợp tham chiếu đến document
trong một cửa sổ hoặc iframe được giữ lại từ JavaScript, tài liệu đó sẽ được lưu giữ trong bộ nhớ ngay cả khi cửa sổ hoặc iframe chứa chuyển đến một URL mới. Điều này có thể đặc biệt rắc rối khi JavaScript lưu giữ tệp tham chiếu đó không phát hiện được rằng cửa sổ/khung đã chuyển đến một URL mới, vì nó không biết thời điểm tệp tham chiếu đó trở thành tệp tham chiếu cuối cùng lưu giữ tài liệu trong bộ nhớ.
Cách cửa sổ tách rời gây ra sự cố rò rỉ bộ nhớ
Khi làm việc với các cửa sổ và iframe trên cùng một miền với trang chính, bạn thường nghe các sự kiện hoặc truy cập vào các thuộc tính trên ranh giới tài liệu. Ví dụ: hãy xem lại một biến thể của ví dụ về trình xem bản trình bày ở đầu hướng dẫn này. Trình xem sẽ mở một cửa sổ thứ hai để hiển thị ghi chú của người thuyết trình. Cửa sổ ghi chú của người nói sẽ lắng nghe các sự kiện click
làm tín hiệu để chuyển sang trang trình bày tiếp theo. Nếu người dùng đóng cửa sổ ghi chú này, thì JavaScript chạy trong cửa sổ gốc vẫn có toàn quyền truy cập vào tài liệu ghi chú của người nói:
<button id="notes">Show Presenter Notes</button>
<script type="module">
let notesWindow;
function showNotes() {
notesWindow = window.open('/presenter-notes.html');
notesWindow.document.addEventListener('click', nextSlide);
}
document.getElementById('notes').onclick = showNotes;
let slide = 1;
function nextSlide() {
slide += 1;
notesWindow.document.title = `Slide ${slide}`;
}
document.body.onclick = nextSlide;
</script>
Hãy tưởng tượng chúng ta đóng cửa sổ trình duyệt do showNotes()
tạo ở trên. Không có trình xử lý sự kiện nào để phát hiện cửa sổ đã đóng, vì vậy, không có gì thông báo cho mã của chúng ta rằng mã đó sẽ xoá mọi tệp tham chiếu đến tài liệu. Hàm nextSlide()
vẫn "sống" vì được liên kết dưới dạng trình xử lý lượt nhấp trong trang chính của chúng ta và việc nextSlide
chứa tham chiếu đến notesWindow
có nghĩa là cửa sổ vẫn được tham chiếu và không thể thu gom rác.
Có một số trường hợp khác mà các tệp tham chiếu bị giữ lại do nhầm lẫn, khiến các cửa sổ tách không đủ điều kiện để thu thập rác:
Bạn có thể đăng ký trình xử lý sự kiện trên tài liệu ban đầu của iframe trước khi khung điều hướng đến URL dự kiến, dẫn đến việc tham chiếu ngẫu nhiên đến tài liệu và khung này vẫn tồn tại sau khi các tham chiếu khác đã được dọn dẹp.
Tài liệu nặng về bộ nhớ được tải trong một cửa sổ hoặc iframe có thể vô tình được lưu giữ trong bộ nhớ trong thời gian dài sau khi chuyển đến một URL mới. Điều này thường là do trang mẹ giữ lại các tệp tham chiếu đến tài liệu để cho phép xoá trình nghe.
Khi truyền một đối tượng JavaScript đến một cửa sổ hoặc iframe khác, chuỗi nguyên mẫu của Đối tượng sẽ bao gồm các tệp tham chiếu đến môi trường mà đối tượng đó được tạo, bao gồm cả cửa sổ đã tạo đối tượng đó. Điều này có nghĩa là bạn cũng cần tránh giữ lại các tệp tham chiếu đến các đối tượng từ các cửa sổ khác như tránh giữ lại các tệp tham chiếu đến chính các cửa sổ đó.
index.html:
<script> let currentFiles; function load(files) { // this retains the popup: currentFiles = files; } window.open('upload.html'); </script>
upload.html:
<input type="file" id="file" /> <script> file.onchange = () => { parent.load(file.files); }; </script>
Phát hiện tình trạng rò rỉ bộ nhớ do các cửa sổ tách rời
Việc theo dõi sự cố rò rỉ bộ nhớ có thể rất khó khăn. Thường khó tạo bản sao riêng biệt của các vấn đề này, đặc biệt là khi có nhiều tài liệu hoặc cửa sổ liên quan. Để làm cho mọi thứ trở nên phức tạp hơn, việc kiểm tra các tệp tham chiếu bị rò rỉ tiềm ẩn có thể dẫn đến việc tạo thêm các tệp tham chiếu ngăn việc thu gom rác đối với các đối tượng đã kiểm tra. Do đó, bạn nên bắt đầu bằng các công cụ giúp tránh việc tạo ra khả năng này.
Một cách hay để bắt đầu gỡ lỗi các vấn đề về bộ nhớ là lấy ảnh chụp nhanh vùng nhớ khối xếp. Điều này cung cấp chế độ xem tại một thời điểm về bộ nhớ mà ứng dụng hiện đang sử dụng – tất cả các đối tượng đã được tạo nhưng chưa được thu gom rác. Ảnh chụp nhanh vùng nhớ khối xếp chứa thông tin hữu ích về các đối tượng, bao gồm kích thước và danh sách các biến và hàm đóng tham chiếu đến các đối tượng đó.
Để ghi lại ảnh chụp nhanh vùng nhớ khối xếp, hãy chuyển đến thẻ Memory (Bộ nhớ) trong Công cụ của Chrome cho nhà phát triển rồi chọn Heap Dump (Ảnh chụp nhanh vùng nhớ khối xếp) trong danh sách các loại hồ sơ có sẵn. Sau khi ghi xong, chế độ xem Summary (Tóm tắt) sẽ hiển thị các đối tượng hiện tại trong bộ nhớ, được nhóm theo hàm khởi tạo.
Việc phân tích tệp báo lỗi có thể là một nhiệm vụ khó khăn và bạn có thể gặp khó khăn khi tìm thông tin phù hợp trong quá trình gỡ lỗi. Để giúp giải quyết vấn đề này, các kỹ sư Chromium yossik@ và peledni@ đã phát triển một công cụ Trình dọn dẹp vùng nhớ khối xếp độc lập có thể giúp làm nổi bật một nút cụ thể như một cửa sổ tách biệt. Việc chạy Trình dọn dẹp vùng nhớ khối xếp trên một dấu vết sẽ xoá các thông tin không cần thiết khác khỏi biểu đồ thời gian lưu giữ, giúp dấu vết trở nên rõ ràng và dễ đọc hơn nhiều.
Đo lường bộ nhớ theo phương thức lập trình
Ảnh chụp nhanh của vùng nhớ khối xếp cung cấp thông tin chi tiết cao và rất hữu ích để tìm ra vị trí xảy ra rò rỉ, nhưng việc chụp ảnh nhanh của vùng nhớ khối xếp là một quy trình thủ công. Một cách khác để kiểm tra rò rỉ bộ nhớ là lấy kích thước vùng nhớ khối xếp JavaScript hiện đang được sử dụng từ API performance.memory
:
API performance.memory
chỉ cung cấp thông tin về kích thước vùng nhớ khối xếp JavaScript, nghĩa là API này không bao gồm bộ nhớ mà tài liệu và tài nguyên của cửa sổ bật lên sử dụng. Để có được thông tin đầy đủ, chúng ta cần sử dụng performance.measureUserAgentSpecificMemory()
API mới đang được thử nghiệm trong Chrome.
Giải pháp để tránh rò rỉ cửa sổ tách
Hai trường hợp phổ biến nhất mà cửa sổ tách rời gây ra rò rỉ bộ nhớ là khi tài liệu mẹ giữ lại các tệp tham chiếu đến cửa sổ bật lên đã đóng hoặc iframe đã xoá và khi thao tác điều hướng không mong muốn của cửa sổ hoặc iframe dẫn đến trình xử lý sự kiện không bao giờ được đăng ký.
Ví dụ: Đóng cửa sổ bật lên
Trong ví dụ sau, hai nút được dùng để mở và đóng cửa sổ bật lên. Để nút Close Popup (Đóng cửa sổ bật lên) hoạt động, tham chiếu đến cửa sổ bật lên đã mở sẽ được lưu trữ trong một biến:
<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
};
</script>
Thoạt nhìn, có vẻ như mã trên tránh được các lỗi phổ biến: không giữ lại tham chiếu đến tài liệu của cửa sổ bật lên và không có trình xử lý sự kiện nào được đăng ký trên cửa sổ bật lên. Tuy nhiên, sau khi nhấp vào nút Open Popup (Mở cửa sổ bật lên), biến popup
hiện tham chiếu đến cửa sổ đã mở và bạn có thể truy cập vào biến đó trong phạm vi của trình xử lý lượt nhấp vào nút Close Popup (Đóng cửa sổ bật lên). Trừ phi popup
được chỉ định lại hoặc trình xử lý lượt nhấp bị xoá, tham chiếu đi kèm của trình xử lý đó đến popup
có nghĩa là không thể thu gom rác.
Giải pháp: Huỷ thiết lập tham chiếu
Các biến tham chiếu đến một cửa sổ hoặc tài liệu khác sẽ khiến cửa sổ hoặc tài liệu đó được giữ lại trong bộ nhớ. Vì các đối tượng trong JavaScript luôn là tham chiếu, nên việc gán giá trị mới cho các biến sẽ xoá tham chiếu của các biến đó đến đối tượng ban đầu. Để "đặt lại" các tệp tham chiếu đến một đối tượng, chúng ta có thể chỉ định lại các biến đó cho giá trị null
.
Áp dụng điều này cho ví dụ về cửa sổ bật lên trước đó, chúng ta có thể sửa đổi trình xử lý nút đóng để "đặt lại" tham chiếu đến cửa sổ bật lên:
let popup;
open.onclick = () => {
popup = window.open('/login.html');
};
close.onclick = () => {
popup.close();
popup = null;
};
Điều này giúp ích, nhưng cũng cho thấy một vấn đề khác liên quan đến các cửa sổ được tạo bằng open()
: nếu người dùng đóng cửa sổ thay vì nhấp vào nút đóng tuỳ chỉnh của chúng ta thì sao? Hơn nữa, nếu người dùng bắt đầu duyệt xem các trang web khác trong cửa sổ mà chúng ta đã mở thì sao? Mặc dù ban đầu có vẻ như việc đặt lại tham chiếu popup
khi nhấp vào nút đóng là đủ, nhưng vẫn có rò rỉ bộ nhớ khi người dùng không sử dụng nút cụ thể đó để đóng cửa sổ. Để giải quyết vấn đề này, bạn cần phát hiện những trường hợp này để huỷ thiết lập các tệp tham chiếu còn lại khi chúng xảy ra.
Giải pháp: Giám sát và loại bỏ
Trong nhiều trường hợp, JavaScript chịu trách nhiệm mở cửa sổ hoặc tạo khung không có quyền kiểm soát riêng đối với vòng đời của các cửa sổ hoặc khung đó. Người dùng có thể đóng cửa sổ bật lên hoặc việc điều hướng đến một tài liệu mới có thể khiến tài liệu trước đó được chứa trong một cửa sổ hoặc khung bị tách rời. Trong cả hai trường hợp, trình duyệt sẽ kích hoạt một sự kiện pagehide
để báo hiệu rằng tài liệu đang được huỷ tải.
Bạn có thể sử dụng sự kiện pagehide
để phát hiện các cửa sổ đã đóng và thao tác rời khỏi tài liệu hiện tại. Tuy nhiên, có một lưu ý quan trọng: tất cả các cửa sổ và iframe mới tạo đều chứa một tài liệu trống, sau đó chuyển đến URL đã cho một cách không đồng bộ nếu có. Do đó, sự kiện pagehide
ban đầu sẽ được kích hoạt ngay sau khi tạo cửa sổ hoặc khung, ngay trước khi tài liệu mục tiêu tải xong. Vì mã dọn dẹp tham chiếu của chúng ta cần chạy khi tài liệu mục tiêu được tải xuống, nên chúng ta cần bỏ qua sự kiện pagehide
đầu tiên này. Có một số kỹ thuật để làm điều này, trong đó đơn giản nhất là bỏ qua các sự kiện ẩn trang bắt nguồn từ URL about:blank
của tài liệu ban đầu. Sau đây là giao diện của ví dụ về cửa sổ bật lên:
let popup;
open.onclick = () => {
popup = window.open('/login.html');
// listen for the popup being closed/exited:
popup.addEventListener('pagehide', () => {
// ignore initial event fired on "about:blank":
if (!popup.location.host) return;
// remove our reference to the popup window:
popup = null;
});
};
Điều quan trọng cần lưu ý là kỹ thuật này chỉ hoạt động đối với các cửa sổ và khung có cùng nguồn gốc hiệu quả với trang mẹ nơi mã của chúng ta đang chạy. Khi tải nội dung từ một nguồn khác, cả location.host
và sự kiện pagehide
đều không dùng được vì lý do bảo mật. Mặc dù tốt nhất là bạn nên tránh lưu giữ các tệp tham chiếu đến các nguồn gốc khác, nhưng trong một số ít trường hợp cần thiết, bạn có thể theo dõi các thuộc tính window.closed
hoặc frame.isConnected
. Khi các thuộc tính này thay đổi để cho biết một cửa sổ đã đóng hoặc iframe đã bị xoá, bạn nên huỷ đặt mọi tệp tham chiếu đến cửa sổ hoặc iframe đó.
let popup = window.open('https://example.com');
let timer = setInterval(() => {
if (popup.closed) {
popup = null;
clearInterval(timer);
}
}, 1000);
Giải pháp: Sử dụng WeakRef
Gần đây, JavaScript đã được hỗ trợ một cách mới để tham chiếu các đối tượng cho phép thu gom rác, được gọi là WeakRef
. WeakRef
được tạo cho một đối tượng không phải là tham chiếu trực tiếp mà là một đối tượng riêng biệt cung cấp một phương thức .deref()
đặc biệt trả về tham chiếu đến đối tượng đó miễn là đối tượng đó chưa được thu gom rác. Với WeakRef
, bạn có thể truy cập vào giá trị hiện tại của một cửa sổ hoặc tài liệu trong khi vẫn cho phép thu thập rác. Thay vì giữ lại tệp tham chiếu đến cửa sổ phải được đặt lại theo cách thủ công để phản hồi các sự kiện như pagehide
hoặc các thuộc tính như window.closed
, quyền truy cập vào cửa sổ sẽ được lấy khi cần. Khi cửa sổ đóng, cửa sổ có thể được thu gom rác, khiến phương thức .deref()
bắt đầu trả về undefined
.
<button id="open">Open Popup</button>
<button id="close">Close Popup</button>
<script>
let popup;
open.onclick = () => {
popup = new WeakRef(window.open('/login.html'));
};
close.onclick = () => {
const win = popup.deref();
if (win) win.close();
};
</script>
Một chi tiết thú vị cần cân nhắc khi sử dụng WeakRef
để truy cập vào các cửa sổ hoặc tài liệu là tham chiếu thường vẫn có sẵn trong một khoảng thời gian ngắn sau khi cửa sổ bị đóng hoặc khung hiển thị được xoá. Lý do là WeakRef
tiếp tục trả về một giá trị cho đến khi đối tượng liên kết của nó được thu gom rác. Quá trình này diễn ra không đồng bộ trong JavaScript và thường là trong thời gian rảnh. Rất may, khi kiểm tra các cửa sổ tách rời trong bảng điều khiển Memory (Bộ nhớ) của Công cụ của Chrome cho nhà phát triển, việc chụp ảnh nhanh vùng nhớ khối xếp thực sự sẽ kích hoạt quá trình thu gom rác và loại bỏ cửa sổ được tham chiếu yếu. Bạn cũng có thể kiểm tra để đảm bảo rằng một đối tượng được tham chiếu qua WeakRef
đã được loại bỏ khỏi JavaScript, bằng cách phát hiện thời điểm deref()
trả về undefined
hoặc sử dụng FinalizationRegistry
API mới:
let popup = new WeakRef(window.open('/login.html'));
// Polling deref():
let timer = setInterval(() => {
if (popup.deref() === undefined) {
console.log('popup was garbage-collected');
clearInterval(timer);
}
}, 20);
// FinalizationRegistry API:
let finalizers = new FinalizationRegistry(() => {
console.log('popup was garbage-collected');
});
finalizers.register(popup.deref());
Giải pháp: Giao tiếp qua postMessage
Việc phát hiện thời điểm cửa sổ đóng hoặc điều hướng tải một tài liệu xuống cho phép chúng ta xoá các trình xử lý và huỷ thiết lập tham chiếu để có thể thu thập rác cho các cửa sổ tách rời. Tuy nhiên, những thay đổi này là các bản sửa lỗi cụ thể cho vấn đề đôi khi có thể là vấn đề cơ bản hơn: ghép nối trực tiếp giữa các trang.
Có một phương pháp thay thế toàn diện hơn để tránh các tệp tham chiếu cũ giữa các cửa sổ và tài liệu: thiết lập sự tách biệt bằng cách giới hạn giao tiếp giữa các tài liệu ở postMessage()
. Hãy nhớ lại ví dụ về ghi chú của người trình bày ban đầu, các hàm như nextSlide()
đã cập nhật trực tiếp cửa sổ ghi chú bằng cách tham chiếu và thao tác nội dung của cửa sổ đó. Thay vào đó, trang chính có thể truyền thông tin cần thiết đến cửa sổ ghi chú một cách không đồng bộ và gián tiếp qua postMessage()
.
let updateNotes;
function showNotes() {
// keep the popup reference in a closure to prevent outside references:
let win = window.open('/presenter-view.html');
win.addEventListener('pagehide', () => {
if (!win || !win.location.host) return; // ignore initial "about:blank"
win = null;
});
// other functions must interact with the popup through this API:
updateNotes = (data) => {
if (!win) return;
win.postMessage(data, location.origin);
};
// listen for messages from the notes window:
addEventListener('message', (event) => {
if (event.source !== win) return;
if (event.data[0] === 'nextSlide') nextSlide();
});
}
let slide = 1;
function nextSlide() {
slide += 1;
// if the popup is open, tell it to update without referencing it:
if (updateNotes) {
updateNotes(['setSlide', slide]);
}
}
document.body.onclick = nextSlide;
Mặc dù điều này vẫn yêu cầu các cửa sổ tham chiếu lẫn nhau, nhưng không có cửa sổ nào giữ lại tham chiếu đến tài liệu hiện tại từ một cửa sổ khác. Phương pháp truyền thông báo cũng khuyến khích các thiết kế trong đó tham chiếu cửa sổ được giữ ở một nơi duy nhất, nghĩa là chỉ cần đặt lại một tham chiếu duy nhất khi cửa sổ đóng hoặc di chuyển ra khỏi. Trong ví dụ trên, chỉ showNotes()
giữ lại tệp tham chiếu đến cửa sổ ghi chú và sử dụng sự kiện pagehide
để đảm bảo tệp tham chiếu đó được dọn dẹp.
Giải pháp: Tránh tham chiếu bằng noopener
Trong trường hợp một cửa sổ bật lên được mở mà trang của bạn không cần giao tiếp hoặc kiểm soát, bạn có thể tránh việc lấy tham chiếu đến cửa sổ đó. Điều này đặc biệt hữu ích khi tạo cửa sổ hoặc iframe sẽ tải nội dung từ một trang web khác. Đối với những trường hợp này, window.open()
chấp nhận tuỳ chọn "noopener"
hoạt động giống như thuộc tính rel="noopener"
cho các đường liên kết HTML:
window.open('https://example.com/share', null, 'noopener');
Tuỳ chọn "noopener"
khiến window.open()
trả về null
, khiến bạn không thể vô tình lưu trữ tham chiếu đến cửa sổ bật lên. Điều này cũng ngăn cửa sổ bật lên tham chiếu đến cửa sổ mẹ, vì thuộc tính window.opener
sẽ là null
.
Phản hồi
Hy vọng một số đề xuất trong bài viết này sẽ giúp bạn tìm và khắc phục sự cố rò rỉ bộ nhớ. Nếu bạn có một kỹ thuật khác để gỡ lỗi cửa sổ tách rời hoặc bài viết này đã giúp bạn phát hiện sự cố rò rỉ trong ứng dụng, tôi rất muốn biết! Bạn có thể tìm thấy tôi trên Twitter @_developit.