根据项目实际情况,选择对应的版本,因项目中暂时不支持android x,因此这里选择leakcanary 2.2版本。
//放开下边的代码,即可在debug包中使用leakcanary 检测内存泄露
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'
在logcat 中,可以查看到泄漏的信息:
2022-08-31 11:14:07.757 32255-10246/com.xxx.miniworld D/LeakCanary: ====================================
//....
1174715 bytes retained by leaking objects
Signature: 4bfb4367c5b0b4b1a7aeb7ff3b45e02cba6a3d87
┬───
│ GC Root: System class
│
├─ android.net.ConnectivityManager class
│ Leaking: NO (a class is never leaking)
│ ↓ static ConnectivityManager.sInstance
│ ~~~~~~~~~
├─ android.net.ConnectivityManager instance
│ Leaking: UNKNOWN
│ ↓ ConnectivityManager.mContext
│ ~~~~~~~~
╰→ org.appplay.minibrowser.BrowserActivity instance
Leaking: YES (ObjectWatcher was watching this because org.appplay.minibrowser.BrowserActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
key = 2354b5c5-3110-4b61-986b-99e7012bf89a
watchDurationMillis = 107085
retainedDurationMillis = 102084
====================================
//...
Build.VERSION.SDK_INT: 31
Build.MANUFACTURER: OPPO
LeakCanary version: 2.2
App process name: com.xxx.miniworld
Analysis duration: 20828 ms
Heap dump file path: /storage/emulated/0/Download/leakcanary-com.xxx.miniworld/2022-08-31_11-13-44_012.hprof
Heap dump timestamp: 1661915647752
====================================
解读以上关键信息:
Leaking: YES 是代表此对象存在泄漏BrowserActivity被ConnectivityManager的mContext持有,而ConnectivityManager 类对象又是静态的,从而导致BrowserActivity无法被释放,造成内存泄漏。/storage/emulated/0/Download/leakcanary-com.xxx.miniworld目录下。当然除开logcat外,当LeakCanary检测到内存泄露以后会弹出通知,我们点击通知即可查看泄漏信息,这里不展开多介绍。
通过导出内存泄漏的hprof文件或者通过studio 捕捉的hprof 文件。
加载hprof文件的方式:studio的底部菜单Profiler->SESSIONS->+号->load from a file->准备好的hprof文件。
先来,解读Hprof 文件的工具界面选项,更好的理解内存信息。
第一个选项,查看指定的堆:

通常只要关注app heap 和jni heap 便可。
第二个选项,选择分配:

通常 , 选择 “Arrange by package” 选项 , 这样就可以比较有条理的查找内存中有哪些对象。
第三个选项, 选择展示的类:

Hprof 中类列表中解读:

比如,Bitmap 类,该对象是74个 ,在native 层占用是5043106,在java层占用是3700,Bitmap对象占用全部内存是5049374。
先来了解下,在Android中可能引起内存泄漏的原因:
接下来,先点击第三个选项,选择show activity/fragment leak。接着,选择其中的某个activity 双击,会出现Instance List 项。选择其中的一个Instance 双击,展开该Instance Detail 详细情况。接着点击References ,勾选 show nearest gc root only,可以看到Activity/Fragment的调用链。如下图所示:

先解读下Instance View/Detail 视图:
Shallow and retained sizes的进一步解读

在上图中,obj1 的Retained Size是 (obj1+obj2+obj4)总和shallow size。更多详细信息,请阅读Shallow and retained sizes
注意点:这里的Retained Size 与类列表中的Retained Size是不同的,一个是单独类对象的表示,一个是全部类对象的总和表示。
回归到内存泄漏的话题,根据上图调用链分析可知: 当 BrowserActivity被销毁时,BrowserActivity是作为ConnectivityManager的context属性被持有,没有被完全释放,从而导致内存泄漏。
解决方式:通过Application来获取各种system severce 对象。
/**
* 处理leakcanary 检测到通过Activity创建system service, 容易被持有未释放
* @param context
* @return
*/
private static Context getSafeContext(Context context){
return context instanceof Application?context:context.getApplicationContext();
}
除此之外,右键 jump to source ,可以跳转到相匹配的源码中。
看下Bitmap类的堆信息,选择Retained Size 倒叙发现最大的Bitmap对象占用内存362947(即354k),调用链是在某个Activity中的layout对象(间接持有bitmap),没有释放,如下所示:

梳理业务发现,该layout 是在加载webview时才嵌在Activity中。明显该Layout对象的持有时间超过需要展示时间。
因此解决办法:在展示其他业务时,释放该layout对象。
看下String类的堆信息,选择Retained Size 倒叙发现最大的String对象 占用内存168152(即164k),调用链是在SharedPreferences中的hashMap 中缓存,没有被释放,如下图所示:

解决方法:使用腾讯的mmkv 替代android SharedPreferences或者采用file文件存储。
其他的类对象占用,也是一壶画瓢,这里便不再详细介绍。
更多有关资料,参考Android 官方的捕获堆转储
前期准备
根据电脑和安装的jdk版本,选择对应的mat版本。下载地址:https://www.eclipse.org/mat/previousReleases.php
studio 或者Leakcanary的hprof文件是无法被mat 打开的。因此,需要在通过android_sdk/platform-tools/hprof-conv执行转换的命令hprof-conv heap-original.hprof heap-converted.hprof。如下图所示:

通过mat 打开转好后的hprof 内存快照文件后,如下图所示:

点击Leak Suspects ,查看内存泄漏情况:

从上面可知,存在Class 和String 占用超大内存。先来看下String问题,点击detail 进一步查看详情。
先来解读下几个关键名词:
List Object项解读:
先来看下哪些对象持有String对象,右键--> List objects-->with incoming references, 如下图所示:

在list_object 视图中,可以清楚看到string 被哪些对象持有。
接下来,查看gc 调用路径:
选择其中一个,右键 path to GCRoot -> exclude all phantom/weak/soft etc. references(去除所有的虚引用,弱引用,软引用) 就会得到调用该类的跟节点gc root。

接下来进入path2gc视图中:

从上图可知,该string 对象被sharedPreferences 中的Hasmap 持有,sharedPreferences被某个类和某个数组持有导致,该string 一直无法被释放。
像这种问题,暂时无法解决,只能先跳过。List_object视图中剩余的string 对象,也是一壶画瓢,这里便不再详细介绍。
总结: 查看类对象中内存调用的步骤,先选择类对象->其次,哪个对象持有了该对象(右键--> List objects-->with incoming references)->最后,gc 调用路径( path to GCRoot -> exclude all phantom/weak/soft etc. references)
搜索Activity 关键字,检索如下所示:

当前游戏已经销毁了BrowserActivity,但内存快照中仍然存在。很明显,该Activity发生泄漏,查看其gc 调用路径(path to GCRoot -> exclude all phantom/weak/soft etc. references),如下图所示:

发现,BrowserActivity是作为ConnectivityManager的context属性被持有,没有被完全释放,这个和用上面的Studio Profiler 分析一样,按照上面的处理方式便可解决。
除此之外,还可以检索Fragment中有没有存在泄漏情况。
搜索Bitmap 关键字,检索如下图所示:

查看BitmapDrawable 对象被哪些对象持有,如下图所示:

选择其中的一个BitmapDrawable 的gc 调用路径,如下图所示:

从上图可知BitmapDrawable对象被BrowserActivity中iBrowser 对象持有,而BrowserActivity被ConnectivityManager的context属性被持有。
该问题和上面问题是同一个,解决方式也是一样的。
通常也可以检索包名,去查看下业务层中哪些对象可能存在泄漏,这里便不做过多介绍。
在此视图中列出了每个对象(Object Instance)与其引用关系的树状结构,同时包含了占用内存的大小和百分比,如下图所示:

通过Shallow Heap 或者Percentage(内存百分比)进行排序,很容易筛选出占用内存较大的对象,如下所示:

当然也可以选择排序方式,如下图所示:

接下来,也是选择占用内存大的对象,查看被哪些对象引用或者引用了哪些对象,查看gc 调用路径,去分析如何优化。