前面我们已经完成了生命周期监控并且可以通过ReferenceQueue和WeakHashMap的比较确定哪些对象发生泄漏了,那么接下来需要考虑的就是如何确定这个对象是被谁持有导致泄漏的呢?

内存泄漏一文中可知,当我们使用Android Studio或MAT分析内存泄漏问题时,通常首先是需要抓取发生内存泄漏时的hprof文件,随后使用分析软件打开该文件,排查泄漏对象被持有的GC Roots,进而解决内存泄漏问题。

同样的,在上述场景中,我们确定了发生内存泄漏的对象,那么接下来自然也是抓取此时的hprof文件,随后使用某些工具库解析该文件,进而查找发生内存泄漏对象的持有关系,找到GC Roots即可。

hprof文件

文件后缀hprof是英文Heap Profile(内存快照)的缩写,该文件是由Java虚拟机(JVM)生成的一种二进制文件,用于记录Java程序运行时内存的使用情况、对象实例分布、方法调用情况、线程统计信息等。

hprof文件通常用于分析Java程序的性能问题,查找内存泄露、对象实例分布情况、方法调用次数等,从而提高程序的性能和稳定性。

hprof文件组成

相对于Java程序生成的hprof文件而言,Android程序生成的hprof文件新增了部分TAG,Android上的hprof文件由文件头和数据区组成,其中文件头占32位,主要包含格式名和版本号,标识符大小,高位时间戳等信息,数据区由若干个Record数据组成,每个Record数据中包含Tag,数据长度,数据内容等信息,如下图所示(更多hprof文件格式可以参考:hprof文件说明):

从LeakCanary看如何生成内存快照-LMLPHP

生成hprof文件

在常用开发工具中都可以生成hprof文件,如Android Studio Profiler,DDMS,eclipse,命令行工具等。通过ADB生成hprof文件的命令如下所示:

adb shell am dumpheap pid/pkgName fileName.hprof

以包名com.poseidon.wanandroid为例,导出该应用当前内存快照的操作如下图所示:

从LeakCanary看如何生成内存快照-LMLPHP

从LeakCanary看如何生成内存快照-LMLPHP

当然也可以通过代码来生成hprof文件,在android.os.Debug类的dumpHprofData方法即可生成hprof文件,函数声明如下:

从LeakCanary看如何生成内存快照-LMLPHP

这两个函数的区别在于:只包含fileName参数的函数是将hprof数据写入fileName对应的文件中,而后者是写入fd参数中,后者主要用于支撑前面提到的shell命令。

接前文,我们已经知道了某个对象的销毁时机,并且知道可以通过ReferenceQueue检查其是否回收,那么检查回收的机制就可以设计成在对象销毁时触发延时任务去比对ReferenceQueue和WeakHashMap,如果比对后对象的key仍在HashMap中,则发生了内存泄漏,调用Debug.dumpHprofData方法生成hprof文件。

修改ObjectWatcher代码如下所示:

public void watch(Object object) {
    removeWeaklyReachableObjects();
    String key = UUID.randomUUID().toString();
    Log.d(TAG, "watch object:" + object + ",key:" + key);
    KeyedWeakReference weakReference = new KeyedWeakReference(key, object, mReferenceQueue);
    mReferences.put(key, weakReference);

    Handler mainHandler = new Handler(Looper.getMainLooper());
    mainHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            removeWeaklyReachableObjects();
            KeyedWeakReference keyedWeakReference = mReferences.get(key);
            if (keyedWeakReference != null) {
                // key对应的object可能发生内存泄漏,dump内存堆栈
                Log.d(TAG, "keyedReference:" + keyedWeakReference.toString());
                try {
                    Debug.dumpHprofData("/sdcard/Download/tmp.hprof");
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }
    }, 5000);
}

private void removeWeaklyReachableObjects() {
    KeyedWeakReference keyedWeakReference = null;
    do {
        keyedWeakReference = (KeyedWeakReference) mReferenceQueue.poll();
        Log.d(TAG, "keyedWeakReference:" + keyedWeakReference);
        if (keyedWeakReference != null) {
            mReferences.remove(keyedWeakReference.getKey());
            Log.d(TAG, "object has been destroyed:" + keyedWeakReference.toString());
        }
    } while (keyedWeakReference != null);
}

在MainActivity制造经典的Handler内存泄漏场景,可以看到MainActivity泄漏确实被监控到了,如下图所示:

从LeakCanary看如何生成内存快照-LMLPHP

同时在sdcard/Download目录中也生成了prof文件,如下所示:

从LeakCanary看如何生成内存快照-LMLPHP

使用AS打开,可以看到MainActivity的GC Roots持有路径,根据持有路径就可以分析泄漏问题并处理。

从LeakCanary看如何生成内存快照-LMLPHP

08-23 15:13