关于Android性能监控Matrix那些事?你知道那些(上)?
Posted 初一十五啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Android性能监控Matrix那些事?你知道那些(上)?相关的知识,希望对你有一定的参考价值。
前两天录制了两节关于android
性能监控Matrix
的视频。
1.面试中问道线上性能监控怎么办,
Android
线上监控种种
2.Matrix
卡顿监控,函数自动埋点监控方案
但是还没有完全录制完全。稍后出~今天先文字分析一下关于Matrix
的种种
文章完整版如下:
1.Matrix
介绍
Matrix
是腾讯微信终端团队开发的一套应用性能监控系统(APM
),GitHub
地址: Tencent -Matrix。
Matrix-android
当前监控范围包括:应用安装包大小、帧率变化、启动耗时、卡顿、慢方法、SQLite
操作优化、文件读写、内存泄漏等。整个库主要由 5 个组件构成:
APK Checker
。针对APK
安装包的分析检测工具,根据一系列设定好的规则,检测APK
是否存在
特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪Resource Canary
。基于WeakReference
的特性和Square Haha
库开发的Activity
泄漏和Bitmap
重复创建检测工具Trace Canary
。监控界面流畅性、启动耗时、页面切换耗时、慢函数及卡顿等问题IO Canary
。检测文件 IO 问题,包括文件IO
监控和Closeable Leak
监控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
包含 tag
、type
等通用字段,详细信息可以通过 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
填充 tag
、type
、process
、time
等通用字段,并回调 PluginListener
的 onReportIssue
方法:
@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
Matrix
和 Plugin
都监听了 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 Checker
、Resource Canary
、Trace Canary
、IO Canary
、SQLite Lint
。其中 APK Checker
独立运行;其它 4 个模块需要在 Application
中,通过统一对外接口Matrix
配置完成后执行。每一个模块相当于一个 Plugin
,在执行初始化、启动、停止、销毁、报告问题等操作时,都会回调 PluginListener
,并更新状态。
每一个 Issue
都有 tag
、type
、process
、time
等 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",
如果 DumpMode
为 AUTO_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
用于控制检测到问题后的行为,可选值有:
NO_DUMP
,是一个轻量级的模式,会回调Plugin
的onDetectIssue
方法,但只报告出现内存泄漏问题的Activity
的名称SILENCE_DUMP
,和NO_DUMP
类似,但会回调IActivityLeakCallback
MANUAL_DUMP
,用于生成一个通知,点击后跳转到对应的Activity
,Activity
由ContentIntent
指定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 Root
和 Object 1
的引用关系解除即可。对于多条 GC Root
引用链的情况,多次检测即可,这样至少保证了每次执行 ResourceCanary
模块的耗时稳定在一个可预计的范围内,不至于在极端情况下耽误其他流程。
LeakCanary
已实现了上述算法,但 Matrix
改进了其中的一些问题:
- 增加一个一定能被回收的“哨兵”对象,用来确认系统确实进行了GC
- 直接通过
WeakReference.get()
来判断对象是否已被回收,避免因延迟导致误判 - 若发现某个
Activity
无法被回收,再重复判断 3 次(0.6.5 版本的代码默认是 10 次),以防在判断时该Activity
被局部变量持有导致误判 - 对已判断为泄漏的
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
时,它会尝试解除 Activity
和InputMethodManager
、View
之间的引用关系:
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
主要的三个方法:start
、stop
、destroy
分别用于启动监听线程、停止监听线程、结束监听。以 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_dump
和 no_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
,且设置了 mDetectDebugger
为 true
,那么,还会生成一个 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 步:
- 根据
result.info
文件拿到hprof
文件、sdkVersion
等信息 - 分析
Activity
泄漏 - 分析重复
Bitmap
- 生成
result.json
文件并写入结果 - 输出重复的
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
内存泄漏检测的关键是找到最短引用路径,原理是:
- 根据
result.info
中的leakedActivityKey
字段获取Activity
结点 - 使用一个集合,存储与该
Activity
存在强引用的所有结点 - 从这些结点出发,使用宽度优先搜索算法,找到最近的一个
GC Root
,GC Root
可能是静态变量、栈帧中的本地变量、JNI 变量等重复Bitmap
检测的原理在上一篇文章有介绍,这里跳过。
总结
Resource Canary
的实现原理
- 注册
ActivityLifeCycleCallbacks
,监听onActivityDestroyed
方法,通过弱引用判断是否出现了内存泄漏,使用后台线程(MatrixHandlerThread
)周期性地检测 - 通过一个“哨兵”对象来确认系统是否进行了
GC
- 若发现某个
Activity
无法被回收,再重复判断 3 次(0.6.5 版本的代码默认是 10 次),且要求从该Activity
被记录起有 2 个以上的Activity
被创建才认为是泄漏(没发现对应的代码),以防在判断时该Activity
被局部变量持有导致误判 - 不会重复报告同一个
Activity
Resource Canary
的限制
- 只能在 Android 4.0 以上的设备运行,因为
ActivityLifeCycleCallbacks
是在 API 14 才加入进来的 - 无法分析 Android 8.0 及以上的设备的重复
Bitmap
情况,因为Bitmap
的mBuffer
字段在 API26 被移除了.
可配置的选项
DumpMode
。有no_dump
(报告Activity
类名)、silence_dump
(报告Activity
类名,回调ActivityLeakCallback
)、auto_dump
(生成堆转储文件)、manual_dump
(发送一个通知) 四 种debug
模式,只有在debug
模式下,DumpMode
才会起作用,否则会持续打印日志ContentIntent
,在DumpMode
模式为manual_dump
时,会生成一个通知,ContentIntent
可指定跳转的目标Activity
- 应用可见/不可见时监测线程的轮询间隔,默认分别是 1min、20min
MaxRedetectTimes
,只有重复检测大于等于MaxRedetectTimes
次之后,如果依然能获取到Activity
,才认为出现了内存泄漏
修复内存泄漏
在监测的同时,Resource Canary
使用 ActivityLeakFixer
尝试修复内存泄漏问题,实现原理是切断InputMethodManager
、View
和 Activity
的引用
hprof
文件处理
- 在
debug
状态下,且DumpMode
为audo_dump
时,Matrix
才会在监测到内存泄漏问题后,自动生成一个hprof
文件 - 由于原文件很大,因此
Matrix
会对该文件进行裁剪优化,并将裁剪后的hprof
文件和一个result.info
文件压缩到一个zip
包中,result.info
包括hprof
文件名、sdkVersion
、设备厂商、出现内存泄漏的Activity
类名等信息 - 拿到这个
zip
文件,输入路径参数,执行matrix-resource-canary-analyzer
中的CLIMain
程序,
即可得到一个result.json
文件,从这个文件能获取Activity
的关键引用路径、重复Bitmap
等信息
CLIMain
的解析步骤
- 根据
result.info
文件拿到hprof
文件、Activity
类名等关键信息 - 分析
Activity
泄漏 - 分析重复
Bitmap
- 生成
result.json
文件并写入结果 - 输出重复的
Bitmap
图像到本地
最短路径查找
Activity
内存泄漏检测的关键是找到最短引用路径,原理是:
- 根据
result.info
中的leakedActivityKey
字段获取Activity
结点 - 使用一个集合,存储与该
Activity
存在强引用的所有结点 - 从这些结点出发,使用宽度优先搜索算法,找到最近的一个
GC Root
,GC 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那些事?你知道那些(中)?
(十八)ATP应用测试平台——关于springboot应用监控的那些事