关于android性能监控matrix那些事?你知道那些(上)?(代码片段)

初一十五啊 初一十五啊     2022-10-22     145

关键词:

前两天录制了两节关于Android性能监控Matrix的视频。

1.面试中问道线上性能监控怎么办,Android线上监控种种
2.Matrix卡顿监控,函数自动埋点监控方案

但是还没有完全录制完全。稍后出~今天先文字分析一下关于Matrix的种种

文章完整版如下:

1.Matrix介绍

Matrix 是腾讯微信终端团队开发的一套应用性能监控系统(APM),GitHub 地址: Tencent -Matrix。

Matrix-android 当前监控范围包括:应用安装包大小、帧率变化、启动耗时、卡顿、慢方法、SQLite操作优化、文件读写、内存泄漏等。整个库主要由 5 个组件构成:

  1. APK Checker。针对 APK 安装包的分析检测工具,根据一系列设定好的规则,检测 APK 是否存在
    特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪
  2. Resource Canary。基于 WeakReference 的特性和 Square Haha 库开发的 Activity 泄漏和Bitmap 重复创建检测工具
  3. Trace Canary。监控界面流畅性、启动耗时、页面切换耗时、慢函数及卡顿等问题
  4. IO Canary。检测文件 IO 问题,包括文件 IO 监控和 Closeable Leak 监控
  5. SQLite Lint。按官方最佳实践自动化检测 SQLite 语句的使用质量

使用

使用Matrix 的使用方式很简单,在 Application 中初始化后启动即可:

  Matrix.Builder builder = new Matrix.Builder(this); 
  // 添加需要的插件 
  builder.plugin(new TracePlugin(...)); 
  builder.plugin(new ResourcePlugin(...)); 
  builder.plugin(new IOCanaryPlugin(...)); 
  builder.plugin(new SQLiteLintPlugin(...)); 
  // 初始化 
  Matrix matrix = Matrix.init(builder.build()); 
  // 启动 
  matrix.startAllPlugins();

Matrix 类相当于整个库的统一对外接口,资源监控、IO 监控、卡顿监控等功能实现是由其它具体的Plugin 完成的。
也可以不在 Application 中启动全部插件,而是在某个场景中启动特定的插件,比如:

  public class TestTraceMainActivity extends Activity  
     @Override 
     protected void onCreate(@Nullable Bundle savedInstanceState)  
        // 启动插件 
        Plugin plugin = Matrix.with().getPluginByClass(TracePlugin.class); 
        if (!plugin.isPluginStarted())  
            plugin.start(); 
         
     
     @Override 
     protected void onDestroy() 
          super.onDestroy(); 
          // 停止插件 
          Plugin plugin = Matrix.with().getPluginByClass(TracePlugin.class); 
          if (plugin.isPluginStarted())  
              plugin.stop(); 
           
       
  

每个具体的 Plugin 都会实现 IPlugin 接口:

  public interface IPlugin  
     Application getApplication(); 
     void init(Application application, PluginListener pluginListener); 
     void start(); 
     void stop(); 
     void destroy(); 
     String getTag(); 
     // 在应用可见/不可见时回调 
     void onForeground(boolean isForeground); 
   

可以通过 PluginListener 监听 Plugin 的生命周期变化,或在 Plugin 上报问题时回调:

  public interface PluginListener  
     void onInit(Plugin plugin); 
     void onStart(Plugin plugin); 
     void onStop(Plugin plugin); 
     void onDestroy(Plugin plugin); 
     void onReportIssue(Issue issue); 
  

上报的问题使用实体类 Issue 包装,Issue 包含 tagtype 等通用字段,详细信息可以通过 JSON 对象content 获取:

  public class Issue  
     private int type; 
     private String tag; 
     private String key; 
     private JSONObject content; 
     private Plugin plugin; 
  

源码简析

Matrix
Matrix 是一个单例类,在构造函数执行时,Matrix 内部的所有 Plugin 都会被初始化:

  public class Matrix  
     private final HashSet<Plugin> plugins; 
     private Matrix(Application app, PluginListener listener, HashSet<Plugin> plugins) 
             this.plugins = plugins; 
             AppActiveMatrixDelegate.INSTANCE.init(application); // 下面会分析 
             // 初始化所有 Plugin,并回调 pluginListener 
             for (Plugin plugin : plugins)  
                  plugin.init(application, pluginListener); 
                  pluginListener.onInit(plugin); 
              
       
  

Plugin

Plugin 是一个抽象类,每次执行 init / start / stop / destroy 等方法时都会更新状态,并回调PluginListener

  public abstract class Plugin implements IPlugin, 
  IssuePublisher.OnIssueDetectListener, IAppForeground  

      private int status = PLUGIN_CREATE; 

      @Override 
      public void init(Application app, PluginListener listener)  
         status = PLUGIN_INITED; 
         AppActiveMatrixDelegate.INSTANCE.addListener(this); // 下面会分析
      

      @Override 
      public void start()  
         status = PLUGIN_STARTED; 
         pluginListener.onStart(this); 
      
      ... 
  

如果某个具体 Plugin 上报了一个问题,父类 Plugin 还会对该 Issue 填充 tagtypeprocesstime等通用字段,并回调 PluginListeneronReportIssue 方法:

  @Override 
  public void onDetectIssue(Issue issue)  
     issue.setPlugin(this); 
     JSONObject content = issue.getContent(); 
     // 拼接 tag、type、process、time 等通用字段 
     content.put(Issue.ISSUE_REPORT_TAG, issue.getTag()); 
     content.put(Issue.ISSUE_REPORT_TYPE, issue.getType()); 
     content.put(Issue.ISSUE_REPORT_PROCESS, 
  MatrixUtil.getProcessName(application)); 
     content.put(Issue.ISSUE_REPORT_TIME, System.currentTimeMillis()); 
     // 回调 
     pluginListener.onReportIssue(issue); 
  

AppActiveMatrixDelegate

MatrixPlugin 都监听了 AppActiveMatrixDelegate,它的主要作用是在应用可见/不可见时通知观察者:

  public enum AppActiveMatrixDelegate  

     INSTANCE; // 单例 

     // 观察者列表 
     private final Set<IAppForeground> listeners = new HashSet(); 

     // 应用可见时通知观察者 
     private void onDispatchForeground(String visibleScene)  
        handler.post(() ->  
            isAppForeground = true; 
            synchronized (listeners)  
               for (IAppForeground listener : listeners)  
                    listener.onForeground(true); 
                
             
         
  

       // 应用不可见时通知观察者,逻辑和上面的一样 
      private void onDispatchBackground(String visibleScene)  
       ... 
       
  

判断应用是否可见的逻辑是通过 ActivityLifecycleCallbacks 接口实现的:

  private final class Controller implements 
  Application.ActivityLifecycleCallbacks, ComponentCallbacks2 
  
     @Override 
     public void onActivityStarted(Activity activity)  
        // 应用可见 
        updateScene(activity); 
        onDispatchForeground(getVisibleScene()); 
     
  
     @Override 
     public void onActivityStopped(Activity activity)  
        // 没有可见的 Activity 了,相当于进入了后台 
       if (getTopActivityName() == null)  
           onDispatchBackground(getVisibleScene()); 
        
     
     ... 

     @Override 
     public void onTrimMemory(int level)  
        // 应用 UI 不可见 
       if (level == TRIM_MEMORY_UI_HIDDEN && isAppForeground)  // fallback 
           onDispatchBackground(visibleScene); 
        
     
  

总结

Matrix-android 主要包含 5 个组件:APK CheckerResource CanaryTrace CanaryIO CanarySQLite Lint。其中 APK Checker 独立运行;其它 4 个模块需要在 Application 中,通过统一对外接口Matrix 配置完成后执行。每一个模块相当于一个 Plugin,在执行初始化、启动、停止、销毁、报告问题等操作时,都会回调 PluginListener,并更新状态。

每一个 Issue 都有 tagtypeprocesstime 等 4 个通用字段。

可以监听 AppActiveMatrixDelegate,在应用可见/不可见时,回调 onForeground 方法,以执行相应的操作。应用可见指的是存在可见的 Activity,应用不可见指的是没有可见的 Activity,或者内存不足了,应用的 UI 不可见

2.内存泄漏监控及原理介绍

ResourceCanary 介绍

Matrix 的内存泄漏监控是由 ResourceCanary 实现的,准确的说,ResourceCanary 只能实现 Activity的内存泄漏检测,但在出现 Activity 内存泄漏时,可以选择 dump 一个堆转储文件,通过该文件,可以分析应用是否存在重复的 Bitmap

使用

ResourceCanary 是基于 WeakReference 特性和 Square Haha 库开发的 Activity 泄漏和 Bitmap 重复创建检测工具,使用之前,需要进行如下配置:

  Matrix.Builder builder = new Matrix.Builder(this); 
  // 用于在用户点击生成的问题通知时,通过这个 Intent 跳转到指定的 Activity 
  Intent intent = new Intent(); 
  intent.setClassName(this.getPackageName(), 
  "com.tencent.mm.ui.matrix.ManualDumpActivity"); 

  ResourceConfig resourceConfig = new ResourceConfig.Builder() 
          .dynamicConfig(new DynamicConfigImplDemo()) // 用于动态获取一些自定义的选项, 不同 Plugin 有不同的选项 
          .setAutoDumpHprofMode(ResourceConfig.DumpMode.AUTO_DUMP) // 自动生成 Hprof 文件 // 
          .setDetectDebuger(true) //matrix test code 
          .setNotificationContentIntent(intent) // 问题通知 
          .build(); 
  builder.plugin(new ResourcePlugin(resourceConfig)); 

  // 这个类可用于修复一些内存泄漏问题 
  ResourcePlugin.activityLeakFixer(this);

如果想要在具体的 Activity 中检测内存泄漏,那么获取 Plugin 并执行 start 方法(一般在 onCreate 方法中执行)即可:

  Plugin plugin = Matrix.with().getPluginByClass(ResourcePlugin.class); 
  if (!plugin.isPluginStarted())  
      plugin.start(); 
  

捕获到问题后,会上报信息如下:

  
    "tag": "memory", 
    "type": 0, 
    "process": "sample.tencent.matrix", 
    "time": 1590396618440, 
    "activity": "sample.tencent.matrix.resource.TestLeakActivity", 
 

如果 DumpModeAUTO_DUMP,还会生成一个压缩文件,里面包含一个堆转储文件和一个result.info 文件,可以根据 result.info 文件发现具体是哪一个 Activity 泄漏了:

   
     "tag":"memory", 
     "process":"com.tencent.mm", "resultZipPath":"/storage/emulated/0/Android/data/com.tencent.mm/cache/matrix_r esource/dump_result_17400_20170713183615.zip", 
     "activity":"com.tencent.mm.plugin.setting.ui.setting.SettingsUI", 
  

配置

ResourcePlugin 执行之前,需要通过 ResourceConfig 配置,配置选项有:

  public static final class Builder  
    private DumpMode mDefaultDumpHprofMode = DEFAULT_DUMP_HPROF_MODE; 
    private IDynamicConfig dynamicConfig; 
    private Intent mContentIntent; 
    private boolean mDetectDebugger = false; 
  

其中, ContentIntent 用于发送通知。
DumpMode 用于控制检测到问题后的行为,可选值有:

  1. NO_DUMP,是一个轻量级的模式,会回调 PluginonDetectIssue 方法,但只报告出现内存泄漏问题的 Activity 的名称
  2. SILENCE_DUMP,和 NO_DUMP 类似,但会回调 IActivityLeakCallback
  3. MANUAL_DUMP,用于生成一个通知,点击后跳转到对应的 ActivityActivityContentIntent指定
  4. AUTO_DUMP,用于生成堆转储文件IDynamicConfig 是一个接口,可用于动态获取一些自定义的选项值:
  public interface IDynamicConfig  
     String get(String key, String defStr); 
     int get(String key, int defInt); 
     long get(String key, long defLong); 
     boolean get(String key, boolean defBool); 
     float get(String key, float defFloat); 
  

Resource Canary 相关的选项有:

  enum ExptEnum  
       //resource 
       clicfg_matrix_resource_detect_interval_millis, // 后台线程轮询间隔 
       clicfg_matrix_resource_detect_interval_millis_bg, // 应用不可见时的轮询间隔 
       clicfg_matrix_resource_max_detect_times, // 重复检测多次后才认为出现了内存泄漏,避 免误判
       clicfg_matrix_resource_dump_hprof_enable, // 没见代码有用到 
  

实现该接口对应的方法,即可通过 ResourceConfig 获取上述选项的值:

  public final class ResourceConfig  
     // 后台线程轮询间隔默认为 1min 
     private static final long DEFAULT_DETECT_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(1); 
     // 应用不可见时,后台线程轮询间隔默认为 1min
     private static final long DEFAULT_DETECT_INTERVAL_MILLIS_BG = TimeUnit.MINUTES.toMillis(20); 
     // 默认重复检测 10 次后,如果依然能获取到 Activity ,才认为出现了内存泄漏 
     private static final int DEFAULT_MAX_REDETECT_TIMES = 10; 
     public long getScanIntervalMillis()  ...  
     public long getBgScanIntervalMillis()  ...  
     public int getMaxRedetectTimes()  ...  
  

可以看到,默认情况下,Resource Canary 在应用可见(onForeground)时每隔 1 分钟检测一次,在应用不可见时每隔 20 分钟检测一次。对于同一个 Activity,在重复检测 10 次后,如果依然能通过弱引用获取,那么就认为出现了内存泄漏。

原理介绍

这部分内容摘抄自官方文档。

监测阶段

在监测阶段,对于 4.0 之前的版本,由于没有 ActivityLifecycleCallbacks,而使用反射有性能问题,使用 BaseActivity 又存在侵入性的问题,因此,ResourceCanary 放弃了对 Android 4.0 之前的版本的支持,直接使用 ActivityLifecycleCallbacks 和弱引用来检测 Activity 的内存泄漏。
分析阶段在分析阶段,由于对 Activity 的强引用链很可能不止一条,因此问题的关键在于找到最短的引用链。比如有如下引用关系:

那么,将 GC RootObject 1 的引用关系解除即可。对于多条 GC Root 引用链的情况,多次检测即可,这样至少保证了每次执行 ResourceCanary 模块的耗时稳定在一个可预计的范围内,不至于在极端情况下耽误其他流程。

LeakCanary 已实现了上述算法,但 Matrix 改进了其中的一些问题:

  1. 增加一个一定能被回收的“哨兵”对象,用来确认系统确实进行了GC
  2. 直接通过 WeakReference.get() 来判断对象是否已被回收,避免因延迟导致误判
  3. 若发现某个 Activity 无法被回收,再重复判断 3 次(0.6.5 版本的代码默认是 10 次),以防在判断时该 Activity 被局部变量持有导致误判
  4. 对已判断为泄漏的 Activity,记录其类名,避免重复提示该 Activity 已泄漏

Hprof 文件获取所有冗余的 Bitmap 对象

对于这个问题,Android Moniter 已经有完整的实现,原理简单粗暴:把所有未被回收的 Bitmap 的数据 buffer 取出来,然后先对比所有长度为 1 的 buffer,找出相同的,记录所属的 Bitmap 对象;再对比所有长度为 2 的、长度为 3 的 buffer……直到把所有 buffer 都比对完,这样就记录了所有冗余的Bitmap 对象,接着再套用 LeakCanary 获取引用链的逻辑把这些Bitmap 对象到 GC Root 的最短强引用链找出来即可。

性能开销

在监测阶段,Resource Canary 的周期性轮询是在后台线程执行的,默认轮询间隔为 1min,以微信通讯录、朋友圈界面的帧率作为参考,接入后应用的平均帧率下降了 10 帧左右,开销并不明显。但Dump Hprof 的开销较大,整个 App 会卡死约 5~15s。分析部分放到了服务器环境中执行。实际使用时分析一个 200M 左右的 Hprof 平均需要 15s 左右的时间。此部分主要消耗在引用链分析上,因为需要广度优先遍历完 Hprof 中记录的全部对象。

3.内存泄漏监控源码分析

修复内存泄漏

在开始监测 Activity 内存泄漏之前,Resource Canary 首先会尝试修复可能的内存泄漏问题,它是通过监听 ActivityLifeCycleCallbacks 实现的,在 Activity 回调 onDestroy 时,它会尝试解除 ActivityInputMethodManagerView 之间的引用关系:

  public static void activityLeakFixer(Application application)  
     application.registerActivityLifecycleCallbacks(new ActivityLifeCycleCallbacksAdapter()  
          @Override 
          public void onActivityDestroyed(Activity activity)  
            ActivityLeakFixer.fixInputMethodManagerLeak(activity); 
            ActivityLeakFixer.unbindDrawables(activity); 
           
     ); 
  

对于 InputMethodManager,它可能引用了 Activity 中的某几个 View,因此,将它和这几个 View 解除引用关系即可:

  public static void fixInputMethodManagerLeak(Context destContext)  
     final InputMethodManager imm = (InputMethodManager) 
  destContext.getSystemService(Context.INPUT_METHOD_SERVICE); 
     final String[] viewFieldNames = new String[]"mCurRootView", "mServedView", "mNextServedView"; 
     for (String viewFieldName : viewFieldNames)  
          final Field paramField = imm.getClass().getDeclaredField(viewFieldName); 
          ... 
          // 如果 IMM 引用的 View 引用了该 Activity,则切断引用关系 
          if (view.getContext() == destContext)  
              paramField.set(imm, null); 
           
      
  

对于 View,它可能通过监听器或 Drawable 的形式关联 Activity,因此,我们需要把每一个可能的引用关系解除掉:

  public static void unbindDrawables(Activity ui)  
     final View viewRoot = ui.getWindow().peekDecorView().getRootView(); 
     unbindDrawablesAndRecycle(viewRoot); 
  
  private static void unbindDrawablesAndRecycle(View view)  
      // 解除通用的 View 引用关系 
      recycleView(view);
      // 不同类型的 View 可能有不同的引用关系,一一处理即可 
      if (view instanceof ImageView)  
          recycleImageView((ImageView) view); 
      

      if (view instanceof TextView)  
          recycleTextView((TextView) view); 
      
       ... 
  
  // 将 Listener、Drawable 等可能存在的引用关系切断 
  private static void recycleView(View view)  
     view.setOnClickListener(null); 
     view.setOnFocusChangeListener(null); 
     view.getBackground().setCallback(null); 
     view.setBackgroundDrawable(null); 
     ... 
  

监测内存泄漏

具体的监测工作,ResourcePlugin 交给了 ActivityRefWatcher 来完成。
ActivityRefWatcher 主要的三个方法:startstopdestroy 分别用于启动监听线程、停止监听线程、结束监听。以 start 为例:

  public class ActivityRefWatcher extends FilePublisher implements Watcher, IAppForeground  
     @Override 
     public void start()  
        stopDetect(); 
        final Application app = mResourcePlugin.getApplication(); 
        if (app != null)  
            // 监听 Activity 的 onDestroy 回调,记录 Activity 信息 
            app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor); 
            // 监听 onForeground 回调,以便根据应用可见状态修改轮询间隔时长 
            AppActiveMatrixDelegate.INSTANCE.addListener(this); 
            // 启动监听线程 
            scheduleDetectProcedure(); 
         
      
  

记录 Activity 信息

其中 mRemovedActivityMonitor 用于在 Activity 回调 onDestroy 时记录 Activity 信息,主要包括Activity 的类名和一个根据 UUID 生成的 key

  // 用于记录 Activity 信息 
  private final ConcurrentLinkedQueue<DestroyedActivityInfo> 
  mDestroyedActivityInfos; 

  private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new ActivityLifeCycleCallbacksAdapter()  
      @Override 
      public void onActivityDestroyed(Activity activity)  
         pushDestroyedActivityInfo(activity); 
       
  ;

  // 在 Activity 销毁时,记录 Activity 信息 
  private void pushDestroyedActivityInfo(Activity activity)  
      final String activityName = activity.getClass().getName(); 
      final UUID uuid = UUID.randomUUID(); 
      final String key = keyBuilder.toString(); // 根据 uuid 生成 
      final  DestroyedActivityInfo destroyedActivityInfo = new DestroyedActivityInfo(key, activity, activityName); 
      mDestroyedActivityInfos.add(destroyedActivityInfo); 
  

DestroyedActivityInfo 包含信息如下:

  public class DestroyedActivityInfo  
     public final String mKey; // 根据 uuid 生成 
     public final String mActivityName; // 类名 
     public final WeakReference<Activity> mActivityRef; // 弱引用 
     public int mDetectedCount = 0; // 重复检测次数,默认检测 10 次后,依然能通过弱引用获 取,才认为发生了内存泄漏 
  

启动监听线程

线程启动后,应用可见时,默认每隔 1min(通过 IDynamicConfig 指定) 将轮询任务发送到默认的后台线程(MatrixHandlerThread)执行:

  // 自定义的线程切换机制,用于将指定的任务延时发送到主线程/后台线程执行 
  private final RetryableTaskExecutor mDetectExecutor; 

  private ActivityRefWatcher(...)  
      HandlerThread handlerThread = MatrixHandlerThread.getDefaultHandlerThread(); 
      mDetectExecutor = new RetryableTaskExecutor(config.getScanIntervalMillis(), handlerThread); 
  
  private void scheduleDetectProcedure()  
      // 将任务发送到 MatrixHandlerThread 执行 
      mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask); 
  

下面看轮询任务 mScanDestroyedActivitiesTask,它是一个内部类,代码很长,我们一点一点分析

设置哨兵检测 GC 是否执行

首先,在上一篇文章关于原理的部分介绍过,ResourceCanary 会设置了一个哨兵元素,检测是否真的执行了 GC,如果没有,它不会往下执行:

  private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask()  
      @Override 
      public Status execute()  
         // 创建指向一个临时对象的弱引用 
         final WeakReference<Object> sentinelRef = new WeakReference<>(new Object());
         // 尝试触发 GC 
         triggerGc(); 
         // 检测弱引用指向的对象是否存活来判断虚拟机是否真的执行了GC 
         if (sentinelRef.get() != null)  
             // System ignored our gc request, we will retry later. 
             return Status.RETRY; 
         
         ... 
         return Status.RETRY; // 返回 retry,这个任务会一直执行 
       
  ;
  private void triggerGc()  
      Runtime.getRuntime().gc(); 
      Runtime.getRuntime().runFinalization(); 
  

过滤已上报的 Activity

接着,遍历所有 DestroyedActivityInfo,并标记该 Activity,避免重复报:

  final Iterator<DestroyedActivityInfo> infoIt = 
  mDestroyedActivityInfos.iterator(); 

  while (infoIt.hasNext())  
     if (!mResourcePlugin.getConfig().getDetectDebugger() 
             && isPublished(destroyedActivityInfo.mActivityName) // 如果已标记,则跳 过 
             && mDumpHprofMode != ResourceConfig.DumpMode.SILENCE_DUMP)  
         infoIt.remove(); 
         continue; 
     
     if (mDumpHprofMode == ResourceConfig.DumpMode.SILENCE_DUMP)  
         if (mResourcePlugin != null && 
  !isPublished(destroyedActivityInfo.mActivityName))  // 如果已标记,则跳过 
          ... 
         
         if (null != activityLeakCallback)  // 但还会回调 ActivityLeakCallback 
             activityLeakCallback.onLeak(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey); 
          
      else if (mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP)  
         ... 
         markPublished(destroyedActivityInfo.mActivityName); // 标记 
      else if (mDumpHprofMode == ResourceConfig.DumpMode.MANUAL_DUMP)  
         ... 
         markPublished(destroyedActivityInfo.mActivityName); // 标记 
      else  // NO_DUMP 
         ... 
         markPublished(destroyedActivityInfo.mActivityName); // 标记 
      
  

多次检测,避免误判

同时,在重复检测大于等于 mMaxRedetectTimes 次时(由 IDynamicConfig 指定,默认为 10),如果还能获取到该 Activity 的引用,才会认为出现了内存泄漏问题:

  while (infoIt.hasNext())  
     ... 
     // 获取不到,Activity 已回收 
     if (destroyedActivityInfo.mActivityRef.get() == null)  
         continue; 
     

     // Activity 未回收,可能出现了内存泄漏,但为了避免误判,需要重复检测多次,如果都能获取到 Activity,才认为出现了内存泄漏 
     // 只有在 debug 模式下,才会上报问题,否则只会打印一个 log 
     ++destroyedActivityInfo.mDetectedCount; 
     if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes 
            || !mResourcePlugin.getConfig().getDetectDebugger())  
         MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \\n" 
                 + "exists in %s times, wait for next detection to confirm.", 
            destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount); 
         continue; 
      
  

需要注意的是,只有在 debug 模式下,才会上报问题,否则只会打印一个 log。上报问题
对于 silence_dumpno_dump 模式,它只会记录 Activity 名,并回调 onDetectIssue

  final JSONObject resultJson = new JSONObject(); 
  resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, 
  destroyedActivityInfo.mActivityName); 
  mResourcePlugin.onDetectIssue(new Issue(resultJson));

对于 manual_dump 模式,它会使用 ResourceConfig 指定的 Intent 生成一个通知:

  ... 
  Notification notification = buildNotification(context, builder); 
  notificationManager.notify(NOTIFICATION_ID, notification);

对于 auto_dump,它会自动生成一个 hprof 文件并对该文件进行分析:

  final File hprofFile = mHeapDumper.dumpHeap(); 
  final HeapDump heapDump = new HeapDump(hprofFile, 
  destroyedActivityInfo.mKey, destroyedActivityInfo.mActivityName); 
  mHeapDumpHandler.process(heapDump);

生成 hprof 文件

dumpHeap 方法做了两件事:生成一个文件,写入 Hprof 数据到文件中:

  public File dumpHeap()  
     final File hprofFile = mDumpStorageManager.newHprofFile(); 
     Debug.dumpHprofData(hprofFile.getAbsolutePath()); 
  

之后 HeapDumpHandler 就会处理该文件:

  protected AndroidHeapDumper.HeapDumpHandler createHeapDumpHandler(...)  
     return new AndroidHeapDumper.HeapDumpHandler()  
  
       @Override 
       public void process(HeapDump result)  
          CanaryWorkerService.shrinkHprofAndReport(context, result); 
        
     ; 
  

处理流程如下:

  private void doShrinkHprofAndReport(HeapDump heapDump)  
     // 裁剪 hprof 文件 
     new HprofBufferShrinker().shrink(hprofFile, shrinkedHProfFile); 
     // 压缩裁剪后的 hprof 文件 
     zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipResFile)));
     copyFileToStream(shrinkedHProfFile, zos); 
     // 删除旧文件 
     shrinkedHProfFile.delete(); hprofFile.delete(); 
     // 上报结果 
     CanaryResultService.reportHprofResult(this, zipResFile.getAbsolutePath(), heapDump.getActivityName()); 
  
   private void doReportHprofResult(String resultPath, String activityName)  
      final JSONObject resultJson = new JSONObject(); 
      resultJson.put(SharePluginInfo.ISSUE_RESULT_PATH, resultPath); 
      resultJson.put(SharePluginInfo.ISSUE_ACTIVITY_NAME, activityName); 
      Plugin plugin = Matrix.with().getPluginByClass(ResourcePlugin.class); 
      plugin.onDetectIssue(new Issue(resultJson)); 
  

可以看到,由于原始 hprof 文件很大,因此 Matrix 先对它做了一个裁剪优化,接着再压缩裁剪后的文件,并删除旧文件,最后回调 onDetectIssue,上报文件位置、Activity 名称等信息。

分析结果

示例

检测到内存泄漏问题后,ActivityRefWatcher 会打印日志如下:

activity with key 
[MATRIX_RESCANARY_REFKEY_sample.tencent.matrix.resource.TestLeakActivity_...] was suspected to be a leaked instance. mode[AUTO_DUMP]

如果模式为 AUTO_DUMP,且设置了 mDetectDebuggertrue,那么,还会生成一个 hprof 文件:

hprof: heap dump 
"/storage/emulated/0/Android/data/sample.tencent.matrix/cache/matrix_resource/du 
mp_*.hprof" starting...

裁剪压缩后在 /sdcard/data/[package name]/matrix_resource 文件夹下会生成一个 zip 文件,比如:

/storage/emulated/0/Android/data/sample.tencent.matrix/cache/matrix_resource/dum p_result_*.zip

zip 文件里包括一个 dump*shinked.hprof 文件和一个 result.info 文件,其中 result.info 包含设备信息和关键 Activity 的信息,比如:

# Resource Canary Result Infomation. THIS FILE IS IMPORTANT FOR THE ANALYZER !! 
sdkVersion=23 
manufacturer=vivo hprofEntry=dump_323ff84d95424d35b0f62ef6a3f95838_shrink.hprof 
leakedActivityKey=MATRIX_RESCANARY_REFKEY_sample.tencent.matrix.resource.TestLea 
kActivity_8c5f3e9db8b54a199da6cb2abf68bd12

拿到这个 zip 文件,输入路径参数,执行 matrix-resource-canary-analyzer 中的 CLIMain 程序,即可得到一个 result.json 文件:

   
     "activityLeakResult":  
         "failure": "null", 
         "referenceChain": ["static 
  sample.tencent.matrix.resource.TestLeakActivity testLeaks", ..., 
  "sample.tencent.matrix.resource.TestLeakActivity instance"], 
         "leakFound": true, 
         "className": "sample.tencent.matrix.resource.TestLeakActivity", 
         "analysisDurationMs": 185, 
         "excludedLeak": false 
     ,
     "duplicatedBitmapResult":  
         "duplicatedBitmapEntries": [], 
         "mFailure": "null", 
         "targetFound": false, 
         "analyzeDurationMs": 387 
      
  

注意,CLIMain 在分析重复 Bitmap 时,需要反射 Bitmap 中的 "mBuffer" 字段,而这个字段在 API 26已经被移除了,因此,对于 API 大于等于 26 的设备,CLIMain 只能分析 Activity 内存泄漏,无法分析重复 Bitmap

分析过程

下面简单分析一下 CLIMain 的执行过程,它是基于 Square Haha 开发的,执行过程分为 5 步:

  1. 根据 result.info 文件拿到 hprof 文件、sdkVersion 等信息
  2. 分析 Activity 泄漏
  3. 分析重复 Bitmap
  4. 生成 result.json 文件并写入结果
  5. 输出重复的 Bitmap 图像到本地
  public final class CLIMain  
     public static void main(String[] args)  
        doAnalyze(); 
     

      private static void doAnalyze() throws IOException  
         // 从 result.info 文件中拿到 hprof 文件、sdkVersion 等信息,接着开始分析 
        analyzeAndStoreResult(tempHprofFile, sdkVersion, manufacturer, leakedActivityKey, extraInfo);
      

      private static void analyzeAndStoreResult(...)  
         // 分析 Activity 内存泄漏 
         ActivityLeakResult activityLeakResult = new ActivityLeakAnalyzer(leakedActivityKey, ).analyze(heapSnapshot); 
         // 分析重复 Bitmap 
         DuplicatedBitmapResult duplicatedBmpResult = new DuplicatedBitmapAnalyzer(mMinBmpLeakSize, excludedBmps).analyze(heapSnapshot); 
         // 生成 result.json 文件并写入结果 
         final File resultJsonFile = new File(outputDir, resultJsonName); 
         resultJsonPW.println(resultJson.toString()); 

         // 输出重复的 Bitmap 图像 
         for (int i = 0; i < duplicatedBmpEntryCount; ++i)  
              final BufferedImage img = BitmapDecoder.getBitmap(...); 
              ImageIO.write(img, "png", os); 
          
      
  

Activity 内存泄漏检测的关键是找到最短引用路径,原理是:

  1. 根据 result.info 中的 leakedActivityKey 字段获取 Activity 结点
  2. 使用一个集合,存储与该 Activity 存在强引用的所有结点
  3. 从这些结点出发,使用宽度优先搜索算法,找到最近的一个 GC RootGC Root 可能是静态变量、栈帧中的本地变量、JNI 变量等重复 Bitmap 检测的原理在上一篇文章有介绍,这里跳过。

总结

Resource Canary 的实现原理

  1. 注册 ActivityLifeCycleCallbacks,监听 onActivityDestroyed 方法,通过弱引用判断是否出现了内存泄漏,使用后台线程(MatrixHandlerThread)周期性地检测
  2. 通过一个“哨兵”对象来确认系统是否进行了 GC
  3. 若发现某个 Activity 无法被回收,再重复判断 3 次(0.6.5 版本的代码默认是 10 次),且要求从该 Activity 被记录起有 2 个以上的 Activity 被创建才认为是泄漏(没发现对应的代码),以防在判断时该 Activity 被局部变量持有导致误判
  4. 不会重复报告同一个 Activity

Resource Canary 的限制

  1. 只能在 Android 4.0 以上的设备运行,因为 ActivityLifeCycleCallbacks 是在 API 14 才加入进来的
  2. 无法分析 Android 8.0 及以上的设备的重复 Bitmap 情况,因为 BitmapmBuffer 字段在 API26 被移除了.

可配置的选项

  1. DumpMode。有 no_dump(报告 Activity 类名)、silence_dump(报告 Activity 类名,回调ActivityLeakCallback)、auto_dump(生成堆转储文件)、manual_dump(发送一个通知) 四 种
  2. debug 模式,只有在 debug 模式下,DumpMode 才会起作用,否则会持续打印日志
  3. ContentIntent,在 DumpMode 模式为 manual_dump 时,会生成一个通知,ContentIntent 可指定跳转的目标 Activity
  4. 应用可见/不可见时监测线程的轮询间隔,默认分别是 1min、20min
  5. MaxRedetectTimes,只有重复检测大于等于 MaxRedetectTimes 次之后,如果依然能获取到Activity,才认为出现了内存泄漏

修复内存泄漏

在监测的同时,Resource Canary 使用 ActivityLeakFixer 尝试修复内存泄漏问题,实现原理是切断InputMethodManagerViewActivity 的引用
hprof 文件处理

  1. debug 状态下,且 DumpModeaudo_dump 时,Matrix 才会在监测到内存泄漏问题后,自动生成一个 hprof 文件
  2. 由于原文件很大,因此 Matrix 会对该文件进行裁剪优化,并将裁剪后的 hprof 文件和一个result.info 文件压缩到一个 zip 包中,result.info 包括 hprof 文件名、sdkVersion、设备厂商、出现内存泄漏的 Activity 类名等信息
  3. 拿到这个 zip 文件,输入路径参数,执行 matrix-resource-canary-analyzer 中的 CLIMain 程序,
    即可得到一个 result.json 文件,从这个文件能获取 Activity 的关键引用路径、重复 Bitmap 等信息

CLIMain 的解析步骤

  1. 根据 result.info 文件拿到 hprof 文件、Activity 类名等关键信息
  2. 分析 Activity 泄漏
  3. 分析重复 Bitmap
  4. 生成 result.json 文件并写入结果
  5. 输出重复的 Bitmap 图像到本地

最短路径查找

Activity 内存泄漏检测的关键是找到最短引用路径,原理是:

  1. 根据 result.info 中的 leakedActivityKey 字段获取 Activity 结点
  2. 使用一个集合,存储与该 Activity 存在强引用的所有结点
  3. 从这些结点出发,使用宽度优先搜索算法,找到最近的一个 GC RootGC Root 可能是静态变量、栈帧中的本地变量、JNI 变量等

重复 Bitmap 的分析原理

把所有未被回收的 Bitmap 的数据 buffer 取出来,然后先对比所有长度为 1 的 buffer,找出相同的,记录所属的 Bitmap 对象;再对比所有长度为 2 的、长度为 3 的 buffer……直到把所有 buffer 都比对完,这样就记录了所有冗余的 Bitmap 对象.

文字太多,下篇分析:

4.Hprof文件分析
5.卡顿监控
6.卡顿监控源码解析
7.插桩
8.资源优化
9.I/O监控及原理解析


关于android性能监控matrix那些事?你知道那些(中)?(代码片段)

昨天更新了关于Android性能监控Matrix那些事?你知道那些(上)?说的的视频也更新了:微信Matrix卡顿监控实战,函数自动埋点监控方案今天我们接着聊下文:4.Hprof文件分析5.卡顿监控6.卡顿监控源码解析7.插... 查看详情

关于性能测试的那些事

 之前有做过几次做性能测试,略有心得和大家分享一下 从测试需求开始,到完成测试,都需要经过很多阶段 首先是测试需求,要评估测试需求是否合理,并不是所有的性能测试需求都需要直接来安排测试,而是评估... 查看详情

(十八)atp应用测试平台——关于springboot应用监控的那些事(代码片段)

前言什么?你一个请求的事,就把我刚刚启动好的项目关停了,又要挨打了吧。哈哈,生活不易,求放过。放过你也行,快快告诉我你的绝招。本节内容我们主要介绍一下springboot应用的常见应用参数监控... 查看详情

(十八)atp应用测试平台——关于springboot应用监控的那些事(代码片段)

前言什么?你一个请求的事,就把我刚刚启动好的项目关停了,又要挨打了吧。哈哈,生活不易,求放过。放过你也行,快快告诉我你的绝招。本节内容我们主要介绍一下springboot应用的常见应用参数监控... 查看详情

关于android架构那些事

  刚开始,因为业务比较赶,我们也没有进行比较好的顶层设计,对代码的要求也是最低要求——完成功能开发就行了。这种短期设计也就造成了我们代码的扩展性几乎为零,稍微添加一点新功能,都要大动干戈。在后... 查看详情

关于android推送的那些事

...重。今天我们要讲的是推送这个功能, 消息推送在Android开发中应用的场景是越来越多了,比如说电商产品进行活动宣传、资讯类产品进行新闻推送等等,1.主流的第三方推送平台分类手机厂商类:小米推送、华... 查看详情

关于android推送的那些事

...重。今天我们要讲的是推送这个功能, 消息推送在Android开发中应用的场景是越来越多了,比如说电商产品进行活动宣传、资讯类产品进行新闻推送等等,1.主流的第三方推送平台分类手机厂商类:小米推送、华... 查看详情

缓存性能html5缓存的那些事

更多前端文章:http://lvtraveler.github.io/关于存储说到存储,你可能会想到这是服务器端的一种设置。服务器端的存储介质大体上分为4种:cache:缓存,它可以让从数据库、磁盘上输出的东西/数据放置在缓存里,从而减少数据库或是... 查看详情

浅谈androidmatrix使用原理(代码片段)

前言看了一下关于对Android性能监控框架Matrix的介绍九个模块的内容,已经有jym阐述过,就不对赘述了,找不到的再说.🤣分别为:Matrix介绍内存泄漏监控及原理介绍内存泄漏监控源码分析Hprof文件分析卡顿监控... 查看详情

浅谈androidmatrix使用原理(代码片段)

前言看了一下关于对Android性能监控框架Matrix的介绍九个模块的内容,已经有jym阐述过,就不对赘述了,找不到的再说.🤣分别为:Matrix介绍内存泄漏监控及原理介绍内存泄漏监控源码分析Hprof文件分析卡顿监控... 查看详情

关于pendingintent您需要知道的那些事

PendingIntent是Android框架中非常重要的组成部分,但是目前大多数与该主题相关的开发者资源更关注它的实现细节,即"PendingIntent是由系统维护的token引用",而忽略了它的用途。由于Android12对PendingIntent进行了重要... 查看详情

关于数据加载那些事

  查看详情

flutter帧率监控|由浅入深,详解获取帧率的那些事

...代码获取实时帧率的需求,这篇文章通过图解配合Flutter性能调试工具的方式一步步通俗易懂地让你明白获取帧率的基础知识,以后再也不愁看不懂调试工具上指标了。说说List<FrameTiming>Flutter中通过如下方式监听帧率,addTimin... 查看详情

关于arraylist的那些事

ArrayList初始化-Java那些事儿ArrayList初始化-Java那些事儿专栏ArrayList底层数组扩容原理-Java那些事儿专栏时间复杂度-Java那些事儿专栏三顾ArrayList-Java那些事儿专栏 查看详情

关于函数那些事第一辑

  查看详情

面试完bat等数十家公司,我想谈谈关于android面试那些事

 一.本文目的笔者将在本文中就Android开发工程师这一岗位,结合自己最近跳槽的经历,谈一谈自己对于面试的一些看法,希望能帮助到正在跳槽中的你们,也给自己的金三跳槽之旅划个圆满的分号。注:本文适合工作三年以... 查看详情

oo_unit2关于性能优化与测试的那些事(代码片段)

OO_Unit2关于性能优化与测试的那些事  OO的第2单元到本周也就正式完结了。尽管这个单元的主旋律是多线程,但“面向对象”的基本思想仍然是我们一切架构与优化的出发点与前提。因此笔者在设计优化策略时,也是本... 查看详情

关于thread的那些事

关于Thread的那些事 1 : 你能够调用线程的实例方法Join来等待一个线程的结束.比如: publicstaticvoidMainThread(){Threadt=newThread(Go);t.Start();t.Join();Console.WriteLine("Threadthasended!");}staticvoidGo(){for(inti 查看详情