生产力

Android 内存泄漏定位与分析

2020-03-04  本文已影响0人  leilifengxingmw

Android Studio版本:3.5.1
测试机: HUAWEI LLD-AL00(华为荣耀9青春版)Android版本9。从手机也可以看出来作者也是囊中羞涩啊,哈哈。

推荐先阅读google的官方文章使用 Memory Profiler 查看 Java 堆和内存分配

然后啰嗦一下堆转储信息怎么看。


memory-profiler-dump_2x.png

在类列表中,您可以查看以下信息:

Native Size,Shallow Size,Retained Size这几组数据分别意味着什么呢?通过一个例子来说明。

我们用下图来表示某段 Heap Dump 记录的应用内存状态。注意红色的节点,在这个示例中,这个节点所代表的对象从我们的工程中引用了 Native 对象:

Heap Dump.png

这种情况不太常见,但在 Android 8.0 之后,使用 Bitmap 便可能产生此类情景,因为 Bitmap 会把像素信息存储在原生内存中来减少 JVM 的内存压力。

Shallow Size:这列数据其实非常简单,就是对象本身消耗的内存大小,在上图中,即为红色节点自身所占内存(以字节为单位)。

Native Size:同样也很简单,它是类对象所引用的 Native 对象 (蓝色节点) 所消耗的内存大小(以字节为单位)。

Native Size.png

Retained Size:稍复杂些,它是下图中所有橙色节点的大小(以字节为单位)。

Retained Size.png

由于一旦删除红色节点,其余的橙色节点都将无法被访问,这时候它们就会被 GC 回收掉。从这个角度上讲,它们是被红色节点所持有的,因此被命名为 "Retained Size"。

注意:默认情况下,此列表按 Retained Size 列排序。要按其他列中的值排序,请点击该列的标题。即我们可以点击Allocations或者Native Size或者Shallow Size或者Retained Size进行排序。多次点击某个标题可以选择排序方式。比如说递增排序或者递增排序。

关于这几列值怎么看,举一个具体的例子。

class ListItem40MClass {

    // 40MB
    // 1024 * 1024 * 40 = 41943040
    var content = ByteArray(1024 * 1024 * 40)

    init {
        for (i in content.indices) {
            content[i] = 1
        }
    }

    var next: ListItem40MClass? = null

}

我们定义一个类,是单链表结构。这个类中有一个40MB的字节数组。40MB计算出来就是41943040。

1024 * 1024 * 40 = 41943040
1024 * 1024 * 40 * 3 = 125829120

class ThirdActivity : AppCompatActivity() {
    
    //head
    private var head: ListItem40MClass? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_third)
    }

    //点击3次
    fun onClick(view: View) {
        when (view.id) {
            R.id.btnAddNode -> {
                addNode()
            }
        }
    }

    private fun addNode() {
        if (head == null) {
            head = ListItem40MClass()
        } else {
            var tmp = head
            while (tmp?.next != null) {
                tmp = tmp.next
            }
            tmp?.next = ListItem40MClass()
        }
    }
}

我们点击按钮3次,添加3个ListItem40MClass对象。添加完毕以后,点击一下Dump Java Heap按钮,等待Android Studio内置分析工具分析。结果如下。

step4.png

Allocations这一列我们可以看到有3个实例对象,没问题。
Native Size这一列忽略,这里没有涉及到Native相关的内存分配。
Shallow Size这一列和Retained Size感觉都不太对啊。先别忙,我们点击ListItem40MClass看看 Instance View面板。


setp6.png

我们先看Depth这一列,我们可以知道Depth为4的这个对象就是我们的head。Depth为6的这个对象就是链表中最后一个对象。点击这个实例查看详细信息。


step7.png

我们可以看到链表中最后一个对象的Shallow Sizes是16,说明ListItem40MClass 对象本身只占用16字节,Retained Size值是41943056(16 + 41943040)。而链表中倒数第二个对象的Retained Size是83886112是41943056的两倍。说明在统计Retained Size值的时候不仅统计了对象自身的大小,还加入了引用的对象的大小。也可以看到链表head的Retained Size是125829168正好是41943056的3倍。

我们点击一下Shallow Size,按照Shallow Size值递减排序。

step9.png

我们可以看到Shallow Size值比较高的一般都是字节数组,基本数据类型数组,String等类型。

总结:

  1. 对于Retained Size值:每个对象的Retained Size除了包括自己的大小,还包括引用对象的大小,整个类的Retained Size大小累加起来就大了很多,Retained Size可以用来大概反应哪种类占的内存比较多。

  2. 如果想要看整体内存占用,看Shallow Size还是相对准确的,Retained Size可以用来大概反应哪种类占的内存比较多,仅仅是个示意,不过还是Retained Size比较常用,因为Shallow Size的大户一般都是String,数组,基本类型意义不大。

  3. 我们在分析的时候可以按照不同的场景可以选择Shallow Size或者Retained Size来进行查看。

扯了这么多,现在进入正题。

我们模拟一个内存泄漏的场景:

定义一个监听接口SampleListener

interface SampleListener {

    void click();
}

定义一个监听管理类ListenerManager,用来添加和删除SampleListener。我们需要创建一个ListenerManager单例类。

public class ListenerManager {

    //静态对象
    private static ListenerManager sInstance;

    private List<SampleListener> listeners = new ArrayList<>();

    private ListenerManager() {
    }

    public static synchronized ListenerManager getInstance() {
        if (sInstance == null) {
            sInstance = new ListenerManager();
        }

        return sInstance;
    }

    public void addListener(SampleListener listener) {
        listeners.add(listener);
    }

    public void removeListener(SampleListener listener) {
        listeners.remove(listener);
    }
}

然后让SecondActivity实现SampleListener接口,在onCreate方法中注册监听。

class SecondActivity : AppCompatActivity(), SampleListener {
   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)
        ListenerManager.getInstance().addListener(this)
    }

    override fun click() {

    }

}

多次打开SecondActivity,因为我们只注册了监听,但是没有取消注册,所以会导致ListenerManager类型的静态实例sInstance持有多个SecondActivity实例。造成内存泄漏。我们使用Android Studio自带的Memory Profiler 查看 Java 内存信息并进行堆转储。获取堆转储信息以后Android Studio会自动帮我们分析堆转储信息,如下图所示。

setp1.jpg

我们可以看到现在内存中有7个SecondActivity实例,点击SecondActivity类,查看对应的Instance View面板。


step2.png

然后我们点击一个实例对象,看看它的引用路径。

step3.jpg

我们重点关注一下Depth,Depth表示从任意GC根到选定实例的最短跳数。在这个例子中从GC根到泄漏的SecondActivity实例的最短路径是4。

注意:如果某个实例的 Depth1的话,这意味着它直接被 GC root 引用,同时也意味着它永远不会被自动回收。

GC根:类静态变量sInstance
\downarrow
ListenerManager类中的listeners
\downarrow
ArrayList中的Object[] elementData
\downarrow
Object[] elementData数组中index为5的对象
\downarrow
泄漏的SecondActivity实例SecondActivity@315745280

泄漏的原因就是这条路径上的某个对象造成的,我们需要仔细分析这条路径上的对象,来判断到底泄漏发生的原因。在这个例子中静态变量sInstance无法被回收,那么sInstance中的成员变量listeners也无法被回收,因为listeners又持有SecondActivity的引用,所以最终导致SecondActivity无法被回收造成内存泄漏。

在这个例子中,我们可以在SecondActivity的onDestroy方法中移除监听就能解决泄漏问题。

override fun onDestroy() {
    ListenerManager.getInstance().removeListener(this)
    super.onDestroy()
}

使用LeakCanary

使用LeakCanary来检测内存泄漏也是美滋滋。直接在app的build.gradle文件中添加LeakCanary的依赖就完事了。

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-beta-4'

然后我们也多次打开SecondActivity,当LeakCanary检测到内存泄露以后会弹出通知,我们点击通知即可查看泄漏信息。

leacanary.jpg

我们要怎么从这个GC路径中找到造成泄漏发生的对象呢?

首先我们从上往下看找到最后一个是否泄漏为NO(Leaking: NO)的对象。在这个图中是:ListenerManager类型的对象sInstance。


leakcanary1.png

然后我们继续往下看找到第一个是否泄漏为YES(Leaking: YES)的对象。在这个图中是:SecondActivity实例


leakcanary2.png

我们可以推断泄漏是由最后一个没有泄漏的对象(Leaking: NO )和第一个泄漏的对象(Leaking: YES)之间的对象导致的,是我们重点排查的对象,如下图所示:


leacanary3.jpeg

我们经过排查可以发现,造成泄漏的原因就是SecondActivity作为SampleListener加入到静态对象sInstance的listeners集合中,然后在SecondActivityonDestroy以后,listeners依然持有SecondActivity的引用,导致SecondActivity无法被回收。

参考链接:

上一篇 下一篇

猜你喜欢

热点阅读