• 内存泄漏优化


    目录介绍:

    • 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/yangchong211/YCBlogs
    • 如果觉得好,可以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持有而无法回收造成内存泄露。
      
      静态变量引用不当会导致内存泄漏
      静态变量ActivityView会导致内存泄漏,在下面这段代码中对ActivityContextTextView设置为静态对象,从而产生内存泄漏。
      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/yangchong211/YCBlogs

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

  • 相关阅读:
    Java 线程
    杂记
    字符流分类详细介绍和各种字符流类介绍与使用 字符集
    内部类 与 匿名内部类
    Java IO流学习总结
    java的集合框架
    jquery 只能投票一次
    C# 简易版的计算器程序
    常用DBHelper类
    jQuery异步提交
  • 原文地址:https://www.cnblogs.com/yc211/p/11091504.html
Copyright © 2020-2023  润新知