Introduction to LeakCanary: How to find memory leaks in your app, and fix them too.

What is memory leak?

Memory leak là 1 khái niệm không còn lạ lẫm đối với giới lập trình viên. Trong Java nói riêng thì nó xảy ra khi quá trình Garbage Collector (GC) không thể thu hồi vùng nhớ đã cấp phát cho 1 đối tượng mặc dù đối tượng đó đã không còn được sử dụng nữa.

Đọc đến đây có thể rất nhiều bạn sẽ tự hỏi: Thế thì liên quan quái gì đến tôi? Chẳng phải GC là 1 quá trình tự động của hệ thống à? Tại sao tôi phải quan tâm đến việc nó hoạt động không hiệu quả?

Câu trả lời là: Nếu app của bạn bị leak memory thì 99% nguyên nhân chính là do bạn chứ ko phải do thằng GC bị ngu đâu ạ. Trong Android thì việc leak memory xảy ra là chuyện như cơm bữa, ngay cả đối với 1 vài thành phần trong framework chính chủ cũng gây ra memory leak và sự thật là các thánh lập trình viên của Google cũng chưa fix được hết. Nói vậy để bạn hiểu nó dễ đến như thế nào để gây ra memory leak nếu bạn không chú ý.

- Thế bị memory leak thì sao? App của tôi vẫn chạy ngon nhé chứ có crash đâu. Toàn làm quá lên (tat2)

- Rất vui vì bạn đã hỏi câu này. Về cơ bản thì có thể bạn sẽ không thấy được tác hại của memory leak trong những ứng dụng nhỏ. Nhưng đối với những app lớn 1 chút thì memory leak là 1 trong những nguyên nhân chủ yếu gây ra hiện tượng OutOfMemoryError (OOM) bên cạnh việc không tối ưu hóa bitmap (cái này là dễ crash nhất này, chắc ai cũng từng gặp rồi đúng không ạ). Đối với Android thì RAM là 1 trong những tài nguyên quý giá nhất vì nó có giới hạn cho từng app (limited heap size), chứ không phải máy bạn có 3GB RAM là ứng dụng của bạn có thể toàn quyền sử dụng hết đâu, quên đi.

Mặc dù Android có thằng máy ảo Dalvik sẽ thực thi việc thu hồi bộ nhớ từ những object đã không còn trong phạm vi sử dụng nhưng nó cũng phải bó tay nếu bạn vẫn giữ tham chiếu đến những object đấy, cho nên đừng chủ quan trong việc quản lý bộ nhớ và giải phóng Reference khi cần.

When are memory leaks occurred?

1. Static field

Trước hết hãy xem đoạn code này:

private static Button mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mTextView = new TextView(this);
}

1 trong những cách dễ nhất để xuất hiện memory leaks là khởi tạo static object mà giữ reference đến những object chiếm nhiều bộ nhớ (ví dụ như static view sẽ có field là mContext, thường thì chúng ta sẽ truyền vào 1 Context). Static field này đc gọi là GC root.

Quá trình GC sẽ xử lý và loại bỏ những object/class/instance mà ko phải là GC root hay được giữ reference bởi GC root. Cho nên nếu chúng ta tạo ra 1 object sau đó xóa toàn bộ reference đến object đó, nó sẽ bị GC nhưng nếu chúng ta cho nó vào GC root như là static field thì nó sẽ không thể bị GC.

2. Inner class

Một trong những trường hợp khác có thể dẫn đến việc leak memory là sử dụng inner class. Chúng ta thường có khá nhiều lí do để làm việc này, ví dụ như để đảm bảo tính tường minh và tính đóng gói của code, cũng như việc inner class có thể truy cập các object/field của outer class. Vấn đề ở đây cũng chính là điểm mạnh của inner class: Nó phải giữ reference đến outer class để có quyền truy vấn các resource của outer class.

3. Annonymous class

Cũng giống như inner class, annonymous class sẽ giữ reference đến class mà trong đó nó được định nghĩa. Vì thế memory leak hoàn toàn có thể xảy ra nếu chúng ta khởi tạo AsyncTask trong 1 Activity. Do AsyncTask chạy dưới background thread, nếu Activity bị destroy trong khi doInBackground() vẫn đang chạy thì reference từ AsyncTask tới Activity sẽ vẫn còn và Activity sẽ không thể bị GC cho đến khi AsyncTask chạy xong.

new AsyncTask<Void, Void, Void>() {
    @Override
    protected Void doInBackground(Void... voids) {
        //Some long operations
         return null;
    }
}.execute();

4. Sensor Manager/Event Listener

SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);

Có 1 số service của system có thể được truy cập bởi hàm gọi getSystemService() từ 1 context. Những service này chạy trên process riêng của chúng và xử lý những công việc dưới background tùy thuộc vào hardware cuả device. Nếu 1 context muốn lắng nghe những sự kiện từ service này, nó phải đăng ký 1 listener, tuy nhiên điều này lại làm cho Service có reference đến Activity. Nếu developer không handle được việc register và unregister listener 1 cách hợp lý trước khi Activity bị destroy thì memory leak hoàn toàn có thể xảy ra.

Chú ý rằng trên đây tôi chỉ liệt kê ra 1 vài case dễ gây ra memory leak nhất chứ đây không phải là toàn bộ nguyên nhân gây ra memory leak.

Does my app have memory leaks?

Cách đơn giản và thủ công nhất để trả lời câu hỏi này là sử dụng Android Device Monitor . Trong Android Studio, chọn Tools > Android > Android Device Monitor.

Screenshot from 2016-07-08 09:55:23.png

Chọn app mà bạn cần kiểm tra ở cột bên trái và ấn vào nút Update Heap.

Tình huống: Có 2 activity A và B, nghi ngờ activity B bị leak memory. Giờ nhìn vào bên tay phải. Cái chúng ta cần quan tâm đó là cột Allocated. Với app của tôi thì sau khi từ mở từ A sang B thì app sẽ chiếm ~ 3,1 MB bộ nhớ.

Screenshot from 2016-07-08 09:50:32.png

Cách kiểm tra nhanh nhất là hãy mở đi mở lại Activity B và chú ý quan sát cột Allocated. Có thể ấn nút Cause GC để kích hoạt quá trình GC để dễ theo dõi hơn.

Screenshot from 2016-07-08 09:51:45.png

Như chúng ta có thể thấy, cột Allocated đã tăng từ 3,1 MB lên đến 4,6 MB, số object đã tăng gấp đôi mặc dù Activity B đã không còn được sử dụng nữa. Đây là 1 việc hết sức nguy hiểm vì app chỉ được cấp 20 MB Heap size và nếu không cẩn thận thì memory leaks hoàn toàn có thể gây ra OutOfMemoryError và làm crash app.

Why does my app have memory leaks? I’ve done nothing wrong!

Việc tìm ra nguyên nhân đối với 1 lập trình viên bình thường là khá phức tạp và khó thực hiện. Về cơ bản thì chúng ta phải làm những thứ sau:

  1. Bạn phải tái hiện được tình huống xảy ra OOM. Không phải trên device nào cũng xảy ra memory leak do cùng 1 nguyên nhân.
  2. Ghi lại heap dump thời điểm xảy ra OOM. (sử dụng class này https://gist.github.com/pyricau/4726389fd64f3b7c6f32)
  3. Phân tích heap dump bằng MAT hoặc YourKit và tìm những object mà bạn cho là nên bị GC.
  4. Tính toán đường dẫn ngắn nhất của reference từ object đó đến GC root.
  5. Tìm ra reference nào không nên tồn tại và từ đó tìm cách fix.

Nhìn có vẻ phức tạp và tốn calo quá đúng không ạ? Sau đây tôi xin giới thiệu 1 thư viện có thể tự động hóa cái quá trình này và đưa cho bạn luôn cái nguyên nhân gây ra leak.

LeakCanary to the rescue

LeakCanary là thư viện mã nguồn mở giúp phát hiện memory leaks đến từ Square.

LeakCanary có khả năng đọc và phân tích bộ nhớ heap (sử dụng Memory Analyzer hay còn gọi là MAT trong Eclipse) để nhận được một danh sách các object và class đã từng ở trong bộ nhớ khi heap dump đc tạo ra. Từ đó nó sẽ lọc ra các object/instance có flag "destroyed", tương ứng với việc object đó đã không còn được sử dụng nhưng vẫn chưa bị GC do vẫn còn một số object khác giữ tham chiếu đến nó. Bằng việc tính toán đường dẫn ngắn nhất đến GC root, LeakCanary giúp chúng ta truy ngược lại những object đang ngăn cản quá trình GC.

Usage

Thêm vào build.gradle của module app của bạn:

 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
 }

Một trong những điểm khiến LeakCanary trở nên thu hút đối với các developer là nó có 1 hệ thống api đơn giản và dễ sử dụng. Chúng ta chỉ cần subclass Application và thêm 1 dòng code là đã có thể sử dụng đc tính năng phát hiện leak trong tất cả các activity:

public class App extends Application  {
    @Override
    public void onCreate()  {
        super.onCreate();
        LeakCanary.install(this);
    }
}

Đối với Fragment, chúng ta cần khởi tạo đối tượng RefWatcher trong Application và lấy nó ra khi cần:

public class App extends Application {
    private RefWatcher refWatcher;
    static RefWatcher getRefWatcher(Context context) {
        App application = (App) context.getApplicationContext();
        return application.refWatcher;
 	}
    @Override public void onCreate() {
        super.onCreate();
        refWatcher = LeakCanary.install(this);
    }
}

Trong Fragment:

public abstract class BaseFragment extends Fragment {
    @Override public void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = App.getRefWatcher(getActivity());
        refWatcher.watch(this);
 	}
}

Chúng ta cũng có thể sử dụng RefWatcher để giám sát 1 object khi bạn nghĩ là object đó nên bị GC:

refWatcher.watch(dummyObject);

Mỗi khi memory leak xảy ra trong app của bạn, LeakCanary sẽ tự động phát hiện và gửi notification. Một báo cáo leak của LeakCanary có thể nhìn giống như sau:

device-2016-07-07-151807.png

Với report này, chúng ta được cung cấp những thông tin quan trọng nhất để tìm ra object đang cản trở quá trình GC. Ở đây chúng ta có thể thấy SingletonContextHolder là 1 static class đang giữ reference đến Context/DetailActivity. Điều này làm cho DetailActivity không thể bị GC và làm leak một lượng lớn memory do Context là 1 god object trong Android và nó giữ reference đến rất nhiều resource khác.

How it works

  1. Refwatcher.watch() sẽ tạo ra 1 instance của KeyedWeakReference đến object được truyền vào.
  2. Sau 1 khoảng thời gian, nó sẽ check xem reference đến object đấy đã được clear chưa ở background thread, nếu chưa thì sẽ gọi Runtime.gc() để kích hoạt GC.
  3. Nếu reference đến object đó vẫn chưa đc clear, nó sẽ lưu lại heap dump vào file .hprof.
  4. HeapAnalyzer sẽ được chạy từ 1 thread khác để parse heap dump.
  5. HeapAnalyzer sẽ tìm KeyedWeakReference trong heap dump nhờ vào id và sẽ xác định vị trí của reference bị leak.
  6. HeapAnalyzer sẽ tính toán đường dẫn ngắn nhất của strong reference đến GC roots để xác nhận lại xem có leak xảy ra không, và sẽ build 1 đường dây các reference gây ra leak.
  7. Kết quả sẽ được trả về và gửi notification đến người dùng.

How to avoid/fix memory leaks

  • Hạn chế giữ reference đến activity context (những object tham chiếu đến activity nên có lifecycle giống như activity).
  • Dùng application context ở những chỗ có thể.
  • Tránh sử dụng inner class trong activity.
  • Tránh sử dụng annonymous class trong activity.
  • Unregister listener trong onDestroyed() của activity.
  • Nếu bắt buộc sử dụng inner class, hãy cho nó làm static nested class và sử dụng WeakReference để giữ ref đến outer class.

Để tôi được giải thích đoạn này 1 chút. Trong Java thì các tham chiếu đối tượng thông thường đều là StrongReference. Bạn có thể có bao nhiêu StrongReference đến 1 object mà bạn muốn cũng đc, và khi số reference đó giảm xuống 0 (khi bạn giải phóng tất cả các ref) thì object đó sẽ bị GC. WeakReference là 1 cách để truy cập object mà không tăng số reference đến object đấy, cho nên nếu không còn StrongReference nào đến object đấy nữa thì WeakReference cũng sẽ tự động bị xóa (khi đó hàm get() của WeakReference sẽ trả về null) => Quá trình GC có thể thu hồi vùng nhớ từ object đó bất cứ lúc nào.

Ví dụ cách sử dụng:

public class DetailActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        new CustomAsyncTask(this).execute();
    }

    void updateText(String string) {
        //do update UI
    }

    private static class CustomAsyncTask extends AsyncTask<Object, String, String> {
        private WeakReference<DetailActivity> activity;

        CustomAsyncTask(DetailActivity activity) {
            // Here we create a WeakReference to the outer activity
            this.activity = new WeakReference<>(activity);
        }

        @Override
        protected String doInBackground(Object... objects) {
            //do some background operation
            doBackgroundWork();
            return "Result";
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            //We get the activity from the WeakReference
            DetailActivity detailActivity = activity.get();
            //check if it is still alive
            if(detailActivity != null) {
                //it is alive, update the UI
                detailActivity.updateText(s);
            }
        }
    }
}

Conclusion

Memory leak là 1 vấn đề không thể xem nhẹ khi lập trình ứng dụng Android. Tuy Java đã hỗ trợ chúng ta rất nhiều trong vấn đề quản lý bộ nhớ với Garbage Collector (không cần phải quản lý bằng tay như các ngôn ngữ khác như C, C++,..) nhưng chúng ta cũng không vì thế mà bỏ qua việc tìm hiểu bộ nhớ đc cấp phát và giải phóng lúc nào, ở đâu... Tối ưu hóa bộ nhớ sẽ giúp ứng dụng của chúng ta chạy mượt mà hơn và ít bị crash hơn.

Bài viết được viết dựa theo những kinh nghiệm và trải nghiệm thực tế của tôi. Nếu có chỗ nào sai sót mong các bạn lượng thứ 😄

Cảm ơn đã theo dõi. Arigatou gozaimasu!

Nguồn tham khảo:

All Rights Reserved