zoukankan      html  css  js  c++  java
  • 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. 把对象引用传递给静态方法(不是静态方法也是一样的),在调用结束时,工具类对象本身并不会引用传入的对象。所以就没有问题。 
     
  • 相关阅读:
    Android与js交互实例
    动态规划-最长公共子序列
    android调用js
    比特币不是虚拟货币,这是一个真实世界----李笑来
    Linux进程同步之POSIX信号量
    编程至死第0天
    JMX操作ActiveMQ(2)
    Oracle层次查询和with函数的使用
    boost::asio async_write也不能保证一次发完所有数据 一
    解决Eclipse一直loading workbench无法启动的问题
  • 原文地址:https://www.cnblogs.com/bellkosmos/p/6291165.html
Copyright © 2011-2022 走看看