Android内存泄漏的本质原因、解决办法、操作实例

今年最后一个迭代终于结束了,把过程中碰到的不熟悉的东西拉出来学习总结一下
 
内存泄漏的本质是:【一个(巨大的)短生命周期对象的引用被一个长生命周期(异步生命周期)的对象持有】
 
这个东西分为两个部分
  1. 获得一个(巨大的)短生命周期的对象
    1. 这个【巨大的短生命周期的对象】在Android中最有可能的就是【Activity】了
    2. 最容易无意识获得它的方式就是【非静态内部类隐式自动持有外部类的强引用】
  2. 把这个对象赋值给了一个长生命周期的对象
    1. 这个有一些常见的套路
      1. 套路一:直接赋值给了一个类的静态成员
        1. 这个静态成员的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收
      2. 套路二:一个匿名内部handler的形式持有acitivity,然后message又持有handler,最后长生命周期的Looper持有了这个message
        1. Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的
        2. 当Android应用启动的时候,会先创建一个UI主线程的Looper对象,Looper实现了一个简单的消息队列,一个一个的处理里面的Message对象。主线程Looper对象在整个应用生命周期中存在。
        3. 当在主线程中初始化Handler时,该Handler和Looper的消息队列关联,同时发送到消息队列的Message会引用发送该消息的Handler对象
        4. 只要Handler发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有
        5. 当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏
      3. 套路三:一个匿名内部Runnable持有acitivity,然后这个Runnable有一个耗时任务,这个耗时任务的生命周期比acitivity长
        1. 异步任务AsyncTask和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用
        2. 线程产生的内存泄漏主要原因在于线程生命周期的不可控。比如线程是Activity的内部类,则线程对象中保存了Activity的一个引用,当线程的run函数耗时较长没有结束时,线程对象是不会被销毁的,因此它引用的老的Activity也不会被销毁,因此就出现了内存泄漏的问题
        3. Java中的Thread有一个特点就是她们都是直接被GC Root所引用,也就是说Dalvik虚拟机对所有被激活状态的线程都是持有强引用,导致GC永远都无法回收掉这些线程对象,除非线程被手动停止并置为null或者用户直接kill进程操作。所以当使用线程时,一定要考虑在Activity退出时,及时将线程也停止并释放掉
      4. 套路四:把这个对象传入了一个异步线程
 
所以解决方法就是(以下只要阻断一处就OK了)
  1. 不要获得一个(巨大的)短生命周期的对象,假如不需要的时候
    1. 比如最容易无意识获得Activity的方式就是【非静态内部类隐式自动持有外部类的强引用】
      1. 为了不让内部类自动持有外部类的强引用,把原来的【非静态内部类或者是匿名内部类】重写为【静态内部类】就可以了,也就是独立出来加一个static
      2. 或者是不要使用内部类,抽出来成为一个外部类
      3. (这样做之后内部类里就无法直接使用外部环境了(调用外部类的变量和方法),如果要使用的话,就通过构造方法传进来)
      4. (这样就避免了内部类自动的无法控制的持有全部外部环境,只让内部类使用指定的外部环境的资源)
  2. 需要获得一个(巨大的)短生命周期的对象时,使用弱引用
    1. 如果一定要持有acitivty的引用,那就把这个引用改成弱引用
    2. 不过在【非静态内部类或者是匿名内部类】的情况下,需要先重写为【静态内部类】;因为得先把自动持有acitivity这东西废了,再通过构造方法把activity传进来,才能把acitivity的引用改为弱引用
  3. 不要赋值给了一个长生命周期的对象,假如可以的话
    1. 所以就是不需要使用静态变量的地方就不用
    2. 不过前面的hander和runnable就没有办法了,改不了
  4. 控制这个长生命周期的对象的生命周期,假如可以的话
    1. 比如静态变量,就可以在该清空的时候清空
  5. 把【(巨大的)短生命周期的对象】换成【(巨大的)长生命周期的对象】
 
所以就能推出大部分场景了(从上面的套路直接可以得到几种场景)
  1. 套路一:单例持有外部引用
    1. 发生场景
      1. 网络访问库中VolleyCreator单例持有外部Context
      2. ToastUtil中Toast单例持有外部Context
      3. NewMsgReceiver单例中的成员变量observers存入了外部Activity引用
    2. 原因:
      1. 单例的静态特性使得单例的生命周期和应用的生命周期一样长
    3. 解决方法:
      1. 适合使用方法五
      2. 单例引用Context要注意Context的生命周期,一般的Context可以使用ApplicationContext,对于单例成员变量注意在onDestory移除引用,比如观察者模式取消注册。
      3. 或者直接在单例里取ApplicationContext就好了
  2. 套路一:activity的静态变量持有自己的引用
    1. 这是之前在场景 “一个acitivity杀掉前面的另一个acitivity” 时我们经常使用的方法
    2. 解决办法
      1. 可以使用方法四:在onDestroy的时候清空这个静态变量
      2. 或者不使用这种方法来杀activity,使用eventbus
  3. 套路一:使用非静态内部类创建静态实例
    1. 很容易理解,你这个对象持有context,然后把这个对象赋值给静态变量,那这个context就直接泄漏了
    2. 举例:在启动频繁的Activity中,为了避免重复创建相同的数据资源,会出现:
      1. 在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏
        1. public class MainActivity extends AppCompatActivity {
        1.     private static TestResource mResource = null;
        1.     @Override
        1.     protected void onCreate(Bundle savedInstanceState) {
        1.         super.onCreate(savedInstanceState);
        1.         setContentView(R.layout.activity_main);
        1.         if(mResource == null){
        1.             mResource = new TestResource();
        1.         }
        1.         //...
        1.     }
        1.     class TestResource {
        1.         //...
        1.     }
        1. }
      2. 这种必须要使用静态变量,同时不知道什么时候释放,解决方法就是方法一,直接把非静态内部类改成静态就好了
  4. 套路二:非静态Handler持有外部Context引用
    1. 这种情况由于handler需要持有activity,所以使用方法二
    2. 然后具体是:
      1. 创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象
      2. 这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以更好的做法是在Activity的Destroy时或者Stop时应该移除消息队列中的消息
    3. 另外,还可以使用开源库WeakHandler,一个防止内存泄漏的Handler,详见banner源码WeakHandler.java
      1. public class MainActivity extends AppCompatActivity {
      1.     private MyHandler mHandler = new MyHandler(this);
      1.     private TextView mTextView ;
      1.     private static class MyHandler extends Handler {
      1.         private WeakReference<Activity> reference;
      1.         public MyHandler(Activity activity) {
      1.             reference = new WeakReference<>(activity);
      1.         }
      1.         @Override
      1.         public void handleMessage(Message msg) {
      1.             if(reference != null && reference.get() != null ){
      1.                 Activity activity = reference.get();
      1.                 activity.mTextView.setText("");
      1.             }
      1.         }
      1.     }
      1.     @Override
      1.     protected void onCreate(Bundle savedInstanceState) {
      1.         super.onCreate(savedInstanceState);
      1.         setContentView(R.layout.activity_main);
      1.         mTextView = (TextView)findViewById(R.id.textview);
      1.         loadData();
      1.     }
      1.     private void loadData() {
      1.         //...request
      1.         Message message = Message.obtain();
      1.         mHandler.sendMessage(message);
      1.     }
      1.     @Override
      1.     protected void onDestroy() {
      1.         super.onDestroy();
      1.         //使用mHandler.removeCallbacksAndMessages(null);是为了移除消息队列中所有消息和所有的Runnable
      1.         mHandler.removeCallbacksAndMessages(null);
      1.     }
      1. }
  5. 套路三:线程造成的内存泄漏
    1. 比较常见,这两个示例可能每个人都写过:
        1. new AsyncTask<Void, Void, Void>() {
        1.     @Override
        1.     protected Void doInBackground(Void... params) {
        1.         SystemClock.sleep(10000);
        1.         return null;
        1.     }
        1. }.execute();
        1. new Thread(new Runnable() {
        1.     @Override
        1.     public void run() {
        1.         SystemClock.sleep(10000);
        1.     }
        1. }).start();
    2. 如果任务可能执行很久,那就需要处理了,使用方法一或方法二就OK了
      1. //在Activity销毁时候也应该取消相应的任务AsyncTask.cancel(),避免任务在后台执行浪费资源
      1. static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
      1.     private WeakReference<Context> weakReference;
      1.     public MyAsyncTask(Context context) {
      1.         weakReference = new WeakReference<>(context);
      1.     }
      1.     @Override
      1.     protected Void doInBackground(Void... params) {
      1.         SystemClock.sleep(10000);
      1.         return null;
      1.     }
      1.     @Override
      1.     protected void onPostExecute(Void aVoid) {
      1.         super.onPostExecute(aVoid);
      1.         MainActivity activity = (MainActivity) weakReference.get();
      1.         if (activity != null) {
      1.             //...
      1.         }
      1.     }
      1. }
      1. static class MyRunnable implements Runnable{
      1.     @Override
      1.     public void run() {
      1.         SystemClock.sleep(10000);
      1.     }
      1. }
      1. //——————使用——————
      1. new Thread(new MyRunnable()).start();
      1. new MyAsyncTask(this).execute();
 
其他套路外会导致内存泄漏的场景(其实深究原因也是套路内的)
  1. MainActivity和HomeFragment EventBus没有反注册
    1. 解决方案: onDestory注意反注册
  2. 资源未关闭造成的内存泄漏
    1. 对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏
  3. 有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存
    1. Bitmap没调用recycle()
      1. Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后将它设置为null。因为加载Bitmap对象的内存空间,一部分是java的,一部分C的(因为Bitmap分配的底层是通过JNI调用的)。而这个recycle()就是针对C部分的内存释放
    2. ViewHolder
      1. 构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用ViewHolder
  4. 集合类
    1. 集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性,全局性的map等即有静态引用或final一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减
 
一些tips
  1. 在activity的onDestroy方法中手动吧所有需要置为Null的静态变量置为null
  2. 不要在类初始时初始化静态成员。可以考虑lazy初始化
  3. 尽量不要在静态内部类中使用非静态外部成员变量,使用的话也用弱引用
  4. 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ;array = null)等,最好遵循谁创建谁释放的原则
 
 

附录:
非静态内部类隐式自动持有外部类的强引用
  1. 这是个非常容易发生的情况,比如你随便new一个匿名内部类的时候就会发生,比如new一个clicklistener,new一个Runnable,new一个Handler,但是这种情况本身没有问题,问题是在搭配其他条件的时候发生的
  2. 这种情况说白了就是:非静态内部类的对象会持有外部对象的强引用
  3. 为什么会这样,很好理解: 因为内部类要能使用外部类的资源,就是通过这个引用实现的.
 
弱引用持有的写法(以handler持有acitivty为例)
public static class MyHandler extends Handler
    //声明一个弱引用对象
    WeakReference<MainActivity> mReference;
 
    MyHandler(MainActivity activity)
        //在构造器中传入Activity,创建弱引用对象
        mReference = new WeakReference<MainActivity>(activity);
    }
 
    public void handleMessage(Message msg)
        //在使用activity之前先判空处理
        if (mReference != null && mReference.get() != null)
            mReference.get().text.setText(hello word);
        }
    }
}
 
向静态方法中传入短生命周期的变量(比如acitivity)不会导致内存泄漏
  1. 场景
    1. 比如静态启动acitivity方法中传入上一个acitivity
    2. 把一些常用的或者公共方法放到一个工具类里,写成静态(static)的形式,如果这个方法需要传递一个参数(外部短生命周期对象的引用)的话
  2. 原因
    1. 要想造成内存泄漏,你的工具类对象本身要持有指向传入对象的引用才行!但是当你的业务方法调用工具类的静态方法时,会生产一个称为方法栈帧的东西(每次方法调用,JVM都会生成一个方法栈帧),当方法调用结束返回的时候,当前方法栈帧就已经被弹出了并且被释放掉了。 整个过程结束时,工具类对象本身并不会持有传入对象的引用。 
    2. 把对象引用传递给静态方法(不是静态方法也是一样的),在调用结束时,工具类对象本身并不会引用传入的对象。所以就没有问题。 
 
posted @ 2017-01-16 21:57  赛艇队长  阅读(2262)  评论(1编辑  收藏  举报