Android Architecture ComponentsAndroid开发

Android 内存泄漏

2017-09-07  本文已影响45人  chauI

认识内存泄漏

根本原因就是当一个对象理应被回收的时候,因为在某个地方持有该对象的引用,导致它不能正常被 JVM 回收,而停留在堆内存中。
在 Android 中具体的例子大部分是:当我们关闭了一个 Activity/Fragment 时,此时 Activity/Fragment 变为不可见,内存中的实例也应当被回收,假如这时候还有别的对象实例强引用了 Activity/Fragment 的实例导致一直无法回收,则出现了内存泄漏。

关于 JAVA 的内存分配和回收,引用一段:

Java内存划分为栈、堆、方法区等区域,其中栈保存的是方法的局部变量,随方法起随方法灭,不需要GC;
堆保存所有对象的实例和数组,是GC和泄露的重点区;
方法区保存的是类信息、常量、静态变量等静态信息,也需要GC。
堆内存的回收中,判断对象存活的算法有引用计数算法和可达性分析算法,引用计数算法无法解决对象间循环引用的问题,虚拟机通常采用可达性分析算法。
常见的垃圾回收算法有:标记 - 清除法、复制算法、标记 - 整理法、分代回收算法。
常见的垃圾回收器种类有:Serial、ParNew、Parallel Scavenge等。

关于强引用:
对应的常用概念还有软引用、弱引用。

强引用特点是 JVM 即使内存耗尽也不会去自动回收该对象:

Object o = new Object();//强引用

而软引用在内存不足时,会被 JVM 回收:

Staff bean = new Staff();//仅用于创建软引用的实例
SoftReference<Staff> staffSR = new SoftReference<Staff>(bean);

//实际调用
String StaffID = staffSR.get().getId();

弱引用的使用和软引用类似,不同的是当 JVM 触发了 GC 时,不管当前内存空间足够与否,都会被回收:

Staff bean = new Staff();//仅用于创建弱引用的实例
WeakReference<Staff> staffWR = new WeakReference<Staff>(bean);

//实际调用
String StaffID = staffSR.get().getId();

内存泄漏的实例

日常的内存泄漏其实追溯到最后还是间接或直接的持有了 Activity/Fragment 的实例,但是有些确实防不胜防。一不注意就会踩雷。

这个就是老生常谈了,因为单例的生命周期同应用一样长,又常有构造方法中需要传入 context 的情况。
这时候就要注意,如果传入了 Activity 的 Context,一不小心就会使这个单例持有了 Activity 的实例而出现内存泄漏。
所以这种情况下,一般用 Application 的 Context 来构造单例对象的实例。

((Activity)context).getApplicationContext();

首先要回忆内部类的特性,内部类可以访问外部类的实例。
对于 JVM 来说,内部类和外部类其实是两个不同的类,正常来说两个类之间调用方法自然是通过两者的实例调用。
而非静态内部类之所以能访问外部类的方法,关键在于非静态内部类会默认隐性的持有外部类的实例
但静态的内部类则不会持有外部类的实例。

相关的典型代码:

public class TestActivity extends Activity {

  private final Handler mTestHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      //TODO
    }
  }
}

这时候的 Handler 是非静态的,所以该实例持有了 TestActivity 的实例。
当调用:

//延时发送消息
mTestHandler.postDelayed(new Runnable() {
    @Override
    public void run() { 
        //TODO
    }
}, 1000);

如果不清楚 Handler 的原理和源码的是时候去补习一下了。这时候 Handler 发送了 Message 实例,而这个 Message 实例引用了 Handler 实例,同时 Message 被主线程的 Looper 引用,此时的引用链:
Looper -> Message -> Handler -> Activity
这个时候即使调用 ((Activity)context).finish() Activity 也不能被回收。

所以上面的情况下要将 Handler 转化成静态类即可,或者继承 Handler 将隐性引用的 Activity 实例改为弱引用:

private static class MyHandler extends Handler {
    private final WeakReference<TestActivity> mActivity;

    public MyHandler(TestActivity activity) {
        mActivity = new WeakReference<TestActivity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        TestActivity activity = mActivity.get();

        if (activity != null) {
                //TODO
        }
    }
}

和这个问题类似的还有线程造成的泄漏,两者的原因都是因为用了非静态的内部类:

public class ThreadTestActivity extends Activity {  

    private class MyThread extends Thread {  
        @Override  
        public void run() {  
            super.run();  
            //TODO 
        }  
    } 

Cursor、InputStream/OutputStream、File 等资源文件,如果仅仅是在使用结束后将引用赋为 null,而不调用关闭的方法,还是有可能会造成内存泄漏。

特别是 Cursor,使用数据库时如果没有处理好可能会出现 OOM 或 Could not allocate CursorWindow,就是因为没有关闭导致:

Cursor c ;
//TODO get Cursor
try { 
    c = query();  
    //TODO something
    c.close(); 
    //如果 try 中抛出异常,上面的 cursor.close() 很大可能不会执行
} catch (Exception e) { 

} finally{
    //如果没有 finally 块会容易出错
    if (c != null) {
        c.close();
    }
}

类似的还有广播、EventBus 等需要注册的同时也要记得在合适的地方注销。

这次负责的其中一个项目是运行在固定的设备上:Android 4.4.2 的大平板上,所以这个坑是实实在在的踩下去了。
AlertDialog 中的监听回调都是靠 Handler 来实现的:

/*
 * 以下代码出自 android-23 - android - app - Dialog
 */
public void show() {
    if (mShowing) {
        if (mDecor != null) {
            if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
            }
            mDecor.setVisibility(View.VISIBLE);
        }
        return;
    }
    
    //省略部分代码

    try {
        mWindowManager.addView(mDecor, l);
        mShowing = true;

        sendShowMessage();
    } finally {
    }
}

@Override
public void dismiss() {
    if (Looper.myLooper() == mHandler.getLooper()) {
        dismissDialog();
    } else {
        mHandler.post(mDismissAction);
    }
}

而我们使用 Dialog 时 .setPositiveButton().setNegativeButton().setNeutralButton() 都以非静态内部类的形式实现,而这些点击的事件同样是通过 Handler 回调,这就会将这些内部类包装成一个 Message 传给 Dialog,这个 Message 就强引用了 Activity 的实例。
而 Dialog 在使用这些 Message 的时候会拷贝一个对象而不是用原来的对象:

private void sendDismissMessage() {
    if (mDismissMessage != null) {
        // Obtain a new message so this dialog can be re-used
        Message.obtain(mDismissMessage).sendToTarget();
    }
}

也就是说后面使用的是 Message 的拷贝。所以原来的 Message 从没有被发送,因此不会被回收,所以永久保存着它的内容,直到发生垃圾回收。

所以现在的引用链:
Thread(CookieSyncManager) -> Message -> AlertDialog$3(OnDismissListener) -> AlertDialog -> Activity

当然 5.0 以上已经解决了这个问题,所以这个案例可以看一看就过。

检测内存泄漏

不管用什么工具和方法,检测是否内存泄漏的方法都依靠 heap dump 文件。
heap dump 文件是一个二进制文件,保存了某一时刻 JVM 堆中对象使用情况,就是生成文件时的 Java 堆栈的快照。我们可以选择用 Heap Analyzer 分析 heap dump 文件,看哪些对象占用了太多的堆栈空间,或者哪些对象应该被回收却还在内存中。而 Leakcanary 等框架可以帮助我们省去一部分的工作。

build.gradle 中添加如下依赖:

dependencies {
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
 }

在 Application 初始化:

public class MyApplication extends Application {

    private RefWatcher mWatcher;

    @Override 
    public void onCreate() {
        super.onCreate();
        mWatcher = LeakCanary.install(this);//此时已经可以检测到 Activity 的内存泄漏
    }

    public static RefWatcher getWatcher(Context context){
        return ((MyApplication)context.getApplicationContext()).mWatcher;
    }
}

检测 Fragment :

@Override
public void onDestroy() {
    super.onDestroy();
    //watch() 也可以传入别的对象实例来检测是否泄露
    MyApplication.getWatcher().watch(this);
}

Leakcanary 的 install(application) 相当于在 Activity 的 onDestroy() 中调用 watch(this)

其原理是在 Activity / Fragment 销毁后,先手动促发一次 GC(系统 GC 并不会在销毁后立刻发生)
如果 watch(Object bean) 中传入的 bean 实例依然存在在内存中,则 dump heap 到本地,
dump 完成后启动 HeapAnalyzerService 服务读取本地 dump下来的文件,使用 HAHA 库进行分析。
如果检测到内存泄漏,将结果返回给 DisplayLeakService 服务,并且弹窗显示通知。

在不想额外的依赖 Leakcanary 等框架是,利用 AS 自带的 Monitor 同样可以检测内存泄漏,只是多了一些步骤。

Android Monitor -> Monitor

Monitors

从左到右:
Initiate GC // 手动触发 GC。
Jump java heap// 获取 hprof 分析文件
Start Allocation Tracking// 开始分配追踪。

在 Jump java heap 之前还是要记得触发一次 GC。点击后会生成一个 后缀为 hprof 的文件,在 AS 打开:

hprof

右边的 Analyzer Tasks 打开分析窗口:

Analyzer Tasks

右上角开始,在 Results 可以看到分析结果。

上一篇下一篇

猜你喜欢

热点阅读