zoukankan      html  css  js  c++  java
  • LeakCanary 与 鹅场Matrix ResourceCanary对比分析

    推荐阅读:

    滴滴Booster移动App质量优化框架-学习之旅 一

    Android 模块Api化演练

    不一样视角的Glide剖析(一)

    LeakCanary是Square公司基于MAT开源的一个内存泄漏检测神器,在发生内存泄漏的时候LeakCanary会自动显示泄漏信息,现在更新了好几个版本,用kotlin语言重新实现了一遍;鹅场APM性能监控框架也集成了内存泄露模块 ResourcePlugin ,这里就两者进行对比。

    1、组件启动

    LeakCanary自动注册启动

    原理:专门定制了一个ContentProvider,来注册启动LeakCanary

    实现如下:

    /**
     * Content providers are loaded before the application class is created. [LeakSentryInstaller] is
     * used to install [leaksentry.LeakSentry] on application start.
     */
    internal class LeakSentryInstaller : ContentProvider() {
    
      override fun onCreate(): Boolean {
        CanaryLog.logger = DefaultCanaryLog()
        val application = context!!.applicationContext as Application
        InternalLeakSentry.install(application)
        return true
      }
      
      ...
    }

     ResourcePlugin 需要手动启动

    public class MatrixApplication extends Application {
        ...
        @Override
        public void onCreate() {
            super.onCreate();
            ...
            ResourcePlugin resPlugin = null;
            if (matrixEnable) {
               resPlugin = new ResourcePlugin(new ResourceConfig.Builder()
                        .dynamicConfig(dynamicConfig)
                        .setDumpHprof(false)
                        .setDetectDebuger(true)     //only set true when in sample, not in your app
                        .build())
                //resource
                builder.plugin(resPlugin );
                ResourcePlugin.activityLeakFixer(this);
    
               ...
            }
    
            Matrix.init(builder.build());
            if(resPlugin != null){
                resPlugin.start(); 
            }
    
        }
      
    }

    2、watch范围和自动watch的对象

     LeakCanary RefWatcher可以watch任何对象(包括Activity、Fragment、Fragment.View)

    class RefWatcher{
        fun watch(watchedInstance: Any) {...}
        fun watch( watchedInstance: Any,name: String) {...}
    }

    支持自动watch Activity、Fragment、Fragment.View对象

    1.自动watcher Activity

    internal class ActivityDestroyWatcher {
    private val lifecycleCallbacks =
        object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
          override fun onActivityDestroyed(activity: Activity) {
            if (configProvider().watchActivities) {
                refWatcher.watch(activity)
            }
          }
        }
    
      companion object {
        fun install(... ) {
          val activityDestroyWatcher =
            ActivityDestroyWatcher(refWatcher, configProvider) 
    application.registerActivityLifecycleCallbacks(activityDestroyWatcher.lifecycleCallbacks)
        }
      }
    }

    ActivityDestroyWatcher.install在LeakSentryInstaller.onCreate间接调用,注册ActivityLifecycleCallbacks 监听Activity的生命周期,从而实现自动watch Activity对象。

    2.自动watch Fragment、Fragment.View

    //子类有
    //SupportFragmentDestroyWatcher
    //AndroidOFragmentDestroyWatcher
    internal interface FragmentDestroyWatcher {
    
      fun watchFragments(activity: Activity)
    
      companion object {
        ...
        fun install(... ) {
        
         ...
          application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
            override fun onActivityCreated( activity: Activity,
    savedInstanceState: Bundle? ) {
              for (watcher in fragmentDestroyWatchers) {
                watcher.watchFragments(activity)
              }
            }
          })
        }
     
      }
    }

    FragmentDestroyWatcher .install在LeakSentryInstaller.onCreate间接调用,注册ActivityLifecycleCallbacks 监听Activity的生命周期函数onCreate,然后对activity.fragmentManager注册FragmentLifecycleCallbacks监听Fragment的周期函数,从而实现自动watch Fragment、Fragment.View如下:

    internal class XXXFragmentDestroyWatcher(...) : FragmentDestroyWatcher {
    
      private val fragmentLifecycleCallbacks = object : FragmentManager.FragmentLifecycleCallbacks() {
    
        override fun onFragmentViewDestroyed(
          fm: FragmentManager,
          fragment: Fragment
        ) {
          val view = fragment.view
          if (view != null && configProvider().watchFragmentViews) {
             //watcher view
             refWatcher.watch(view)
          }
        }
    
        override fun onFragmentDestroyed(
          fm: FragmentManager,
          fragment: Fragment
        ) {
          if (configProvider().watchFragments) {
            //watcher fragment
            refWatcher.watch(fragment)
          }
        }
      }
      
    
      //AndroidOFragmentDestroyWatcher
      override fun watchFragments(activity: Activity) {
        val fragmentManager = activity.fragmentManager
        fragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
      }
      
     //SupportFragmentDestroyWatcher
      override fun watchFragments(activity: Activity) {
        if (activity is FragmentActivity) {
          val supportFragmentManager = activity.supportFragmentManager
          supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleCallbacks, true)
        }
      }
    }

    从源码上可以看出,貌似只自动watch 以及Fragment,嵌套的Fragment就不行了,如果是watch其他对象(包括子Fragment),则需要手动调用 RefWatcher.watch方法。

    Replugin 只有一个ActivityRefWatcher,只支持watcher Activity,也是通过注册ActivityLifecycleCallbacks 监听Activity的生命周期,从而实现自动watcher Activity对象。

    public class ActivityRefWatcher extends FilePublisher implements Watcher {
         @Override
        public void start() {
            stopDetect();
            final Application app = mResourcePlugin.getApplication();
            if (app != null) {
                app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
                //轮询检测是否发生溢出
                scheduleDetectProcedure();
               
            }
        }

    private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter() {

    @Override
    public void onActivityDestroyed(Activity activity) {
    //push mDestroyedActivityInfos集合中,通过轮询检测对mDestroyedActivityInfos进行处理
    pushDestroyedActivityInfo(activity);
    synchronized (mDestroyedActivityInfos) {
    mDestroyedActivityInfos.notifyAll();
    }
    }
    };
     

    3、检测泄露实现

    1.检测线程

      LeakCanay检测实现,旧版本是在一个HandlerThread 轮询检测,现在发生改变,先在主线程中触发检测,由RefWatcher.watch主动触发,对activity,Fragment,Fragment.view的检测,即由生命周期触发,然后在 非主线程中进行真正的check

    现在主线中被动触发检测依据如下:

    class RefWatcher{
       
     fun watch( watchedInstance: Any,name: String) {
        ...
        watchedInstances[key] = reference
        checkRetainedExecutor.execute {
          moveToRetained(key)
        }
       }
    }
    
    
    internal object InternalLeakSentry {
    
      ...
      private val checkRetainedExecutor = Executor {
      //主线程handler mainHandler.postDelayed(it, LeakSentry.config.watchDurationMillis) } val refWatcher
    = RefWatcher( clock = clock, checkRetainedExecutor = checkRetainedExecutor, onInstanceRetained = { listener.onReferenceRetained() }, isEnabled =
    { LeakSentry.config.enabled } ) ... }

    从moveToRetained调用,最终辗转到HeapDumpTrigger的方法scheduleRetainedInstanceCheck方法,然后在非主线中进行真正check,代码如下:

    internal class HeapDumpTrigger() {
     private fun scheduleRetainedInstanceCheck(reason: String) {
        if (checkScheduled) {
          CanaryLog.d("Already scheduled retained check, ignoring ($reason)")
          return
        }
        checkScheduled = true
        //非主线程hanlder
        backgroundHandler.post {
          checkScheduled = false
          checkRetainedInstances(reason)
        }
      }
    ...
    }

    ResourcePlugin参考LeakCanary旧版本,采用线程轮询检测,依据如下:

    
    

    //ActivityRefWatcher.start
    private void scheduleDetectProcedure() {

        //检测轮询 mScanDestroyedActivitiesTask execute函数一直返回RetryableTask.Status.RETRY
      mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
    }


    class
    RetryableTaskExecutor{ private void postToBackgroundWithDelay(final RetryableTask task, final int failedAttempts) { //非主线程 handler mBackgroundHandler.postDelayed(new Runnable() { @Override public void run() { RetryableTask.Status status = task.execute(); if (status == RetryableTask.Status.RETRY) { postToBackgroundWithDelay(task, failedAttempts + 1); } } }, mDelayMillis); } }

     2、检测泄露逻辑实现

       LeakCanay Check检测

      原理:VM会将可回收的对象加入 WeakReference 关联的 ReferenceQueue 

       1)根据retainedReferenceCount > 0,触发一次gc请求,再次获取retainedReferenceCount 

     var retainedReferenceCount = refWatcher.retainedInstanceCount
    
        if (retainedReferenceCount > 0) {
          gcTrigger.runGc()
          retainedReferenceCount = refWatcher.retainedInstanceCount
        }

       2)判断retainedReferenceCount  是否大于retainedVisibleThreshold(默认为5),小于则跳过接下来的检测

    if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return

      3)根据dumpHeapWhenDebugging开关和是否在Debug调试,如果配置开关开启且在调试,则延时轮询等待,调试结束

    if (!config.dumpHeapWhenDebugging && DebuggerControl.isDebuggerAttached) {
          showRetainedCountWithDebuggerAttached(retainedReferenceCount)
          scheduleRetainedInstanceCheck("debugger was attached", WAIT_FOR_DEBUG_MILLIS)
          return
        }

      4)dump Hprof文件

     val heapDumpFile = heapDumper.dumpHeap()
     if (heapDumpFile == null) {
        showRetainedCountWithHeapDumpFailed(retainedReferenceCount)
        return
    }

      5)开启HeapAnalyzerService进行Hprof分析

    在旧版本中,在个别系统上可能存在误报,原因大致如下:

    • VM 并没有提供强制触发 GC 的 API ,通过 System.gc()Runtime.getRuntime().gc()只能“建议”系统进行 GC ,如果系统忽略了我们的 GC 请求,可回收的对象就不会被加入 ReferenceQueue

    • 将可回收对象加入 ReferenceQueue 需要等待一段时间,LeakCanary 采用延时 100ms 的做法加以规避,但似乎并不绝对管用

    • 监测逻辑是异步的,如果判断 Activity 是否可回收时某个 Activity 正好还被某个方法的局部变量持有,就会引起误判

    • 若反复进入泄漏的 Activity ,LeakCanary 会重复提示该 Activity 已泄漏

    现在这个2.0-alpha-2版本也没有进行排重,当然这个也不好说,假如一个Activity有多处泄露,且泄露原因不同,排重 就会导致漏报。

    ResourcePlugin Check检测

     原理:直接通过WeakReference.get()来判断对象是否已被回收,避免因延迟导致误判

    1)判断当前mDestroyedActivityInfos是否空,为空的话,就没必要泄露,因为是轮询,所以要防止CPU空转,浪费电

    // If destroyed activity list is empty, just wait to save power.
    while (mDestroyedActivityInfos.isEmpty()) {
        synchronized (mDestroyedActivityInfos) {
            try {
                   mDestroyedActivityInfos.wait();
            } catch (Throwable ignored) {
               // Ignored.
            }
        }
    }

    2)根据配置开关和是否在Debug调试,如果配置开关开启且在调试,跳过此次check,等待下次轮询,调试结束

    // Fake leaks will be generated when debugger is attached.
    if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) {
            MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed.");
            return Status.RETRY;
    }

    3)增加一个一定能被回收的“哨兵”对象,用来确认系统确实进行了GC,没有进行GC,则跳过此次check,等待下次轮询

    final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
    triggerGc();
    if (sentinelRef.get() != null) {
       // System ignored our gc request, we will retry later.
       MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");
       return Status.RETRY;
    }

    4)对已判断为泄漏的Activity,记录其类名,避免重复提示该Activity已泄漏,有效期一天

    final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
    if (isPublished(destroyedActivityInfo.mActivityName)) {
        MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
        infoIt.remove();
        continue;
    }

    前面已经提过排重还是有缺陷的,比如一个Activity有多处泄露,且泄露原因不同,排重 就会导致漏报

    5)若发现某个Activity无法被回收,再重复判断3次,且要求从该Activity被记录起有2个以上的Activity被创建才认为是泄漏,以防在判断时该Activity被局部变量持有导致误判

    ++destroyedActivityInfo.mDetectedCount;
    long createdActivityCountFromDestroy = mCurrentCreatedActivityCount.get() - destroyedActivityInfo.mLastCreatedActivityCount;
    if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
                        || (createdActivityCountFromDestroy < CREATED_ACTIVITY_COUNT_THRESHOLD && !mResourcePlugin.getConfig().getDetectDebugger())) {
        // Although the sentinel tell us the activity should have been recycled,
        // system may still ignore it, so try again until we reach max retry times.
       continue;
    }

    6.根据是否设置了mHeapDumper(即配置快关),若设置了,进行dumpHeap,然后开启服务CanaryWorkerService,进行shrinkHprofAndReport,否则进行简单的onDetectIssue

    if (mHeapDumper != null) {
        final File hprofFile = mHeapDumper.dumpHeap();
        if (hprofFile != null) {
            markPublished(destroyedActivityInfo.mActivityName);
            final HeapDump heapDump = new HeapDump(hprofFile, destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName);
            mHeapDumpHandler.process(heapDump);
            infoIt.remove();
        } else {
             infoIt.remove();
         }
    } else {
                       
           markPublished(destroyedActivityInfo.mActivityName);
           if (mResourcePlugin != null) {
                 ...           
                 mResourcePlugin.onDetectIssue(new Issue(resultJson));
                      
           }
    }

    4、Hprof裁剪和分析(暂时不详细分析)

     LeakCanary没有对Hprof文件进行shrink裁剪,使用haha进行解析,分析出其泄露对象的GC Root引用链,把检测和分析都放在客户端。

    ResourcePlugin只有检测和Hprof文件shrink功能,不支持在客户端Hprof文件,需要利用其分析库源码打成jar单独Hprof对进行分析,在分析过程中也可以把找出冗余Bitmap的GC ROOT链。

    裁剪Hprof文件源码见:HprofBufferShrinker().shrink

    冗余Bitmap分析器:DuplicatedBitmapAnalyzer

    Activity泄露分析器:ActivityLeakAnalyzer

    Hprof 文件的大小一般约为 Dump 时的内存占用大小,Dump 出来的 Hprof 大则一百多M,,如果不做任何处理直接将此 Hprof 文件上传到服务端,一方面会消耗大量带宽资源,另一方面服务端将 Hprof 文件长期存档时也会占用服务器的存储空间。通过分析 Hprof 文件格式可知,Hprof 文件中 buffer 区存放了所有对象的数据,包括字符串数据、所有的数组等,而我们的分析过程却只需要用到部分字符串数据和 Bitmap 的 buffer 数组,其余的 buffer 数据都可以直接剔除,这样处理之后的 Hprof 文件通常能比原始文件小 1/10 以上。

    LeakCanary 中的引用链查找算法都是针对单个目标设计的,ResourceCanary 中查找冗余 Bitmap 时可能找到多个结果,如果分别对每个结果中的 Bitmap 对象调用该算法,在访问引用关系图中的节点时会遇到非常多的重复访问的节点,降低了查找效率。ResourcePlugin 修改了 LeakCanary 的引用链查找算法,使其在一次调用中能同时查找多个目标到 GC Root 的最短引用链。

    总结 

    参考资料:

    Matrix ResourceCanary -- Activity 泄漏及Bitmap冗余检测

    如果您对博主的更新内容持续感兴趣,请关注公众号!


  • 相关阅读:
    12.python笔记之mysqldb模块
    13.python笔记之pyyaml模块
    11.python之线程,协程,进程,
    2.saltstack笔记之目标,模块,返回写入数据库
    6.django笔记之orm
    5.django笔记之form保存表单信息,动态select
    4.django笔记之admin
    docker批量删除none镜像
    docker 给none镜像打镜像
    jenkins卡在等待界面解决方法
  • 原文地址:https://www.cnblogs.com/sihaixuan/p/11140479.html
Copyright © 2011-2022 走看看