稀土掘金地址:https://juejin.cn/post/7276412839585415187
前言
對matrix框架的分析,第一篇文章 Android性能優化系列-騰訊matrix-IO監控-IOCanaryPlugin 用來分析io監控與優化的方向。接下來準備從卡頓優化入手,卡頓是項目中最容易影響用戶體驗的一個問題,所以也是至關重要的一個優化點。卡頓優化功能對應于matrix中的matrix-trace-canary模塊,包含了多方面的卡頓監控,如啟動監控、慢方法監控、Anr監控等等,而這些都依賴于matrix底層的一個基礎能力-字節碼插樁,所以在進行卡頓優化的代碼分析前,有必要對這個基礎能力的實現有一個直觀的了解。
插件入口
在源碼中找到matrix-gradle-plugin這個模塊,找到插件的入口。
resources/META-INF/gradle-plugins/com.tencent.matrix-plugin.properties
implementation-class=com.tencent.matrix.plugin.MatrixPlugin
搜索MatrixPlugin,開始分析源碼,今天的分析著重于matrix插樁原理,而不關注gradle插件的實現,所以有些gradle插件相關的內容會一筆帶過,讀者可以自行搜索相關內容。
MatrixPlugin-apply
apply是插件執行的入口,在這里會讀取到build.gradle文件中的配置,配置內容包含兩個方面,一是trace,一是removeUnusedResources, 本篇只分析trace任務,removeUnusedResources會在后邊的文章中進行分析。
override fun apply(project: Project) {
...
//進入MatrixTasksManager
MatrixTasksManager().createMatrixTasks(
project.extensions.getByName("android") as AppExtension,
project,
traceExtension,
removeUnusedResourcesExtension
)
}
traceExtension和removeUnusedResourcesExtension對應的正是build.gradle中的配置。
matrix {
trace {
}
removeUnusedResources {
}
}
createMatrixTasks
createMatrixTraceTask和createRemoveUnusedResourcesTask是插件的兩個核心點。
fun createMatrixTasks(android: AppExtension,
project: Project,
traceExtension: MatrixTraceExtension,
removeUnusedResourcesExtension: MatrixRemoveUnusedResExtension) {
createMatrixTraceTask(android, project, traceExtension)
createRemoveUnusedResourcesTask(android, project, traceExtension)
}
createMatrixTraceTask
方法中針對不同gradle版本創建了兩個不同的transform:
- MatrixTraceTransform
- MatrixTraceLegacyTransform
最終這兩個transform會匯集到一個入口,那就是MatrixTrace.
MatrixTrace(
ignoreMethodMapFilePath = config.ignoreMethodMapFilePath,
methodMapFilePath = config.methodMapFilePath,
baseMethodMapPath = config.baseMethodMapPath,
blockListFilePath = config.blockListFilePath,
mappingDir = config.mappingDir,
project = project
).doTransform(
classInputs = inputFiles,
changedFiles = changedFiles,
isIncremental = isIncremental,
skipCheckClass = config.skipCheckClass,
traceClassDirectoryOutput = outputDirectory,
inputToOutput = inputToOutput,
legacyReplaceChangedFile = null,
legacyReplaceFile = null,
uniqueOutputName = true
)
來看doTransform方法,官方注釋很清楚,分為關鍵的三步,接下來我們一步一步來讀一下代碼。
fun doTransform() {
...
/**
* step 1
*/
futures.add(executor.submit(ParseMappingTask(
mappingCollector, collectedMethodMap, methodId, config)))
for (file in classInputs) {
if (file.isDirectory) {
futures.add(executor.submit(CollectDirectoryInputTask()))
} else {
futures.add(executor.submit(CollectJarInputTask()))
}
}
/**
* step 2
*/
val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)
methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)
/**
* step 3
*/
val methodTracer = MethodTracer(executor, mappingCollector, config, methodCollector.collectedMethodMap, methodCollector.collectedClassExtendMap)
methodTracer.trace(dirInputOutMap, jarInputOutMap, traceClassLoader, skipCheckClass)
}
第一步
包含三項任務
ParseMappingTask
這個任務是用來解析mapping.txt文件的,通過調用一個名為MappingReader的類去解析文件,解析的內容又可以分為類解析和類成員解析。mapping文件解析之后,我們就獲得了混淆前和混淆后類的映射關系以及混淆前和混淆后方法的映射關系。
val mappingFile = File(config.mappingDir, "mapping.txt")
if (mappingFile.isFile) {
val mappingReader = MappingReader(mappingFile)
mappingReader.read(mappingCollector)
}
mappingReader.read()
if (!line.startsWith("#")) {
// a class mapping
if (line.endsWith(":")) {
className = parseClassMapping(line, mappingProcessor);
} else if (className != null) {
// a class member mapping
parseClassMemberMapping(className, line, mappingProcessor);
}
}
parseClassMapping
解析出混淆前的類名和混淆后的類名,將映射關系保存在MappingProcessor(實現類MappingCollector)映射表中,對應于下邊的三個map。
private String parseClassMapping(String line, MappingProcessor mappingProcessor) {
...
boolean ret = mappingProcessor.processClassMapping(className, newClassName);
}
這兩個集合中的className可能包含包名
HashMap<String, String> mObfuscatedRawClassMap
key | value |
---|---|
混淆后的類名 | 原類名 |
HashMap<String, String> mRawObfuscatedClassMap
key | value |
---|---|
原類名 | 混淆后的類名 |
HashMap<String, String> mRawObfuscatedPackageMap
key | value |
---|---|
包名 | 混淆后的包名 |
parseClassMemberMapping
邏輯也是比較直接的,解析出每個類下的方法方法信息, 最終還是保存在了MappingProcessor中的映射表中,對應于下邊的兩個map。
private void parseClassMemberMapping(String className, String line, MappingProcessor mappingProcessor) {
...
mappingProcessor.processMethodMapping(className, type, name, arguments, newClassName, newName);
}
Map<String, Map<String, Set<MethodInfo>>> mObfuscatedClassMethodMap
這個map用來記錄一個類中所有的方法信息
key | value |
---|---|
混淆后的類名為key | 一個以混淆后的方法名為key, 以MethodInfo集合為value的map(注意:MethodInfo中的類名方法名都是未混淆的) |
Map<String, Map<String, Set<MethodInfo>>> mOriginalClassMethodMap
key | value |
---|---|
未混淆的類名為key | 一個未混淆的方法名為key, 以MethodInfo集合為value的map(注意:MethodInfo中的類名方法名都是混淆后的) |
下面兩個任務針對directory和jar類型的文件分別處理
CollectDirectoryInputTask
此任務的輸入是一個map映射表, 記錄輸入到數據的映射關系,對于支持增量編譯的情況下,記錄的是所有發生改變的文件的映射,未改變的文件不做記錄。
resultOfDirInputToOut: MutableMap<File, File>
CollectJarInputTask
這個類的作用也類似,只不過操作對象是一個jar包,同樣輸入一個map集合記錄映射關系。
第二步
MethodCollector
MethodCollector是用來收集所有需要被trace的方法的,它會過濾掉一些沒有trace價值的方法,如構造方法、空方法,get、set方法等,還有一些指定不需要trace的類或者指定包下的類也會被過濾掉。
注意,methodId是一個比較關鍵的點,是一個從0開始遞增的值,每一個值表示一個方法名,用數字表示方法名,并記錄數字和方法名映射關系,用于后期分析時解析,是matrix內很巧妙的一個做法,感興趣可以深入研究一下,這里不過多的解釋了。
val methodCollector = MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap)
methodCollector.collect(dirInputOutMap.keys, jarInputOutMap.keys)
collect方法
- 從文件夾中遍歷得到所有文件,針對每個文件執行CollectSrcTask任務
- 遍歷所有jar,針對每個jar執行CollectJarTask
- 上邊兩個任務執行完成后,再執行saveIgnoreCollectedMethod和saveCollectedMethod,等待全部完成后返回。
public void collect(Set<File> srcFolderList, Set<File> dependencyJarList) throws ExecutionException, InterruptedException {
...
futures.add(executor.submit(new CollectSrcTask(classFile)));
...
futures.add(executor.submit(new CollectJarTask(jarFile)));
...
saveIgnoreCollectedMethod(mappingCollector);
...
saveCollectedMethod(mappingCollector);
}
CollectSrcTask、CollectJarTask
相同的邏輯,只不過一個針對class,一個針對jar包。
這里使用了Asm(一個字節碼插樁的庫,自行百度了解其用法,這里默認讀者具備了相關知識),所以關鍵操作在TraceClassAdapter中。
InputStream is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = new TraceClassAdapter(AgpCompat.getAsmApi(), classWriter);
classReader.accept(visitor, 0);
TraceClassAdapter
private class TraceClassAdapter extends ClassVisitor {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
//如果是接口或者抽象類,則isABSClass為true
if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
this.isABSClass = true;
}
//又一個map記錄類和父類
collectedClassExtendMap.put(className, superName);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
//抽象類和接口被繞過,不做處理
if (isABSClass) {
return super.visitMethod(access, name, desc, signature, exceptions);
} else {
//這里是記錄類中是否含有onWindowFocusChanged,這是Activity的一個回調,matrix將此用于頁面可見的記錄時機。
if (!hasWindowFocusMethod) {
hasWindowFocusMethod = isWindowFocusChangeMethod(name, desc);
}
//進入CollectMethodNode中
return new CollectMethodNode(className, access, name, desc, signature, exceptions);
}
}
}
CollectMethodNode
只看它的visitEnd方法
@Override
public void visitEnd() {
super.visitEnd();
TraceMethod traceMethod = TraceMethod.create(0, access, className, name, desc);
if ("<init>".equals(name)) {
isConstructor = true;
}
//判斷方法是否需要插樁,哪些方法可以插樁呢,看下邊
boolean isNeedTrace = isNeedTrace(configuration, traceMethod.className, mappingCollector);
// 空方法,get、set方法,single method都會被過濾掉,并記錄數量,加入map中存儲
if ((isEmptyMethod() || isGetSetMethod() || isSingleMethod())
&& isNeedTrace) {
ignoreCount.incrementAndGet();
collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
return;
}
if (isNeedTrace && !collectedMethodMap.containsKey(traceMethod.getMethodName())) {
//需要插樁的方法記錄到collectedMethodMap中
traceMethod.id = methodId.incrementAndGet();
collectedMethodMap.put(traceMethod.getMethodName(), traceMethod);
incrementCount.incrementAndGet();
} else if (!isNeedTrace && !collectedIgnoreMethodMap.containsKey(traceMethod.className)) {
ignoreCount.incrementAndGet();
//不需要插樁的方法記錄到collectedIgnoreMethodMap中
collectedIgnoreMethodMap.put(traceMethod.getMethodName(), traceMethod);
}
}
isNeedTrace
public static boolean isNeedTrace(Configuration configuration, String clsName, MappingCollector mappingCollector) {
boolean isNeed = true;
//指定不需要插樁的方法,過濾掉
if (configuration.blockSet.contains(clsName)) {
isNeed = false;
} else {
if (null != mappingCollector) {
//從上邊的分析,我們知道,這是從mObfuscatedRawClassMap中獲取未混淆的方法名
clsName = mappingCollector.originalClassName(clsName, clsName);
}
clsName = clsName.replaceAll("/", ".");
for (String packageName : configuration.blockSet) {
//指定包名下的類也被過濾掉
if (clsName.startsWith(packageName.replaceAll("/", "."))) {
isNeed = false;
break;
}
}
}
return isNeed;
}
最終還是產出了兩個map映射表:collectedMethodMap、collectedIgnoreMethodMap,結構相同,但是代表的含義不同,collectedMethodMap存儲需要被插樁的方法,collectedIgnoreMethodMap存儲不需要插樁的方法。
key | value |
---|---|
方法名 | TraceMethod |
此時再回頭看看MethodCollector這個類的確像它的命名一樣,只是方法的收集者,并不做插樁,而真正執行插樁的,是MethodTracer。
saveIgnoreCollectedMethod
邏輯很簡單,就是將上邊收集到的collectedIgnoreMethodMap中記錄的不需要插樁的方法信息寫入到指定的ignoreMethodMapFilePath文件中,方便查找,不屬于本次分析的核心,不做過多解釋。
private void saveIgnoreCollectedMethod(MappingCollector mappingCollector) {
...
}
saveCollectedMethod
和saveIgnoreCollectedMethod方法類似,只不過保存的是collectedMethodMap中的方法信息,保存在配置的methodMapFilePath文件路徑中。有些特殊的是,它主動將Handler的dispatchMessage也加進去了,目的是什么,暫不看了。
private void saveCollectedMethod(MappingCollector mappingCollector) {
...
TraceMethod extra = TraceMethod.create(TraceBuildConstants.METHOD_ID_DISPATCH, Opcodes.ACC_PUBLIC, "android.os.Handler",
"dispatchMessage", "(Landroid.os.Message;)V");
collectedMethodMap.put(extra.getMethodName(), extra);
...
}
第三步
接下來才是整個插件的重中之重,真正要開始插樁了
MethodTracer
trace
public void trace(Map<File, File> srcFolderList, Map<File, File> dependencyJarList, ClassLoader classLoader, boolean ignoreCheckClass) throws ExecutionException, InterruptedException {
...
traceMethodFromSrc(srcFolderList, futures, classLoader, ignoreCheckClass);
traceMethodFromJar(dependencyJarList, futures, classLoader, ignoreCheckClass);
...
}
traceMethodFromSrc
只保留核心代碼
private void innerTraceMethodFromSrc(File input, File output, ClassLoader classLoader, boolean ignoreCheckClass) {
...
is = new FileInputStream(classFile);
ClassReader classReader = new ClassReader(is);
ClassWriter classWriter = new TraceClassWriter(ClassWriter.COMPUTE_FRAMES, classLoader);
ClassVisitor classVisitor = new TraceClassAdapter(AgpCompat.getAsmApi(), classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
is.close();
...
if (!ignoreCheckClass) {
try {
ClassReader cr = new ClassReader(data);
ClassWriter cw = new ClassWriter(0);
ClassVisitor check = new CheckClassAdapter(cw);
cr.accept(check, ClassReader.EXPAND_FRAMES);
} catch (Throwable e) {
}
}
...
}
TraceClassAdapter
private class TraceClassAdapter extends ClassVisitor {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
this.isActivityOrSubClass = isActivityOrSubClass(className, collectedClassExtendMap);
this.isNeedTrace = MethodCollector.isNeedTrace(configuration, className, mappingCollector);
if ((access & Opcodes.ACC_ABSTRACT) > 0 || (access & Opcodes.ACC_INTERFACE) > 0) {
this.isABSClass = true;
}
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
//類中是否包含onWindowFocusChanged方法,Activity中的方法,matrix將它作為頁面可見的時機。
if (!hasWindowFocusMethod) {
hasWindowFocusMethod = MethodCollector.isWindowFocusChangeMethod(name, desc);
}
//是否是抽象類或接口,是則直接跳過,不插樁
if (isABSClass) {
return super.visitMethod(access, name, desc, signature, exceptions);
} else {
MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className,
hasWindowFocusMethod, isActivityOrSubClass, isNeedTrace);
}
}
}
TraceMethodAdapter
private class TraceMethodAdapter extends AdviceAdapter {
...
@Override
protected void onMethodEnter() {
//在方法入口插入AppMethodBeat.i(timestamp)方法
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i", "(I)V", false);
if (checkNeedTraceWindowFocusChangeMethod(traceMethod)) {
//在onWindowFocusChanged方法插入AppMethodBeat.at(timestamp)方法,
//用于記錄onWindowFocusChanged的執行時間,分析啟動耗時。
traceWindowFocusChangeMethod(mv, className);
}
}
}
@Override
protected void onMethodExit(int opcode) {
//在方法出口插入AppMethodBeat.o(timestamp)方法
TraceMethod traceMethod = collectedMethodMap.get(methodName);
if (traceMethod != null) {
traceMethodCount.incrementAndGet();
mv.visitLdcInsn(traceMethod.id);
mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o", "(I)V", false);
}
}
}
總結
插件的核心內容在于TraceMethodAdapter中的三項操作:
- 在符合條件的方法入口插入AppMethodBeat.i()
- 在符合條件的方法出口插入AppMethodBeat.o()
- 在Activity的onWindowFocusChanged方法中插入AppMethodBeat.at()
在編譯期間,除被排除掉的方法外,大量方法的入口和出口處被插入了AppMethodBeat的方法,意在能通過這兩個方法計算出方法執行的耗時,于是,每一個方法執行的耗時情況就清晰的展現在我們開發者眼前,借助這些數據才能更好的發現卡頓問題的原因。
有了這些基礎,后邊進行啟動優化或者anr分析的時候就有跡可循,matrix會幫我們將方法按耗時時長排列出來,方便我們有的放矢的去解決問題。