likes
comments
collection
share

Android性能优化系列-腾讯matrix-内存泄漏监控之ResourcePlugin源码分析

作者站长头像
站长
· 阅读数 35

前言

在前边的15篇文章中,我们已经对卡顿监控、IO监控相关的源码与实现原理进行了全面的分析,接下来要进行的是内存泄漏方面的分析。matrix中内存泄漏的监控的实现是在matrix-resource-canary中,我们从ResourcePlugin这个入口开始,和TracePlugin一样,ResourcePlugin继承自Plugin,所以它的生命周期同样有如下几个方法:

  • init
  • start
  • onForeground
  • stop
  • destroy

init

ResourcePlugin初始化的时候创建了一个ActivityRefWatcher对象,ResourcePlugin的实现都交给了ActivityRefWatcher。

@Override
public void init(Application app, PluginListener listener) {
    super.init(app, listener);
    mWatcher = new ActivityRefWatcher(app, this);
}

ActivityRefWatcher

在ActivityRefWatcher创建的时候,做了两件事:

  1. ActivityRefWatcher继承自FilePublisher,FilePublisher是用来记录有效期内已经被publish的问题页面。在ActivityRefWatcher一创建,FilePublisher就会将所有有效的本地缓存记录读取到内存中。
  2. 初始化其他配置信息。
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的取值范围如下,它定义了发现内存泄漏时的处理方式。

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方法。

public void start() {
    super.start();
    mWatcher.start();
}

ActivityRefWatcher的start方法包含两步,首先通过调用registerActivityLifecycleCallbacks设置Activity页面生命周期的监听,然后调用scheduleDetectProcedure开启消费线程轮询发生泄漏的页面集合。这两个逻辑可以看作一个生产消费模型。

@Override
public void start() {
    stopDetect();
    final Application app = mResourcePlugin.getApplication();
    if (app != null) {
        app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
        scheduleDetectProcedure();
    }
}

生产端

registerActivityLifecycleCallbacks对Activity注册生命周期的监听了页面的onDestroy方法。

@Override
public void onActivityDestroyed(Activity activity) {
    pushDestroyedActivityInfo(activity);
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            triggerGc();
        }
    }, 2000);
}

在页面执行onDestroy后,将activity对象以弱引用包裹并存储到队列中,这里的实现和LeakCanary是有差异的,感兴趣的读者可以自行对比。然后延迟2s调用triggerGc触发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向虚拟机申请进行垃圾回收。

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方法。

首先,当队列为空时,消费线程休眠等待生产端产出数据。

if (mDestroyedActivityInfos.isEmpty()) {
    synchronized (mDestroyedActivityInfos) {
        try {
            while (mDestroyedActivityInfos.isEmpty()) {
                mDestroyedActivityInfos.wait();
            }
        } catch (Throwable ignored) {
        }
    }
    return Status.RETRY;
}

当有数据产生时,调用triggerGc向虚拟机深情触发一次垃圾回收。

triggerGc

最后遍历队列中的所有数据。

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,我们就以它为例来分析一下处理流程。

private static final DumpMode DEFAULT_DUMP_HPROF_MODE = DumpMode.MANUAL_DUMP;

ManualDumpProcessor

@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是怎么操作的,这里就不深究了,感兴趣的读者可以自行深入查看。

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

应用回到前台之后,重新启动消费者线程。

public void onForeground(boolean isForeground) {
    if (isForeground) {
        mDetectExecutor.clearTasks();
        mDetectExecutor.setDelayMillis(mFgScanTimes);
        mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
    } else {
        mDetectExecutor.setDelayMillis(mBgScanTimes);
    }
}

stop

停止操作。

@Override
public void stop() {
    stopDetect();
}
private void stopDetect() {
    final Application app = mResourcePlugin.getApplication();
    if (app != null) {
        app.unregisterActivityLifecycleCallbacks(mRemovedActivityMonitor);
        unscheduleDetectProcedure();
    }
}

destroy

销毁资源的操作。

@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之后分析内存引用链,从而找出泄漏问题。