• 内存泄露根本原因
  • 内存泄露常见情况
  • 内存泄露定位方案
  • 最近碰到的示例
  • 常见内存泄露解决方案

该章节需配合 Java GC 机制 一起看

0x0001. 内存泄露根本原因

  1. GCRoot 对象长期持有其他已经无用的对象(本该被回收,却未被回收)。通俗的说就是 生命周期较长的对象(如 static 类)持有生命周期较短的对象的引用(某个 Activity)

  2. GCRoot 对象包括

    1. 方法区静态属性 ,常量(多数伴随整个虚拟机的生命周期)

    2. 栈帧中本地变量表中的引用对象 (即当前线程正在运行的代码持有的对象)

    3. JNI 本地方法引用的对象 (native 区引用的对象)

0x0002. 内存泄露常见情况

  1. 静态对象

    • static activity

    • static view

  2. activity/fragment 销毁,结束耗时任务(异步网络请求)

  3. inner class 存在异步等情况

  4. SingleInstance 持有局部生命周期 Activity

  5. 资源类 cursor 没有关闭

补充,lamda 表达式是匿名内部类

0x0003. 内存泄露定位方案

  1. 静态分析

    Lint,注意看 Warning ,会针对 handler 等,进行提示

  2. 动态分析

    • leakCanary
    • adb shell dumpsys meminfo [PackageName]
    • Android Profiler (见下方内存泄露实例详解)

    以上三者,根据个人经验可以结合起来使用

    1. leakCanary 可以用于快速定位内存泄露问题
    2. 当面对 leakCanary 无法直接确定内存泄露的时,可以用 adb shell dumpsys 进行辅助确认泄露 activity
    3. 根据前两者确定的 activity 配合 Android Profiler GC 后进行分析,查看没有回收的对象的引用链,即可定位内存泄漏位置

0x0004. 最近碰到的示例

​ 背景介绍: 一个商品详情页,有内存泄露 。

  1. LeakCanary 报错截图,发现是一个 UI 消息队列上的一条消息泄露了,无法直接定位具体代码。
    img

  2. 此时,利用 adb shell dumpsys meminfo,然后多次进出 SpuDetailActivity ,验证确认是 SpuDetailActivity 存在泄露。

➜ adb shell dumpsys meminfo com.leixun.taofen8.haoda
Applications Memory Usage (in Kilobytes):
Uptime: 693212294 Realtime: 787382396

** MEMINFO in pid 14754 [com.leixun.taofen8.haoda] **
                   Pss  Private  Private  SwapPss     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    74230    74108        0      230    98688    86999    11688
  Dalvik Heap     6442     5952        0       25    11519     5760     5759
 Dalvik Other     2683     2680        0        4
        Stack       84       84        0        0
       Ashmem        2        0        0        0
      Gfx dev    28316    28316        0        0
    Other dev       28        0       28        0
     .so mmap    15415      280     7720        7
    .jar mmap     3762        0     1844        0
    .apk mmap      432        0      132        0
    .ttf mmap      107        0       48        0
    .dex mmap    19419    19344       32        0
    .oat mmap      621        0        4        0
    .art mmap     4696     4108        0        9
   Other mmap     5421      476     3348        0
      Unknown     2842     2808        0        6
        TOTAL   164781   138156    13156      281   110207    92759    17447

 App Summary
                       Pss(KB)
                        ------
           Java Heap:    10060
         Native Heap:    74108
                Code:    29404
               Stack:       84
            Graphics:    28316
       Private Other:     9340
              System:    13469

               TOTAL:   164781       TOTAL SWAP PSS:      281

 Objects
               Views:      341         ViewRootImpl:        2
         AppContexts:        7           Activities:        2
              Assets:       18        AssetManagers:        0
       Local Binders:       25        Proxy Binders:       37
       Parcel memory:       11         Parcel count:       48
    Death Recipients:        2      OpenSSL Sockets:       13
            WebViews:        0

 SQL
         MEMORY_USED:        0
  PAGECACHE_OVERFLOW:        0          MALLOC_SIZE:        0
 
 Asset Allocations
      : 162K
  1. 由于个人经验丰富,直接借助 LeakCanaryadb shell dumpsys meminfo 配合几次试错,定位到问题

    整个推导流程:

    UI -> Handler -> MutableLiveData -> MutableLiveData.observe()

    UI 消息发送,自己代码中没有 handler 之类的,用了只有 MutableLiveData 来更新 View,快速定位两个 LiveData Observe 的代码,配合 adb shell dumpsys meminfo 直接验证注释掉几个方法,看 Activity 数量,快速定位出问题出在 spuDetailBinding.hdPtrList.refreshComplete(), 该第三方库调用错误时会出现内存泄露。

    ...       
      viewModel.loadSuccess.observe(this, aBoolean -> {
          	if (aBoolean) {
          		adapter.submitList(viewModel.itemViewModels);
          	}
          	hideProgressDialog();
          	spuDetailBinding.hdPtrList.refreshComplete();
       });
       viewModel.loadRelatedSuccess.observe(this, aBoolean -> {
           adapter.submitList(viewModel.itemViewModels);
       });
     ...
    
  2. Android Profiler 内存进行举例性分析,定位问题所在

使用 profiler 的时候,最好把 leakCanary 去掉,影响手动 GC,而且还会引起卡顿

不熟练 profiler,可以配合 Android Profiler 官方使用指南食用

  1. 连接 profiler
    😀

  2. 手动触发GC
    😁

  3. 选中内存区域,找到泄露的 Activity
    😂

  4. 根据之前 LeakCanary 报错,直接定位 MessageQueue 持有的对象
    🤣

0x0005. 常见内存泄露解决方案

  1. WeakReference 进行包裹
  2. 用谷歌官方提供的生命周期组件,类似 MutableLiveData 之类的