关于Android 内存泄漏的分享

前情提要

java中四种引用类型

StrongReference强引用

如 Object o = new Object()

  • 回收时机:从不回收
  • 使用:对象的一般保存
  • 生命周期:JVM停止的时候才会终止
SoftReference软引用
  • 回收时机:当内存不足的时候;
  • 使用:SoftReference结合- ReferenceQueue构造有效期短;
  • 生命周期:内存不足时终止
WeakReference,弱引用
  • 回收时机:在垃圾回收的时候;
  • 使用:同软引用;
  • 生命周期:GC后终止
PhatomReference 虚引用
  • 回收时机:在垃圾回收的时候;
  • 使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动;
  • 生命周期:GC后终止

Java 程序运行时的内存分配

Java 程序运行时的内存分配策略有三种:静态分配、栈式分配和堆式分配。
对应的存储区域如下:

  • 静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

  • 栈区 :方法体内的局部变量都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。

  • 堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。

栈和堆的区别

栈内存:在方法体内定义的局部变量(一些基本类型的变量和对象的引用变量)都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存:用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

栈内存:基本类型变量、对象引用变量 、方法内局部变量
堆内存: new 出来的对象

看下面代码

public class A {

    int a = 0; // 栈内

    B b = new B(); // new B()堆内  b在栈内
    public void test(){
        int a1 = 1;  //栈内
        B b1 = new B(); // b1在栈内 new B() 在堆内
    }
}

A object = new A(); //object栈内  new A() 堆内

A类内的局部变量都存在于栈中,包括基本数据类型a1和引用变量b1,b1指向的B对象实体存在于堆中

引用变量object存在于栈中,而object指向的对象实体存在于堆中。new A 对象的所有成员变量a和b在栈内(句柄),而引用变量b指向的B类对象实体存在于堆中。

主线程的Looper对象的生命周期 = 该应用程序的生命周期
在Java中,非静态内部类 & 匿名内部类都默认持有 外部类的引用

举例handler内部msg —— handler实例 ——Activity实例


造成内存泄漏情景

  • 非静态内部类导致的内存泄露,比如Handler,解决方法是将内部类写成静态内部类,在静态内部类中使用软引用/弱引用持有外部类的实例

  • IO操作后,没有关闭文件导致的内存泄露,比如Cursor、FileInputStream、FileOutputStream使用完后没有关闭

  • 自定义View中使用TypedArray后,没有recycle

  • Context 造成的内存泄漏 如单例模式中的内存泄漏。解决方法:使用Application的Context

  • 注册监听器的泄漏 没有在destory时 unregisterxxx()

  • 集合中对象没清理造成的内存泄漏 解决方法 :在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序

  • WebView造成的泄露
    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。


adb dumpsys meminfo packageName 查找内存泄漏

对比两次的Activity和View的数量变化.png

adb shell dumpsys meminfo packagename -d命令,反复进入、退出同一界面,并对比两次的Activity和View的数量变化。如果有差异,则说明存在内存泄露(在使用命令查看Activity和View的数量之前,记得手动触发GC)。

native leak.png

要继续观察dumpsys meminfo 包名, 输出的结果信息,关注点放在 UnKnown那一行 和 Native Heap 那一行,关注Heap Alloc 或者 Pss Total, 如果你的总TOTAL一直再增加,但是是由于这两行的增加,那么这个问题你不需要再继续在MAT上花时间了,因为这种内存泄露问题,出在Native层(C)那么你需要去找你程序中使用到JNI的地方,so库或者其他一些特殊调用上,分析它们是否可能造成内存泄露问题。

adb shell showmap -a PID

然兴许你依旧没有头绪,那么没关系,另一个命令就是为了你而存在的,(首先某个应用的PID号, 用dumpsys meminfo 包名,那边已经可以查到)

譬如我上面那个mms, PID号为2786, 接着adb shell showmap -a PID号 (adb shell showmap -a 2786)

然后根据结果[....]这的信息,在去google上面找关键字, 譬如:[ anon ] bash的堆

(4)当你最终还是不知道是由哪边的.so库引起的话,你可以查看下Native Heap的内存分配情况,这时候你依旧需要借助DDMS,

需要先执行以下命令:

adb shell setprop libc.debug.malloc 1

adb shell stop

adb shell start

然后你还需要改一下eclipse中的配置参数值【因为如果你不配置的话,你的DDMS打开默认是看不到Native Heap那个Tab项的】

在ddms.cfg文件(实在找不到的话,就用Everything搜索下吧)最后增加一行native=true并save。ddms.cfg位于c:\Users\xxx.android目录下。

在Device中选择好你要的应用的包名项,然后按下Snapshot按钮, 就可以观察到Native Heap的使用情况了,然后反复执行脚本,再观察观察,你会找到你需要的东西的。


1.单例造成的内存泄漏

public class AppManager {
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
        //this.context = context.getApplicationContext(); 解决方式
    }
    public static AppManager getInstance(Context context) {
        if (instance != null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

2.Handler造成的内存泄漏

当 Android 应用程序启动时,framework 会为该应用程序的主线程创建一个 Looper 对象。Looper 对象包含一个简单的消息队列 Message Queue,并且能够循环的处理队列中的消息。这些消息包括大多数应用程序 framework 事件,例如 Activity 生命周期方法调用、button 点击等,这些消息都会被添加到消息队列中并被逐个处理。主线程的 Looper 对象会伴随该应用程序的整个生命周期。

当我们在主线程中实例化一个 Handler 对象后,会自动与主线程 Looper 的消息队列关联起来。所有发送到消息队列的消息 Message 都会拥有一个对 Handler 的引用,而此时当前 Activity 如果已经结束/销毁,而 Handler 由于是非静态内部类就会持有外部类的对象,抓住当前 Activity 对象不放,此时就极有可能导致内存泄漏。

public class SampleActivity extends AppCompatActivity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    };
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { /* ... */ }
        },  1000 * 60 * 1);
        // Go back to the previous Activity.
        finish();
    }
}

静态内部类不会持有外部类的引用,其跟外部类的关系,可以看成平级。

解决办法就是使用静态内部类加 WeakRefrence,如下所示:

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

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

        @Override
        public void handleMessage   (Message msg) {
            Sample2Activity activity = mActivity.get();
            if (activity != null) {
                // ...
            }
        }
    }

或者也可以在Activity的onDestory()中 removeCallbackandMessag(null)

3.非静态内部类持有外部类的实例

public class Sample4Activity extends AppCompatActivity {
    private static LeakSample mLeakSample = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mLeakSample == null){
            mLeakSample = new LeakSample();
        }
        //...
    }
    class LeakSample {
        //...
    }
}

上述代码在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据(避免了资源的重复创建),这种写法却会造成内存泄漏,同样因为非静态内部类持有外部类对象的原因。正确的做法为: 将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请使用ApplicationContext。

属性动画导致内存泄漏
属性动画中有一类无线循环的动画,如果在当前 Activity 中播放此类动画,并且没有在结束的时候(onDestory)去停止该动画,那么动画会一直播放下去,尽管在界面上无法看见动画的运转,但是在此时 Activity 的 View 会被动画所持有,而 View 又持有当前 Activity,最终导致 Activity 无法被释放。动画的特征代码如下:

animator.setRepeatCount(ValueAnimator.INFINITE);
解决办法自然很简单,在 OnDestory() 中去取消动画即可。

Dialog 导致的内存泄漏
在当前 Dialog 所依附的 Activity 销毁之前,我们没有去将当前的 Dialgo 销毁(dismiss) 话也是很容易导致内存泄漏的。

匿名内部类
android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露

public class MainActivity extends Activity {
 ...
 Runnable ref1 = new MyRunable();
 Runnable ref2 = new Runnable() {
     @Override
     public void run() {

     }
 };
    ...
}

ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:


image

使用 Memory Profiler 查看 Java 堆和内存分配
深入理解 Android 之内存泄漏
Android 内存泄漏总结
Android 内存泄漏分析心得

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

推荐阅读更多精彩内容

  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,624评论 0 8
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,216评论 2 7
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 790评论 0 5
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,360评论 0 12
  • OdooRPC是一个Python包,提供了一种通过RPC访问Odoo服务的简便方法。 主要功能: 1. 使用类似于...
    千年码妖阅读 4,737评论 0 2