废话环节
Matrix
相信很多国内的开发者都很熟悉,不熟悉的人去 Github
上搜一下。在大致读完 它的 ResourcePlugin
源码后,有一点感触。读国内大厂的开源项目的代码,有一个明显的感触,相对于国外知名大厂的开源项目的源码,比如 Google
,Square
,确实代码质量要低很多的,持续维护性也要低很多。而且 Github
上有 Issue
和 Pull Request
都有很多没有人处理的。腾讯还是国内开源做得非常不错的尚且如此,阿里可以说是更加完蛋。所以我认为国内大厂的开源库慎用吧,除非你能够自己 fork
一份来自己维护其中的代码。我阅读 ResourcePlugin
的源码时就发现他们一个核心功能的 Bug
,本来想着提交一个 Issue
和 PR
的,但是看到 Matrix
上成吨的没有处理的 Issue
和 PR
,我也不浪费自己的时间了。虽然直接使用他们的代码要慎重,但是还是有学习的价值,我也会在文章中指出我发现的 Bug
。
Matrix
的功能分为各种 Plugin
实现,所以我们按照不同的功能的 Plugin
去阅读源码就好了,今天是阅读 ResourcePlugin
的源码,它的主要功能是检测 Activity
的泄漏,相对于泄漏检测,LeakCanary
确实强大许多。
源码阅读
说了好多废话,直接以 ResourcePlugin#start()
方法作为源码阅读的入口函数:
Java
@Override
public void start() {
super.start();
if (!isSupported()) {
MatrixLog.e(TAG, "ResourcePlugin start, ResourcePlugin is not supported, just return");
return;
}
mWatcher.start();
MatrixHandlerThread.getDefaultHandler().post(new Runnable() {
@Override
public void run() {
HprofFileManager.INSTANCE.checkExpiredFile();
}
});
}
直接调用了 ActivityRefWatcher#start()
方法:
Java
@Override
public void start() {
stopDetect();
final Application app = mResourcePlugin.getApplication();
if (app != null) {
// 监听记录销毁的 Activity
app.registerActivityLifecycleCallbacks(mRemovedActivityMonitor);
scheduleDetectProcedure();
MatrixLog.i(TAG, "watcher is started.");
}
}
然后监听 Activity
的销毁,回调对象是 mRemovedActivityMonitor
,还会通过 scheduleDetectProcedure()
方法开启一个后台任务,等下看这个后台任务。
先看看如何处理销毁的 Activity
:
Java
private final Application.ActivityLifecycleCallbacks mRemovedActivityMonitor = new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
pushDestroyedActivityInfo(activity);
// 延迟 2s 触发 gc
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
triggerGc();
}
}, 2000);
}
};
public void triggerGc() {
long current = System.currentTimeMillis();
if (mDumpHprofMode == ResourceConfig.DumpMode.NO_DUMP
&& current - lastTriggeredTime < getResourcePlugin().getConfig().getScanIntervalMillis() / 2 - 100) {
MatrixLog.v(TAG, "skip triggering gc for frequency");
return;
}
lastTriggeredTime = current;
MatrixLog.v(TAG, "triggering gc...");
Runtime.getRuntime().gc();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
MatrixLog.printErrStackTrace(TAG, e, "");
}
Runtime.getRuntime().runFinalization();
MatrixLog.v(TAG, "gc was triggered.");
}
首先通过 pushDestroyedActivityInfo()
方法将已经销毁的 Activity
添加到一个队列中,当然是以弱引用的方式持有 Activity
的实例,然后再延迟 2 秒手动触发一次 GC
,这个触发 GC
的代码和 LeakCanary
中的一样,都是来自 AOSP
中的测试代码。
继续看看上面说到的后台任务:
Java
private void scheduleDetectProcedure() {
mDetectExecutor.executeInBackground(mScanDestroyedActivitiesTask);
}
private final RetryableTask mScanDestroyedActivitiesTask = new RetryableTask() {
@Override
public Status execute() {
// If destroyed activity list is empty, just wait to save power.
// 如果当前没有被销毁的 Activity 等待
if (mDestroyedActivityInfos.isEmpty()) {
MatrixLog.i(TAG, "DestroyedActivityInfo is empty! wait...");
synchronized (mDestroyedActivityInfos) {
try {
while (mDestroyedActivityInfos.isEmpty()) {
mDestroyedActivityInfos.wait();
}
} catch (Throwable ignored) {
// Ignored.
}
}
MatrixLog.i(TAG, "DestroyedActivityInfo is NOT empty! resume check");
// 有新的被销毁的 Activity 了,重试
return Status.RETRY;
}
// Fake leaks will be generated when debugger is attached.
if (Debug.isDebuggerConnected() && !mResourcePlugin.getConfig().getDetectDebugger()) {
MatrixLog.w(TAG, "debugger is connected, to avoid fake result, detection was delayed.");
return Status.RETRY;
}
// final WeakReference<Object[]> sentinelRef = new WeakReference<>(new Object[1024 * 1024]); // alloc big object
triggerGc();
// if (sentinelRef.get() != null) {
// // System ignored our gc request, we will retry later.
// MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");
// return Status.RETRY;
// }
final Iterator<DestroyedActivityInfo> infoIt = mDestroyedActivityInfos.iterator();
// 默认 HprofMode 是 MANUAL_DUMP,DetectDebugger 为 false
while (infoIt.hasNext()) {
final DestroyedActivityInfo destroyedActivityInfo = infoIt.next();
if ((mDumpHprofMode == ResourceConfig.DumpMode.NO_DUMP || mDumpHprofMode == ResourceConfig.DumpMode.AUTO_DUMP)
&& !mResourcePlugin.getConfig().getDetectDebugger()
&& isPublished(destroyedActivityInfo.mActivityName)) {
MatrixLog.v(TAG, "activity with key [%s] was already published.", destroyedActivityInfo.mActivityName);
infoIt.remove();
continue;
}
triggerGc();
if (destroyedActivityInfo.mActivityRef.get() == null) {
// The activity was recycled by a gc triggered outside.
MatrixLog.v(TAG, "activity with key [%s] was already recycled.", destroyedActivityInfo.mKey);
infoIt.remove();
continue;
}
++destroyedActivityInfo.mDetectedCount;
if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes
&& !mResourcePlugin.getConfig().getDetectDebugger()) {
// Although the sentinel tell us the activity should have been recycled,
// system may still ignore it, so try again until we reach max retry times.
MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still exists in %s times, wait for next detection to confirm.",
destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount);
triggerGc();
continue;
}
MatrixLog.i(TAG, "activity with key [%s] was suspected to be a leaked instance. mode[%s]", destroyedActivityInfo.mKey, mDumpHprofMode);
if (mLeakProcessor == null) {
throw new NullPointerException("LeakProcessor not found!!!");
}
// 处理泄漏
if (mLeakProcessor.process(destroyedActivityInfo)) {
MatrixLog.i(TAG, "the leaked activity [%s] with key [%s] has been processed. stop polling", destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey);
infoIt.remove();
}
}
return Status.RETRY;
}
};
后台任务的逻辑就是,一直在等待被销毁的 Activity
中的队列,如果有需要检测的 Activity
就再触发一次 GC
,GC
后再检查对应的 Activity
的弱引用是否还在,如果弱引用已经指向空了,那么表示通过了检查,把它从队列中移除,如果没有指向空,那就表示出现了泄漏,就继续下一步的处理。调用了 mLeakProcessor#process()
方法去处理,默认的实现方法是 ManualDumpProcessor#process()
方法。
Java
@Override
public boolean process(final DestroyedActivityInfo destroyedActivityInfo) {
getWatcher().triggerGc();
if (destroyedActivityInfo.mActivityRef.get() == null) {
MatrixLog.v(TAG, "activity with key [%s] was already recycled.", destroyedActivityInfo.mKey);
return true;
}
MatrixLog.i(TAG, "show notification for activity leak. %s", destroyedActivityInfo.mActivityName);
if (isMuted) {
MatrixLog.i(TAG, "is muted, won't show notification util process reboot");
return true;
}
Pair<String, String> data = dumpAndAnalyse(destroyedActivityInfo.mActivityName, destroyedActivityInfo.mKey);
if (data != null) {
if (!isMuted) {
MatrixLog.i(TAG, "shown notification!!!3");
sendResultNotification(destroyedActivityInfo, data.first, data.second);
} else {
MatrixLog.i(TAG, "mute mode, notification will not be shown.");
}
}
return true;
}
继续调用 dumpAndAnalyse()
方法:
Java
private Pair<String, String> dumpAndAnalyse(String activity, String key) {
getWatcher().triggerGc();
// 生成空的 HPROF 文件
File file = null;
try {
file = HprofFileManager.INSTANCE.prepareHprofFile("MDP", false);
} catch (FileNotFoundException e) {
MatrixLog.printErrStackTrace(TAG, e, "");
}
if (file == null) {
MatrixLog.e(TAG, "prepare hprof file failed, see log above");
return null;
}
// 执行 Dump HPROF 文件和执行分析
final ActivityLeakResult result = MemoryUtil.dumpAndAnalyze(file.getAbsolutePath(), key, 600);
if (result.mLeakFound) {
final String leakChain = result.toString();
publishIssue(
SharePluginInfo.IssueType.LEAK_FOUND,
ResourceConfig.DumpMode.MANUAL_DUMP,
activity, key, leakChain, String.valueOf(result.mAnalysisDurationMs),
0,
file.getAbsolutePath()
);
return new Pair<>(file.getAbsolutePath(), leakChain);
} else if (result.mFailure != null) {
publishIssue(
SharePluginInfo.IssueType.ERR_EXCEPTION,
ResourceConfig.DumpMode.MANUAL_DUMP,
activity, key, result.mFailure.toString(), "0"
);
return null;
} else {
return new Pair<>(file.getAbsolutePath(), null);
}
}
首先生成一个空的 HPROF
文件,然后调用 MemoryUtil.dumpAndAnalyze()
方法执行 dump
和 分析 HPROF
文件。
Kotlin
@JvmStatic
@JvmOverloads
fun dumpAndAnalyze(
hprofPath: String,
referenceKey: String,
timeout: Long = DEFAULT_TASK_TIMEOUT,
): ActivityLeakResult =
createTask(hprofPath, referenceKey, timeout, ::forkDumpAndAnalyze)
Kotlin
private fun createTask(
hprofPath: String,
referenceKey: String,
timeout: Long,
forkTask: (String, String, String, Long) -> Int
): ActivityLeakResult = initSafe { exception ->
if (exception != null) return@initSafe ActivityLeakResult.failure(exception, 0)
val analyzeStart = currentTime
// 创建泄漏结果的写入文件
val resultFile = createResultFile()
?: return ActivityLeakResult.failure(
RuntimeException("Failed to create temporary analyze result file."), 0
)
val resultPath = resultFile.absolutePath
// Native 创建一个子进程去解析 HPROF 文件和找到泄漏的路径,泄漏的路径最终会写入到 resultFile 中
val leakResult = when (val pid = forkTask(hprofPath, resultPath, referenceKey, timeout)) {
-1 -> ActivityLeakResult.failure(ForkException(), 0)
else -> run { // current process
info("Wait for task process [${pid}] complete executing.")
// 等待子进程结束
val result = waitTask(pid)
if (result.exception != null) {
info("Task process [${pid}] complete with error: ${result.exception!!.message}.")
return@run ActivityLeakResult.failure(
result.exception, currentTime - analyzeStart
)
} else {
info("Task process [${pid}] complete without error.")
}
return@run try {
// 解析子进程最后的解析结果。
val chains = deserialize(resultFile)
if (chains.isEmpty()) {
// 没有泄漏
ActivityLeakResult.noLeak(currentTime - analyzeStart)
} else {
// TODO: support reporting multiple leak chain.
chains.first().run {
ActivityLeakResult.leakDetected(
false,
nodes.last().objectName,
convertToReferenceChain(),
currentTime - analyzeStart
)
}
}
} catch (throwable: Throwable) {
ActivityLeakResult.failure(throwable, currentTime - analyzeStart)
}
}
}
if (resultFile.exists()) resultFile.delete()
return@initSafe leakResult
}
这个就是关键的方法了,首先创建一个泄漏结果的写入文件,Native
的代码会将结果写入到这个文件中;然后调用 forkTask()
方法来 fork
一个子进程来 dump
和分析 HPROF
文件。然后主进程等待子进程结束,解析的结果是写在文件中,然后通过 deserialize()
方法去反序列化解析结果。
forkTask()
方法实际上是指向 forkDumpAndAnalyze()
,它是一个 Native
方法,我们后面再看这个方法,在处理任务前首先会调用 initSafe()
方法初始化,我们先看看他的实现:
Kotlin
/**
* JNI 获取 TaskResult 的 JavaClass和对应的构造函数
*/
private external fun loadJniCache()
/**
* 在 storageDirPath 目录下创建 ts 和 err 两个子文件夹
*/
private external fun syncTaskDir(storageDirPath: String)
/**
* 获取 libart.so 中触发虚拟机 dump 相关的符号地址
*/
private external fun initializeSymbol()
private val initialized: InitializeException? = run {
try {
System.loadLibrary("matrix_mem_util")
loadJniCache()
syncTaskDir(storageDir.absolutePath)
initializeSymbol()
return@run null
} catch (throwable: Throwable) {
return@run InitializeException(throwable)
}
}
执行初始化时,首先加载 so
库,然后分别调用 loadJniCache()
,syncTaskDir()
和 initializeSymbol()
方法。他们都是 Native
方法。我们一个一个看。
C++
extern "C"
JNIEXPORT void JNICALL
Java_com_tencent_matrix_resource_MemoryUtil_loadJniCache(JNIEnv *env, jobject) {
_info_log(TAG, "initialize: load JNI pointer cache");
if (task_result_constructor == nullptr) {
if (task_result_class == nullptr) {
jclass local = env->FindClass("com/tencent/matrix/resource/MemoryUtil$TaskResult");
if (local == nullptr) {
log_and_throw_runtime_exception(env, "Failed to find class TaskResult");
return;
}
// Make sure the class will not be unloaded.
// See: https://developer.android.com/training/articles/perf-jni#jclass-jmethodid-and-jfieldid
task_result_class = reinterpret_cast<jclass>(env->NewGlobalRef(local));
if (task_result_class == nullptr) {
log_and_throw_runtime_exception(env, "Failed to create global reference of class TaskResult");
return;
}
}
task_result_constructor =
env->GetMethodID(task_result_class, "<init>", "(IIBLjava/lang/String;)V");
if (task_result_constructor == nullptr) {
log_and_throw_runtime_exception(env, "Failed to find constructor of class TaskResult");
return;
}
}
}
上面的方法非常简单就是提前加载 TaskResult
类和它的构造函数,供后面使用。
C++
extern "C"
JNIEXPORT void JNICALL
Java_com_tencent_matrix_resource_MemoryUtil_syncTaskDir(JNIEnv *env, jobject, jstring path) {
_info_log(TAG, "initialize: sync and create task info directories path");
const char *value = env->GetStringUTFChars(path, nullptr);
task_state_dir = ({
std::stringstream builder;
builder << value << "/ts";
builder.str();
});
task_error_dir = ({
std::stringstream builder;
builder << value << "/err";
builder.str();
});
env->ReleaseStringUTFChars(path, value);
create_directory(env, task_state_dir.c_str());
create_directory(env, task_error_dir.c_str());
}
上面代码就是初始化后面需要用到的结果输出目录。
initializeSymbol()
方法就要复杂一些了,他主要是去拿到虚拟机的 dump
内存相关的方法地址,在 Debug
类中也有 dump
内存的方法,但是只能 debug
的包可以调用,如果想要 release
包也能调用,就需要通过 Matrix
的处理方式,我们来看看它怎么做的。
C++
extern "C"
JNIEXPORT void JNICALL
Java_com_tencent_matrix_resource_MemoryUtil_initializeSymbol(JNIEnv *env, jobject) {
_info_log(TAG, "initialize: initialize symbol");
if (!initialize_symbols()) {
log_and_throw_runtime_exception(env, "Failed to initialize symbol");
return;
}
}
C++
bool initialize_symbols() {
android_version_ = android_get_device_api_level();
if (android_version_ <= 0) return false;
ds_mode(android_version_);
// 手动加载虚拟机 libart.so 动态链接库
auto *art_lib = ds_open("libart.so");
if (art_lib == nullptr) {
_error_log(TAG, "open libart.so failed");
return false;
}
#define load_symbol(ptr, type, sym, err) \
ptr = reinterpret_cast<type>(ds_find(art_lib, sym)); \
if ((ptr) == nullptr) { \
_error_log(TAG, err); \
goto on_error; \
}
// 加载 art::hprof::DumpHeap() 符号地址,写入到 dump_heap_ 指针中。
load_symbol(dump_heap_,
void(*)(const char *, int, bool ),
"_ZN3art5hprof8DumpHeapEPKcib",
"cannot find symbol art::hprof::DumpHeap()")
if (android_version_ > __ANDROID_API_Q__) {
// 加载 ScopedGCCriticalSection 构造函数和析构函数
load_symbol(mirror::sgc_constructor,
void(*)(void * , mirror::Thread *, art::gc::GcCause, art::gc::CollectorType),
"_ZN3art2gc23ScopedGCCriticalSectionC1EPNS_6ThreadENS0_7GcCauseENS0_13CollectorTypeE",
"cannot find symbol art::gc::ScopedGCCriticalSection()")
load_symbol(mirror::sgc_destructor,
void(*)(void * ),
"_ZN3art2gc23ScopedGCCriticalSectionD1Ev",
"cannot find symbol art::gc::~ScopedGCCriticalSection()")
}
if (android_version_ > __ANDROID_API_Q__) {
mirror::ReadWriteMutex **lock_sym;
// 获取读写锁的地址
load_symbol(lock_sym,
mirror::ReadWriteMutex **,
"_ZN3art5Locks13mutator_lock_E",
"cannot find symbol art::Locks::mutator_lock_")
mutator_lock_ = *lock_sym;
// 获取 exclusive_lock() 方法
load_symbol(mirror::exclusive_lock,
void(*)(void * , mirror::Thread *),
"_ZN3art17ReaderWriterMutex13ExclusiveLockEPNS_6ThreadE",
"cannot find symbol art::ReaderWriterMutex::ExclusiveLock()")
// 获取 exclusive_unlock() 方法
load_symbol(mirror::exclusive_unlock,
void(*)(void * , mirror::Thread *),
"_ZN3art17ReaderWriterMutex15ExclusiveUnlockEPNS_6ThreadE",
"cannot find symbol art::ReaderWriterMutex::ExclusiveUnlock()")
}
if (android_version_ > __ANDROID_API_Q__) {
// 虚拟机的恢复和暂停
load_symbol(suspend_all_ptr_,
void*,
"_ZN3art16ScopedSuspendAllC1EPKcb",
"cannot find symbol art::ScopedSuspendAll()")
load_symbol(resume_all_ptr_,
void*,
"_ZN3art16ScopedSuspendAllD1Ev",
"cannot find symbol art::~ScopedSuspendAll()")
} else {
// 虚拟机的恢复和暂停
load_symbol(suspend_all_ptr_,
void*,
"_ZN3art3Dbg9SuspendVMEv",
"cannot find symbol art::Dbg::SuspendVM()")
load_symbol(resume_all_ptr_,
void*,
"_ZN3art3Dbg8ResumeVMEv",
"cannot find symbol art::Dbg::ResumeVM()")
}
ds_clean(art_lib);
return true;
on_error:
ds_close(art_lib);
return false;
}
- 首先获取虚拟机动态链接库
libart.so
的地址。 - 获取
dump
内存的方法art::hprof::DumpHeap()
的符号地址,对应的符号是_ZN3art5hprof8DumpHeapEPKcib
。 - 获取
ScopedGCCriticalSection
类的构造函数和析构函数的符号地址,对应的符号分别是_ZN3art2gc23ScopedGCCriticalSectionC1EPNS_6ThreadENS0_7GcCauseENS0_13CollectorTypeE
和_ZN3art2gc23ScopedGCCriticalSectionD1Ev
(Android 10
以后的版本,不包含Android 10
)。 - 获取
art::Locks::mutator_lock_
锁的地址,对应的符号是_ZN3art5Locks13mutator_lock_E
(Android 10
以后的版本,不包含Android 10
)。 - 获取
art::ReaderWriterMutex::ExclusiveLock()
方法和art::ReaderWriterMutex::ExclusiveUnlock()
方法的符号地址,对应的符号分别是_ZN3art17ReaderWriterMutex13ExclusiveLockEPNS_6ThreadE
和_ZN3art17ReaderWriterMutex15ExclusiveUnlockEPNS_6ThreadE
(Android 10
以后的版本,不包含Android 10
)。 - 获取暂停和恢复虚拟机的方法分为
Android 10
以后的版本和其他版本。Android 10
以后的版本对应的方法是art::ScopedSuspendAll()
和art::~ScopedSuspendAll()
,对应的符号分别是_ZN3art16ScopedSuspendAllC1EPKcb
和_ZN3art16ScopedSuspendAllD1Ev
;Android 10
及其以前的版本对应的方法是art::Dbg::SuspendVM()
和art::Dbg::ResumeVM()
,对应的符号分别是_ZN3art3Dbg9SuspendVMEv
和_ZN3art3Dbg8ResumeVMEv
。
拿到上面的需要的这些方法的地址后就供后续流程使用,如果不是看 Matrix
的源码,我们要怎么获取这些方法的符号呢??就只有去看 Android
的源码和直接在 libart.so
的符号表中找了。
OK 在看完获取 libart.so
中的某些符号地址后,我们继续看 forkDumpAndAnalyze()
方法的实现:
C++
extern "C"
JNIEXPORT jint JNICALL
Java_com_tencent_matrix_resource_MemoryUtil_forkDumpAndAnalyze(JNIEnv *env, jobject,
jstring java_hprof_path,
jstring java_result_path,
jstring java_reference_key,
jlong timeout) {
const std::string hprof_path = extract_string(env, java_hprof_path);
const std::string result_path = extract_string(env, java_result_path);
const std::string reference_key = extract_string(env, java_reference_key);
int task_pid = fork_task("matrix_mem_d&a", timeout);
if (task_pid != 0) {
// 主进程
return task_pid;
} else {
// dump 进程
/* dump */
// 执行 dump
execute_dump(hprof_path.c_str());
/* analyze */
// 分析 dump 文件,找到泄漏的路径
const std::optional<std::vector<LeakChain>> result =
execute_analyze(hprof_path.c_str(), reference_key.c_str());
if (!result.has_value()) _exit(TC_ANALYZE_ERROR);
/* serialize result */
// 将泄漏的结果写入到文件中(这是一个自定义的文件格式)
const bool serialized = execute_serialize(result_path.c_str(), result.value());
if (!serialized) _exit(TC_SERIALIZE_ERROR);
/* end */
// 结束进程
_exit(TC_NO_ERROR);
}
}
如果对 Linux
的 fork
机制一点都不懂的同学,建议先去了解一下相关的资料哈。首先调用 fork_task()
方法执行 fork
,如果是主进程就直接返回了;如果是 dump
进程就执行调用 execute_dump()
方法执行 dump
操作,然后调用 execute_analyze()
方法,执行泄漏分析,再然后将泄漏链写入到文件中,然后退出进程。
我看来看看 fork_task()
方法的实现:
C++
static int fork_task(const char *task_name, unsigned int timeout) {
auto *thread = current_thread();
// 暂停虚拟机
suspend_runtime(thread);
// fork
int pid = fork();
if (pid == 0) {
// dump 进程
task_process = true;
if (timeout != 0) {
alarm(timeout);
}
prctl(PR_SET_NAME, task_name);
} else {
// 主进程
// 恢复虚拟机
resume_runtime(thread);
}
return pid;
}
首先通过 suspend_runtime()
方法暂停虚拟机,然后执行 fork()
操作,主进程在 fork()
完成后就调用 resume_runtime()
方法恢复虚拟机。
C++
/**
* Points to symbol `art::Dbg::SuspendVM()`.
*/
static void *suspend_all_ptr_ = nullptr;
void suspend_runtime(mirror::Thread *thread) {
if (android_version_ > __ANDROID_API_Q__) {
mirror::ScopedGCCriticalSection sgc(thread, kGcCauseHprof, kCollectorTypeHprof);
reinterpret_cast<void (*)(mirror::ScopedSuspend *, const char *, bool)>
(suspend_all_ptr_)(&suspend_, "matrix_dump_hprof", true);
mutator_lock_->ExclusiveUnlock(thread);
} else {
reinterpret_cast<void (*)()>(suspend_all_ptr_)();
}
}
暂停虚拟机,如果是 Android 10
及其以前的版本就直接调用上面拿到的 art::Dbg::SuspendVM()
方法地址就行了;Android 10
以后的版本就要麻烦一点了,首先会创建 ScopedGCCriticalSection
的实例,它的构造函数和析构函数的地址前面都拿到了,然后分别再调用 art::ScopedSuspendAll()
和 art::Locks::mutator_lock_->exclusive_unlock()
方法,这两个方法的地址前面也拿到了。
再看看 resume_runtime()
方法的实现:
C++
void resume_runtime(mirror::Thread *thread) {
if (android_version_ > __ANDROID_API_Q__) {
mutator_lock_->ExclusiveLock(thread);
reinterpret_cast<void (*)(mirror::ScopedSuspend *)>(resume_all_ptr_)(&suspend_);
} else {
reinterpret_cast<void (*)()>(resume_all_ptr_)();
}
}
也是朴实无华的代码,Android 10
及其以下的版本,直接调用前面获取到的 art::Dbg::ResumeVM()
的方法就好了;Android 10
以上的版本就需要先调用 art::Locks::mutator_lock_->exclusive_lock()
然后再调用 art::~ScopedSuspendAll()
。
我们继续看看 dump
进程中的 execute_dump()
方法是如何工作的:
C++
// ! execute on task process
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);
}
void dump_heap(const char *file_name) {
dump_heap_(file_name, -1, false);
}
代码也非常简单,获取前面获取到的 art::hprof::DumpHeap()
方法的符号地址,然后直接调用方法就好了。
然后就是通过 execute_analyze()
方法解析 HPROF
文件,找到泄漏的引用路径,这个方法也就是 ResourcePlugin
插件中最最核心的方法了,如果不懂 HPROF
文件格式的同学可以看看我前面写的文章:Android HPROF 内存快照文件详解,后面具体解析 HPROF
的细节代码就不多聊了。
C++
static std::optional<std::vector<LeakChain>>
execute_analyze(const char *hprof_path, const char *reference_key) {
_info_log(TAG, "task_process %d: analyze", getpid());
update_task_state(TS_ANALYZER_CREATE);
// 打开 hprof 文件
const int hprof_fd = open(hprof_path, O_RDONLY);
if (hprof_fd == -1) {
std::stringstream error_builder;
error_builder << "invoke open() failed on HPROF with errno " << errno;
on_error(error_builder.str().c_str());
return std::nullopt;
}
HprofAnalyzer::SetErrorListener(analyzer_error_listener);
// 初始化 HprofAnalyzer 对象
HprofAnalyzer analyzer(hprof_fd);
update_task_state(TS_ANALYZER_INITIALIZE);
// 添加不需要计算泄漏的类
if (!exclude_default_references(analyzer)) {
on_error("exclude default references rules failed");
return std::nullopt;
}
update_task_state(TS_ANALYZER_EXECUTE);
// 执行分析 HPROF 文件和查找泄漏的对象
return analyzer.Analyze([reference_key](const HprofHeap &heap) {
// 该 Lambada 是查找 heap 中的泄漏的对象,heap 中保存了解析的 HPROF 文件中的内容。
// HPROF 解析的结果存放在 heap 中
// 查找 DestroyedActivityInfo 类在 HPROF 中的 id,它是存放 activity 弱引用的类
const object_id_t leak_ref_class_id = unwrap_optional(
heap.FindClassByName(
"com.tencent.matrix.resource.analyzer.model.DestroyedActivityInfo"),
return std::vector<object_id_t>());
std::vector<object_id_t> leaks;
// 找到 DestroyedActivityInfo 所有的实例
for (const object_id_t leak_ref: heap.GetInstances(leak_ref_class_id)) {
// 找到 DestroyedActivityInfo#mKey 引用
const object_id_t key_string_id = unwrap_optional(
heap.GetFieldReference(leak_ref, "mKey"), continue);
// 解析 mKey 对应的 String 实例,并解析它。
const std::string &key_string = unwrap_optional(
heap.GetValueFromStringInstance(key_string_id), continue);
if (key_string != reference_key)
continue;
// 找到 Activity 的弱引用
const object_id_t weak_ref = unwrap_optional(
heap.GetFieldReference(leak_ref, "mActivityRef"), continue);
// 找到 Activity 的引用
const object_id_t leak = unwrap_optional(
heap.GetFieldReference(weak_ref, "referent"), continue);
leaks.emplace_back(leak);
}
return leaks;
});
}
继续看看 HprofAnalyzerImpl::Analyze()
方法的实现:
C++
std::vector<LeakChain>
HprofAnalyzerImpl::Analyze(
const std::function<std::vector<object_id_t>(const HprofHeap &)> &leak_finder) {
internal::heap::Heap heap;
// 其中这个 data_ 中就是存放 HPROF 文件的字节数组,是通过 mmap 内存映射的
internal::reader::Reader reader(reinterpret_cast<const uint8_t *>(data_), data_size_);
// 解析 HPROF,结果都存放在 heap 中
parser_->Parse(reader, heap, exclude_matcher_group_);
// 通过 lambda 回调,查找泄漏的对象
return Analyze(heap, leak_finder(HprofHeap(new HprofHeapImpl(heap))));
}
看看 Parse()
方法的实现:
Kotlin
void
HeapParserEngineImpl::Parse(reader::Reader &reader, heap::Heap &heap,
const ExcludeMatcherGroup &exclude_matcher_group,
const HeapParserEngine &next) const {
// 解析 Header
next.ParseHeader(reader, heap);
while (true) {
const uint8_t tag = reader.ReadU1();
reader.SkipU4(); // Skip timestamp.
const uint32_t length = reader.ReadU4();
switch (tag) {
case tag::kStrings:
// 解析 String 的 Record
next.ParseStringRecord(reader, heap, length);
break;
case tag::kLoadClasses:
// 解析 LoadClass 的 Record
next.ParseLoadClassRecord(reader, heap, length);
break;
case tag::kHeapDump:
case tag::kHeapDumpSegment:
// 解析 Dump 的 Record
next.ParseHeapContent(reader, heap, length, exclude_matcher_group, next);
break;
case tag::kHeapDumpEnd:
// 结束
goto break_read_loop;
default:
// 跳过其他不感兴趣的 Record
reader.Skip(length);
break;
}
}
break_read_loop:
// 做一些还没有解析的内容,主要处理实例的成员 Field 中的内容和找到 Exclude 的 GCRoot
next.LazyParse(heap, exclude_matcher_group);
}
HPROF
文件的解析内容放在 Heap
中,其中它只解析 String
,LoadClass
,HeapDump
和 HeapDumpSegment
等 Record
,其他的 Record
直接跳过,在解析完基础的 Records
后再去解析所有的实例中引用类型的 Field
。 看看 HeapParserEngine#ParseHeapContent()
方法是如何处理 HeapDump
和 HeapDumpSegment
的子 Record
的。
C++
void HeapParserEngineImpl::ParseHeapContent(reader::Reader &reader, heap::Heap &heap,
size_t record_length,
const ExcludeMatcherGroup &exclude_matcher_group,
const HeapParserEngine &next) const {
size_t bytes_read = 0;
while (bytes_read < record_length) {
const uint8_t tag = reader.ReadU1();
bytes_read += sizeof(uint8_t);
switch (tag) {
case tag::kHeapRootUnknown:
bytes_read += next.ParseHeapContentRootUnknownSubRecord(reader, heap);
break;
case tag::kHeapRootJniGlobal:
bytes_read += next.ParseHeapContentRootJniGlobalSubRecord(reader, heap);
break;
case tag::kHeapRootJniLocal:
bytes_read += next.ParseHeapContentRootJniLocalSubRecord(reader, heap);
break;
case tag::kHeapRootJavaFrame:
bytes_read += next.ParseHeapContentRootJavaFrameSubRecord(reader, heap);
break;
case tag::kHeapRootNativeStack:
bytes_read += next.ParseHeapContentRootNativeStackSubRecord(reader, heap);
break;
case tag::kHeapRootStickyClass:
bytes_read += next.ParseHeapContentRootStickyClassSubRecord(reader, heap);
break;
case tag::kHeapRootThreadBlock:
bytes_read += next.ParseHeapContentRootThreadBlockSubRecord(reader, heap);
break;
case tag::kHeapRootMonitorUsed:
bytes_read += next.ParseHeapContentRootMonitorUsedSubRecord(reader, heap);
break;
case tag::kHeapRootThreadObject:
bytes_read += next.ParseHeapContentRootThreadObjectSubRecord(reader, heap);
break;
case tag::kHeapRootInternedString:
bytes_read += next.ParseHeapContentRootInternedStringSubRecord(reader, heap);
break;
case tag::kHeapRootFinalizing:
bytes_read += next.ParseHeapContentRootFinalizingSubRecord(reader, heap);
break;
case tag::kHeapRootDebugger:
bytes_read += next.ParseHeapContentRootDebuggerSubRecord(reader, heap);
break;
case tag::kHeapRootReferenceCleanup:
bytes_read += next.ParseHeapContentRootReferenceCleanupSubRecord(reader, heap);
break;
case tag::kHeapRootVMInternal:
bytes_read += next.ParseHeapContentRootVMInternalSubRecord(reader, heap);
break;
case tag::kHeapRootJniMonitor:
bytes_read += next.ParseHeapContentRootJniMonitorSubRecord(reader, heap);
break;
case tag::kHeapRootUnreachable:
bytes_read += next.ParseHeapContentRootUnreachableSubRecord(reader, heap);
break;
case tag::kHeapClassDump:
bytes_read += next.ParseHeapContentClassSubRecord(reader, heap,
exclude_matcher_group);
break;
case tag::kHeapInstanceDump:
bytes_read += next.ParseHeapContentInstanceSubRecord(reader, heap);
break;
case tag::kHeapObjectArrayDump:
bytes_read += next.ParseHeapContentObjectArraySubRecord(reader, heap);
break;
case tag::kHeapPrimitiveArrayDump:
bytes_read += next.ParseHeapContentPrimitiveArraySubRecord(reader, heap);
break;
case tag::kHeapPrimitiveArrayNoDataDump:
bytes_read += next.ParseHeapContentPrimitiveArrayNoDataDumpSubRecord(reader,
heap);
break;
case tag::kHeapDumpInfo:
bytes_read += next.SkipHeapContentInfoSubRecord(reader, heap);
break;
default:
std::stringstream error_builder;
error_builder << "unsupported heap dump tag " << std::to_string(tag);
pub_fatal(error_builder.str());
}
}
}
它处理了所有的 HPROF
中的 SubRecord
。具体的细节就不看了,也是保存在 Heap
中。
到这里解析 HPROF
文件就全部完了,然后我们再看看如何构建泄漏链,对应的方法是 HprofAnalyzerImpl::Analyze()
:
C++
std::vector<LeakChain> HprofAnalyzerImpl::Analyze(const internal::heap::Heap &heap,
const std::vector<object_id_t> &leaks) {
// Key 是泄漏对象的 ID;Value 也是一个 Map:Key 是 GCRoot 的 ID,Value 是到达泄漏对象的路径。
const auto chains = ({
const HprofHeap hprof_heap(new HprofHeapImpl(heap));
// 找到泄漏链
internal::analyzer::find_leak_chains(heap, leaks);
});
std::vector<LeakChain> result;
for (const auto&[_, chain]: chains) {
// 构建泄漏链
const std::optional<LeakChain> leak_chain = BuildLeakChain(heap, chain);
if (leak_chain.has_value()) result.emplace_back(leak_chain.value());
}
return std::move(result);
}
其中上面的代码中 leaks
表示泄漏的对象,是我们前面代码的 Lambda
表达式获取的,代码可以再往上翻翻,代码注释我写得很详细。我们再看看 internal::analyzer::find_leak_chains()
方法:
C++
std::map<heap::object_id_t, std::vector<std::pair<heap::object_id_t, std::optional<heap::reference_t>>>>
find_leak_chains(const heap::Heap &heap, const std::vector<heap::object_id_t> &tracked) {
/* Use Breadth-First Search algorithm to find all the references to tracked objects. */
std::map<heap::object_id_t, std::vector<std::pair<heap::object_id_t, std::optional<heap::reference_t>>>> ret;
// 遍历泄漏的对象
for (const auto &leak: tracked) {
// 已经遍历过的节点, Key 对象 ID;Value 是引用它的节点信息,在 LeakCanary 中的命名为 dominator
std::map<heap::object_id_t, ref_node_t> traversed;
// 等待查找的节点
std::deque<ref_node_t> waiting;
// 添加所有的 GCRoot 到等待查找的对象
for (const heap::object_id_t gc_root: heap.GetGcRoots()) {
ref_node_t node = {
.referent_id = gc_root,
.super = std::nullopt,
.depth = 0
};
traversed[gc_root] = node;
waiting.push_back(node);
}
bool found = false;
while (!waiting.empty()) {
// 取出一个需要查找的节点
const ref_node_t node = waiting.front();
waiting.pop_front();
const heap::object_id_t referrer_id = node.referent_id;
if (heap.GetLeakReferenceGraph().count(referrer_id) == 0) continue;
// 遍历节点的实例的所有引用类型的成员 Field
for (const auto &[referent, reference]: heap.GetLeakReferenceGraph().at(referrer_id)) {
try {
// 如果已经遍历过了,并且 GCRoot 的深度小于当前的深度就跳过,因为我们只需要一条到 GCRoot 最短的路径
if (traversed.at(referent).depth <= node.depth + 1) continue;
} catch (const std::out_of_range &) {}
// 构建新的节点添加到已经遍历过
ref_node_t next_node = {
.referent_id = referent,
.super = ref_super_t{
// 持有该引用的实例ID
.referrer_id = referrer_id,
// 持有该引用的实例的 Field
.reference = reference
},
.depth = node.depth + 1
};
traversed[referent] = next_node;
// 判断当前节点是否是泄漏的对象,如果是,跳出循环,如果不是添加到下一个需要遍历的节点
if (leak == referent) {
found = true;
goto traverse_complete;
} else {
waiting.push_back(next_node);
}
}
}
traverse_complete:
if (found) {
// 已经找到泄漏对象
// Pair:first 对象的 ID,second 引用该对象的 Field 信息
ret[leak] = std::vector<std::pair<heap::object_id_t, std::optional<heap::reference_t>>>();
std::optional<heap::object_id_t> current = leak;
std::optional<heap::reference_t> current_reference = std::nullopt;
while (current != std::nullopt) {
ret[leak].push_back(std::make_pair(current.value(), current_reference));
const auto &super = traversed.at(current.value()).super;
if (super.has_value()) {
current = super.value().referrer_id;
current_reference = super.value().reference;
} else {
current = std::nullopt;
}
}
// 反向,让 GCRoot 排在前面
std::reverse(ret[leak].begin(), ret[leak].end());
}
}
return std::move(ret);
}
总的逻辑是:从所有的 GCRoot
开始往下查找,需要查找的对象放在 waiting
本地变量中,已经遍历过的节点放在 traversed
中,Key
对象 ID
,Value
是引用它的节点信息,在 LeakCanary
中的命名为 dominator
。遍历 waiting
中的对象,直到它变成空,或者已经找到目标的泄漏对象,查询对象引用的其他对象是通过 heap.GetLeakReferenceGraph().at()
方法,它能够返回目标对象所引用的其他对象,而且还包含 Field
的信息,在其所引用的其他对象中,如果已经遍历过了,并且 GCRoot
的深度小于当前的深度就跳过,因为我们只需要一条到 GCRoot
最短的路径。如果不需要跳过,就检查是不是我们需要的泄漏对象,如果是我们要找的泄漏对象就退出循环,如果不是我们要找的泄漏的对象就添加到 traversed
中。
上面找到的泄漏信息又会被封装在 LeakChain
中,对应的方法是 HprofAnalyzerImpl::BuildLeakChain()
:
C++
std::optional<LeakChain> HprofAnalyzerImpl::BuildLeakChain(const internal::heap::Heap &heap,
const std::vector<std::pair<internal::heap::object_id_t, std::optional<internal::heap::reference_t>>> &chain) {
if (chain.empty()) return std::nullopt;
std::optional<LeakChain::GcRoot> gc_root;
std::vector<LeakChain::Node> nodes;
internal::heap::reference_t next_reference{};
for (size_t i = 0; i < chain.size(); ++i) {
const auto &chain_node = chain[i];
// 获取 Class 的名字
const std::string referent = ({
const object_id_t class_id = (chain_node.second.has_value() &&
chain_node.second.value().type ==
internal::heap::kStaticField)
? chain_node.first
: unwrap(heap.GetClass(chain_node.first),
return std::nullopt);
unwrap(heap.GetClassName(class_id), return std::nullopt);
});
if (i == 0) {
// GCRoot
const LeakChain::GcRoot::Type gc_root_type =
convert_gc_root_type(heap.GetGcRootType(chain_node.first));
gc_root = create_leak_chain_gc_root(referent, gc_root_type);
} else {
// 非 GCRoot
// Field 的名字
const std::string reference = next_reference.type == internal::heap::kArrayElement
? std::to_string(next_reference.index)
: heap.GetString(
next_reference.field_name_id).value_or("");
// Field 的类型
const LeakChain::Node::ReferenceType reference_type =
convert_reference_type(next_reference.type);
// 引用实例的类型
const LeakChain::Node::ObjectType referent_type =
convert_object_type(heap.GetInstanceType(chain_node.first));
nodes.emplace_back(
create_leak_chain_node(reference, reference_type, referent, referent_type));
}
next_reference = unwrap(chain_node.second, break);
}
return create_leak_chain(gc_root.value(), nodes);
}
LeakChain
中会保存 GCRoot
和泄漏的路径;路径的节点用 LeakChain::Node
封装,其中包含实例对应的 Class
名字,Field
的名字,Field
的类型,Field
引用的实例的类型。
在找到泄漏链后就会序列化写入到文件中,在写入文件完成后,Kotlin
的代码就会反序列化读取器中的内容,然后上报泄漏链,Bug
就发生在 Kotlin
代码中读取泄漏链的过程中。
我们先看序列化的代码:
C++
// ! execute on task process
static bool execute_serialize(const char *result_path, const std::vector<LeakChain> &leak_chains) {
_info_log(TAG, "task_process %d: serialize", getpid());
update_task_state(TS_CREATE_RESULT_FILE);
bool result = false;
int result_fd = open(result_path, O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR);
if (result_fd == -1) {
std::stringstream error_builder;
error_builder << "invoke open() failed on result file with errno " << errno;
on_error(error_builder.str().c_str());
return false;
}
update_task_state(TS_SERIALIZE);
// See comment documentation of <code>MemoryUtil.deserialize</code> for the file format of
// result file.
#define write_data(content, size) \
if (write(result_fd, content, size) == -1) { \
std::stringstream error_builder; \
error_builder << "invoke write() failed on result file with errno " << errno; \
on_error(error_builder.str().c_str()); \
goto write_leak_chain_done; \
}
const uint32_t byte_order_magic = 0x1;
const uint32_t leak_chain_count = leak_chains.size();
// 前 4 个字节固定为 1
write_data(&byte_order_magic, sizeof(uint32_t))
// 4 个字节表示泄漏链的长度
write_data(&leak_chain_count, sizeof(uint32_t))
// 遍历所有的泄漏链,一个泄漏链表示一个泄漏的对象
for (const auto &leak_chain : leak_chains) {
const uint32_t leak_chain_length = leak_chain.GetDepth() + 1;
// 4 个字节泄漏链的深度
write_data(&leak_chain_length, sizeof(uint32_t))
int32_t gc_root_type;
if (leak_chain_length == 1) {
// 如果链的长度为 1 就表示只有 GCRoot 一个节点
gc_root_type = serialize_object_type(LeakChain::Node::ObjectType::kInstance);
} else {
// 获取 GCRoot 持有的第一个引用的引用类型
const auto ref_type = leak_chain.GetNodes()[0].GetReferenceType();
switch (ref_type) {
case LeakChain::Node::ReferenceType::kStaticField:
gc_root_type = serialize_object_type(LeakChain::Node::ObjectType::kClass);
break;
case LeakChain::Node::ReferenceType::kInstanceField:
gc_root_type = serialize_object_type(
LeakChain::Node::ObjectType::kInstance);
break;
case LeakChain::Node::ReferenceType::kArrayElement:
gc_root_type = serialize_object_type(
LeakChain::Node::ObjectType::kObjectArray);
break;
}
}
// 4 个字节表示 GCRoot 的类型
write_data(&gc_root_type, sizeof(int32_t))
const uint32_t gc_root_name_length = leak_chain.GetGcRoot().GetName().length();
// 4 个字节表示 GCRoot 类型名字的的长度
write_data(&gc_root_name_length, sizeof(uint32_t))
// 写入 GCRoot 类型名字
write_data(leak_chain.GetGcRoot().GetName().c_str(), gc_root_name_length)
// 遍历泄漏链接的节点
for (const auto &node : leak_chain.GetNodes()) {
// 获取引用类型
const int32_t serialized_reference_type =
serialize_reference_type(node.GetReferenceType());
// 4 个字节表示引用类型
write_data(&serialized_reference_type, sizeof(int32_t))
const uint32_t reference_name_length = node.GetReference().length();
// 4 个字节表示引用类型名字长度
write_data(&reference_name_length, sizeof(uint32_t))
// 写入引用类型名字
write_data(node.GetReference().c_str(), reference_name_length)
const int32_t serialized_object_type =
serialize_object_type(node.GetObjectType());
// 4 个字节表示实例类型
write_data(&serialized_object_type, sizeof(int32_t))
const uint32_t object_name_length = node.GetObject().length();
// 4 个字节表示实例类型名字的长度
write_data(&object_name_length, sizeof(uint32_t))
// 写入实例类型的名字
write_data(node.GetObject().c_str(), object_name_length)
}
const uint32_t end_tag = 0;
// 4 个字节表示写入单个泄漏链完毕
write_data(&end_tag, sizeof(uint32_t))
}
result = true;
write_leak_chain_done:
close(result_fd);
return result;
}
上面代码比较简单,我也写了注释,看着应该没压力。
我们再来看 Kotlin
中反序列化的代码:
Kotlin
private fun deserialize(file: File): List<LeakChain> {
val stream = run {
val input = file.inputStream()
val magic = ByteArray(4)
input.read(magic, 0, 4)
// 确定大端或者小端
if (magic.contentEquals(byteArrayOf(0x00, 0x00, 0x00, 0x01)))
OrderedStreamWrapper(ByteOrder.BIG_ENDIAN, input)
else
OrderedStreamWrapper(ByteOrder.LITTLE_ENDIAN, input)
}
try {
// 读取泄漏引用链数量
val chainCount = stream.readOrderedInt()
if (chainCount == 0) {
stream.close()
return emptyList()
}
val result = mutableListOf<LeakChain>()
for (chainIndex in 0 until chainCount) {
val nodes = mutableListOf<LeakChain.Node>()
// 链的节点数量
val chainLength = stream.readOrderedInt()
// TODO:这里貌似有一个 BUG,少了读取 GCRoot 的类型和 GCRoot 的名字.
// 1. 4 个字节 GCRoot 类型
// 2. 4 个字节 GCRoot 类型名字的长度
// 3. GC Root 类型的名字
for (nodeIndex in 0 until chainLength) {
// 1. 4 个字节引用类型
// 2. 4 个字节引用类型名字长度
// 3. 引用类型名字
// 4. 4 个字节实例类型
// 5. 4 个字节实例类型名字长度
// 6. 实例类型名字
// node
val objectType = stream.readOrderedInt()
val objectName = stream.readString(stream.readOrderedInt())
// reference
val referenceType = stream.readOrderedInt()
val referenceName =
if (referenceType == 0) "" // reached end tag
else stream.readString(stream.readOrderedInt())
nodes += LeakChain.Node(objectName, objectType, referenceName, referenceType)
}
result += LeakChain(nodes)
}
return result
} catch (exception: IOException) {
throw exception
} finally {
stream.close()
}
}
上面 Bug
的位置我已经标记出来了,每个链写入的时候会写入 GCRoot
的信息,但是 Kotlin
的代码中没有读这个数据,就会导致后面的数据读取发生一个错位。也就是后面节点中读取引用类型和引用的实例类型信息发生错位。
最后
在 ResourcePlugin
的代码中还有裁剪 HPROF
的代码,我就不再分析源码了,我这里可以在讲讲它裁剪的方案,供大家参考。ResourcePlugin
主要裁剪的是 PrimaryArray
类型的实例,只保留 String
和 Bitmap
中的 PrimaryArray
,重复的 Bitmap
的 PrimaryArray
也会被裁剪掉。