Android体系课之--LeakCanary内存泄露检测原理解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: #### 内存泄露不需要的对象实例,无法被垃圾回收,比如被静态片段保留,就说可能发生内存泄露##### 常见场景:- 1.不清楚fragment视图的字段的情况下,将fragment添加到backstack中- 2.Activity以context的形式被添加到一些类中,比如静态类,则gc无法清除,如Activity被非静态内部类Handler引用- 3.注册一个监听器,广播接收器或者RxJava订阅时,引用了一个生命周期的对象,生命周期结束后,没有取消注册

前言

在讲解LeakCanary前我们先来介绍基础概念:

基础知识:

内存泄露

不需要的对象实例,无法被垃圾回收,比如被静态片段保留,就说可能发生内存泄露

常见场景:
  • 1.不清楚fragment视图的字段的情况下,将fragment添加到backstack中
  • 2.Activity以context的形式被添加到一些类中,比如静态类,则gc无法清除,如Activity被非静态内部类Handler引用
  • 3.注册一个监听器,广播接收器或者RxJava订阅时,引用了一个生命周期的对象,生命周期结束后,没有取消注册

对象引用类型:

  • 强引用:强引用关系是导致内存泄露最直接的原因
  • 软引用:在内存不足时会被回收
  • 弱引用:垃圾收集器回收的时候会被直接回收
  • 虚引用:这个很少用,不做介绍

了解了内存泄露和引用的关系后:

我们来介绍下LeakCanary的基本使用原理

1.检测保留对象

LeakCanary挂钩到Android的生命周期内,以自动检测Activity或者Fragment何时被销毁并且被gc,这些被销毁的对象会被传递给一个ObjectWatcher,它持有对他们的弱引用,对象类型包括:

  • Activity对象
  • Fragment对象
  • View实例对象
  • ViewModel对象
  • Service实例

1.1:注册方式:

 AppWatcher.objectWatcher.watch(activity,"view was detached")

1.2:等待5s后,调用gc,如果持有的弱引用没有被清除,则被监视器认为产生了一个内存泄露,LeakCanary会将其记录到Logcat中
1.3:记录保留对象的计数,达到阈值后,会调用转储堆

2.转储堆

保留对象达到阈值后,LeakCanary生成一个内存的hprof文件内存快照到到本地目录中,生成过程中,应用无法再使用,会显示一个toast

3.分析堆

使用Shark分析堆内存,并定位到保留的对象,寻找保留对象的Gc root分析完成后,可以在通知栏看到分析结果,对于同一个对象可能会有几个不同的泄露路径,根据Gc roots生成签名,在LogCat中会显示 每个应用桌面上会对应生成一个LeakCanary图标,可以通过这个图标进入应用的LeakCanary分析结果界面

每个Gc root显示,可以显示一段路径上哪个对象持有待销毁的Context

4.对泄露进行分类

        应用程序泄露和库泄露
        泄露类型可以在Logcat中看到
        ====================================
        HEAP ANALYSIS RESULT
        ====================================
        0 APPLICATION LEAKS

        ====================================
        1 LIBRARY LEAK

        ...
        ┬───
        │ GC Root: Local variable in native code
        │
        ...

1673c300751d483c982d421907b007e0_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.awebp

使用LeakCanary如何修复内存泄露

泄露对象一般都被以下对象持有,导致无法回收 1.属于一个线程的局部变量 2.活动的java线程实例 3.系统类且会一直存在 3.native引用,属于native代码范围

修复步骤:
    1.查找泄露痕迹
    2.缩小可疑参考范围
        LeakCanary会自动检测类的生命周期,将可疑泄露对象显示出来
        Leak:No  Leak:yes

    3.找到导致泄露的参考
        根据Gc root找到最大可能泄露的引用
    4.修复漏洞
        找到泄露的引用后,在对象被销毁前,打断Gc root即可,如置为null等操作。

来分析一个案例:

我们在app中设置创建了一个list

class MyApp:Application(){
    val viewList = mutableListOf<View>()
    override fun onCreate() {
        super.onCreate()
    }

}
fun MyApp.setView(view: View){
    viewList.add(view)
}
fun MyApp.removeView(view: View){
    viewList.remove(view)
}

在Activity中调用:


onCreate{
    1.将view放入app的viewList中
    app?.setView(tv)
    2.调用:watcher监控tv
    AppWatcher.objectWatcher.expectWeaklyReachable(tv,"test main leak")
}

此时运行后: 日志出现:

D/LeakCanary: Found 1 object retained, not dumping heap yet (app is visible & < 5 threshold)

LeakCanary检测到有个对象没有被回收,通知栏会有显示,点击通知栏logcat中会显示对应的对象信息 我们退出该应用查看日志系统:

D/LeakCanary: Found 5 objects retained, dumping heap now (app is invisible)

    ┬───
    │ GC Root: System class
    │
    ├─ android.provider.FontsContract class
    │    Leaking: NO (MyApp↓ is not leaking and a class is never leaking)
    │    ↓ static FontsContract.sContext
    ├─ com.allinpay.testkotlin.leakcanary.MyApp instance
    │    Leaking: NO (Application is a singleton)
    │    mBase instance of android.app.ContextImpl
    │    ↓ MyApp.viewList
    │            ~~~~~~~~
    ├─ java.util.ArrayList instance
    │    Leaking: UNKNOWN
    │    Retaining 101.2 kB in 1415 objects
    │    ↓ ArrayList[0]
    │               ~~~
    ╰→ com.google.android.material.textview.MaterialTextView instance
         Leaking: YES (ObjectWatcher was watching this because test main leak and View.mContext references a destroyed
         activity)
         Retaining 101.1 kB in 1413 objects
         key = f15737ab-8866-4235-af60-7cec30a144b3
         watchDurationMillis = 17861
         retainedDurationMillis = 12856
         View is part of a window view hierarchy
         View.mAttachInfo is null (view detached)
         View.mID = R.id.tv
         View.mWindowAttachCount = 1
         mContext instance of com.allinpay.testkotlin.MainActivity with mDestroyed = true

此时可以看到可疑溢出的地方:

分析Gc路径:

FontsContract.sContext->MyApp.viewList->ArrayList[0]->MaterialTextView instance->activity

问题原因:MyApp.viewList持有activity中的TextView引用,导致Activity退出的时候,TextView没有退出,Activity对象无法被回收,最终引起内存溢出

解决办法:Activity退出的时候清除viewList中的view,如下: 我们在Activity的onDestroy处调用如下代码: tv?.let { app.viewList.remove(it) } 退出应用后,通知栏没有显示,logcat日志显示: LeakCanary: All retained objects have been garbage collected 说明并没有内存溢出的情况

源码层面来分析下具体流程:

1.启动应用的时候,在启动ContentProvider期间,注册应用的Activity,Fragment,Service和ViewModel的的生命周期:

在AndroidManfest文件中:


<application>
        <provider
            android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
            android:authorities="${applicationId}.leakcanary-installer"
            android:enabled="@bool/leak_canary_watcher_auto_install"
            android:exported="false" />
    </application>
    MainProcessAppWatcherInstaller.kx
    internal class MainProcessAppWatcherInstaller : ContentProvider() {
        override fun onCreate(): Boolean {
            val application = context!!.applicationContext as Application
            AppWatcher.manualInstall(application)
            return true
        }
    }

调用了AppWatcher.manualInstall(application),进入AppWatcher

fun manualInstall(..watchersToInstall: List<InstallableWatcher> = appDefaultWatchers(application)){
    ...
        LeakCanaryDelegate.loadLeakCanary(application)//1
        watchersToInstall.forEach {//2
          it.install()
        }
    ...

    }

看1处:创建了一个InternalLeakCanary的单例类,并执行了loadLeakCanary()方法,传入application,这会执行InternalLeakCanary内部的invoke方法,这里埋个点,后面分析

val loadLeakCanary by lazy {
        try {
          val leakCanaryListener = Class.forName("leakcanary.internal.InternalLeakCanary")
          leakCanaryListener.getDeclaredField("INSTANCE")
            .get(null) as (Application) -> Unit
        } catch (ignored: Throwable) {
          NoLeakCanary
        }
      }

看2处:对watchersToInstall进行遍历调用install操作

来看下这个List:appDefaultWatchers(application)

fun appDefaultWatchers(..reachabilityWatcher: ReachabilityWatcher = objectWatcher):List<InstallableWatcher> { //这里有个reachabilityWatcher用于后期监听引用对象的生命周期
        return listOf(
          ActivityWatcher(application, reachabilityWatcher),
          FragmentAndViewModelWatcher(application, reachabilityWatcher),
          RootViewWatcher(reachabilityWatcher),
          ServiceWatcher(reachabilityWatcher)
        )
    }

可以看到这个list包括:

ActivityWatcher FragmentAndViewModelWatcher RootViewWatcher ServiceWatcher

调用了每个Watcher的install操作

我们用ActivityWatcher作为例子来看下:

class ActivityWatcher(
        override fun install() {
            application.registerActivityLifecycleCallbacks(lifecycleCallbacks)
        }
        private val lifecycleCallbacks =
        object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
          override fun onActivityDestroyed(activity: Activity) {
            reachabilityWatcher.expectWeaklyReachable(
              activity, "${activity::class.java.name} received Activity#onDestroy() callback"
            )
          }
        }
    }

这里可以看出注册了Activity的生命周期回调,其他Watcher也类似

这里的reachabilityWatcher在AppWatcher中实现:前面已经分析过:用于监听引用对象的生命周期回调

val objectWatcher = ObjectWatcher(
        clock = { SystemClock.uptimeMillis() },
        checkRetainedExecutor = {
          check(isInstalled) {
            "AppWatcher not installed"
          }
          mainHandler.postDelayed(it, retainedDelayMillis)
        },
        isEnabled = { true }
      )

2.在Activity调用onDestroy的时候,使用一个WeakReference来引用这个Activity对象,并将Activity的实例放入一个watchMap中,以WeakReference为key,并传入一个ReferenceQueue来监听这个引用实例的回收情况

Activity生命周期走到onDestroy的时候会回调reachabilityWatcher.expectWeaklyReachable

@Synchronized override fun expectWeaklyReachable(...)(
        removeWeaklyReachableObjects()//1
        val reference = KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)//2
        watchedObjects[key] = reference//3
        checkRetainedExecutor.execute {
          moveToRetained(key)//4
        }
      }

分析1处:从queue:ReferenceQueue中取出头部数据,如果不为null,则去watchedObjects中删除对应的key:这个key对应引用对象的包装类:KeyedWeakReference.key

private fun removeWeaklyReachableObjects() {
        var ref: KeyedWeakReference?
        do {
          ref = queue.poll() as KeyedWeakReference?
          if (ref != null) {
            watchedObjects.remove(ref.key)
          }
        } while (ref != null)
      }

分析2处:创建了一个KeyedWeakReference,并传入一个回收监听者ReferenceQueue,这个监听者在引用对象被回收的时候会将weakReference放入这个ReferenceQueue中 分析3处:将用weakReference包装引用对象后放入watchedObjects集合中,用于检测是否存在内存泄露 分析4处:这里又做了一次removeWeaklyReachableObjects,前面分析可知这里是用于检测是否有未回收的引用出现可能内存泄露的表现

并回调遍历onObjectRetainedListeners集合的onObjectRetained方法,


private fun moveToRetained(key: String) {
        removeWeaklyReachableObjects()
        val retainedRef = watchedObjects[key]
        if (retainedRef != null) {
          retainedRef.retainedUptimeMillis = clock.uptimeMillis()
          onObjectRetainedListeners.forEach { it.onObjectRetained() }
        }
      }

来看下onObjectRetainedListeners是在哪里赋值的

前面我们埋了个点InternalLeakCanary方法创建执行invoke方法

override fun invoke(application: Application) {
        _application = application

        checkRunningInDebuggableBuild()

        AppWatcher.objectWatcher.addOnObjectRetainedListener(this)//1

        val gcTrigger = GcTrigger.Default //2

        val configProvider = { LeakCanary.config } //3

        val handlerThread = HandlerThread(LEAK_CANARY_THREAD_NAME)
        handlerThread.start()
        val backgroundHandler = Handler(handlerThread.looper)

        heapDumpTrigger = HeapDumpTrigger(
          application, backgroundHandler, AppWatcher.objectWatcher, gcTrigger,
          configProvider
        )//4
        application.registerVisibilityListener { applicationVisible ->
          this.applicationVisible = applicationVisible
          heapDumpTrigger.onApplicationVisibilityChanged(applicationVisible)
        }
        registerResumedActivityListener(application)//5
        addDynamicShortcut(application)

      }

在这个方法中: 看到1处添加了addOnObjectRetainedListener回调为this,说明这个moveToRetained调用的是当前InternalLeakCanary的onObjectRetained方法 在2处创建了GC器GcTrigger.Default 在3处设置了configProvider 在4处创建了一个headDump触发器HeapDumpTrigger,后期dumpheap操作都由这个来完成 在5处注册了Activity的resume回调

来仔细对1处分析下:

1处说明当前InternalLeakCanary实现了OnObjectRetainedListener接口,然后看下他的方法实现onObjectRetained

override fun onObjectRetained() = scheduleRetainedObjectCheck()

继续看scheduleRetainedObjectCheck

fun scheduleRetainedObjectCheck() {
        if (this::heapDumpTrigger.isInitialized) {
          heapDumpTrigger.scheduleRetainedObjectCheck()
        }
      }

调用了heapDumpTrigger.scheduleRetainedObjectCheck()

HeapDumpTrigger.kt
      fun scheduleRetainedObjectCheck(
        delayMillis: Long = 0L
      ) {
        backgroundHandler.postDelayed({
          checkScheduledAt = 0
          checkRetainedObjects()
        }, delayMillis)
     }

这里使用一个backgroundHandler实现了checkRetainedObjects任务 这个checkRetainedObjects是LeakCanary核心地方 我们一步一步来分析下这个方法:

private fun checkRetainedObjects() {
    val iCanHasHeap = HeapDumpControl.iCanHasHeap()

    val config = configProvider()

    if (iCanHasHeap is Nope) {
      if (iCanHasHeap is NotifyingNope) {
        ...
        //这里只是对显示通知进行处理
      return
    }
    //获取持有的被持有的个数
    var retainedReferenceCount = objectWatcher.retainedObjectCount

    //大于0,则调用gcTrigger的runGc提醒垃圾回收器回收下
    if (retainedReferenceCount > 0) {
      gcTrigger.runGc()
      retainedReferenceCount = objectWatcher.retainedObjectCount
    }
    //这里再次检查下retainedReferenceCount,如果没有留置的对象,则直接返回,有则继续执行下面任务
    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
    ...
    //这里调用了dumpHeap方法去dump当前Heap状态
    dumpHeap(
      retainedReferenceCount = retainedReferenceCount,
      retry = true,
      reason = "$retainedReferenceCount retained objects, app is $visibility"
    )

}

我们进入dumpHeap方法中看下:

private fun dumpHeap(
    retainedReferenceCount: Int,
    retry: Boolean,
    reason: String
  ) {
    //步骤1:创建一个内存泄露的文件目录提供者
    val directoryProvider =
      InternalLeakCanary.createLeakDirectoryProvider(InternalLeakCanary.application)
    val heapDumpFile = directoryProvider.newHeapDumpFile()


    try {
        //步骤2:发送一个DumpingHeap的事件
      InternalLeakCanary.sendEvent(DumpingHeap(currentEventUniqueId!!))
      //步骤3:这里开始调用dumpHeap事件,传入heapDumpFile文件参数
      configProvider().heapDumper.dumpHeap(heapDumpFile)
        //步骤4:发送一个HeapDump的事件,这里会将dump信息依次在logcat,toast和通知栏通知,且内部进行dumpfile的分析
      InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
    } catch (throwable: Throwable) {
      ...
    }
  }
}

继续看sendEvent方法

fun sendEvent(event: Event) {
    for(listener in LeakCanary.config.eventListeners) {
      listener.onEvent(event)
    }
}

这里依次调用了LeakCanary.config.eventListeners中的onEvent方法 找到这个eventListeners:

val eventListeners: List<EventListener> = listOf(
  LogcatEventListener,
  ToastEventListener,
  LazyForwardingEventListener {
    if (InternalLeakCanary.formFactor == TV) TvEventListener else NotificationEventListener
  },
  when {
      RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath ->
        RemoteWorkManagerHeapAnalyzer
      WorkManagerHeapAnalyzer.validWorkManagerInClasspath -> WorkManagerHeapAnalyzer
      else -> BackgroundThreadHeapAnalyzer
  }
),

Listener包括: 1.LogcatEventListener :可知当sendEvent时Logcat会收到通知 2.ToastEventListener :可知当sendEvent时也会Toast出事件 3.如果是TV则还会有TvEventListener,其他则会有NotificationEventListener :这里是在通知栏提示事件 4.如果RemoteWorkManagerHeapAnalyzer.remoteLeakCanaryServiceInClasspath为true:则还会增加一个RemoteWorkManagerHeapAnalyzer来处理HeapDump 如果上面为false:那么查看WorkManagerHeapAnalyzer.validWorkManagerInClasspath是否为true,如果是则会增加一个WorkManagerHeapAnalyzer来处理HeapDump 如果上面为false:那么会使用一个BackgroundThreadHeapAnalyzer来处理HeapDump 4中会有一个处理请求,看当前应用版本: 我们查看默认的BackgroundThreadHeapAnalyzer来看看

object BackgroundThreadHeapAnalyzer : EventListener {

  internal val heapAnalyzerThreadHandler by lazy {
    val handlerThread = HandlerThread("HeapAnalyzer")
    handlerThread.start()
    Handler(handlerThread.looper)
  }

  override fun onEvent(event: Event) {
    if (event is HeapDump) {
      heapAnalyzerThreadHandler.post {
        val doneEvent = AndroidDebugHeapAnalyzer.runAnalysisBlocking(event) { event -> //关注点1
          InternalLeakCanary.sendEvent(event)
        }
        InternalLeakCanary.sendEvent(doneEvent)
      }
    }
  }
}

BackgroundThreadHeapAnalyzer内部使用一个HandlerThread来处理dumpHeap事件 onEvent中如果是HeapDump事件:则AndroidDebugHeapAnalyzer.runAnalysisBlocking(event)对dumpfile进行分析,并将分析结果回调给上层

来看关注点1:runAnalysisBlocking

fun runAnalysisBlocking(
    heapDumped: HeapDump,
    isCanceled: () -> Boolean = { false },
    progressEventListener: (HeapAnalysisProgress) -> Unit
  ): HeapAnalysisDone<*> {

    ...
    analyzeHeap(heapDumpFile, progressListener, isCanceled)
    ...
}

analyzeHeap内部就是通过之前保存的HeapDump文件进行分析,使用的是开源库shark进行分析,感兴趣的同学可以自行阅读这块代码

到这里我们已经分析了:

  • 1.应用启动给的时候使用Provider对Activity生命周期进行监听
  • 2.Activity在调用onDestroy的时候回调触发LeakCanary的事件监听,使用的是一个 ReferenceQueue进行监听
  • 3.如果内存中持有了未释放的Activity实例,则会调用自定义的GC处理器的runGc方法进行回收
  • 4.经过3回收后,还有对象未释放,则会在logcat,toast和通知栏提示dumpHeap操作,并在dumpHeap后对dump文件进行分析,使用的是shark开源库
  • 5.dump成功后以通知形式显示,并挂载了一个Intent,在点击通知的时候会执行这个挂载Intent,显示heap的状态信息

上面就是一个整个LeakCanary的工作过程

总结:

本文梳理了下LeakCanary基本使用流程和内存泄露以及内存检测的一些原理。有错误之处欢迎指正,喜欢的互相关注下,后期会推出更多Android体系课文章

相关文章
|
24天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
33 6
|
26天前
|
Java 开发工具 Android开发
Android与iOS开发环境搭建全解析####
本文深入探讨了Android与iOS两大移动操作系统的开发环境搭建流程,旨在为初学者及有一定基础的开发者提供详尽指南。我们将从开发工具的选择、环境配置到第一个简单应用的创建,一步步引导读者步入移动应用开发的殿堂。无论你是Android Studio的新手还是Xcode的探索者,本文都将为你扫清开发道路上的障碍,助你快速上手并享受跨平台移动开发的乐趣。 ####
|
26天前
|
监控 JavaScript Java
Node.js中内存泄漏的检测方法
检测内存泄漏需要综合运用多种方法,并结合实际的应用场景和代码特点进行分析。及时发现和解决内存泄漏问题,可以提高应用的稳定性和性能,避免潜在的风险和故障。同时,不断学习和掌握内存管理的知识,也是有效预防内存泄漏的重要途径。
121 52
|
19天前
|
存储 Linux API
深入探索Android系统架构:从内核到应用层的全面解析
本文旨在为读者提供一份详尽的Android系统架构分析,从底层的Linux内核到顶层的应用程序框架。我们将探讨Android系统的模块化设计、各层之间的交互机制以及它们如何共同协作以支持丰富多样的应用生态。通过本篇文章,开发者和爱好者可以更深入理解Android平台的工作原理,从而优化开发流程和提升应用性能。
|
15天前
|
监控 Java Android开发
深入探索Android系统的内存管理机制
本文旨在全面解析Android系统的内存管理机制,包括其工作原理、常见问题及其解决方案。通过对Android内存模型的深入分析,本文将帮助开发者更好地理解内存分配、回收以及优化策略,从而提高应用性能和用户体验。
|
19天前
|
Java 调度 Android开发
安卓与iOS开发中的线程管理差异解析
在移动应用开发的广阔天地中,安卓和iOS两大平台各自拥有独特的魅力。如同东西方文化的差异,它们在处理多线程任务时也展现出不同的哲学。本文将带你穿梭于这两个平台之间,比较它们在线程管理上的核心理念、实现方式及性能考量,助你成为跨平台的编程高手。
|
17天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
1月前
|
监控 Java Android开发
深入探讨Android系统的内存管理机制
本文将深入分析Android系统的内存管理机制,包括其内存分配、回收策略以及常见的内存泄漏问题。通过对这些方面的详细讨论,读者可以更好地理解Android系统如何高效地管理内存资源,从而提高应用程序的性能和稳定性。
66 16
|
1月前
|
Android开发 开发者
Android性能优化——内存管理的艺术
Android性能优化——内存管理的艺术
|
25天前
|
开发框架 Dart Android开发
安卓与iOS的跨平台开发:Flutter框架深度解析
在移动应用开发的海洋中,Flutter作为一艘灵活的帆船,正引领着开发者们驶向跨平台开发的新纪元。本文将揭开Flutter神秘的面纱,从其架构到核心特性,再到实际应用案例,我们将一同探索这个由谷歌打造的开源UI工具包如何让安卓与iOS应用开发变得更加高效而统一。你将看到,借助Flutter,打造精美、高性能的应用不再是难题,而是变成了一场创造性的旅程。

推荐镜像

更多
下一篇
DataWorks