美文网首页android
LeakCanary2.0使用及原理分析 — Kotlin重构版

LeakCanary2.0使用及原理分析 — Kotlin重构版

作者: Geekholt | 来源:发表于2019-12-30 23:08 被阅读0次

目录

前言

写给程序员的内存泄漏治理手册中我们介绍了android内存泄漏的原理以及治理方案。通过上一节的学习我们可以做到尽可能的避免写出有可能内存泄漏的代码。但是实际开发过程中,由于一个项目往往有多人一起开发,以及有时候项目开发节奏比较快,所以项目开发过程中依然很有可能会出现一些内存泄漏问题,但是内存泄漏问题往往比较隐蔽,不容易发现。所以这里就介绍一款非常好用的内存泄漏检测工具LeakCanary

LeakCanary官方网站:https://square.github.io/leakcanary/

LeakCanary的使用

添加依赖

LeakCanary升级到2.0之后,使用起来非常简单,只需要在build.gradle中添加依赖

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0'
}

模拟内存泄漏

  1. 先创建一个单例类

SingleInstance.java

public class SingleInstance {
    private Context context;

    private SingleInstance(Context context) {
        this.context = context;
    }

    public static class Holder {
        private static SingleInstance INSTANCE;

        public static SingleInstance newInstance(Context context) {
            if (INSTANCE == null) {
                INSTANCE = new SingleInstance(context);
            }
            return INSTANCE;
        }
    }
}
  1. SecondActivity的引用加入到单例中

按back键回到MainActivity,这时候SecondActivityonDestory()会执行,但是单例类中依然持有SecondActivity的引用,这时候SecondActivity就会出现内存泄漏

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        SingleInstance.Holder.newInstance(this);
    }
}
  1. LeakCanary监测到内存泄漏后会发送一个通知
内存泄漏通知
  1. 点开通知,就可以看到详细的内存泄漏堆栈信息
内存泄漏详细信息

同样我们也可以通过Logcat查看内存泄漏信息,如下所示

2019-12-30 16:36:40.130 20875-21568/com.geekholt.leakcanarydemo D/LeakCanary: ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    80988 bytes retained
    ┬
    ├─ android.os.HandlerThread
    │    Leaking: NO (PathClassLoader↓ is not leaking)
    │    Thread name: 'LeakCanary-Heap-Dump'
    │    GC Root: Local variable in native code
    │    ↓ thread HandlerThread.contextClassLoader
    ├─ dalvik.system.PathClassLoader
    │    Leaking: NO (Object[]↓ is not leaking and A ClassLoader is never leaking)
    │    ↓ PathClassLoader.runtimeInternalObjects
    ├─ java.lang.Object[]
    │    Leaking: NO (SingleInstance$Holder↓ is not leaking)
    │    ↓ array Object[].[757]
    ├─ com.geekholt.leakcanarydemo.SingleInstance$Holder
    │    Leaking: NO (a class is never leaking)
    │    ↓ static SingleInstance$Holder.INSTANCE
    │                                   ~~~~~~~~
    ├─ com.geekholt.leakcanarydemo.SingleInstance
    │    Leaking: UNKNOWN
    │    ↓ SingleInstance.context
    │                     ~~~~~~~
    ╰→ com.geekholt.leakcanarydemo.SecondActivity
         Leaking: YES (Activity#mDestroyed is true and ObjectWatcher was watching this)
         key = 2a99b464-42c4-4b8c-a0a8-5edb348f90bb
         watchDurationMillis = 10486
         retainedDurationMillis = 5485
    ====================================
    0 LIBRARY LEAKS
    
    Leaks coming from the Android Framework or Google libraries.
    ====================================
    METADATA
    
    Please include this in bug reports and Stack Overflow questions.
    
    Build.VERSION.SDK_INT: 28
    Build.MANUFACTURER: HUAWEI
    LeakCanary version: 2.0
    App process name: com.geekholt.leakcanarydemo
    Analysis duration: 11596 ms
    Heap dump file path: /data/user/0/com.geekholt.leakcanarydemo/files/leakcanary/2019-12-30_16-36-25_973.hprof
    Heap dump timestamp: 1577695000128
    ====================================

可以看出,LeakCanary能够实时地帮我们监测出程序中内存泄漏问题,且定位非常准确,可以说是非常的强大!既然这个工具如此强大,所以我们在使用的同时,最好也能理解其中的原理,这样才能真正融会贯通,为己所用

LeakCanary原理

这里再提醒一下,本文的源码都是基于LeakCanary2.0的哦

用过LeakCanary1.x的同学一定知道,过去LeakCanary初始化的时候都是需要在Application中调用LeakCanary.install()进行注册的,升级到2.0之后连注册的代码都省了。那LeanCanary2.0是如何生效的呢?

LeakCanary初始化

查看LeakCanary源码,依然发现了install()相关的代码

AppWatcher.java

/**
 * [AppWatcher] is automatically installed on main process start by
 * [leakcanary.internal.AppWatcherInstaller] which is registered in the AndroidManifest.xml of
 * your app. If you disabled [leakcanary.internal.AppWatcherInstaller] or you need AppWatcher
 * or LeakCanary to run outside of the main process then you can call this method to install
 * [AppWatcher].
 */
fun manualInstall(application: Application) = InternalAppWatcher.install(application)

从这个方法的注释中我们得出了以下信息:

  1. AppWatcher#manualInstall()会在主进程中自动被AppWatcherInstaller调用
  2. AppWatcherInstaller会在AndroidManifest.xml中被注册
  3. 如果要在非主进程监听内存泄漏,需要手动调用AppWatcher#manualInstall()方法

既然这个AppWatcherInstaller会在AndroidManifest.xml中被注册,那么它一定是四大组件之一,查看源码发现,其实AppWatcherInstaller就是ContentProvider的子类

AppWatcherInstaller.java

internal sealed class AppWatcherInstaller : ContentProvider() {

  /**
   * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process.
   */
  internal class MainProcess : AppWatcherInstaller()

  /**
   * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`,
   * [LeakCanaryProcess] automatically sets up the LeakCanary code
   */
  internal class LeakCanaryProcess : AppWatcherInstaller() {
    override fun onCreate(): Boolean {
      super.onCreate()
      AppWatcher.config = AppWatcher.config.copy(enabled = false)
      return true
    }
  }

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    //执行LeakCanary初始化操作
    InternalAppWatcher.install(application)
    return true
  }

  override fun query(
    uri: Uri,
    strings: Array<String>?,
    s: String?,
    strings1: Array<String>?,
    s1: String?
  ): Cursor? {
    return null
  }

  override fun getType(uri: Uri): String? {
    return null
  }

  override fun insert(
    uri: Uri,
    contentValues: ContentValues?
  ): Uri? {
    return null
  }

  override fun delete(
    uri: Uri,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }

  override fun update(
    uri: Uri,
    contentValues: ContentValues?,
    s: String?,
    strings: Array<String>?
  ): Int {
    return 0
  }
}

APP构建经过manifest-merge后会合并多个清单文件,这个ContentProvider会被合并到唯一的manifest.xml中. 当APP初始化时会加载这个LeakSentryInstaller,就会自动帮我们执行InternalLeakSentry.install(application)

作为ContentProvider他的其他CRUD实现都是空的,作者只是巧妙利用了ContentProvider无需显式初始化的特性(对比Service、BroadcastReceiver)来实现了自动注册

如何检测Activity内存泄漏

我们再来看看InternalAppWatcher#install()方法做了什么

InternalAppWatcher.java

这个方法中比较关键的就是ActivityDestroyWatcher#install()FragmentDestroyWatcher#install()

这两个方法内部实现思路相似,所以就这里只分析ActivityDestroyWatcher#install()

fun install(application: Application) {
  SharkLog.logger = DefaultCanaryLog()
  SharkLog.d { "Installing AppWatcher" }
  checkMainThread()
  if (this::application.isInitialized) {
    return
  }
  InternalAppWatcher.application = application

  val configProvider = { AppWatcher.config }
  ActivityDestroyWatcher.install(application, objectWatcher, configProvider)
  FragmentDestroyWatcher.install(application, objectWatcher, configProvider)
  onAppWatcherInstalled(application)
}

ActivityDestroyWatcher.java

这个方法实际上就是调用了application#registerActivityLifecycleCallbacks()对整个应用中的Activity的生命周期进行监听

internal class ActivityDestroyWatcher private constructor(
  private val objectWatcher: ObjectWatcher,
  private val configProvider: () -> Config
) {

  private val lifecycleCallbacks =
    object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
      override fun onActivityDestroyed(activity: Activity) {
        if (configProvider().watchActivities) {
          //当activity onDestory的时候做处理
          objectWatcher.watch(activity)
        }
      }
    }

  companion object {
    fun install(
      application: Application,
      objectWatcher: ObjectWatcher,
      configProvider: () -> Config
    ) {
      val activityDestroyWatcher =
        ActivityDestroyWatcher(objectWatcher, configProvider) 
     //生命周期监听
   application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
    }
  }
}

ObjectWatcher.java

源码看到目前为止,小结一下其实就是Activity会在onDestory()之后,调用下面的ObjectWatcher#watch(),这个方法比较关键,不仅仅可以检测Activity的内存泄漏,还可以通过这个方法检测任何对象的内存泄漏

@Synchronized fun watch(
  watchedObject: Any,
  name: String
) {
  if (!isEnabled()) {
    return
  }
  //1.移除弱可达引用
  //弱可达:一个对象只被弱引用所引用,由于弱引用的特性,这样的对象是不会出现内存泄漏的
  removeWeaklyReachableObjects()
  val key = UUID.randomUUID()
      .toString()
  val watchUptimeMillis = clock.uptimeMillis()
  //2.将activity加入到WeakReference中
  val reference =
    KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)
  SharkLog.d {
      "Watching " +
          (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
          (if (name.isNotEmpty()) " named $name" else "") +
          " with key $key"
  }
    //3.将reference保存到一个数组中
  watchedObjects[key] = reference
  checkRetainedExecutor.execute {
    //4.五秒后执行后续流程(checkRetainedExecutor配置了watchDurationMillis是5秒,具体可以自己看代码)
    moveToRetained(key)
  }
}

/**
 * 移除弱可达引用
 */
private fun removeWeaklyReachableReferences() {
    // WeakReferences are enqueued as soon as the object to which they point to becomes weakly
    // reachable. This is before finalization or garbage collection has actually happened.
    var ref: KeyedWeakReference?
    // 已经回收掉的弱引用对象会存放在RefrenceQueue中,循环移除
    do {
      ref = queue.poll() as KeyedWeakReference?
      //如果RefrenceQueue里存在,说明这个弱引用对象被回收了
      if (ref != null) {
        val removedRef = watchedReferences.remove(ref.key)
        //如果watchedReferences中的这个弱引用对象被回收了,retainedReferences也移除掉这个弱引用
        if (removedRef == null) {
          retainedReferences.remove(ref.key)
        }
      }
    } while (ref != null)
}

思考1:如何判断一个对象是否被回收

其实注释中已经基本解释了

如果一个对象除了弱引用以外,没有被其他对象所引用,当发生GC时,这个弱引用对象就会被回收,并且被回收掉的对象会被存放到ReferenceQueue中,所以当ReferenceQueue中有这个对象就代表这个对象已经被回收,反之就是没有被回收

思考2: 这里为什么要延迟五秒执行任务

我们都知道GC不是即时的, 页面销毁后预留5秒的时间给GC操作, 再后续分析引用泄露, 避免无效的分析

HeapDumpTrigger.java

//仅展示关键代码
private fun checkRetainedObjects(reason: String) {
    ...
  //移除弱可达对象后,统计中还剩下的引用数
  var retainedReferenceCount = objectWatcher.retainedObjectCount

  if (retainedReferenceCount > 0) {
    //手动进行一次GC
    gcTrigger.runGc()
    //在GC后, 再次统计剩下的引用数
    //到这一步剩下的就是没被回收掉的就是可能发生泄露的引用. 需要后续的dump分析
    retainedReferenceCount = objectWatcher.retainedObjectCount
  }
    
  //判断当前泄露实例个数如果小于5个,仅仅只是给用户一个通知,不会进行heap dump 操作,并在5s后再次发起检测
  if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
    ...
  //生成内存泄漏堆栈信息
  val heapDumpFile = heapDumper.dumpHeap()
    .....
}

private fun checkRetainedCount(
    retainedKeysCount: Int,
    retainedVisibleThreshold: Int   // retainedVisibleThreshold默认为 5 个
  ): Boolean {
    val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
    lastDisplayedRetainedObjectCount = retainedKeysCount
    if (retainedKeysCount == 0) {
      SharkLog.d { "No retained objects" }
      if (countChanged) {
        showNoMoreRetainedObjectNotification()
      }
      return true
    }
    if (retainedKeysCount < retainedVisibleThreshold) {
      if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
        SharkLog.d {
            "Found $retainedKeysCount retained objects, which is less than the visible threshold of $retainedVisibleThreshold"
        }
        // // 通知用户 "App visible, waiting until 5 retained instances"
        showRetainedCountBelowThresholdNotification(retainedKeysCount, retainedVisibleThreshold)
        // 5s 后再次发起检测
        scheduleRetainedObjectCheck(
            "Showing retained objects notification", WAIT_FOR_OBJECT_THRESHOLD_MILLIS
        )
        return true
      }
    }
    return false
}

生成heap dump 文件

AndroidHeapDumper.java

这个过程主要就是两步

1.发送通知

2.使用Debug.dumpHprofData(heapDumpFile.absolutePath)捕获堆转储

override fun dumpHeap(): File? {
  val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return null

  val waitingForToast = FutureResult<Toast?>()
  showToast(waitingForToast)

  if (!waitingForToast.wait(5, SECONDS)) {
    SharkLog.d { "Did not dump heap, too much time waiting for Toast." }
    return null
  }

  //1.发送通知
  val notificationManager =
    context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
  if (Notifications.canShowNotification) {
    val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping)
    val builder = Notification.Builder(context)
        .setContentTitle(dumpingHeap)
    val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW)
    notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification)
  }

  val toast = waitingForToast.get()

  return try {
    //2.捕获堆转储
    Debug.dumpHprofData(heapDumpFile.absolutePath)
    if (heapDumpFile.length() == 0L) {
      SharkLog.d { "Dumped heap file is 0 byte length" }
      null
    } else {
      heapDumpFile
    }
  } catch (e: Exception) {
    SharkLog.d(e) { "Could not dump heap" }
    // Abort heap dump
    null
  } finally {
    cancelToast(toast)
    notificationManager.cancel(R.id.leak_canary_notification_dumping_heap)
  }
}

分析 heap dump 文件

最后启动一个前台服务 HeapAnalyzerService 来分析 heap dump 文件。然后通过解析库找到最短 GC Roots 引用路径,展示给用户。这里不是我们的重点,就不做具体分析了,感兴趣的可以自己看一下源码

总结

  1. LeakCanary2.0利用了ContentProvider无需显式初始化的特性来实现了自动注册
  2. 通过application#registerActivityLifecycleCallbacks()对Activity的生命周期进行监听
  3. Activity销毁时,将Activity添加到一个WeakReference中,利用WeakReferenceReferenceQueue的特性,如果一个对象除了弱引用以外,没有被其他对象所引用,当发生GC时,这个弱引用对象就会被回收,并且被回收掉的对象会被存放到ReferenceQueue中,所以当ReferenceQueue中有这个对象就代表这个对象已经被回收,反之就是没有被回收
  4. 调用Android原生提供的捕获堆转储的方法Debug.dumpHprofData(heapDumpFile.absolutePath)
  5. 使用解析库来分析 heap dump 文件

相关文章

网友评论

    本文标题:LeakCanary2.0使用及原理分析 — Kotlin重构版

    本文链接:https://www.haomeiwen.com/subject/gjumoctx.html