## LeakCanary

904 阅读4分钟
  1. 什么是内存泄漏

    内存泄漏并不是内存空间部分被移除,而是指本应该被回收的内存空间,目前无法回收(被一个长生命周期的对象所引用),从而相对本应持有空闲的内存空间就减少了,实际内存中整体空间大小并没有改变。当出现一定数量内存泄漏,程序运行所需要的内存大小大于了所能提供的最大内存,之后将会出现内存溢出(Out Of Memory)的情况。

  2. 内存泄漏场景

    • 单例持有Activity实例

      • 持有Activity的context

        public class SingleClass {
            private Context mContext;
            private volatile static SingleClass mInstance;
        
            public SingleClass(Context context) {
                mContext = context;
            }
        
            private static SingleClass getInstance(Context context) {
                if (mInstance != null) {
                    synchronized (SingleClass.class) {
                        if (mInstance != null) {
                            mInstance = new SingleClass(context);
                        }
                    }
                }
                return mInstance;
            }
        }
        

        避免方法:将Activity的context换成Application的context,我们知道Activity、Application、Service都是间接的继承自Context的,而且Application的生命周期是最长(随着应用的销毁而onDestroy)

      • 通过内部类间接持有Activity实例(容易被忽略

        public class SingleClass {
            private CharViewCallback mCharViewCallback //接口
            private volatile static SingleClass mInstance;
        
            public SingleClass(Context context) {
                mContext = context;
            }
        
            private static SingleClass getInstance(Context context) {
                if (mInstance != null) {
                    synchronized (SingleClass.class) {
                        if (mInstance != null) {
                            mInstance = new SingleClass(context);
                        }
                    }
                }
                return mInstance;
            }
          
            private static void init(CharViewCallback charViewCallback)  {
                this.mCharViewCallback = charViewCallback;
            }
        }
        
        public class TestMainActivity extends Activity {
        		
        		public void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                SingleClass.getInstance.init(new CharViewCallback() {
                		@Override
                    public void build() {
                        Log.d("CharViewCallback","you ara F-Man");
                    }
                });
            }
        }
        

        我们都知道在Java中非静态内部类或者匿名内部类都是默认持有外部类的引用的,然而此时单例类持有了该匿名内部类,因此单例类也就持有了外部类(Activity),因此同样是会导致内存泄漏(长生命周期对象持有短生命周期对象引用,导致该Activity虽然被destroy了,但是GC无法对它进行回收,内存空间无法被释放)。

    • Handler、ViewBinding在Activity、Fragment中的使用,都可能发生内存泄漏

      • viewBinding:AndroidStudio 4.0推出,替换findViewById初始化控件

        // 避免内存泄漏:在Activity、Fragment销毁时将ActivityBinding、FragmentBinding置为null
        override fun onDestroy() {
          	super.onDestroy()
          	mBinding = null
        }
        
      • Handler:线程通信的桥梁

        大家不要被搞混了!Handler内存泄漏并不是Handler持有了Activity或Fragment的引用,这只是前提条件,当Activity被销毁时,Handler仍然还在进行消息传递(可能是进行着一个比较耗时的任务),那么就会导致内存泄漏。我们要知道消息传递最终是要落地于数据或ui的变化的,但是这些操作只有在主线程上执行了我们才能直观的看出变化。

        // NO.1 
        override fun onDestroy() {
          	super.onDestroy()
          	handler.removeCallbacksAndMessages(null)
        }
        
        // NO.2 静态内部类天然不持有外部类引用
        private static class MyHandler extends Handler {
            // 弱引用解决静态内部类访问外部类内部类,避免强引用
            private WeakReference<AlaSmallGiftViewHolder> mSmallGiftViewHolder;
        
            public MyHandler(AlaSmallGiftViewHolder smallGiftViewHolder) {
              mSmallGiftViewHolder = new WeakReference<>(smallGiftViewHolder);
            }
        
            @Override
            public void handleMessage(Message message) {
              AlaSmallGiftViewHolder alaSmallGiftViewHolder = mSmallGiftViewHolder.get();
              switch (message.what) {
                case SHOW_GIFT_FLAG:
                  alaSmallGiftViewHolder.showSmallGiftView();
                  break;
                case UPDATE_GIFT_VIEW:
                  alaSmallGiftViewHolder.updateSmallGiftViewCountData();
                  break;
                case HIDE_GIFT_VIEW:
                  alaSmallGiftViewHolder.hideSmallGiftView();
                  break;
                default:
                  break;
              }
            }
        }
        

    以上是我本人目前开发中所遇到的内存泄漏比较频繁出现的例子,并不是所有内存泄漏的场景。

  3. 安装(version 2.X)

    dependencies {
      implementation 'com.squareup.leakcanary:plumber-android:2.4' //发行版本调用
      debugImplementation 'com.squareup.leakcanary:plumber-android:2.4' // 调试版本调用
    }
    

    在2.X版本开始,我们不需要在Application中LeakCanary.install(this)

    • 提醒

      1. 从版本2.0开始LeakCanary代码库从Java迁移到了Kotlin

      2. 从2.0 Beta 1开始,Shark成为LeakCanary 2新的堆分析器,相交于AndroidStudio堆分析结果占用内存空间更少,并且可以在任何Java VM中运行

      3. 重要API变更:

        AppWatcher替换了LeakSentry,

        ObjectWatcher替换了RefWatcher,

        AndroidReferenceMatchers替换了AndroidExcludedRefs,

        OnHeeapAnalyzedListener替换了AnalysisResultListener,

        AndroidObjectInspectors替换了AndroidLeakTraceInspectors

      4. 主要区别:当应用程序处于前台状态时,LeakCanary 2不会在每个保留的实例上触发。相反,它将等到应用程序进入后台或达到前台的5个保留实例的阀值。

  4. 如何使用Leaks排查内存泄漏?

    在单个内存泄漏的引用链中,我们应该关注的红色引用链,导致内存泄漏的地方就在其中某一处,这时你需要查看该红色引用链相关代码,找到能与内存泄漏场景相似的代码,修改、调试、发布。

  5. LeakCanary如何运作?

    1. 检测残留物(Detecting retained objects)

      LeakCanary对Activity、Fragment、View、ViewModel的destroy会自动检测残留对象

      AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")
      

      如果在GC完5秒以后,所观察的对象还没有被回收,那么判定该对象发生了内存泄漏,并将此记录到Logcat中,依次这样检测,直至残留对象达到规定的阀值,或者应用返回至后台(不可见状态)。

    2. 转储堆(Heap Dump)

      当残留对象的数量达到阀值时,LeakCanary将Java堆转储到存储在Android文件系统上的.hprof文件中。转储堆会使应用程序冻结一小段时间,在此期间LeakCanary会显示相应的Toast信息。

    3. 分析堆(Analyzing Heap Dump)

      .hprof使用Shark解析文件,并在该堆转储中找到保留的对象,并得到导致该对象无法被回收的引用链

      在LeakCanary 2中会对所有泄漏链进行分组(group)

    4. 泄漏分类(Categorizing leaks)

      LeakCanary将在应用程序中发现的泄漏分为两类:应用程序泄漏和库泄漏(Library Leak标签)。