这是性能优化系列之matrix框架的第16
篇文章,我将在性能优化专栏中对matrix apm框架做一个全面的代码分析,性能优化是Android高级工程师必知必会的点,也是面试过程中的高频题目,对性能优化感兴趣的读者可以去我主页查看所有关于matrix的分享。
前言
在前边的15篇文章中,我们已经对卡顿监控、IO监控相关的源码与实现原理进行了全面的分析,接下来要进行的是内存泄漏方面的分析。matrix中内存泄漏的监控的实现是在matrix-resource-canary中,我们从ResourcePlugin这个入口开始,和TracePlugin一样,ResourcePlugin继承自Plugin,所以它的生命周期同样有如下几个方法:
- init
- start
- onForeground
- stop
- destroy
init
ResourcePlugin初始化的时候创建了一个ActivityRefWatcher对象,ResourcePlugin的实现都交给了ActivityRefWatcher。
typescript
@Override
public void init(Application app, PluginListener listener) {
super.init(app, listener);
mWatcher = new ActivityRefWatcher(app, this);
}
ActivityRefWatcher
在ActivityRefWatcher创建的时候,做了两件事:
- ActivityRefWatcher继承自FilePublisher,FilePublisher是用来记录有效期内已经被publish的问题页面。在ActivityRefWatcher一创建,FilePublisher就会将所有有效的本地缓存记录读取到内存中。
- 初始化其他配置信息。
ini
private ActivityRefWatcher(Application app,
ResourcePlugin resourcePlugin,
ComponentFactory componentFactory) {
super(app, FILE_CONFIG_EXPIRED_TIME_MILLIS, resourcePlugin.getTag(), resourcePlugin);
this.mResourcePlugin = resourcePlugin;
final ResourceConfig config = resourcePlugin.getConfig();
mHandlerThread = MatrixHandlerThread.getNewHandlerThread("matrix_res", Thread.NORM_PRIORITY);
mHandler = new Handler(mHandlerThread.getLooper());
//定义的dump模式
mDumpHprofMode = config.getDumpHprofMode();
mBgScanTimes = config.getBgScanIntervalMillis();
mFgScanTimes = config.getScanIntervalMillis();
mDetectExecutor = componentFactory.createDetectExecutor(config, mHandlerThread);
mMaxRedetectTimes = config.getMaxRedetectTimes();
//针对不同的dump模式设置的不同的处理类
mLeakProcessor = componentFactory.createLeakProcess(mDumpHprofMode, this);
mDestroyedActivityInfos = new ConcurrentLinkedQueue<>();
}
其中DumpHprofMode的取值范围如下,它定义了发现内存泄漏时的处理方式。
perl
NO_DUMP, // report only
AUTO_DUMP, // auto dump hprof
MANUAL_DUMP, // notify only
SILENCE_ANALYSE, // dump and analyse hprof when screen off
FORK_DUMP, // fork dump hprof immediately
FORK_ANALYSE, // fork dump and analyse hprof immediately
LAZY_FORK_ANALYZE, // fork dump immediately but analyze hprof until the screen is off
不同的DumpHprofMode使用不同的LeakProcess进行处理,这也是ResourcePlugin区别于LeakCanary的一个地方,LeakCanary是发现泄漏后直接在当前进程dump,这会导致所有线程冻结,表现上就是卡5-20s左右;而ResourcePlugin提供了多种模式,如ForkDumpProcessor会通过fork子进程的方式dump,减轻对主进程的影响,这几种实现模式将是分析的关键点。
AutoDumpProcessor
ManualDumpProcessor
SilenceAnalyseProcessor
ForkDumpProcessor
NativeForkAnalyzeProcessor
LazyForkAnalyzeProcessor
NoDumpProcessor
start
初始化完成之后开始运行,可以看到进入了ActivityRefWatcher的start方法。
scss
public void start() {
super.start();
mWatcher.start();
}
ActivityRefWatcher的start方法包含两步,首先通过调用registerActivityLifecycleCallbacks设置Activity页面生命周期的监听,然后调用scheduleDetectProcedure开启消费线程轮询发生泄漏的页面集合。这两个逻辑可以看作一个生产消费模型。
scss
@Override
public void start() {
stopDetect();
final Application app = mResourcePlugin.getApplication();
if (app != null) {
app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
scheduleDetectProcedure();
}
}
生产端
registerActivityLifecycleCallbacks对Activity注册生命周期的监听了页面的onDestroy方法。
typescript
@Override
public void onActivityDestroyed(Activity activity) {
pushDestroyedActivityInfo(activity);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
triggerGc();
}
}, 2000);
}
在页面执行onDestroy后,将activity对象以弱引用包裹并存储到队列中,这里的实现和LeakCanary是有差异的,感兴趣的读者可以自行对比。然后延迟2s调用triggerGc触发Java虚拟机的垃圾回收。
java
private void pushDestroyedActivityInfo(Activity activity) {
final String key = keyBuilder.toString();
//封装为DestroyedActivityInfo,内部以WeakReference包裹activity对象
final DestroyedActivityInfo destroyedActivityInfo
= new DestroyedActivityInfo(key, activity, activityName);
mDestroyedActivityInfos.add(destroyedActivityInfo);
synchronized (mDestroyedActivityInfos) {
//通知消费者有数据了
mDestroyedActivityInfos.notifyAll();
}
}
调用triggerGc向虚拟机申请进行垃圾回收。
ini
public void triggerGc() {
long current = System.currentTimeMillis();
lastTriggeredTime = current;
Runtime.getRuntime().gc();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
Runtime.getRuntime().runFinalization();
}
所以我们可以看到onActivityDestroyed方法中会收集执行过onDestroy方法的activity对象,并以弱引用的形式存储到队列mDestroyedActivityInfos中,不断的向队列添加数据,这里属于生产端线程。
消费端
消费端指的指scheduleDetectProcedure方法创建的task-mScanDestroyedActivitiesTask,我们一步一步来看下它的execute方法。
首先,当队列为空时,消费线程休眠等待生产端产出数据。
java
if (mDestroyedActivityInfos.isEmpty()) {
synchronized (mDestroyedActivityInfos) {
try {
while (mDestroyedActivityInfos.isEmpty()) {
mDestroyedActivityInfos.wait();
}
} catch (Throwable ignored) {
}
}
return Status.RETRY;
}
当有数据产生时,调用triggerGc向虚拟机深情触发一次垃圾回收。
triggerGc
最后遍历队列中的所有数据。
scss
final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
while (infoIt.hasNext()) {
final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
//再次触发gc
triggerGc();
//gc之后已经变为null,则移除activity对象
if (destroyedActivityInfo.mActivityRef.get() == null) {
infoIt.remove();
continue;
}
//引用仍存在,检测次数+1
++destroyedActivityInfo.mDetectedCount;
//检测次数只要没有超过设定的最大次数,就继续触发gc,重复上边的流程
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
&& !mResourcePlugin.getConfig().getDetectDebugger()) {
triggerGc();
continue;
}
//走到这里说明超过了最大检测次数,但是对象依然没有被回收,则认为有泄漏发生了,开始处理
if (mLeakProcessor.process(destroyedActivityInfo)) {
infoIt.remove();
}
}
接下来就进入了泄漏的处理流程,在ResoucePlugin初始化的时候,我们提到过,根据不同的dump mode会创建出不同的Processor来处理,所以这里使用哪个Processor取决于dump mode。Processor的基类为BaseLeakProcessor,从源码中我们可以找到matrix默认的dump mode为MANUAL_DUMP,我们就以它为例来分析一下处理流程。
java
private static final DumpMode DEFAULT_DUMP_HPROF_MODE = DumpMode.MANUAL_DUMP;
ManualDumpProcessor
kotlin
@Override
public boolean process(final DestroyedActivityInfo destroyedActivityInfo) {
//再次gc,这种永不放弃的精神值得学习。如果gc后被回收了,则不必进行后续处理。
getWatcher().triggerGc();
if (destroyedActivityInfo.mActivityRef.get() == null) {
return true;
}
//记录到集合
mLeakedActivities.add(destroyedActivityInfo);
//如果设置了mute,则等进程重启后再通知,直接返回
if (isMuted) {
return true;
}
dumpAndAnalyzeAsync(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey, new ManualDumpCallback() {
@Override
public void onDumpComplete(@Nullable ManualDumpData data) {
if (data != null) {
if (!isMuted) {
//显示Notification通知
sendResultNotification(destroyedActivityInfo, data);
}
}
}
});
return true;
}
dumpAndAnalyzeAsync方法会在子线程执行dumpAndAnalyse,具体dump是怎么操作的,这里就不深究了,感兴趣的读者可以自行深入查看。
scss
private ManualDumpData dumpAndAnalyse(String activity, String key) {
getWatcher().triggerGc();
//创建一个文件用来存储dump的内容
File file = getDumpStorageManager().newHprofFile();
//开始dump, 内部会fork子进程进行dump操作
final ActivityLeakResult result = MemoryUtil.dumpAndAnalyze(file.getAbsolutePath(), key, 600);
if (result.mLeakFound) {
final String leakChain = result.toString();
//成功后上报信息
publishIssue(
SharePluginInfo.IssueType.LEAK_FOUND,
ResourceConfig.DumpMode.FORK_ANALYSE,
activity, key, leakChain, String.valueOf(result.mAnalysisDurationMs)
);
return new ManualDumpData(file.getAbsolutePath(), leakChain);
} else if (result.mFailure != null) {
publishIssue(
SharePluginInfo.IssueType.ERR_EXCEPTION,
ResourceConfig.DumpMode.FORK_ANALYSE,
activity, key, result.mFailure.toString(), "0"
);
return null;
} else {
return new ManualDumpData(file.getAbsolutePath(), null);
}
}
onForeground
应用回到前台之后,重新启动消费者线程。
scss
public void onForeground(boolean isForeground) {
if (isForeground) {
mDetectExecutor.clearTasks();
mDetectExecutor.setDelayMillis(mFgScanTimes);
mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
} else {
mDetectExecutor.setDelayMillis(mBgScanTimes);
}
}
stop
停止操作。
typescript
@Override
public void stop() {
stopDetect();
}
scss
private void stopDetect() {
final Application app = mResourcePlugin.getApplication();
if (app != null) {
app.unregisterActivityLifecycleCallbacks(mRemovedActivityMonitor);
unscheduleDetectProcedure();
}
}
destroy
销毁资源的操作。
scss
@Override
public void destroy() {
mDetectExecutor.quit();
mHandlerThread.quitSafely();
mLeakProcessor.onDestroy();
MatrixLog.i(TAG, "watcher is destroyed.");
}
总结
ResourcePlugin监控内存泄漏的方式只针对Activity,当Activity执行onDestroy之后将Activity对象用弱引用包装,并通过多次手动触发gc,检测gc之后对象是否为空的方式来判断是否发生内存泄漏,当泄漏发生时,通过fork子进程的方式进行dump,dump之后分析内存引用链,从而找出泄漏问题。