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

这是性能优化系列之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创建的时候,做了两件事:

  1. ActivityRefWatcher继承自FilePublisher,FilePublisher是用来记录有效期内已经被publish的问题页面。在ActivityRefWatcher一创建,FilePublisher就会将所有有效的本地缓存记录读取到内存中。
  2. 初始化其他配置信息。
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之后分析内存引用链,从而找出泄漏问题。

相关推荐
锋风Fengfeng1 小时前
安卓15预置第三方apk时签名报错问题解决
android
User_undefined2 小时前
uniapp Native.js原生arr插件服务发送广播到uniapp页面中
android·javascript·uni-app
苏三说技术2 小时前
Redis 性能优化的18招
数据库·redis·性能优化
贵州晓智信息科技3 小时前
如何优化求职简历从模板选择到面试准备
面试·职场和发展
程序员厉飞雨3 小时前
Android R8 耗时优化
android·java·前端
百罹鸟3 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
程序猿会指北4 小时前
【鸿蒙(HarmonyOS)性能优化指南】内存分析器Allocation Profiler
性能优化·移动开发·harmonyos·openharmony·arkui·组件化·鸿蒙开发
丘狸尾4 小时前
[cisco 模拟器] ftp服务器配置
android·运维·服务器
van叶~6 小时前
探索未来编程:仓颉语言的优雅设计与无限可能
android·java·数据库·仓颉
程序猿会指北7 小时前
【鸿蒙(HarmonyOS)性能优化指南】启动分析工具Launch Profiler
c++·性能优化·harmonyos·openharmony·arkui·启动优化·鸿蒙开发