30

内存泄漏优化 - 专注于技术的研究 - SegmentFault 思否

 3 years ago
source link: https://segmentfault.com/a/1190000019591222?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

内存泄漏优化

发布于 2019-06-26

目录介绍:

  • 01.什么是内存泄漏
  • 02.内存泄漏造成什么影响
  • 03.内存泄漏检测的工具有哪些
  • 04.关于Leakcanary使用介绍
  • 05.错误使用单例造成的内存泄漏
  • 06.Handler使用不当造成内存泄漏
  • 07.Thread未关闭造成内容泄漏
  • 08.错误使用静态变量导致引用后无法销毁
  • 09.AsyncTask造成的内存泄漏
  • 10.非静态内部类创建静态实例造成内存泄漏
  • 11.不需要用的监听未移除会发生内存泄露
  • 12.资源未关闭造成的内存泄漏
  • 13.广播注册之后没有被销毁
  • 14.错误使用context上下文引起内存泄漏
  • 15.静态集合使用不当导致的内存泄漏
  • 16.动画资源未释放导致内存泄漏
  • 17.系统bug之InputMethodManager导致内存泄漏
  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计N篇[近100万字,陆续搬到网上],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong2...
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

01.什么是内存泄漏

  • 一些对象有着有限的声明周期,当这些对象所要做的事情完成了,我们希望它们会被垃圾回收器回收掉。但是如果有一系列对这个对象的引用存在,那么在我们期待这个对象生命周期结束时被垃圾回收器回收的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。
  • 比如:当Activity的onDestroy()方法被调用后,Activity以及它涉及到的View和相关的Bitmap都应该被回收掉。但是,如果有一个后台线程持有这个Activity的引用,那么该Activity所占用的内存就不能被回收,这最终将会导致内存耗尽引发OOM而让应用crash掉。

02.内存泄漏造成什么影响

  • 它是造成应用程序OOM的主要原因之一。由于android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就

03.内存泄漏检测的工具有哪些

  • 最常见的是:Leakcanary

04.关于Leakcanary使用介绍

  • leakCanary是Square开源框架,是一个Android和Java的内存泄露检测库,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知,所以可以把它理解为傻瓜式的内存泄露检测工具。通过它可以大幅度减少开发中遇到的oom问题,大大提高APP的质量。
  • 关于如何配置,这个就不说呢,网上有步骤

05.错误使用单例造成的内存泄漏

  • 在平时开发中单例设计模式是我们经常使用的一种设计模式,而在开发中单例经常需要持有Context对象,如果持有的Context对象生命周期与单例生命周期更短时,或导致Context无法被释放回收,则有可能造成内存泄漏,错误写法如下:
  • 问题引起内存泄漏代码

    public class LoginManager {
        private static LoginManager mInstance;
        private Context mContext;
    
        private LoginManager(Context context) {
            this.mContext = context;          
            //修改代码:this.mContext = context.getApplicationContext();
        }
    
        public static LoginManager getInstance(Context context) {
            if (mInstance == null) {
                synchronized (LoginManager.class) {
                    if (mInstance == null) {
                        mInstance = new LoginManager(context);
                    }
                }
            }
            return mInstance;
        }
    
        public void dealData() {}
    }
    • 在一个Activity中调用的,然后关闭该Activity则会出现内存泄漏。
    LoginManager.getInstance(this).dealData();
  • 看看报错截图

    • image
  • 解决办法:

    • 要保证Context和AppLication的生命周期一样,修改后代码如下:
    • this.mContext = context.getApplicationContext();
    • 1、如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
    • 2、如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

06.Handler使用不当造成内存泄漏

  • handler是工作线程与UI线程之间通讯的桥梁,只是现在大量开源框架对其进行了封装,我们这里模拟一种常见使用方式来模拟内存泄漏情形。
  • 解决Handler内存泄露主要2点

    • 有延时消息,要在Activity销毁的时候移除Messages
    • 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。
  • public class MainActivity extends AppCompatActivity {
        private Handler mHandler = new Handler();
        private TextView mTextView;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTextView = (TextView) findViewById(R.id.text);        //模拟内存泄露
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mTextView.setText("yangchong");
                }
            }, 2000);
        }
    }
  • 造成内存泄漏原因分析

    • 上述代码通过内部类的方式创建mHandler对象,此时mHandler会隐式地持有一个外部类对象引用这里就是MainActivity,当执行postDelayed方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
  • 查看报错结果如下:

    • image
    • 第一种解决办法

      • 要想避免Handler引起内存泄漏问题,需要我们在Activity关闭退出的时候的移除消息队列中所有消息和所有的Runnable。
      • 上述代码只需在onDestroy()函数中调用mHandler.removeCallbacksAndMessages(null);就行了。
      @Override
      protected void onDestroy() {
          super.onDestroy();
          if(handler!=null){
              handler.removeCallbacksAndMessages(null);
              handler = null;
          }
      }
    • 第二种解决方案

      //自定义handler
      public static class HandlerHolder extends Handler {
          WeakReference<OnReceiveMessageListener> mListenerWeakReference;
          /**
           * @param listener 收到消息回调接口
           */
          HandlerHolder(OnReceiveMessageListener listener) {
              mListenerWeakReference = new WeakReference<>(listener);
          }
      
          @Override
          public void handleMessage(Message msg) {
              if (mListenerWeakReference!=null && mListenerWeakReference.get()!=null){
                  mListenerWeakReference.get().handlerMessage(msg);
              }
          }
      }
      
      //创建handler对象
      private HandlerHolder handler = new HandlerHolder(new OnReceiveMessageListener() {
          @Override
          public void handlerMessage(Message msg) {
              switch (msg.what){
                  case 1:
                      TextView textView1 = (TextView) msg.obj;
                      showBottomInAnimation(textView1);
                      break;
                  case 2:
                      TextView textView2 = (TextView) msg.obj;
                      showBottomOutAnimation(textView2);
                      break;
              }
          }
      });
      
      //发送消息
      Message message = new Message();
      message.what = 1;
      message.obj = textView;
      handler.sendMessageDelayed(message,time);
      
      
      即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。

07.Thread未关闭造成内容泄漏

  • 当在开启一个子线程用于执行一个耗时操作后,此时如果改变配置(例如横竖屏切换)导致了Activity重新创建,一般来说旧Activity就将交给GC进行回收。但如果创建的线程被声明为非静态内部类或者匿名类,那么线程会保持有旧Activity的隐式引用。当线程的run()方法还没有执行结束时,线程是不会被销毁的,因此导致所引用的旧的Activity也不会被销毁,并且与该Activity相关的所有资源文件也不会被回收,因此造成严重的内存泄露。
  • 因此总结来看, 线程产生内存泄露的主要原因有两点:

    • 1.线程生命周期的不可控。Activity中的Thread和AsyncTask并不会因为Activity销毁而销毁,Thread会一直等到run()执行结束才会停止,AsyncTask的doInBackground()方法同理
    • 2.非静态的内部类和匿名类会隐式地持有一个外部类的引用
  • 例如如下代码,在onCreate()方法中启动一个线程,并用一个静态变量threadIndex标记当前创建的是第几个线程

    public class ThreadActivity extends AppCompatActivity {
    
        private final String TAG = "ThreadActivity";
    
        private static int threadIndex;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_thread);
            threadIndex++;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int j = threadIndex;
                    while (true) {
                        Log.e(TAG, "Hi--" + j);
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    
    }
    • 旋转几次屏幕,可以看到输出结果为:
    04-04 08:15:16.373 23731-23911/com.yc.leakdemo E/ThreadActivity: Hi--2
    04-04 08:15:16.374 23731-26132/com.yc.leakdemo E/ThreadActivity: Hi--4
    04-04 08:15:16.374 23731-23970/com.yc.leakdemo E/ThreadActivity: Hi--3
    04-04 08:15:16.374 23731-23820/com.yc.leakdemo E/ThreadActivity: Hi--1
    04-04 08:15:16.852 23731-26202/com.yc.leakdemo E/ThreadActivity: Hi--5
    04-04 08:15:18.374 23731-23911/com.yc.leakdemo E/ThreadActivity: Hi--2
    04-04 08:15:18.374 23731-26132/com.yc.leakdemo E/ThreadActivity: Hi--4
    04-04 08:15:18.376 23731-23970/com.yc.leakdemo E/ThreadActivity: Hi--3
    04-04 08:15:18.376 23731-23820/com.yc.leakdemo E/ThreadActivity: Hi--1
    04-04 08:15:18.852 23731-26202/com.yc.leakdemo E/ThreadActivity: Hi--5
    ...
    • 即使创建了新的Activity,旧的Activity中建立的线程依然还在执行,从而导致无法释放Activity占用的内存,从而造成严重的内存泄漏。想要避免因为 Thread 造成内存泄漏,可以在 Activity 退出后主动停止 Thread
  • 例如,可以为 Thread 设置一个布尔变量 threadSwitch 来控制线程的启动与停止

    public class ThreadActivity extends AppCompatActivity {
    
        private final String TAG = "ThreadActivity";
    
        private int threadIndex;
    
        private boolean threadSwitch = true;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_thread);
            threadIndex++;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int j = threadIndex;
                    while (threadSwitch) {
                        Log.e(TAG, "Hi--" + j);
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            threadSwitch = false;
        }
    
    }
  • 如果想保持Thread继续运行,可以按以下步骤来:

    • 1.将线程改为静态内部类,切断Activity 对于Thread的强引用
    • 2.在线程内部采用弱引用保存Context引用,切断Thread对于Activity 的强引用
    public class ThreadActivity extends AppCompatActivity {
    
        private static final String TAG = "ThreadActivity";
    
        private static int threadIndex;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_thread);
            threadIndex++;
            new MyThread(this).start();
        }
    
        private static class MyThread extends Thread {
    
            private WeakReference<ThreadActivity> activityWeakReference;
    
            MyThread(ThreadActivity threadActivity) {
                activityWeakReference = new WeakReference<>(threadActivity);
            }
    
            @Override
            public void run() {
                if (activityWeakReference == null) {
                    return;
                }
                if (activityWeakReference.get() != null) {
                    int i = threadIndex;
                    while (true) {
                        Log.e(TAG, "Hi--" + i);
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

08.错误使用静态变量导致引用后无法销毁

  • 在平时开发中,有时候我们创建了一个工具类。比如分享工具类,十分方便多处调用,因此使用静态方法是十分方便的。但是创建的对象,建议不要全局化,全局化的变量必须加上static。这样会引起内存泄漏!
    • image
    • 在Activity中引用后,关闭该Activity会导致内存泄漏
    DoShareUtil.showFullScreenShareView(PNewsContentActivity.this, title, title, shareurl, logo);
    • image
    • 静态方法中,创建对象或变量,不要全局化,全局化后的变量或者对象会导致内存泄漏;popMenuView和popMenu都不要全局化
  • 非静态内部类,静态实例化
    public class MyActivity extends AppCompatActivity {
        //静态成员变量
        public static InnerClass innerClass = null;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_my);
            innerClass = new InnerClass();
        }
    
        class InnerClass {
            public void doSomeThing() {}
        }
    }
    这里内部类InnerClass隐式的持有外部类MyActivity的引用,而在MyActivity的onCreate方法中调用了。
    这样innerClass就会在MyActivity创建的时候是有了他的引用,而innerClass是静态类型的不会被垃圾回收,
    MyActivity在执行onDestory方法的时候由于被innerClass持有了引用而无法被回收,所以这样MyActivity就总是被innerClass持有而无法回收造成内存泄露。
    
    静态变量引用不当会导致内存泄漏
    静态变量Activity和View会导致内存泄漏,在下面这段代码中对Activity的Context和TextView设置为静态对象,从而产生内存泄漏。
    public class MainActivity extends AppCompatActivity {
    
        private static Context context;
        private static TextView textView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            context = this;
            textView = new TextView(this);
        }
    }

09.AsyncTask造成的内存泄漏

  • 早时期的时候处理耗时操作多数都是采用Thread+Handler的方式,后来逐步被AsyncTask取代,直到现在采用RxJava的方式来处理异步。这里以AsyncTask为例,可能大部分人都会这样处理一个耗时操作然后通知UI更新结果:
  • public class MainActivity extends AppCompatActivity {
    
        private AsyncTask<Void, Void, Integer> asyncTask;
        private TextView mTextView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTextView = (TextView) findViewById(R.id.text);
            testAsyncTask();
            finish();
        }
    
        private void testAsyncTask() {
            asyncTask = new AsyncTask<Void, Void, Integer>() {
                @Override
                protected Integer doInBackground(Void... params) {
                    int i = 0;
                    //模拟耗时操作
                    while (!isCancelled()) {
                        i++;
                        if (i > 1000000000) {
                            break;
                        }
                        Log.e("LeakCanary", "asyncTask---->" + i);
                    }
                    return i;
                }
    
                @Override
                protected void onPostExecute(Integer integer) {
                    super.onPostExecute(integer);
                    mTextView.setText(String.valueOf(integer));
                }
            };
            asyncTask.execute();
        }
    }
  • 造成内存泄漏原因分析

    • 在处理一个比较耗时的操作时,可能还没处理结束MainActivity就执行了退出操作,但是此时AsyncTask依然持有对MainActivity的引用就会导致MainActivity无法释放回收引发内存泄漏
  • 查看报错结果如下:

    • image
    • 在使用AsyncTask时,在Activity销毁时候也应该取消相应的任务AsyncTask.cancel()方法,避免任务在后台执行浪费资源,进而避免内存泄漏的发生
    private void destroyAsyncTask() {
        if (asyncTask != null && !asyncTask.isCancelled()) {
            asyncTask.cancel(true);
        }
        asyncTask = null;
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        destroyAsyncTask();
    }

10.非静态内部类创建静态实例造成内存泄漏

  • 有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法
  • private static TestResource mResource = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if(mResource == null){
            mResource = new TestResource();
        }
    }
    
    class TestResource {
         //里面代码引用上下文,Activity.this会导致内存泄漏
    }
    • 将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。
    • 这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

11.不需要用的监听未移除会发生内存泄露

  • //add监听,放到集合里面
    tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
        @Override
        public void onWindowFocusChanged(boolean b) {
            //监听view的加载,view加载出来的时候,计算他的宽高等。
        }
    });
  • //计算完后,一定要移除这个监听
    tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
  • 注意事项:

    tv.setOnClickListener();//监听执行完回收对象,不用考虑内存泄漏
    tv.getViewTreeObserver().addOnWindowFocusChangeListene,add监听,放到集合里面,需要考虑内存泄漏

12.资源未关闭造成的内存泄漏

  • BroadcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。值得注意的是,关闭的语句必须在finally中进行关闭,否则有可能因为异常未关闭资源,致使activity泄漏。

13.广播注册之后没有被销毁

  • 比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个广播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄露。因此注册广播后在Activity销毁后一定要取消注册。
  • 在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用Retrofit+RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。

    public class MeAboutActivity extends BaseActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            this.registerReceiver(mReceiver, new IntentFilter());
        }
    
        private BroadcastReceiver mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // 接收到广播需要做的逻辑
            }
        };
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            this.unregisterReceiver(mReceiver);
        }
    }

14.错误使用context上下文引起内存泄漏

  • 先来看看造成内存泄漏的代码

    • 通过查看Toast类的源码可以看到,Toast类内部的mContext指向传入的Context。而ToastUtils中的toast变量是静态类型的,其生命周期是与整个应用一样长的,从而导致activity得不到释放。因此,对Context的引用不能超过它本身的生命周期。
    /**
     * 吐司工具类    避免点击多次导致吐司多次,最后导致Toast就长时间关闭不掉了
     * @param context       注意:这里如果传入context会报内存泄漏;传递activity..getApplicationContext()
     * @param content       吐司内容
     */
    private static Toast toast;
    @SuppressLint("ShowToast")
    public static void showToast(Context context, String content) {
        if (toast == null) {
            toast = Toast.makeText(context , content, Toast.LENGTH_SHORT);
        } else {
            toast.setText(content);
        }
        toast.show();
    }
    • 是改为使用 ApplicationContext即可,因为ApplicationContext会随着应用的存在而存在,而不依赖于Activity的生命周期

15.静态集合使用不当导致的内存泄漏

  • 有时候我们需要把一些对象加入到集合容器(例如ArrayList)中,当不再需要当中某些对象时,如果不把该对象的引用从集合中清理掉,也会使得GC无法回收该对象。如果集合是static类型的话,那内存泄漏情况就会更为严重。因此,当不再需要某对象时,需要主动将之从集合中移除。

16.动画资源未释放导致内存泄漏

  • public class LeakActivity extends AppCompatActivity {
    
        private TextView textView;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_leak);
            textView = (TextView)findViewById(R.id.text_view);
            ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView,"rotation",0,360);
            objectAnimator.setRepeatCount(ValueAnimator.INFINITE);
            objectAnimator.start();
        }
    }
    • 在属性动画中有一类无限循环动画,如果在Activity中播放这类动画并且在onDestroy中去停止动画,那么这个动画将会一直播放下去,这时候Activity会被View所持有,从而导致Activity无法被释放。解决此类问题则是需要早Activity中onDestroy去去调用objectAnimator.cancel()来停止动画。
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mAnimator.cancel();
    }

17.系统bug之InputMethodManager导致内存泄漏

  • 每次从MainActivity退出程序时总会报InputMethodManager内存泄漏,原因系统中的InputMethodManager持有当前MainActivity的引用,导致了MainActivity不能被系统回收,从而导致了MainActivity的内存泄漏。查了很多资料,发现这是 Android SDK中输入法的一个Bug,在15<=API<=23中都存在,目前Google还没有解决这个Bug。

01.关于博客汇总链接

02.关于我的博客

项目开源地址:https://github.com/yangchong2...

项目开源地址:https://github.com/yangchong2...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK