关于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 mActivity;

        public MyHandler(Sample2Activity activity) {
            mActivity = new WeakReference(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 内存泄漏分析心得

你可能感兴趣的:(关于Android 内存泄漏的分享)