从Matrix-ResourceCanary看内存快照生成-ForkAnalyseProcessor(1)

139 阅读4分钟

前文看到AutoDumpProcessor的处理逻辑主要是生成,裁剪hprof文件并回调到PluginListener中,接下来我们来看下ForkAnalyseProcessor的处理逻辑。

ForkAnalyseProcessor

 public class ForkAnalyseProcessor extends BaseLeakProcessor {
 ​
     private static final String TAG = "Matrix.LeakProcessor.ForkAnalyse";
 ​
     public ForkAnalyseProcessor(ActivityRefWatcher watcher) {
         super(watcher);
     }
 ​
     @Override
     public boolean process(DestroyedActivityInfo destroyedActivityInfo) {
         ......
         getWatcher().triggerGc();
 ​
         if (dumpAndAnalyse(
                 destroyedActivityInfo.mActivityName,
                 destroyedActivityInfo.mKey
         )) {
             getWatcher().markPublished(destroyedActivityInfo.mActivityName, false);
             return true;
         }
         return false;
     }
 ​
     private boolean dumpAndAnalyse(String activity, String key) {
 ​
         /* Dump */
 ​
         final long dumpStart = System.currentTimeMillis();
 ​
         File hprof = null;
         try {
             hprof = HprofFileManager.INSTANCE.prepareHprofFile("FAP", true);
         } catch (FileNotFoundException e) {
             MatrixLog.printErrStackTrace(TAG, e, "");
         }
 ​
         if (hprof != null) {
             if (!MemoryUtil.dump(hprof.getPath(), 600)) {
                 MatrixLog.e(TAG, String.format("heap dump for further analyzing activity with key [%s] was failed, just ignore.",
                         key));
                 return false;
             }
         }
 ​
         if (hprof == null || hprof.length() == 0) {
             publishIssue(
                     SharePluginInfo.IssueType.ERR_FILE_NOT_FOUND,
                     ResourceConfig.DumpMode.FORK_ANALYSE,
                     activity, key, "FileNull", "0");
             MatrixLog.e(TAG, "cannot create hprof file");
             return false;
         }
 ​
         MatrixLog.i(TAG, String.format("dump cost=%sms refString=%s path=%s",
                 System.currentTimeMillis() - dumpStart, key, hprof.getPath()));
 ​
         /* Analyse */
 ​
         try {
             final long analyseStart = System.currentTimeMillis();
 ​
             final ActivityLeakResult leaks = analyze(hprof, key);
             MatrixLog.i(TAG, String.format("analyze cost=%sms refString=%s",
                     System.currentTimeMillis() - analyseStart, key));
 ​
             if (leaks.mLeakFound) {
                 final String leakChain = leaks.toString();
                 publishIssue(
                         SharePluginInfo.IssueType.LEAK_FOUND,
                         ResourceConfig.DumpMode.FORK_ANALYSE,
                         activity, key, leakChain,
                         String.valueOf(System.currentTimeMillis() - dumpStart));
                 MatrixLog.i(TAG, leakChain);
             } else {
                 MatrixLog.i(TAG, "leak not found");
             }
 ​
         } catch (OutOfMemoryError error) {
             publishIssue(
                     SharePluginInfo.IssueType.ERR_ANALYSE_OOM,
                     ResourceConfig.DumpMode.FORK_ANALYSE,
                     activity, key, "OutOfMemoryError",
                     "0");
             MatrixLog.printErrStackTrace(TAG, error.getCause(), "");
         } finally {
             //noinspection ResultOfMethodCallIgnored
             hprof.delete();
         }
 ​
         /* Done */
 ​
         return true;
     }
 }

从上述代码可以看到在ForkAnalyseProcessor中,主要是通过dumpAndAnalyse来处理发现的内存泄漏问题,在该函数内,主要分为以下几步:

  1. prepareHprofFile:创建hprof文件
  2. MemoryUtil.dump:生成hprof文件内容
  3. analyze:分析hprof文件
  4. publishIssue:报告问题
  5. hprof.delete():删除hprof文件

prepareHprofFile

在HprofFileManager.INSTANCE.prepareHprofFile主要是进行hprof文件的预创建工作,包含清理历史文件,确保有足够的存储空间,判断存储空间是否可用,拼接hprof文件名等操作,这里预创建的hprof文件并没有数据内容,prepareHprofFile实现代码如下:

 @Throws(FileNotFoundException::class)
 fun prepareHprofFile(prefix: String = "", deleteSoon: Boolean = false): File {
     hprofStorageDir.prepare(deleteSoon)
     return File(hprofStorageDir, getHprofFileName(prefix))
 }

MemoryUtil.dump

MemoryUtil.dump函数主要完成了hprof文件的真实内容填充工作,代码如下所示:

 @JvmStatic
 @JvmOverloads
 fun dump(
     hprofPath: String,
     timeout: Long = DEFAULT_TASK_TIMEOUT
 ): Boolean = initSafe { exception ->
     if (exception != null) {
         error("", exception)
         return@initSafe false
     }
     return when (val pid = forkDump(hprofPath, timeout)) {
         -1 -> run {
             error("Failed to fork task executing process.")
             false
         }
         else -> run { // current process
             info("Wait for task process [${pid}] complete executing.")
             val result = waitTask(pid)
             result.exception?.let {
                 info("Task process [${pid}] complete with error: ${it.message}.")
             } ?: info("Task process [${pid}] complete without error.")
             return result.exception == null
         }
     }
 }
 ​
 private external fun forkDump(hprofPath: String, timeout: Long): Int
 private external fun waitTask(pid: Int): TaskResult

可以看到代码中主要逻辑是执行forkDump获取进程id,如果进程id为-1,则返回false,dump失败,如果进程id不为-1,则执行waitTask方法,如果返回的TaskResult对象中没有异常,说明dump成功,否则失败,而forkDump和waitTask都是native方法,接下来我们一起看下这两函数的实现。

forkDump

MemoryUtil.dump对应的native实现如下所示:

 extern "C"
 JNIEXPORT jint JNICALL
 Java_com_tencent_matrix_resource_MemoryUtil_forkDump(JNIEnv *env, jobject,
                                                      jstring java_hprof_path,
                                                      jlong timeout) {
     const std::string hprof_path = extract_string(env, java_hprof_path);
 ​
     int task_pid = fork_task("matrix_mem_dump", timeout);
     if (task_pid != 0) {
         return task_pid;
     } else {
         /* dump生成hprof文件 */
         execute_dump(hprof_path.c_str());
         /* 退出进程 */
         _exit(TC_NO_ERROR);
     }
 }
 static int fork_task(const char *task_name, unsigned int timeout) {
     auto *thread = current_thread();
     // 调用art::Dbg::SuspendVM()暂停进程运行
     suspend_runtime(thread);
     // fork创建进程
     int pid = fork();
     if (pid == 0) {
         task_process = true;
         if (timeout != 0) {
             alarm(timeout);
         }
         // 设置线程名称
         prctl(PR_SET_NAME, task_name);
     } else {
         // 调用art::Dbg::ResumeVM()恢复进程运行
         resume_runtime(thread);
     }
     return pid;
 }

结合注释,我们可以看出这是一段创建子进程并根据子进程pid运行逻辑的代码,那么这一段代码是怎么执行的呢?

要了解上述代码怎么执行的,我们首先应该清楚fork函数创建子进程的作用和特点,针对fork创建的子进程而言,其和父进程拥有相同的内存空间,fork函数返回值如下所示:

image-20230827113118648

可以看出fork在父进程执行时返回所创建子进程的pid信息,在子进程自身执行时返回0,结合代码,可以得到下图:

forkDump.drawio

接下来我们继续来看下子进程execute_dump和_exit的实现。

execute_dump
 static void execute_dump(const char *file_name) {
     _info_log(TAG, "task_process %d: dump", getpid());
     update_task_state(TS_DUMP);
     dump_heap(file_name);
 }
 ​
 static void (*dump_heap_)(const char *, int, bool) = nullptr;
 ​
 void dump_heap(const char *file_name) {
     dump_heap_(file_name, -1, false);
 }
 ​
 load_symbol(dump_heap_,
                 void(*)(const char *, int, bool ),
                 "_ZN3art5hprof8DumpHeapEPKcib",
                 "cannot find symbol art::hprof::DumpHeap()")

可以看到execute_dump最终调用的是art::hprof::DumpHeap()方法,Debug.dumpHprofData最终也是通过jni调用该方法,生成hprof文件。

_exit

image-20230827123154594

结合文档可以看出_exit函数主要用于停止进程运行。

waitTask
 extern "C" JNIEXPORT jobject JNICALL
 Java_com_tencent_matrix_resource_MemoryUtil_waitTask(JNIEnv *env, jobject, jint pid) {
     int status;
     // 通过waitpid等待子进程状态通知
     if (waitpid(pid, &status, 0) == -1) {
         _error_log(TAG, "invoke waitpid failed with errno %d", errno);
         return create_task_result(env, TR_TYPE_WAIT_FAILED, errno, TS_UNKNOWN, "none");
     }
 ​
     const int8_t task_state = get_task_state_and_cleanup(pid);
     const std::string task_error = get_task_error_and_cleanup(pid);
     if (WIFEXITED(status)) {
         return create_task_result(env, TR_TYPE_EXIT, WEXITSTATUS(status), task_state, task_error);
     } else if (WIFSIGNALED(status)) {
         return create_task_result(env, TR_TYPE_SIGNALED, WTERMSIG(status), task_state, task_error);
     } else {
         return create_task_result(env, TR_TYPE_UNKNOWN, 0, task_state, task_error);
     }
 }

从waitpid阻塞等待获取到子进程退出状态后,将子进程执行结果包装成TaskResult对象返回。

waitpid

waitpid说明如下图,可以看到waitpid用于阻塞当前线程执行,直到给定的pid关联的子进程状态发生改变时唤醒,唤醒后说明子进程已退出,查看子进程退出原因,并返回结果给上层,至此MemoryUtil.dump流程结束。

image-20230827124402500

image-20230827124551875