Android性能優化系列-騰訊matrix-卡頓監控-gradle插件- 字節碼插樁代碼分析

稀土掘金地址: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會幫我們將方法按耗時時長排列出來,方便我們有的放矢的去解決問題。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容