android Leakcanary/Studio Profiler/MAT 处理内存问题(泄漏和Big超大内存对象)

1.Android Leakcanary 使用

1.1在项目中依赖

根据项目实际情况,选择对应的版本,因项目中暂时不支持android x,因此这里选择leakcanary 2.2版本。

    //放开下边的代码,即可在debug包中使用leakcanary 检测内存泄露
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2'

1.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 classLeaking: 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
    ====================================

解读以上关键信息:

  • 泄漏对象锁造成内存占用是 1174715 bytes,即1.12M。
  • Leaking: YES 是代表此对象存在泄漏
  • gc 调用链:是BrowserActivity被ConnectivityManager的mContext持有,而ConnectivityManager 类对象又是静态的,从而导致BrowserActivity无法被释放,造成内存泄漏。
  • heap dump file(即hprof 内存快照文件)存储:在/storage/emulated/0/Download/leakcanary-com.xxx.miniworld目录下。

当然除开logcat外,当LeakCanary检测到内存泄露以后会弹出通知,我们点击通知即可查看泄漏信息,这里不展开多介绍。

2.Android Studio 查看 Hprof 堆内存快照

通过导出内存泄漏的hprof文件或者通过studio 捕捉的hprof 文件。

2.1Hprof文件包含的信息

  • 您的应用分配了哪些类型的对象,以及每种对象有多少。
  • 每个对象当前使用多少内存。
  • 在代码中的什么位置保持着对每个对象的引用。
  • 对象所分配到的调用堆栈

2.2 Studio 打开Hprof文件

加载hprof文件的方式:studio的底部菜单Profiler->SESSIONS->+号->load from a file->准备好的hprof文件

先来,解读Hprof 文件的工具界面选项,更好的理解内存信息。

第一个选项,查看指定的堆

在这里插入图片描述

  • app heap: 当前app 使用的堆
  • Image heap: 系统启动映像,包含启动期间预加载的类。此处的分配确保绝不会移动或消失
  • zgote heap: 写时复制堆,其中的应用进程是从 Android 系统中派生的

通常只要关注app heap 和jni heap 便可。

第二个选项,选择分配

在这里插入图片描述

  • Arrange by class:根据类名称对所有分配进行分组。这是默认值
  • Arrange by package:根据软件包名称对所有分配进行分组
  • Arrange by callstack:将所有分配分组到其对应的调用堆栈

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

第三个选项, 选择展示的类

在这里插入图片描述

  • show all classes: 展示全部类
  • show activity/fragment leak: 展示内存泄漏的activity/fragment
  • show project class: 展示项目中有关的类。

Hprof 中类列表中解读

在这里插入图片描述

  • Class Name: 类名
  • Allocations: 该类全部对象个数(通过 malloc() 或 new 运算符分配的对象数),若出现许多个,可考虑内存泄漏问题
  • Native Size: 该类全部对象在native 层占用的内存(以byte为单位)
  • Shallow Size: 该类全部对象在Java 层占用的内存,不包含其引用对象的内存(以byte为单位)
  • Retained Size:该类全部对象占用的全部内存(包含java和native层,包含其引用对象的内存)

比如,Bitmap 类,该对象是74个 ,在native 层占用是5043106,在java层占用是3700,Bitmap对象占用全部内存是5049374。

2.3查看Activity/Fragment的内存泄漏

先来了解下,在Android中可能引起内存泄漏的原因

  • 长时间引用Activity/Fragment/View/Drawable /其他对象,导致Fragment/Activity被持有无法被释放。
  • Activity中非静态内部类(Runnable),即Activity非静态内部类等同于隐式外部类,间接持有Activity。
  • 对象被缓存/持有时间超出使用范围时间,比如单例中长时间缓存超大对象。

接下来,先点击第三个选项,选择show activity/fragment leak。接着,选择其中的某个activity 双击,会出现Instance List 项。选择其中的一个Instance 双击,展开该Instance Detail 详细情况。接着点击References ,勾选 show nearest gc root only,可以看到Activity/Fragment的调用链。如下图所示:
在这里插入图片描述

先解读下Instance View/Detail 视图

  • Depth 是指GC根节点到所选实例的最短路径的深度,简单来说就是调用链的节数。
  • Native Size: 此对象在native 层占用的内存(以byte为单位)
  • Shallow Size: 此对象在Java 层占用的内存,不包含其引用对象的内存(以byte为单位)
  • Retained Size:此对象占用的全部内存(包含java和native层,包含其引用对象的内存)

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 ,可以跳转到相匹配的源码中。

2.4查看其他Java 对象的占用可能导致的泄漏

看下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 官方的捕获堆转储

3.使用mat分析hprof 文件

前期准备

根据电脑和安装的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。如下图所示:
在这里插入图片描述

3.1 Leak Suspects 查看内存泄漏情况

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

在这里插入图片描述
点击Leak Suspects ,查看内存泄漏情况:
在这里插入图片描述

从上面可知,存在Class 和String 占用超大内存。先来看下String问题,点击detail 进一步查看详情。

先来解读下几个关键名词

  • Shallow Heap: 该对象占用的内存,不包含其引用的对象内存。
  • Retain Heap: 释放该对象引用(包含其引用对象)后,可以减少的内存。

List Object项解读

  • with outgoing references是指 这个对象持有了哪个对象
  • with incoming references是指 哪个对象持有了该对象

先来看下哪些对象持有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)

3.2通过Histogram 搜索查找匹配

搜索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属性被持有。

该问题和上面问题是同一个,解决方式也是一样的。

通常也可以检索包名,去查看下业务层中哪些对象可能存在泄漏,这里便不做过多介绍

3.3.Dominator Tree(支配树) 查看大内存对象

在此视图中列出了每个对象(Object Instance)与其引用关系的树状结构,同时包含了占用内存的大小和百分比,如下图所示:

在这里插入图片描述

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

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

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

猜你喜欢

转载自blog.csdn.net/hexingen/article/details/126645702