参考LeakCanary,100行代码写出自己的内存泄露监测工具

1,268 阅读4分钟

前言

核心代码其实100行不到,包教包会,学不会赔女盆友 ,赔男盆友吗?赔赔赔。由于小本经营,大家请高抬贵手。当然这只是一个简易版的,写完了,你就已经对LeakCanary了解70%了。对付面试已经足以,面试一般就问这个核心吧。要是面试官问到其他的了,你就说文章里面没有有教.....

流程图

我们先看一下流程图吧!接着我们按照流程图的方式编写代码。

精心准备一个内存泄漏例子

精心准备一个可食用内存泄漏栗子,你可以烤可以煮,这里你可以自由发挥,自由发挥,自由发挥。我就用一个static list,来引用一个activity, 新建一个Activity,在里面onCreate添加一行,ActivityManage.get().add(this)。如下

class TestLeakActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_test_leak)
        ActivityManage.get().add(this)
    }

注册全局的activity生命监听

首先注册一个全局的activity生命监听,监听onActivityDestroyed,等待触发onActivityDestroyed了,五秒后去观察。(让 gc 飞一会),具体代码如下。

 application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
            override fun onActivityStarted(activity: Activity) {}
            override fun onActivityResumed(activity: Activity) {}
            override fun onActivityPaused(activity: Activity) {}
            override fun onActivityStopped(activity: Activity) {}
            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
            override fun onActivityDestroyed(activity: Activity) {
                val key = UUID.randomUUID().toString()
                val weakReference = KeyedWeakReference(activity, key, "描述:" + activity.localClassName, SystemClock.uptimeMillis(), queue)
                retainedKeys[key] = weakReference
                //五秒后去观察,让 gc 飞一会, 这块应该用线程池,简单起见,不写了。
                Handler().postDelayed(
                    { watchActivity(application, weakReference) },
                    5000
                )
            }
        })

判断监听引用对象是否可达

这里需要了解一下弱引用(WeakReference)和 ReferenceQueue。这里简单了解一下,最好是看完文章去详细了解一下。

弱引用(WeakReference)

弱引用的对象,不管当前内存空间是否充足,一但GC,都会回收它的内存。

ReferenceQueue

引用队列,在检测到适当的可达性更改后,GC回收器会将已注册的引用对象添加到其中。这个时候内存回收完毕了。

函数功能

removeWeaklyReachableObjects(),这快跟LeakCanary是一样的,直接搬了过来。引用对象不入队就说明泄漏了。

runGC(),主要是调用GC回收内存。但这里是否执行gc回收内存操作,由虚拟机决定。我们这里只是建议。(不知道这里说得对不对。望大佬指正!)

    private fun removeWeaklyReachableObjects() {
        //如果gc回收了这个对象,这个引用对象会被放到queue中。
        var ref: KeyedWeakReference?
        do {
            ref = queue.poll() as KeyedWeakReference?
            if (ref != null) {
                retainedKeys.remove(ref.key)
            }
        } while (ref != null)
    }

    private fun runGC() {
        Runtime.getRuntime().gc()
        enqueueReferences()
        System.runFinalization()
    }

dump一份堆内存快照

这里简单粗暴,直接调用Android系统的dumpHprofData()。

Debug.dumpHprofData(heapDumpFile.absolutePath)

分析内存泄漏结果

旧版是使用haha库去分析的,新版的重写的堆内存分析,就是shark,分析速度更快了。这里直接调用 LeakCanary中的 shark api,需要的可以去官网了解一下,可官网api不是最新的。写到这里我总感觉我的标题需要改成,使用LeakCanary api 写自己的.......

shark api

  val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
        //分析堆内存快照
        val analysis = heapAnalyzer.analyze(
            heapDumpFile = heapDumpFile,
            leakingObjectFinder = KeyedWeakReferenceFinder,
            referenceMatchers = AndroidReferenceMatchers.appDefaults,
            computeRetainedHeapSize = true,
            objectInspectors = AndroidObjectInspectors.appDefaults,
            metadataExtractor = AndroidMetadataExtractor
        )

        Log.d(TAG, "\u200B\n分析结果:\u200B\n$analysis");

分析结果Log

非完整log,只是前面一部分而已。带“~~~”下划线的引用可能是造成内存泄漏的原因,请仔细看看。

    ====================================
    HEAP ANALYSIS RESULT
    ====================================
    1 APPLICATION LEAKS
    
    References underlined with "~~~" are likely causes.
    Learn more at https://squ.re/leaks.
    
    161537 bytes retained by leaking objects
    Signature: daf62034cd9a555da3c8ed4d565e75d54ca163
    ┬───
    │ GC Root: System class
    │
    ├─ com.memory.monitor.ActivityManage class
    │    Leaking: NO (a class is never leaking)
    │    ↓ static ActivityManage.list//看这里
    │                            ~~~~
    ├─ java.util.ArrayList instance
    │    Leaking: UNKNOWN
    │    Retaining 161597 bytes in 1384 objects
    │    ↓ ArrayList.elementData//看这里
    │                ~~~~~~~~~~~
    ├─ java.lang.Object[] array
    │    Leaking: UNKNOWN
    │    Retaining 161577 bytes in 1383 objects
    │    ↓ Object[].[0]//看这里
    │               ~~~
    ╰→ com.memory.monitor.TestLeakActivity instance

核心代码

核心代码这里,demo看下面的链接。

class MonitorMemory {
    private val TAG = "MonitorMemory"
    private val queue = ReferenceQueue<Any>()
    private val retainedKeys = mutableMapOf<String, KeyedWeakReference>()

    /**
     *这块应该用线程池,简单起见,没写。
     * KeyedWeakReference 弱引用
     */
    private fun watchActivity(application: Application, weakReference: KeyedWeakReference) {
        //判断引用对象是否已达
        removeWeaklyReachableObjects()
        //运行GC
        runGC()
        removeWeaklyReachableObjects()
        //2次尝试查看这对象是否已被回收,回收了就从retainedKeys移除,否则证明这个对象泄漏了。
        if (retainedKeys.contains(weakReference.key)) {
            //activity 泄漏
            Log.d(TAG, "-----泄漏:activity leak----$weakReference.description::::: Activity${weakReference.get()}")
            val storageDirectory = File(application.cacheDir.toString() + "/watchActivity")
            if (!storageDirectory.exists()) {
                storageDirectory.mkdir()
            }
            val heapDumpFile = File(storageDirectory, UUID.randomUUID().toString() + ".hprof")
            try {
                //dump 一份堆内存快照,这里比较慢。
                Debug.dumpHprofData(heapDumpFile.absolutePath)
                val heapAnalyzer = HeapAnalyzer(OnAnalysisProgressListener.NO_OP)
                //分析堆内存快照
                val analysis = heapAnalyzer.analyze(
                    heapDumpFile = heapDumpFile,
                    leakingObjectFinder = KeyedWeakReferenceFinder,
                    referenceMatchers = AndroidReferenceMatchers.appDefaults,
                    computeRetainedHeapSize = true,
                    objectInspectors = AndroidObjectInspectors.appDefaults,
                    metadataExtractor = AndroidMetadataExtractor
                )

                Log.d(TAG, "\u200B\n分析结果:\u200B\n$analysis");
            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
    }

    private fun enqueueReferences() {
        try {
            Thread.sleep(100)
        } catch (e: InterruptedException) {
        }
    }

    private fun removeWeaklyReachableObjects() {
        //如果gc回收了这个对象,这个引用对象会被放到queue中。
        var ref: KeyedWeakReference?
        do {
            ref = queue.poll() as KeyedWeakReference?
            if (ref != null) {
                retainedKeys.remove(ref.key)
            }
        } while (ref != null)
    }

    private fun runGC() {
        Runtime.getRuntime().gc()
        enqueueReferences()
        System.runFinalization()
    }

    fun initRegisterActivityLifecycleCallbacks(application: Application) {
        application.registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
            override fun onActivityStarted(activity: Activity) {}
            override fun onActivityResumed(activity: Activity) {}
            override fun onActivityPaused(activity: Activity) {}
            override fun onActivityStopped(activity: Activity) {}
            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
            override fun onActivityDestroyed(activity: Activity) {
                Log.d(TAG, "Destroyed Activity:$activity.javaClass.name")
                val key = UUID.randomUUID().toString()
                val weakReference = KeyedWeakReference(activity, key, "描述:" + activity.localClassName, SystemClock.uptimeMillis(), queue)
                retainedKeys[key] = weakReference
                //五秒后去观察,让 gc 飞一会, 这块应该用线程池,简单起见,不写了。
                Handler().postDelayed(
                    { watchActivity(application, weakReference) },
                    5000
                )
            }
        })
    }
}

demo代码

完整的代码在这里

总结

你看这个像不像你欠我的赞。

总结一下流程: 大概就是监听activity的Destroy,然后等待五秒去观察它,如果发现泄露,dump堆的内存快照,接着分析堆内存,找出GC引用路径。更详细的请看掘金一些大佬的源码分析或者看源码吧。

有点小感冒,希望感冒早点好,接着,谢谢大家。你的赞就像冬日暖阳,温暖心窝。