[VN] Android Memory Leak Phần 1: Inner class

Link tiếng Anh: https://medium.com/@huynhquangthao/85cebdc97ab3

Đây là bài viết đầu tiên của mình về memory leak trong Android. Nói nôm na là “rò rĩ bộ nhớ", nhưng mình nghĩ dùng đúng thuật ngữ sẽ hay hơn. Nói chung chung, rất nhiều người trong chúng ta thấy ngộ ngộ, vì tại sao Java (hay Android) có thể bị memory leak được chứ? Java sử dụng một bộ thu gom rác tự động, tạm gọi là Garbage Collector, tự động thu hồi tất cả đối tượng không còn sử dụng nữa. Memory leak chỉ có thể xảy ra khi lập trình C/C++ thôi, right? Chúng ta bắt đầu cuộc hành trình vào thế giới thú vị này nhé.

Giới thiệu vấn đề

Java Inner Class là gì

Có một hình minh hoạ rất rõ ràng về Java inner class.

Cấu trúc trên chỉ ra có 3 dạng inner class. Hôm nay, chúng ta chỉ phân tích dạng đầu tiên.

public class OuterClass {
private int mCount = 5;
    class InnerClass {
// inner class can reference to outer class members
public void customMethod() {
mCount = 10;
}
}
}

Dựa vào code mẫu ở trên, inner class có các thuộc tính sau:

  1. Có thể sử dụng tất cả các biến (kể cả biến có thuộc tính private), hàm … của lớp ngoài chứa nó mà không cần tham chiếu cụ thể.
  2. Vòng đời tồn tại “được cho" là phụ thuộc vào lớp ngoài chứa nó.
  3. Tự động có tham chiếu ngầm đến lớp ngoài chứa nó. Chính điều này đã dẫn đến lí do chính xảy ra memory leak mà mình sẽ trình bày sau đây.

Inner class Memory Leak:

Sau đây sẽ là từng bước để tạo ra lỗi memory leak:

  1. Tạo ra một inner class. Ở ví dụ dưới sẽ là lớp Leaky.
  2. Tạo ra một đối tượng static thuộc lớp trên. Ví dụ dưới đối tượng đó có tên là sLeak.
  3. Cùng xoay xoay thiết bị nào =))) Và activity đầu tiên sẽ bị leak. Huuuray :))

Đây là phần demo code:

public class MainActivity extends AppCompatActivity {
    static Leaky sLeak;
private byte[] mBigMemory;
    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
        mBigMemory = new byte[8000000];
        if (sLeak == null) {
sLeak = new Leaky();
}
}
    class Leaky {
}
}

Giải thích

Mình sẽ bắt đầu giải thích chi tiết tại sao memory leak lại xảy ra:

  1. Đối tượng sLeaky có một tham chiếu ngầm định tới MainActivity.
  2. Khi bắt đầu xoay điện thoại, sự kiện configuration change được diễn ra. Activity cũ sẽ được huỷ bỏ và thay thế bởi activity mới.
  3. Tuy nhiên, activity mới có tham chiếu tới đối tượng leaky theo tính chất static. Đối tượng leaky lại có tham chiếu ngầm đến activity cũ, từ đó bộ Garbage Collector không thể thu hồi vùng nhớ của activity cũ được. Memory leak :)

Hình vẽ dưới đây minh hoạ toàn bộ cấu trúc các đối tượng trên vùng nhớ heap:

Vấn đề chúng ta gặp phải dẫn đến memory leak đó là: Cố gắng để vòng đời của đối tượng inner class dài hơn vòng đời của lớp ngoài chứa nó.

Phân tích lỗi memory leak

Chúng ta đã tìm hiểu và chứng minh tại sao xảy ra lỗi memory leak khi sử dụng inner class. Tiếp tục, mình sẽ kiểm chứng lại điều trên bằng cách sử dụng công cụ Memory Viewer Tool.

memory leak screen shot

Hình vẽ trên biểu diễn đồ thị vùng nhớ sử dụng trong quá trình mình “vọc" app:

  1. Trạng thái ban đầu: Khi ứng dụng bắt đầu được khởi chạy, ứng dụng tốn 16MB RAM, bao gồm 8MB RAM cho đối tượng mBigMemory mình tạo ra trong code.
  2. Lúc xoay màn hình: Tiếp tục, mình bắt đầu nghịch nghịch với app của mình. Mình bắt đầu xoay tứ phía, ngang rồi dọc, dọc rồi ngang … nhiều lần. Do việc sử dụng bộ nhớ rất nhiều (cấp phát lại 8MB cho mỗi lần xoay), Garbage Collector sẽ thường xuyên hoạt động để cấp phát và thu hồi vùng nhớ. Do vậy, chúng ta sẽ thấy những dốc lên và xuống liên tục ở bước 2.
  3. Trạng thái ổn định: Sau khi quậy chán chê, mình không xoay điện thoại nữa (kẻo nó chóng mặt). Mình bắt thằng Garbage Collector hoạt động liên tục (click vào nút màu cam cam bên trái). Mình click thật nhiều lần đến khi Garbage Collector không thể lấy lại bất kì vùng nhớ nào nữa. Ứng dụng hệ thống dừng ở việc sử dụng 24MB.

Memory leak xảy ra ở đây. Chúng ta bắt đầu chạy ứng dụng với 16MB. Nhưng sau khi xoay màn hình, ứng dụng chúng ta dừng ở 24MB, bất kể GC hoạt động thế nào. Khác biệt 8MB chính là kích thước của activity đầu tiên chúng ta đã bị leak. Poor :(

Bây giờ, chúng ta bắt đầu trải nghiệm mới. Mình không sử dụng inner class cho lớp Leaky nữa. Thay vào đó sẽ tạo một lớp Java bình thường (File>New Java File> …) đặt tên là Leaky. Trên thực tế, việc thay đổi này có thể dẫn đến refactor code lại rất nhiều, và trong vài trường hợp là không thể.

fixing memory leak screen shot

Rất rất rất là ấn tượng. Tại bước 3 bây giờ, vùng nhớ được cấp phát bằng với vùng nhớ ban đầu (16MB). Không còn lỗi memory leak nữa ^^

Chúng ta vừa kết thúc hành trình đầu tiên qua thế giới của memory leak trong Java và Android. Tổng kết lại, đừng bao giờ tạo một đối tượng static cho inner class. Dựa trên kết luận này, chúng ta có thêm một bài học mới: Đừng bao giờ tạo một class mà có tham chiếu tới đối tượng view (activity, fragment, imageview …) sẽ dẫn đến các hậu quả tương tự.

Trong các bài post tiếp theo, mình sẽ nói tiếp về các lỗi memory leak khác, và cách sử dụng các tool khác để phát hiện lỗi. Keep calm :)

Link

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.