Android应用内存泄露分析以及优化方案

文章转载http://blog.csdn.net/Soiol/article/details/52486871

        本篇博客是介绍Android内存优化方面的知识,在读本篇博客之前需要你熟练掌握Java 基础知识(例如,静态变量的生命周期,匿名内部类的使用,匿名对象等),并且具有一定的Android应用开发经验(Android多线程编程,Android异步回调等)。

话不多说,本篇博客分为三个部分:


一,内存泄露的危害。

二,内存泄露情景分析。

三,如何优化。


先来谈谈内存泄露的危害。


谈到内存泄露的危害,笔者第一时间想到了内存泄露最终会导致应用内存不够用,毕竟Android系统不会给应用分配无限多的运行内存的,所以,内存泄露的问题不解决必定会引发一些不可预见的危害。那么内存溢出究竟会带来哪些危害呢?笔者抽出了打一盘dota的时间总结出两个方面:

1内存泄露越积越多,最终应用无可用内存,内存溢出,应用崩溃

2一些对象由于常驻在内存中,例如Activity,在长时间的停留后,由于系统本身内存不足最终被回收,但是其他异步的任务在返回结果是调用了直接引用了该Activity,导致空指针异常。

先谈谈内存溢出情况。由于笔者一时间没有办法模拟出一个真实环境的内存溢出(懒),所以使用最简单粗暴的方法,如下代码所示:

  1. public class MainActivity extends AppCompatActivity {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.   
  8.         while(true) {  
  9.             new Thread().start();  
  10.         }  
  11.     }  
  12. }  
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        while(true) {
            new Thread().start();
        }
    }
}

当运行这段代码后,不出意料,应用直接挂了,并且抛出如下图所示的错误信息:

这就是我们既熟悉又陌生的内存溢出错误:OutOfMemoryErrorOOM)。内存溢出会直接导致应用发生错误并结束运行,在没有任何防护措施的情况下,应用直接停止运行必定会影响到用户的操作体验,用户可能一气之下就卸载了你的应用(事实证明,这个真的会,笔者自己就卸载过微信很多次... 无奈还是得用)。一般运气好的情况下,在调试阶段日志会清晰告诉你哪里的代码发生了内存溢出,但是也仅仅是告诉你程序运行到那几行代码时发生内存溢出,这其实并不代表那几行代码是有问题的,想反,可能是程序其他地方已经发生了大量的内存泄露,之后程序运行到这里时,应用内存刚好达到了系统分配给你的应用运行内存的最大值从而发生内存泄露。可是一旦应用安装到用户手机上。那么,迎接你的可能就是一堆bug的修复过程,甚至主管,老板的一顿批评啊。

再来说第二种情况,内存泄露导致空指针的情况。由于这种情况需要太多的时间去模拟,所以笔者借用Android中另一个相似的空指针情况来说明这个问题。不知道读者在做Android开发的时候有没有做过调用系统相机拍照功能,如果做过的同学应该遇到过,在调用系统拍照的时候由于旋转屏幕,原本拍的照片,照片文件路径也正确的获取到了,但是在使用是,发现存储照片路径的字符串变量为空了?我的天呐(捂住嘴巴...),太神奇了 !于是上网百度一通发现,原来旋转屏幕导致Activity重新创建进而导致Activity里面的变量重新初始化了,于是那个保存照片路径的变量为空了,于是同学们针对这个问题,做出了防止屏幕旋转,或者通过onSaveInstance方法来保存数据等等。那么内存泄露如何和这个例子相关呢?其实如上面所说,假如某一个Activity发生了内存泄露,由于长时间的在后台运行,当系统内存不足时,系统优先杀死后台运行的Activity,那么假如其它界面的一个异步的任务完成后回调中调用了这个Activity内部的成员变量,那么调用一个被杀死的Activity肯定会发生内存泄露。同样的道理,加入打开系统拍照界面的哪个Activity由于旋转屏幕被重新创建了(一个全新的Activity),拍照结束时使用这个Activity里面的变量肯定也会报空指针异常的。感觉这个例子并不是很恰当,但是意思应该达到了,也能说明这个问题,读者不用太过纠结。

类似的内存溢出错误还有StackOverFlow(栈溢出),这个错误同样是内存溢出,不过一般发生在大量的递归过程中,导致栈内存变量快速增加。这个错误类型读者可以自己百度一下含义,笔者在这里就不在多说了。


接着我们分析一些常见的的内存泄露场景。

以下分析的情况如不特殊说明,均指Context(Activity)内存泄露。


好了,内存泄露的危害就说到这里了,内存泄露的问题是迫在眉睫啊,解决了内存泄露,就可以有效的避免内存溢出和空指针现象。笔者再抽出一盘dota的时间整理并列举了几个内存泄露的高发情况。其他情况暂时不讨论。那么,常见的内存泄露大致可以分为以下两种情况:

1,在异步情况下,非静态内部类对象持有外部类引用引发的内存泄露。

2,静态变量直接或者间接持有非静态对象引用。

常见的内存泄露大部分是Context内存泄露,进而导致资源泄漏(本地图片尤其危险),小部分内存泄露就是一下野生的对象了没有被回收,例如不需要静态对象的情况下new了太多静态对象。


下面分析异步情况下,非静态内部类对象持有外部类引用引发的内存泄露


假设有这样一个需求:一个Activity内部有个无限轮播图,每隔3s切换一次图片,百度一下轮播图效果会出来很多实现,目前大部分轮播图效果不外乎使用定时器或者消息队列来实现延迟更新操作。笔者这里采用消息队列模拟了一个轮播图效果(并没有图片),代码如下:

  1. public class HandleLeakActivity extends AppCompatActivity {  
  2.   
  3.     public static final String TAG = HandleLeakActivity.class.getSimpleName();  
  4.   
  5.     MyHandler handler = null;  
  6.   
  7.     @Override  
  8.     protected void onCreate(Bundle savedInstanceState) {  
  9.         super.onCreate(savedInstanceState);  
  10.         setContentView(R.layout.activity_handle_leak);  
  11.   
  12.         handler = new MyHandler();  
  13.     }  
  14.   
  15.     class MyHandler extends Handler {  
  16.   
  17.         final int TOTAL = 4;  
  18.         int count = 0;  
  19.   
  20.         public MyHandler() {  
  21.             startLoop();  
  22.         }  
  23.   
  24.         @Override  
  25.         public void handleMessage(Message msg) {  
  26.             Log.w(TAG, "切换到第 " + count + " 张图片");  
  27.             count = ++count % TOTAL;  
  28.             sendEmptyMessageDelayed(0, 2000);  
  29.         }  
  30.     }  
  31.   
  32.     public void startLoop() {  
  33.         handler.sendEmptyMessageDelayed(0, 2000);  
  34.     }  
  35.   
  36.     @Override  
  37.     protected void onDestroy() {  
  38.         super.onDestroy();  
  39.   
  40.         Log.w(TAG, "退出当前Activity");  
  41.   
  42.         if (handler != null) {  
  43.             handler.removeCallbacksAndMessages(null);  
  44.             handler = null;  
  45.         }  
  46.     }  
  47. }  
public class HandleLeakActivity extends AppCompatActivity {

    public static final String TAG = HandleLeakActivity.class.getSimpleName();

    MyHandler handler = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handle_leak);

        handler = new MyHandler();
    }

    class MyHandler extends Handler {

        final int TOTAL = 4;
        int count = 0;

        public MyHandler() {
            startLoop();
        }

        @Override
        public void handleMessage(Message msg) {
            Log.w(TAG, "切换到第 " + count + " 张图片");
            count = ++count % TOTAL;
            sendEmptyMessageDelayed(0, 2000);
        }
    }

    public void startLoop() {
        handler.sendEmptyMessageDelayed(0, 2000);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        Log.w(TAG, "退出当前Activity");

        if (handler != null) {
            handler.removeCallbacksAndMessages(null);
            handler = null;
        }
    }
}
布局代码:

  1. <RelativeLayout  
  2.     xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     xmlns:tools="http://schemas.android.com/tools"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent"  
  6.     android:background="@mipmap/img_dota"  
  7.     android:paddingBottom="@dimen/activity_vertical_margin"  
  8.     android:paddingLeft="@dimen/activity_horizontal_margin"  
  9.     android:paddingRight="@dimen/activity_horizontal_margin"  
  10.     android:paddingTop="@dimen/activity_vertical_margin"  
  11.     tools:context="com.example.memoryoptimization.HandleLeakActivity">  
  12.   
  13. </RelativeLayout>  
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/img_dota"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.memoryoptimization.HandleLeakActivity">

</RelativeLayout>

上面贴出了模拟一个无线轮播的实现。从代码中,我们并没有看到有什么问题,同时,模拟的轮播效果也正常的运行起来。如下图所示:

那么当我们退出这个界面会发生什么呢?再来看下关闭当前界面的结果:

从我们测试用的代码可以看出,界面finish后会打印一句 “退出当前Activity”,同时你也会发现轮播的Log依然在打印,这是不是太奇怪了?我的界面都finish了,轮播图尽然还再继续?其实这里轮播图依然在继续轮播,就表明当前这个Activity发生内存泄露了,那么上面几张图并没有清晰的体现内存泄露的具体证据,所以笔者再贴出有关这个界面finish前后的内存变化对比图。

界面退出前:

界面退出后:


在界面退出前的截图上,红色箭头所指的是轮播图所在界面加载完成后内存的波动情况,由于笔者为了测试内存占用,在这个界面加了一张背景图片,所以内存有个显著的提升,但是从界面退出后在观察内存情况,发现内存并没有降低,这个内存测试结果是笔者在app静置了5分钟后截图到的,那么很明显,这里的内存泄露了。我们都知道,非静态内部类会自动持有外部类对象的引用,那么在这个测试代码中,MyHandler对象会持有HandleLeakActivity.this的引用,(正式因为这个HandleLeakActivity.this,我们才能顺利的调用Activity的成员方法而不受约束) ,因此在MyHandler的构造方法中我们可以调用外部Activity的startLoop方法开启轮播模式,由于Handler的消息队列本身牵扯到异步的过程(并不一定是在子线程),在多线程的情况下,我们要在子线程获取Handler对象来和主线程进行通信,当Activity结束后,异步任务还在继续执行(多线程情况下亦是如此),Java GC 在回收 Activity对象(这里Activity只是一个对象,并没有Android四大组件的概念了)时,发现任然有其他对象引用了(这个Handler对象),所以GC就放弃了对Activity对象的回收,进而引发Activity对象持久性的占用着内存。


下面分析静态变量引发的内存泄露


分析了第一种在异步情况下,非静态内部类引发的内存泄露后,我们继续来分析静态变量引发的内存泄露。有人可能压根没听说过?静态变量会引发内存泄露?我可是经常使用静态变量的,感觉没什么啊。这里笔者要说明一下,这里的静态变量值得是可以指向通过new关键字创建的对象 变量(字符串变量除外)。接下来,我们具体看几个常见的静态变量直接或者间接引用非静态对象导致的内存泄露:

第一种情况,也是最简单的情况:代码如下

  1. public class HandleLeakActivity extends AppCompatActivity {  
  2.   
  3.     public static final String TAG = HandleLeakActivity.class.getSimpleName();  
  4.   
  5.     private static TextView sTextView;  
  6.   
  7.     @Override  
  8.     protected void onCreate(Bundle savedInstanceState) {  
  9.         super.onCreate(savedInstanceState);  
  10.         setContentView(R.layout.activity_handle_leak);  
  11.   
  12.         sTextView = new TextView(this);  
  13.         sTextView.setText("我是一个TextView");  
  14.     }  
  15.   
  16.     @Override  
  17.     protected void onDestroy() {  
  18.         super.onDestroy();  
  19.         Log.w(TAG, "退出当前Activity");  
  20.     }  
  21. }  
public class HandleLeakActivity extends AppCompatActivity {

    public static final String TAG = HandleLeakActivity.class.getSimpleName();

    private static TextView sTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handle_leak);

        sTextView = new TextView(this);
        sTextView.setText("我是一个TextView");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.w(TAG, "退出当前Activity");
    }
}

布局代码依然是上面提供的布局代码....此处就不贴了。

上面代码我们在Activity内部声明了一个静态修饰的TextView变量sTextView,并且在onCreate方法内部new 了一个TextView赋值给它,此时这个sTextView指向了一个非静态的TextView对象(也就是我们new 出来的这个)。读者有没从这段代码里面看出什么问题?或者说读者自己就曾经写过这样的代码呢?不卖关子了,我们直接打开这个界面,从内存波动图来分析吧,如下图所示。

退出界面前:


退出界面后:


上面两图中,退出界面前的图中的红色箭头依然是我为了测试内存波动给布局加的背景图片(布局代码中的RelativeLayout的背景图:img_dota),很明显这个界面加载进来后,由于图片的加载内存增加的很明显,而通过观察退出界面后的内存波动发现,内存并没有降低,这是怎么回事呢?这里笔者就要强调下Java基础知识的重要性了。Java中,静态变量会优先对象而分配内存,并且其生命周期是和类的生命周期是相同的,也就是说静态变量的生命周期大于非静态变量的生命周期,也就是说静态变量是不会随着对象的回收而回收的。那么很容易理解这里的内存泄露了,当Activity结束后,由于这里sTextView变量是静态修饰的,指向一个非静态的TextView对象,同时这个Textview对象在创建的时候传递了一个this(也就是当前Activity),也就是说这个TextView对象持有了当前Activity的引用。sTextView持有TextView,而TextView对象又持有Activity,那么由于sTextView是静态的,并不会被回收(直到应用停止运行被回收),那么Java GC 并不会回收一个任然具有引用指向的对象,也就是说这个TextView不会被回收,那么TextView既然不会被回收,那Activity也不会被回收了,Activity都不会被回收了,这里就明显发生内存泄漏了。

第二种情况:全局Toast工具类引发的内存泄露。笔者在优化项目的过程被一个Toast工具类困扰了很久,笔者是真的知道这个工具类有内存泄露,但是缺无法对它就地正法,因为项目用到的地方太多,牵一发就会动全身啊,那么究竟是什么的代码呢?

  1. public class ToastUtil {  
  2.   
  3.     public static Toast toast = null;  
  4.     private static View v;  
  5.     private static TextView text;  
  6.   
  7.     public static void show(Context context, String msg) {  
  8.         if (toast == null) {  
  9.             toast = new Toast(context);  
  10.             v = LayoutInflater.from(context).inflate(R.layout.toast, null);  
  11.             text = (TextView) v.findViewById(R.id.text);  
  12.             text.setText(msg);  
  13.             toast.setDuration(Toast.LENGTH_SHORT);  
  14.             toast.setView(v);  
  15.             toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);  
  16.             toast.show();  
  17.         } else {  
  18.             text.setText(msg);  
  19.             toast.show();  
  20.         }  
  21.     }  
  22.   
  23.     public static void show(Context context, int resId) {  
  24.         if (toast == null) {  
  25.             toast = new Toast(context);  
  26.             v = LayoutInflater.from(context).inflate(R.layout.toast, null);  
  27.             text = (TextView) v.findViewById(R.id.text);  
  28.             text.setText(resId);  
  29.             toast.setDuration(Toast.LENGTH_SHORT);  
  30.             toast.setView(v);  
  31.             toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);  
  32.             toast.show();  
  33.         } else {  
  34.             text.setText(resId);  
  35.             toast.show();  
  36.         }  
  37.     }  
  38. }  
public class ToastUtil {

    public static Toast toast = null;
    private static View v;
    private static TextView text;

    public static void show(Context context, String msg) {
        if (toast == null) {
            toast = new Toast(context);
            v = LayoutInflater.from(context).inflate(R.layout.toast, null);
            text = (TextView) v.findViewById(R.id.text);
            text.setText(msg);
            toast.setDuration(Toast.LENGTH_SHORT);
            toast.setView(v);
            toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);
            toast.show();
        } else {
            text.setText(msg);
            toast.show();
        }
    }

    public static void show(Context context, int resId) {
        if (toast == null) {
            toast = new Toast(context);
            v = LayoutInflater.from(context).inflate(R.layout.toast, null);
            text = (TextView) v.findViewById(R.id.text);
            text.setText(resId);
            toast.setDuration(Toast.LENGTH_SHORT);
            toast.setView(v);
            toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);
            toast.show();
        } else {
            text.setText(resId);
            toast.show();
        }
    }
}
这是一个真实而悲伤的故事.....不知道各位看官有没有第一时间注意到那个静态的TextView,在上面第一种情况中,我们分析过那个静态的sTextView引发的内存泄露,那么这里又是这样一个静态的TextView,读者能根据上面的分析过程分析下这个ToastUtil类的问题吗?不管你有没有分析,我就当你分析了....,

可是笔者却不是第一时间观察到这TextView,而是这里的静态Toast引用,很明显这个ToastUtil犯的错误和上面的是同一个错误,这里的Toast引用通过new了一个Toast对象来初始化了,各位看官应该看到了这Toast对象也是需要传递一个Context作为参数的,那么这Context肯定99.9%是Activity的引用这个没人反对吧。既然这个静态的Toast引用直接引用了Activity,那么结果就如上面那种情况,Toast在应用生命周期内活着,在不主动置为空的情况不会轻易被回收,因此Activity也不会被回收,那么内存泄露再一次来临了。


我们总结下上面两种情况,发现都是静态的变量直接或者间接引用了Activity而引发的内存泄露,那么平时的代码中还有其他的情况吗?答案是肯定的。下面笔者会再举几个平时开发常见的写法,各位看过看好了...

  1. public class SingleInstance {  
  2.   
  3.     private Context mContext;  
  4.   
  5.     private static SingleInstance sSingleInstance;  
  6.   
  7.     private SingleInstance(Context context) {  
  8.         mContext = context;  
  9.     }  
  10.   
  11.     public SingleInstance getInstance(Context context) {  
  12.         if (sSingleInstance == null) {  
  13.             synchronized (SingleInstance.class) {  
  14.                 if (sSingleInstance == null) {  
  15.                     sSingleInstance = new SingleInstance(context);  
  16.                 }  
  17.             }  
  18.         }  
  19.         return sSingleInstance;  
  20.     }  
  21.   
  22.     public void doSomething() {  
  23.         //do some thing  
  24.     }  
  25. }  
public class SingleInstance {

    private Context mContext;

    private static SingleInstance sSingleInstance;

    private SingleInstance(Context context) {
        mContext = context;
    }

    public SingleInstance getInstance(Context context) {
        if (sSingleInstance == null) {
            synchronized (SingleInstance.class) {
                if (sSingleInstance == null) {
                    sSingleInstance = new SingleInstance(context);
                }
            }
        }
        return sSingleInstance;
    }

    public void doSomething() {
        //do some thing
    }
}
这段代码不知道看官有没有写过,反正我是写过的... 这不就是普通的单例模式嘛,没什么问题啊,Context也不是静态的,确实没什么问题啊。如果你认为这段代码没什么为题,那我还贴它干嘛啊。事实上在仔细观察一下,你会发现,确实没有静态的Context了,也没有静态的TextView了,但是这个类是单例啊,单例都是静态的才能保持应用唯一啊,那么唯一可能的为题就是这个单例了。事实上,问题就是处在这个单例,这个单例在第一次初始化的时候new 了一个自己赋值给sSingleInstance变量上,但是有没有发现这个对象传递了一个Context啊,单例在应用内唯一,那么单例模式中具体干活的对象也就不会轻易的回收了,那么这个对象在第一次调用时传递了哪个Context(Activity),那么这个Activity就会被这个单例对象持有了,那么这个Activity就泄露了 。就是这么霸道,泄露了...


那是不是我写的单例没有用到Context是不是就不会有内存泄露了?这个不一定哦,比方举另一个单例。

在没有EventBus的时代,远程通知还可以使用单例观察者来实现观察者模式(不知道是不是这样叫...),代码如下:

  1. public final class DataChangeObserverProxy {  
  2.   
  3.     private static DataChangeObserverProxy instance = null;  
  4.   
  5.     public static synchronized final DataChangeObserverProxy getInstance() {  
  6.         if (instance == null) {  
  7.             instance = new DataChangeObserverProxy();  
  8.         }  
  9.         return instance;  
  10.     }  
  11.   
  12.     private DataChangeObserverProxy() { }  
  13.   
  14.     public interface DataChangeObserver {  
  15.         void onDataChanged();  
  16.     }  
  17.     private DataChangeObserver dataChangeObserver = null;  
  18.   
  19.     public void setDataChangeObserver(DataChangeObserver dataChangeObserver) {  
  20.         this.dataChangeObserver = dataChangeObserver;  
  21.     }  
  22.   
  23.     public synchronized void notifyBeKilled() {  
  24.         if (dataChangeObserver != null) {  
  25.             dataChangeObserver.notifyDataChanged();  
  26.         }  
  27.     }  
  28. }  
public final class DataChangeObserverProxy {

    private static DataChangeObserverProxy instance = null;

    public static synchronized final DataChangeObserverProxy getInstance() {
        if (instance == null) {
            instance = new DataChangeObserverProxy();
        }
        return instance;
    }

    private DataChangeObserverProxy() { }

    public interface DataChangeObserver {
		void onDataChanged();
	}
    private DataChangeObserver dataChangeObserver = null;

    public void setDataChangeObserver(DataChangeObserver dataChangeObserver) {
        this.dataChangeObserver = dataChangeObserver;
    }

	public synchronized void notifyBeKilled() {
		if (dataChangeObserver != null) {
            dataChangeObserver.notifyDataChanged();
		}
	}
}
有没有发现很想观察者模式呢?没错,它就是一个观察者模式,不过是个单例模式实现远程通知,这个类里面并没有任何Context引用,静态sInstance的初始化也没有传递Context来创建 单例对象,那么这个类是不是没有问题了?给你们2分钟考虑一下。

..............................

好了,这个类的生命确实没有问题,但是要看使用场景,我们都知道,观察者模式是通过调用实现了观察者接口的实现类来来通知的,那么前提是被通知者要实现观察者的接口方法。在使用的时候你需要调用setDataChangeObserver方法来传递一个DataChangeObserver对象赋值给本例中的dataChangeObse变量。一般而言我们有如下的做法:

  1. DataChangeObserverProxy.getsInstance().setDataChangeObserver(new DataChangeObserver() {  
  2.     @Override  
  3.     public void notifyDataChanged() {  
  4.         // do something...  
  5.     }  
  6. });  
        DataChangeObserverProxy.getsInstance().setDataChangeObserver(new DataChangeObserver() {
            @Override
            public void notifyDataChanged() {
                // do something...
            }
        });
或者这样:

  1. public class MainActivity extends AppCompatActivity implements DataChangeObserverProxy.DataChangeObserver {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.   
  8.         findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener(){  
  9.             @Override  
  10.             public void onClick(View v) {  
  11.                 startActivity(new Intent(MainActivity.this, HandleLeakActivity.class));  
  12.             }  
  13.         });  
  14.           
  15.         DataChangeObserverProxy.getsInstance().setDataChangeObserver(this);  
  16.     }  
  17.   
  18.     @Override  
  19.     public void onDataChanged() {  
  20.           
  21.     }  
  22. }  
public class MainActivity extends AppCompatActivity implements DataChangeObserverProxy.DataChangeObserver {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, HandleLeakActivity.class));
            }
        });
        
        DataChangeObserverProxy.getsInstance().setDataChangeObserver(this);
    }

    @Override
    public void onDataChanged() {
        
    }
}
不管是哪种情况,都需要提供一个DataChangeObserver接口的实现赋值给 本例中的dataChangeObserver变量。那么会出现什么问题呢?第一种情况,new 了一个DataChangeObserver的匿名对象,我们知道匿名内部类对象会持有外部类引用(往前翻!),一旦dataChangeObserver指向这个匿名对象后,就相当于间接引用了Activity对象,而这个dataChageObserver对象是单例的成员变量,单例前面已经说过是不容易回收的,那么直接导致这个Activity无法回收,这就发生了内存泄露了。但是如果这个单例并没有使用在任何Context相关的组件中(Activity,自定义控件,Service,广播等等),那么这个单例倒是没有多大问题。


单例说完了,继续看....

很多应用里面会有那种一次性清空所有Activity的管理类,通常这样的管理类也是单例的(也有可能是静态调用,并不使用单例),内部持有一个集合来持有所有打开的Activity,

具体的写法是这样的:

  1. public class ActivityStackManager {  
  2.   
  3.     private static List<Activity> sActivityList = new ArrayList<>();  
  4.   
  5.     public static void addActivity(Activity activity) {  
  6.         if (activity != null) {  
  7.             sActivityList.add(activity);  
  8.         }  
  9.     }  
  10.   
  11.     public static void removeAllActivity() {  
  12.         for (Activity activity : sActivityList) {  
  13.             activity.finish();  
  14.         }  
  15.     }  
  16. }  
public class ActivityStackManager {

    private static List<Activity> sActivityList = new ArrayList<>();

    public static void addActivity(Activity activity) {
        if (activity != null) {
            sActivityList.add(activity);
        }
    }

    public static void removeAllActivity() {
        for (Activity activity : sActivityList) {
            activity.finish();
        }
    }
}

可能这段代码和你写的不太一样,例如初始化的顺序有些不同,但是基本相同吧。有同学就说了,这段代码我用过,写的没错啊,为了防止Activity没有结束,可以在最后一个界面直接调用removeAllActivity方法关闭所有可能没有关闭的Activity啊。笔者认为说的完全没有错,很好的做法啊。

但是同学,你就没有想过,为啥Activity没有finish,为啥需要收集这些Activity,难道你的应用切换几个页面后,最终回到了最开始的页面的时候,应用还隐藏了很多其他的页面没有finish吗?我告诉你,你这样是会被主管骂的!

这么一想这个想法完全行不通吧,为什么你的应用会隐藏了一堆界面没有finish...这是需要反思的地方。再者,既然是说内存泄露,相信各位看官应该能观察那个静态的sActivityList,一看到是个静态的,必然就会警惕起来,前面已经分析过几种静态变量引发内存泄露了,那么这里应该很容看出,就是sActivityList持有了很多Activity引用,最终导致很多Activity无法回收进而发生内存泄露。还有,这样的工具类,有add方法,没有remove方法,你会气死java的设计者们的,无论是观察者模式,集合类,都会存在例如add,remove,register,unregister 这样的方法互相对应的,所以这个类应该有个removeActivity方法才对。写完你就会发现,有了add,remove,和removeAll,是不是使用的时候在Activity的onCreate里面add,然后onDestroy里面remove呢?很合理吧?那既然先add,随后又remove了,就这个工具类到底干了啥?它到底干了啥?

存在即合理,我却不这么认为。我对这段代码的看法:并没有什么卵用!我不认为一个android程序员在处理界面回收的问题会出现有Activity没有finish掉。有人可能觉得我说的太绝对,有时候有的需求是这样的 A ->B->C->D......Z 然后需要在Z界面处理完某些业务逻辑后关闭前面所有的Activity?且不说产品设计这样的是否合理,就算让你实现起来也很费劲,而且就算真的有这样的需求,我觉得你让A界面前面的界面设置成singleTask启动模式,也能解决问题吧。

说了这么多,差不多也说完了情景分析了,总的来说,在使用单例,静态变量的时候,但凡涉及到Context的引用,一定要慎重。决不能随手就写了,而不考虑后果。其实真正在做项目的时候,基本很难遇到需要使用静态变量的时候,单例确实会常见,下面会具体针对上面发生的各种内存泄露而给出解决措施。


最后,我们来聊聊如何处理内存泄露的问题了。


其实,在分析完内存泄露几种常见的场景后,如何处理内存泄露以及不是难事了,毕竟你已经从知其然到了知其所以然的地步了。那么我们一个一个来。

先说轮播图例子,轮播图的例子中,由于Handler是非静态内部类对象,因此会持有当前Activity的引用,在异步任务下,进而引发内存泄露。这个例子中,handleMessage方法不断的处理发送过的消息,同时,再一次发送消息,这样形成永久循环。处理这样的问题有两种方法:

第一,在Activityfinishi的时候我们溢出这个Handle先关的消息队列中的暂存的所有消息,没有消息处理后,在将handler值为空就可以。安全的做法是这样的:

  1. <span style="font-size:12px;">    @Override  
  2.     protected void onDestroy() {  
  3.         super.onDestroy();  
  4.   
  5.         if (handler != null) {  
  6.             handler.removeCallbacksAndMessages(null);  
  7.             handler = null;  
  8.         }  
  9.     }</span>  
<span style="font-size:12px;">    @Override
    protected void onDestroy() {
        super.onDestroy();

        if (handler != null) {
            handler.removeCallbacksAndMessages(null);
            handler = null;
        }
    }</span>
第二,非静态内部类是通过持有外部类的引用而发生的内存泄露,那么我们可以让这个内部类静态化,因为静态的内部类不会持有外部类引用。当你把MyHandler类的声明静态化后,你会发现startLoop方法无法调用了,这就对了,因为静态的内部类不持有外部类对象了,也就没法调用外部类对象的方法了。那么 还是有其他办法的,比如:

  1. static class MyHandler extends Handler {  
  2.     private WeakReference<HandleLeakActivity> reference;  
  3.     private HandleLeakActivity activity;  
  4.     final int TOTAL = 4;  
  5.     int count = 0;  
  6.   
  7.     public MyHandler(HandleLeakActivity outerObject) {  
  8.         reference = new WeakReference<>(outerObject);  
  9.         activity = reference.get();  
  10.         activity.startLoop();  
  11.     }  
  12.   
  13.     @Override  
  14.     public void handleMessage(Message msg) {  
  15.         Log.w(TAG, "切换到第 " + count + " 张图片");  
  16.         count = ++count % TOTAL;  
  17.         sendEmptyMessageDelayed(0, 2000);  
  18.     }  
  19. }  
    static class MyHandler extends Handler {
        private WeakReference<HandleLeakActivity> reference;
        private HandleLeakActivity activity;
        final int TOTAL = 4;
        int count = 0;

        public MyHandler(HandleLeakActivity outerObject) {
            reference = new WeakReference<>(outerObject);
            activity = reference.get();
            activity.startLoop();
        }

        @Override
        public void handleMessage(Message msg) {
            Log.w(TAG, "切换到第 " + count + " 张图片");
            count = ++count % TOTAL;
            sendEmptyMessageDelayed(0, 2000);
        }
    }
采用弱引用来引用外部类引用的好处是,弱引用不会影响外部类对象的回收。这样又能调用外部类的方法了,开森...

再说静态变量直接或者间接持有Activity对象引用。

第一个例子,

  1. public class HandleLeakActivity extends AppCompatActivity {  
  2.   
  3.     public static final String TAG = HandleLeakActivity.class.getSimpleName();  
  4.   
  5.     private static TextView sTextView;  
  6.   
  7.     @Override  
  8.     protected void onCreate(Bundle savedInstanceState) {  
  9.         super.onCreate(savedInstanceState);  
  10.         setContentView(R.layout.activity_handle_leak);  
  11.   
  12.         sTextView = new TextView(this);  
  13.         sTextView.setText("我是一个TextView");  
  14.     }  
  15.   
  16.     @Override  
  17.     protected void onDestroy() {  
  18.         super.onDestroy();  
  19.         Log.w(TAG, "退出当前Activity");  
  20.     }  
  21. }  
public class HandleLeakActivity extends AppCompatActivity {

    public static final String TAG = HandleLeakActivity.class.getSimpleName();

    private static TextView sTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handle_leak);

        sTextView = new TextView(this);
        sTextView.setText("我是一个TextView");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.w(TAG, "退出当前Activity");
    }
}
这个例子我不知道各位看官在平时会不会这样写,或者说又没有这样声明一个静态的TextView,如果有我建议你改掉这个习惯,这里没有必要使用静态来声明这样一个TextView,如果没有,我就不细说了,没有必要处理这样不可能发生的问题。

第二个例子是ToastUtil工具类,这个类我是要说明一下的,因为我的同事真的这么写过,这里我要说的是,肯定不能用这样的静态的Toast工具类,来保证一次只有一个Toast实例,这个很单例还是有点区别的,我推荐的做法是这样的:

  1. public class ToastUtil{  
  2.   
  3.     private static ToastUtil sShowToast = null;  
  4.   
  5.     public static synchronized void init(Context context) {  
  6.         if (sShowToast == null) {  
  7.             sShowToast = new ShowToast(context);  
  8.         }  
  9.     }  
  10.   
  11.     public static synchronized void destroy() {  
  12.         if (sShowToast != null) {  
  13.             sShowToast = null;  
  14.         }  
  15.     }  
  16.   
  17.     public static final synchronized ShowToast getInstance() {  
  18.         if (sShowToast == null) {  
  19.             throw new IllegalArgumentException("You must call 'ShowToast.init()' in onCreate of 'application'");  
  20.         }  
  21.         return sShowToast;  
  22.     }  
  23.   
  24.     private Toast toast = null;  
  25.     private View v;  
  26.     private TextView text;  
  27.   
  28.     private ToastUtil(Context context) {  
  29.         toast = new Toast(context);  
  30.         v = LayoutInflater.from(context).inflate(R.layout.toast, null);  
  31.         text = (TextView) v.findViewById(R.id.text);  
  32.         toast.setDuration(Toast.LENGTH_SHORT);  
  33.         toast.setView(v);  
  34.         toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);  
  35.     }  
  36.   
  37.     public void show(String msg) {  
  38.         text.setText(msg);  
  39.         toast.show();  
  40.     }  
  41.   
  42.     public void show(int resId) {  
  43.         text.setText(resId);  
  44.         toast.show();  
  45.     }  
  46. }  
public class ToastUtil{

    private static ToastUtil sShowToast = null;

    public static synchronized void init(Context context) {
        if (sShowToast == null) {
            sShowToast = new ShowToast(context);
        }
    }

    public static synchronized void destroy() {
        if (sShowToast != null) {
            sShowToast = null;
        }
    }

    public static final synchronized ShowToast getInstance() {
        if (sShowToast == null) {
            throw new IllegalArgumentException("You must call 'ShowToast.init()' in onCreate of 'application'");
        }
        return sShowToast;
    }

    private Toast toast = null;
    private View v;
    private TextView text;

    private ToastUtil(Context context) {
        toast = new Toast(context);
        v = LayoutInflater.from(context).inflate(R.layout.toast, null);
        text = (TextView) v.findViewById(R.id.text);
        toast.setDuration(Toast.LENGTH_SHORT);
        toast.setView(v);
        toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);
    }

    public void show(String msg) {
        text.setText(msg);
        toast.show();
    }

    public void show(int resId) {
        text.setText(resId);
        toast.show();
    }
}
这个写法也是我在群里面得知原来ApplicationContext也可以用... 见笑了。

第三个例子,单例的实现。单例由于有个静态对象持久占据内存,所以为了让它在干完指定的活之后,及时释放内存,我们可以手动将单例中实例置为空。但是这么一来岂不是下次再使用时有得重新new 一个赋值?所以这里我更推荐单例中的Context使用ApplicationContext。可能又有人疑惑了,那万一单例里面的Context就必须使用Activity呢,这样的情况下我觉得这个单例已经超出原本的职责所在。我们不做考虑。单例能做的事情最好不要牵扯过多的业务逻辑,否则原本一个很方便的设计到头来你会深受其害的。目前我还没有接触过哪个单例需要实时和Activity打交道,如果读者有发现,可以留言回复我。

第四个例子,静态的单例观察者。代码如下:

  1. public final class DataChangeObserverProxy {  
  2.   
  3.     private static DataChangeObserverProxy instance = null;  
  4.   
  5.     public static synchronized final DataChangeObserverProxy getInstance() {  
  6.         if (instance == null) {  
  7.             instance = new DataChangeObserverProxy();  
  8.         }  
  9.         return instance;  
  10.     }  
  11.   
  12.     private DataChangeObserverProxy() { }  
  13.   
  14.     public interface DataChangeObserver {  
  15.         void onDataChanged();  
  16.     }  
  17.     private DataChangeObserver dataChangeObserver = null;  
  18.   
  19.     public void setDataChangeObserver(DataChangeObserver dataChangeObserver) {  
  20.         this.dataChangeObserver = dataChangeObserver;  
  21.     }  
  22.   
  23.     public synchronized void notifyBeKilled() {  
  24.         if (dataChangeObserver != null) {  
  25.             dataChangeObserver.notifyDataChanged();  
  26.         }  
  27.     }  
  28. }  
public final class DataChangeObserverProxy {

    private static DataChangeObserverProxy instance = null;

    public static synchronized final DataChangeObserverProxy getInstance() {
        if (instance == null) {
            instance = new DataChangeObserverProxy();
        }
        return instance;
    }

    private DataChangeObserverProxy() { }

    public interface DataChangeObserver {
		void onDataChanged();
	}
    private DataChangeObserver dataChangeObserver = null;

    public void setDataChangeObserver(DataChangeObserver dataChangeObserver) {
        this.dataChangeObserver = dataChangeObserver;
    }

	public synchronized void notifyBeKilled() {
		if (dataChangeObserver != null) {
            dataChangeObserver.notifyDataChanged();
		}
	}
}

这样的设计还是很常见的,至少我是用过的,这样的单例中,类似dataChangeObserver这样的对象往往是指向一个实现了DataChangeObserver的Activity或者是一个持有Activity引用的内部类对象。这样的情况,建议在Activity的onCreate方法中去调用setDataChangeObserver,在onDestory方法中setDataChangeObserver(null)即可,代码如下:

  1. public class MainActivity extends AppCompatActivity implements DataChangeObserverProxy.DataChangeObserver {  
  2.   
  3.     @Override  
  4.     protected void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         setContentView(R.layout.activity_main);  
  7.         DataChangeObserverProxy.getsInstance().setDataChangeObserver(this);  
  8.     }  
  9.   
  10.     @Override  
  11.     public void onDataChanged() {  
  12.   
  13.     }  
  14.   
  15.     @Override  
  16.     protected void onDestroy() {  
  17.         super.onDestroy();  
  18.         DataChangeObserverProxy.getsInstance().setDataChangeObserver(null);  
  19.     }  
  20. }  
public class MainActivity extends AppCompatActivity implements DataChangeObserverProxy.DataChangeObserver {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DataChangeObserverProxy.getsInstance().setDataChangeObserver(this);
    }

    @Override
    public void onDataChanged() {

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        DataChangeObserverProxy.getsInstance().setDataChangeObserver(null);
    }
}
第五个例子,ActivityStackManager。这个例子其实并没有什么卵用。我前面也说过了,如果添加一个removeActivity的方法,那么在Activity的onCreate和onDestroy方法对应调用addActivity和removeActivity方法简直就是多次一举啊,做了一堆无用功,因为最终你的这个Stack还是为空啊。所以不做处理了。


关于Android内存泄露的博客差不多到这就结束了,本篇博客通过对实际项目里面可能发生的,不可能发生的,或者不应该发生的各种内存泄露的场景均作了分析,代码是真真切切的我看过,改过的代码。如果各位看官不信,可以自己问问周围的同事什么的,有没有这么写过。

好了,花了差不多5盘dota的时间完成了这篇博客,也是第一次写博客,写的不好的地方,或者有错误的,概念混淆的地方,麻烦留言指出,我会及时修正过来的。


下一篇博客,我会教大家如何在Android Studio 利用MAT分析内存泄露问题。











评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值