Android内存泄露分析笔记

很久以前就谋划着做个学习笔记,但由于懒癌晚期,一直都没能提起干劲儿开始它,但这都快要过年了,不能让这个问题留着过年,今天必须得要写下去。所谓问题的分析就得要用分析问题的方式。

零、大纲

  1. 问题:内存泄露带来的问题。
  2. 原因:为何会造成内存泄露。
  3. 解决方法
    分析工具:Android Profiler
    常见的示例

一、问题

现象

内存泄露,即系统分配的内存需要被回收时不能被及时回收,导致内存使用不断累积,这很容易造成OOM(内存溢出)。

二、原因

如上所说,内存泄露是由于该回收时无法回收造成的,那么回收是谁来回收,什么时候需要回收呢?

1. 原理知识

谁来回收--java内存分配策略(栈与堆)

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。栈内存随着作用域范围来进行自动释放内存。
堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。堆内存由java的垃圾回收器GC(Garbage Collection)来管理,它决定是否释放内存。

什么能够回收--java回收机制(无引用)

释放对象的根本原则就是该对象不再被引用(java有四种引用方式,其中强引用影响回收)。
有向图,监管对象是否被引用。每一个对象不能从GC根节点到达,则GC会将其回收。


有向图.png

触发方式

一般在Andriod中,Activity和Fragment作为界面的载体必不可少,而资源的创建与释放也通常以这两个作为界限。而Activity的创建,并不像一个对象那样直接被new出来的,也不会有Activity直接置空来进行释放,而是通过生命周期来管理的。若一个Activity在销毁后仍然有对象对其持有引用,那么GC是不会将其回收的,这也就造成了Activity中分配的内存得不到释放。Fragment也是同理。

三、解决方法

分析工具

网络上常见的分析工具有MAT「Memory AnalysisTools」、LeakCanary以及将要着重介绍的Android Studio自带工具性能分析器--Android Profiler。

Android Profiler

  • 视图介绍


    Android Profiler.png

    连接上设备后(这里我使用了模拟器),就可以看到有这个选项了,点开后就可以查看提供的各个性能记录了。


    性能监视视图窗口.png

    ①为所选中查看的设备。
    ②为选择你所要查看的调试应用进程。

    ③为性能监视视图窗口,依次为:事件记录、CPU使用情况记录、内存使用情况记录和网络使用情况记录。其中我们所要关注的是内存使用记录情况(MEMORY),用来分析内存泄露。
    双击内存记录区域后,将会看到详细的内存使用情况。


    内存操作按钮.png
  • 内存操作按钮
    ①强制GC,主动释放内存。
    ②捕获堆转储。
    ③记录内存分配(高版本API8.0以上没有此按钮)。

介绍到这里,暂时回过头来想想,通过上面的原理可以知道,如果我们在一个Activity或某个活动后需要释放某个对象所占用的内存,如果这个对象在GC后仍然存在,那么就可以说发生内存泄露了,我们就得需要查看到底是谁在持续地引用这个对象导致没有成功的释放。

实例演示

在这里,我们以常见的Handler引起的内存泄露作为例子来说明。

public class HandlerLeakActivity extends AppCompatActivity {
    private static final String TAG = "HandlerLeakActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_leak);

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG, "持续引用结束!");
            }
        }, 1000 * 100);
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        finish();
    }
}

这里我创建了一个匿名内部类Handler,并使用Handler延迟10秒执行,这样由于匿名内部类会持有外部引用,所以在这100秒内按返回键销毁此Activity时是无法释放该Activity的。
我们这里在100秒内按返回后,使用强制GC按钮,并捕获堆转储,查看当前内存分配情况。


查看分类.png

①导出HProf文件,如用MAT进行分析。
②Heap 共分为:
zygote heap: 与 android zygote 进程共享的 heap
image heap: 与系统共享的 heap
app heap: 应用本身的 heap
③分类查看实例分配信息,这里为方便定位,使用按包名查看即可。

引用对象查看.png

①通过包名一路查看下来,果然在我们退出HandlerLeakActivity后并没有被GC掉,其中这个"$"代表类中创建的类,即我们的内部类也仍然存在。我点进去后继续查看。
②这里查看选中的实例,继续双击,看有哪些在持有它。
③其中我们发现了熟悉的身影,其中在HandlerLeakActivity的内部类对它进行了持有,而在①中也看到它仍然存在,所以正因为它的持续引用HandlerLeakActivity,造成了HandlerLeakActivity的释放不及时。

找到了原因后,我们该怎么避免内存泄露呢?我们可以使用静态类替换使用内部类来避免对外部类的持有。这里由于100秒已经属于很长的耗时行为了,所以不仅要避免持有,我们还得要及时地中断这个耗时行为(这里及时清空消息,也可以避免持续持有,这两个方法都能避免内存泄露)。

public class HandlerLeakActivity extends AppCompatActivity {
    private static final String TAG = "HandlerLeakActivity";
    DelayHandler mDelayHandler = new DelayHandler();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_leak);

        mDelayHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG, "持续引用结束!");
            }
        }, 1000 * 100);

//        new Handler().postDelayed(new Runnable() {
//            @Override
//            public void run() {
//                Log.i(TAG, "持续引用结束!");
//            }
//        }, 1000 * 100);
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        //清空Message,避免有耗时行为存在
        mDelayHandler.removeCallbacksAndMessages(null);
        finish();
    }

    static class DelayHandler extends Handler{
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}

再次按原来的操作进行,查看内存分配。

图片.png

果然如我们所预料的那样,HandlerLeakActivity被及时释放掉,没有这个实例存在了。是不是对内存泄露分析不再那么头大了?-

  • 这里解释一下右边的几个参数:
    Allocations:实例个数。
    depth:从任何GC根到所选实例的跳数最短。
    native size:8.0之后的手机会显示,主要反映Bitmap所使用的像素内存(8.0之后,转移到了native)
    Shallow size:就是对象本身占用内存的大小,不包含其引用的对象。
    retained size:指包含该对象及其引用对象所占用的内存大小,是该对象被GC之后所能回收到内存的总和。

常见的内存泄露

  1. 内部类
    问题:内部类对外部类持有。
    方案:将内部类改为静态类。
  2. 订阅事件
    问题:订阅事件耗时操作,对其持有。
    方案:及时解除订阅,一般在OnDestroy()的生命周期中执行。
  3. Context
    问题:使用到Activity的context的耗时操作。
    方案:可用getApplicationContext()来替代该Activity的context。
  4. 引用(强引用、弱引用和软引用...)
    问题:某些实例需要被GC及时回收,但强引用无法被及时回收掉。
    方案:使用弱引用或软引用来获取该实例,并注意判空后再使用。
    注意的是,引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。
Fragment fragment = new Fragment();
WeakReference reference = new WeakReference(fragment);
Fragment weakFragment = (Fragment) reference.get();

5.待补充啦....

PS:总算填坑完成了,第一次写笔记难免有所疏漏,若有哪些不对的地方还望指正。

参考文章:
Java中关于内存泄漏出现的原因以及如何避免内存泄漏
Java堆:Shallow Size和Retained Size
AndroidStudio3.0最新 Android Profiler分析器(cpu memory network 分析器)
Android Studio 中的 HProf静态分析
Java 如何有效地避免OOM:善于利用软引用和弱引用

你可能感兴趣的:(Android内存泄露分析笔记)