+3

Memory Leaks trong android

Nguồn: https://www.raywenderlich.com/4690472-memory-leaks-in-android

Android Profiler và LeakCanary

Android Profiler để thay thế cho công cụ Android Monitor, xuất hiện từ phiên bản Android Studio 3.0 về sau. Nó sẽ đo lường một số app và thực hiện tính tỷ lệ hiệu năng real-time:

  • Battery
  • Network
  • CPU
  • Memory LeakCanary là một thư viện được thiết kế để phát hiện memory leaks. Bạn sẽ học được nó là gì, các case thông thường để phòng tránh chúng.

Trong bài hướng dẫn này sẽ tập trung vào phân tích memory để phát hiện leak.

Memory Leaks

Memory leak xảy ra khi trong code bạn khởi tạo một ô nhớ cho một object, nhưng lại không huỷ nó đi. Nó có thể xảy ra vì nhiều lý do. Bạn sẽ học về các nguyên nhân này sau.

Không quan trọng nguyên nhân, khi xảy ra memory leak thì Garbage Collector sẽ cho rằng object đó vẫn cần thiết bởi vì nó vẫn được tham chiếu đến bởi một object khác. Nhưng những thao chiếu này đúng ra phải được xoá bỏ rồi.

Phần memory đã khởi tạo cho đối tượng bị leak sẽ hoạt động như một block không thể di chuyển, ép phần còn lại của app chạy trong phần còn lại của heap. Nó sẽ gây ra tập hợp nhiều thông tin không cần thiết. Và nếu app vẫn tiếp tục bị leak memory, nó sẽ bị đầy và dẫn đến crash.

Đôi khi, leak rất lớn và nguy hiểm. Đổi khi nó là lỗi nhỏ. Lỗi nhỏ thì sẽ khó tìm ra hơn bởi nó thường xảy ra sau một thời gian dài sử dụng app.

Bắt đầu

Để bắt đầu, tải project trong link nguồn.

Thông qua bài hướng dẫn này, bạn sẽ làm việc với TripLogGuessCount.

TripLog sẽ để người dùng viết note về những thứ họ đang làm và đang cảm thấy trong cả quá trình. Nó cũng lưu lại ngày và địa điểm.

GuessCount là game sẽ hỏi bạn đếm nội bộ một số trong vài giây. Bạn ấn một button khi bạn nghĩ số đếm đã vượt. Sau đó game sẽ nói cho bạn biết sự khác nhau giữa số bạn đếm và số đếm thực tế trong vài giây.

Mở bản Android Studio 3.4.1 hoặc mới hơn, chọn File ▸ New ▸ Import Project. Chọn top-level của thư mục project cho một trong các starter project bạn đã tải về.

Build và run TripLog và GuessCount để làm quen với nó: TripLog project chứa các file chính sau:

  • MainActivity.kt chứa các màn hình chính. Nó sẽ hiển thị log ở đây.
  • DetailActivity.kt cho phép user tạo hoặc xem log.
  • MainApplication.kt cung cấp các phụ thuộc cho activity: một repository và formatter.
  • TripLog.kt đại diện cho data của log.
  • Repository.kt lưu và lấy log.
  • CoordinatesFormatter.kt và DateFormatter.kt format data để hiển thị trên màn hình.

GuessCount project có các file:

  • MainActivity.kt chứa màn hình chính. Nó cho phép user đặt số giây để đếm.
  • CountActivity.kt cho phép user ấn một button khi user nghĩ nó đã kết thúc.

Tìm Leak sử dụng Android Profiler

Sử dụng Android Profiler để bắt đầu profiling the GuessCount app. Bạn có thể mở nó bằng cách vào đi đến View ‣ Tool Windows ‣ Profiler. Nhập 240 giây vào trường Seconds to count và ấn vào button Start. Bạn sẽ thấy một màn hình loading nhỏ. Ở đây, bạn hy vọng sẽ đếm đến 240 và ấn Guess, nhưng thay vào đó, ấn button Back.

Trong profiler, tạo một heap dump bằng cách ấn vào button Dump Java heap: Lọc bởi CountActivity: Bạn sẽ thấy: CountActivity chưa được huỷ bỏ, mặc dù nó ko hiển thị trên màn hình. Có thể là Garbage Collector vẫn chưa vượt qua. Vì thế, buộc một vài garbage collections bằng cách ấn vào button phản hồi trên profiler: Giờ tạo một heap dump mới để kiểm tra xem liệu nó đã bị huỷ chưa: Và nó không bị huỷ. Vì thế, click vào CountActivity để xem chi tiết hơn: Trong Instance View, bạn sẽ thấy và theo như thuộc tính mFinished, thì activity này đã kết thúc. Vì vậy Garbage collector đúng ra đã ghi nhận được nó rồi.

Nó là memory leak bởi vì CountActivity vì vẫn trong memory nhưng không ai cần nó cả.

Đợi một vài phút. Lặp lại quá trình và tạo một heap dump mới. Bạn sẽ thấy là cuối cùng thì activity cũng được huỷ: Nó thực ra là một temporal memory leak. Trong ví dụ này, leak tạm thời CountActivity không phải một vấn đề lớn lắm. Nhưng hãy nhớ, bị leak toàn bộ activity thực sự rất là tệ, bởi nó sẽ chứa toàn bộ view và các object mà nó tham chiếu tới. Nó cũng đúng với tất cả các object khác đang tham chiếu tới nó, như là TextView.

Để phân tích vấn đề này, mở heap dump đầu tiên nơi mà CountActivity vẫn còn lại, mặc dù bạn đã close nó. Làm nó bằng cách chọn: Lọc bởi CountActivity và chọn nó. Sau đó chọn instance trong Instance View và bạn sẽ thấy như bên dưới: Danh sách References sẽ hiện tất cả các object đang thao chiếu tới CountActivity. Cái đầu tiên, this$0 in CountActivity$timeoutRunnable$1 thì khả thi từ CountActivity. Vậy, mở file CountActivity.kt và tìm biến này:

  private val timeoutRunnable = object : Runnable {
    override fun run() {
      this@CountActivity.stopCounting()
      this@CountActivity.showTimeoutResults()
    }
  }

Biến này giữ một tham chiếu tới một class vô danh của Runnable. Trong class vô danh này bạn có thể thao chiếu đến container của bạn, trong trường hợp này là CountActivity.

Đó là lý do vì sao code có thể gọi stopCounting()showTimeoutResults(). Nếu bạn click chuột phải vào biến timeoutRunnable và chọn Find Usages, bạn sẽ thấy là nó được gọi từ method:

  private fun startCounting() {
    startTimestamp = System.currentTimeMillis()
    val timeoutMillis = userCount * 1000 + 10000L
    timeoutHandler.postDelayed(timeoutRunnable, timeoutMillis)
  }

startCounting() được gọi từ onCreate(). Ở đây, bạn sẽ thấy một timeoutHandler cái mà đã được delay việc thực thi của timeoutRunnable. App sử dụng timeout này nếu bạn không ấn vào button Guess.

TIếp tục khám phá CountActivity class và bạn sẽ thấy là timeoutHandler không bao giờ được gọi đến khi bạn thoát activity. Tuy vậy, nó sẽ thực thực cái bên trong timeoutRunnable sau timeoutMillis. Trong trường hợp này, nó đã gọi stopCounting()showTimeoutResults() từ CountActivity. Nó sẽ phải giữ lại, gây ra leak.

Memory Leaks thường gặp

Anonymous Classes

ĐÔi khi một instance của anonymous class tồn tại lâu hơn instance của container. Nếu như vậy, instance của anonymous class sẽ gọi method bất kỳ, hoặc đọc hoặc viết thuộc tính bất kỳ của container class, nó sẽ giữ lại instance của container. Nó sẽ gây ra leak memory của container.

Leak Canary hiện Memory Leak như thế nào

Trước khi fix leak, bạn sẽ sử dụng LeakCanary để xem công cụ này hiển thị như thế nào khi có một leak. Mở file build.gradle và thêm đoạn sau vào dependence:

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-3'

Build và run app, click vào button Start và ngày lập tức ấn back. Bạn sẽ thấy như sau: Màn hình LeakCanary thể hiện một destroyed activity. Nó sẽ đợi 5 giây và sau đó ép garbage collection. Nếu activity vẫn còn, LeakCanary xem xét việc giữ lại và đánh gía khả năng leak.

Click vào thông báo và nó sẽ bắt đầu dumping heap: Sau khi dump heap, LeakCanary sẽ tìm tham chiếu strong ngắn nhất từ GC root để giữ lại instance. Nó cũng được gọi là leak trace.

Click vào thông báo. Đợi một vai giây trong khi nó phân tích heap dump. Click lại vào thông báo và thông qua LeakCanary app để xem leak trace: Ở dưới cùng, nó nói là CountActivity bị leak bởi vì mDestroyedtrue. Nó giống với kết quả mà bạn đã phân tích trước đó với Android profiler.

Ở trên cùng, bạn sẽ thấy một instance của MessageQueue không bị leak. Lý do là bị nó là một GC root.

LeakCanary sử dụng chuẩn đoán để các định trạng thái lifecycle của chuỗi node và nói xem liệu nó có đang bị leak hay không. Một thao chiếu sau lần cuối cùng Leaking: NO và trước Leaking: YES gây ra leak. Những đường bên gạch đỏ tham chiếu là ứng viên.

Nếu bạn đi từ trên xuống dưới, bạn sẽ tìm thấy CountActivity$timeoutRunnable$1.this$0.Một lần nữa, nó giống với biến bạn đã tìm thấy khi sử dụng Android Profiler. Nó các nhận rằng timeoutRunnable đang tham chiếu đến activiry và Garbage Collector chưa được huỷ.

Để fix leak này, mở CountActivity.kt và thêm đoạn sau:

  override fun onDestroy() {
    stopCounting()
    super.onDestroy()
  }

Hàm stopCounting() gọi đến timeoutHandler.removeCallbacksAndMessages().

Build và run app lần nữa để xem liệu leak đã được fix chưa bằng cả Android Profiler và Leak Canary.

Inner Classes

Inner Classes là một lỗi thường gặp của memory leak vởi nó cũng có thể tham chiếu đến container class của nó. Ví dụ, giả sử bạn có AsyncTask trong onDestroy() như bên dưới:

class MyActivity : AppCompatActivity() {
  ...
  inner class MyTask : AsyncTask<Void, Void, String>() {
    override fun doInBackground(vararg params: Void): String {
      // Perform heavy operation and return a result
    }
    override fun onPostExecute(result: String) {
      this@MyActivity.showResult(result)
    }
  }
}

Nó sẽ gây ra leak nếu bạn để activity và task không kết thúc. Bạn có thể thử đóng AsyncTask trong onDestroy(). Tuy nhiên, bởi vì cách mà AsyncTask làm việc, hàm doInBackground() sẽ không đóng và bạn sẽ tiếp tục leak activity.

Để sửa nó, bạn sẽ xoá inner để convert thành MyTask sang một static class. Static inner class không cần access vào container class, vì vậy bạn sẽ không thể leak activity. Nhưng bạn sẽ không thể gọi showResult nữa.

Vậy, bạn có thể nghĩ về việc truyền activity như là một parameter như sau:

class MyActivity : AppCompatActivity() {
  ...
  class MyTask(private val activity: MainActivity) : AsyncTask<Void, Void, String>() {
    override fun doInBackground(vararg params: Void): String {
      // Perform heavy operation and return a result
    }
    override fun onPostExecute(result: String) {
      activity.showResult(result)
    }
  }
}

Tuy nhiên, bạn có thể bị leak cả activity nữa. Phương pháp khả thi là sử dụng WeakReference:

class MyActivity : AppCompatActivity() {
  ...
  class MyTask(activity: MainActivity) : AsyncTask<Void, Void, String>() {
    private val weakRef = WeakReference<MyActivity>(activity)
    override fun doInBackground(vararg params: Void): String {
      // Perform heavy operation and return a result
    }
    override fun onPostExecute(result: String) {
      weakRef.get()?.showResult(result)
    }
  }
}

Một WeakReference tham chiếu một object như một tham chiếu thông thường, nhưng nó không đủ khoẻ để giữ trong memory. Tuy vậy, khi thông qua Garbage Collector và không tìm thấy bất kỳ tham chiếu strong nào đến object. Bất kỳ WeakReference tham chiếu đến object đã được collect sẽ bị set thành null.

Static Variables

Biến bên trong companion object là biến static. có những biến sẽ tương tác với một class chứ không phải một instance của class. Vì vậy, chúng sẽ tồn tại từ khi system load class.

Bạn nên tránh chứa các tham chiếu biến static trong activity. Chúng sẽ không garbage collected mặc dù chúng không còn cần đến nữa.

Singletons

Nếu singleton object giữ một tham chiếu đên một activity và tồn tại lâu hơn activity cyar bạn, bạn sẽ bị leak.

Như là một workaround, bạn có thể cung cấp một method trong singleton object cái sẽ xoá tham chiếu đó. Bạn có thể gọi method đó trong activity của onDestroy().

Registering Listeners

Trong nhiều Android API và SDK mở rộng, bạn phải đăng ký một activity như là một listener và cung cập callback method đến một sự kiện nhận. Ví dụ, bạn sẽ cần phải làm việc này với location update và system event.

Nó có thể tạo ra memory leak nếu bạn quên bỏ đăng ký. Thông thường, object mà bạn đăng ký sẽ không tồn tại lâu hơn activity của bạn.

Để xem kiểu memory leak này trong thực tế, mở TripLog project. Build và run nó.

Ấn vào dấu + để thêm log mới. Đồng ý cấp quyền cho location permission và bạn sẽ thấy nó hiển thị location của user. Bạn có thể sẽ phải bật Location service của thiết bị và thử thêm một log vài lần để thấy location của user: Mửo Android Profiler và phất đầu profiling app. Ấn Back, ép garbage collection một vài lần và tạo một heap dump.

Bạn sẽ để ý thấy DetailActivity vẫn còn tồn tại.

Nếu bạn muốn, thêm LeakCanary vào dependence và kiểm tra leak.

Để sửa nó, bạn cần thêm đoạn sau:

  override fun onPause() {
    fusedLocationClient.removeLocationUpdates(locationCallback)
    super.onPause()
  }

Profile app lần nữa hoặc sử dụng LeakCanary. Thêm log mới.

Thoát app và ép garbage collection. Bạn sẽ chú ý thấy là vẫn bị leak, nhưng là một leak khác với trước.

Lần này vấn đề là do thư viện play-services-location mà bạn đang sử dụng.

Để sửa nó, tạo một file WeakLocationCallback.kt với nội dung sau:

class WeakLocationCallback(locationCallback: LocationCallback) 
    : LocationCallback() {

    private val locationCallbackRef = WeakReference(locationCallback)

    override fun onLocationResult(locationResult: LocationResult?) {
        locationCallbackRef.get()?.onLocationResult(locationResult)
    }

    override fun onLocationAvailability(
        locationAvailability: LocationAvailability?
    ) {
        locationCallbackRef.get()?.onLocationAvailability(locationAvailability)
    }
}

Mở DetailActivity.kt và thay thế dòng nơi mà bạn set locationCallback bằng đoạn dưới đây:

locationCallback = WeakLocationCallback(object : LocationCallback() {
  ...
})

Build và run lần nữa để check xem có leak nào ko nhé 😀


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí