Android 内存泄漏

  • 内存泄漏的原因
  • 常见的内存泄漏与解决方法
  • 检测内存泄漏

认识内存泄漏

根本原因就是当一个对象理应被回收的时候,因为在某个地方持有该对象的引用,导致它不能正常被 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 
        }  
    } 
  • 关于非静态内部类还有另一个注意点
    如果在非静态内部类中创建了一个静态的实例,这个操作相当于上面说的用 Activity 的实例来创建单例对象的实例:静态实例会一直持有 Activity(外部类) 的实例。

  • 资源未关闭导致

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 等框架可以帮助我们省去一部分的工作。

  • 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 服务,并且弹窗显示通知。

  • Android Studio

在不想额外的依赖 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 可以看到分析结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,624评论 0 8
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,216评论 2 7
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 790评论 0 5
  • 忙了一天坐下看电视才知道今天是重阳节,中央台播出九九重阳节的晚会,其中让我感触很深的是一个环节,给父母化老年妆,当...
    花开墨城阅读 226评论 0 0