As mentioned before, the key of Matrix katon monitoring lies in the insertion of piles. Here is how it is implemented.

Gradle plugin configuration

Matrix Gradle plugin implements the MatrixPlugin class, which does three things:

  1. Add Extension to provide user-defined configuration options
class MatrixPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        project.extensions.create("matrix", MatrixExtension)
        project.matrix.extensions.create("trace", MatrixTraceExtension)
        project.matrix.extensions.create("removeUnusedResources", MatrixDelUnusedResConfiguration)
    }
}
Copy the code

Trace can be configured as follows:

public class MatrixTraceExtension {
    boolean enable; // Whether to enable the piling function
    String baseMethodMapFile;  // Custom method mapping files, described below
    String blackListFile; // The method specified in this file will not be staked
    String customDexTransformName; 
}
Copy the code

RemoveUnusedResources can be configured as follows:

class MatrixDelUnusedResConfiguration {
    boolean enable // Whether to enable it
    String variant // Specify that a build variant is enabled for piling, or if empty, all build variants are enabled
    boolean needSign // Whether a signature is required
    boolean shrinkArsc // Whether to crop the ARSC file
    String apksignerPath // Path to the signature file
    Set<String> unusedResources // Specify the unused resource to delete
    Set<String> ignoreResources // Specify resources that do not need to be deleted
}
Copy the code
  1. Read the configuration and, if piling is enabled, perform MatrixTraceTransform, count the method and pile
AfterEvaluate = build.gradle file completed This is because proguard operation before the task is completed in the project. AfterEvaluate {android. ApplicationVariants. All {variant - > the if (the configuration. The trace. The enable) {/ / if enabled, Can in gradle file configuration MatrixTraceTransform. Inject (project, the configuration. The trace, the variant getVariantData () getScope ())}... // RemoveUnusedResourcesTask } }Copy the code
  1. Reading configuration, if you enable removeUnusedResources function, then execute RemoveUnusedResourcesTask, remove unwanted resources

Methods Statistics and pile insertion

Configure the Transform

MatrixTraceTransform inject method is mainly used for reading configuration, proxy transformClassesWithDexTask:

public class MatrixTraceTransform extends Transform {

    public static void inject(Project project, MatrixTraceExtension extension, VariantScope variantScope) {...// Generate the Configuration variable config based on the parameters

        String[] hardTask = getTransformTaskName(extension.getCustomDexTransformName(), variant.getName());
        for (Task task : project.getTasks())
            for (String str : hardTask)
                if (task.getName().equalsIgnoreCase(str) && task instanceof TransformTask) {
                    Field field = TransformTask.class.getDeclaredField("transform");
                    field.set(task, new MatrixTraceTransform(config, task.getTransform()));
                    break; }}// These two transforms are used to compile the Class file into the Dex file
    // Therefore, the piling needs to be completed before the two transforms are executed
    private static String[] getTransformTaskName(String customDexTransformName, String buildTypeSuffix) {
        return new String[] {
                    "transformClassesWithDexBuilderFor" + buildTypeSuffix,
                    "transformClassesWithDexFor"+ buildTypeSuffix, };; }}Copy the code

The main configuration of MatrixTraceTransform is as follows:

  1. Scope for the entire project (including the current project, subprojects, dependent libraries, etc.)
  2. Process files of type Class
public class MatrixTraceTransform extends Transform {
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS; }

    @Override
    public Set<QualifiedContent.Scope> getScopes() { returnTransformManager.SCOPE_FULL_PROJECT; }}Copy the code

Execution method statistics and piling tasks

Transform is mainly executed in three steps:

  1. Analyze method statistics rules based on the configuration file, such as the mapping between the confused class name and the original class name, and the blacklist of methods that do not need to be staked
private void doTransform(TransformInvocation transformInvocation) throws ExecutionException, InterruptedException {
    // Files related to analysis and method statistics, such as mapping.txt, blackmethodList.txt, etc
    // Save the mapping rules to mappingCollector, collectedMethodMap
    futures.add(executor.submit(new ParseMappingTask(mappingCollector, collectedMethodMap, methodId)));
}
Copy the code
  1. The statistical method and its ID are written to the file
private void doTransform(TransformInvocation transformInvocation) {
    MethodCollector methodCollector = new MethodCollector(executor, mappingCollector, methodId, config, collectedMethodMap);
    methodCollector.collect(dirInputOutMap.keySet(), jarInputOutMap.keySet());
}
Copy the code
  1. Insert the pile
private void doTransform(TransformInvocation transformInvocation) {
    MethodTracer methodTracer = new MethodTracer(executor, mappingCollector, config,
            methodCollector.getCollectedMethodMap(), methodCollector.getCollectedClassExtendMap());
    methodTracer.trace(dirInputOutMap, jarInputOutMap);
}
Copy the code

Analysis method statistical rules

ParseMappingTask is mainly used to analyze method statistics related files such as Mapping.txt (generated by ProGuard), BlackMethodList.txt, etc., and save the mapping rules to HashMap.

Mapping. TXT is generated by ProGuard to map class and method names before and after confusion. The content is as follows:

MTT.ThirdAppInfoNew -> MTT.ThirdAppInfoNew: // oldClassName -> newClassName java.lang.String sAppName -> sAppName // oldMethodName -> newMethodName java.lang.String  sTime -> sTime ...Copy the code

Blackmethodlist.txt is used to avoid staking specific methods, and reads as follows:

[package] -keeppackage com/huluxia/logger/ -keepmethod com/example/Application attachBaseContext (Landroid/content/Context;) V ...Copy the code

If necessary, you can specify baseMethodMapFile to write the custom method and its corresponding method ID to a file in the following format:

/ / method id, access logo, the name of the class, name, description, 1, 1, eu. Chainfire. Libsuperuser. Application $1 the run () 2, 9 V, eu. Chainfire. Libsuperuser. Application toast (Landroid.content.Context; Ljava.lang.String;) VCopy the code

The above options can be configured in a Gradle file as shown in the following example:

matrix {
    trace {
        enable = true
        baseMethodMapFile = "{projectDir.absolutePath}/baseMethodMapFile.txt"
        blackListFile = "{projectDir.absolutePath}/blackMethodList.txt"
    }
}
Copy the code

Methods statistical

As the name suggests, MethodCollector collects methods, first encapsulating them as TraceMethod, assigning method ids, saving them to a HashMap, and finally writing them to a file.

To do this, we first need to get all the class files:

public void collect(Set<File> srcFolderList, Set<File> dependencyJarList) throws ExecutionException, InterruptedException {
    for (File srcFile : srcFolderList) {
        ...
        for (File classFile : classFileList) {
            futures.add(executor.submit(newCollectSrcTask(classFile))); }}for (File jarFile : dependencyJarList) {
        futures.add(executor.submit(newCollectJarTask(jarFile))); }}Copy the code

Next, use ASM to access each Class file:

class CollectSrcTask implements Runnable {
    @Override
    public void run(a) {
        InputStream is = new FileInputStream(classFile);
        ClassReader classReader = new ClassReader(is);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        ClassVisitor visitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
        classReader.accept(visitor, 0); }}Copy the code

And methods in the Class file:

private class TraceClassAdapter extends ClassVisitor {

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions) {
        if (isABSClass) { // No statistics are required for abstract classes or interfaces
            return super.visitMethod(access, name, desc, signature, exceptions);
        } else {
            return newCollectMethodNode(className, access, name, desc, signature, exceptions); }}}Copy the code

Finally, the method data is recorded and saved to a HashMap:

private class CollectMethodNode extends MethodNode { @Override public void visitEnd() { super.visitEnd(); TraceMethod TraceMethod TraceMethod = TraceMethod. Create (0, access, className, name, desc); // Whether it is necessary to insert a pile, Boolean isNeedTrace = isNeedTrace(Configuration, traceMethod. ClassName, mappingCollector); / / filter empty methods, get and set methods such as simple method if ((isEmptyMethod () | | isGetSetMethod () | | isSingleMethod () && isNeedTrace) {return; If (isNeedTrace &&! collectedMethodMap.containsKey(traceMethod.getMethodName())) { traceMethod.id = methodId.incrementAndGet(); collectedMethodMap.put(traceMethod.getMethodName(), traceMethod); incrementCount.incrementAndGet(); } else if (! isNeedTrace && ! collectedIgnoreMethodMap.containsKey(traceMethod.className)) { ... // Record the method that does not require piling}}}Copy the code

When the statistics are complete, write the methods and their ids into a file — since only method IDS will be reported later, you need to use this file to resolve the specific method names and their elapsed time.

While the code above is long, it’s actually quite simple: access all the methods in the Class file, record the method ids, and write them to the file.

The details to pay attention to are:

  1. Methods of counting include the application’s own, JAR dependencies, and an additional dispatchMessage method with a fixed ID
  2. Abstract or interface classes do not require statistics
  3. Simple methods such as empty methods and GET & set methods do not require statistics
  4. The methods specified in BlackmethodList.txt do not require statistics

Insert the pile

As well as method statistics, staking is also based on ASM implementation, first of all to find all Class files, and then for each method in the file processing.

The processing process mainly includes four steps:

  1. To enter the method, execute AppMethodBeat. I, pass in the method ID, and record the timestamp
public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";

private class TraceMethodAdapter extends AdviceAdapter {

    @Override
    protected void onMethodEnter(a) {
        TraceMethod traceMethod = collectedMethodMap.get(methodName);
        if(traceMethod ! =null) { // omit empty methods, set & get, and other simple methods
            mv.visitLdcInsn(traceMethod.id);
            mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "i"."(I)V".false); }}}Copy the code
  1. Appmethodbeat. o is executed when exiting the method, passing in the method ID and logging the timestamp
private class TraceMethodAdapter extends AdviceAdapter {

    @Override
    protected void onMethodExit(int opcode) {
        TraceMethod traceMethod = collectedMethodMap.get(methodName);
        if(traceMethod ! =null) {...// Trace the onWindowFocusChanged method to calculate the startup time
            mv.visitLdcInsn(traceMethod.id);
            mv.visitMethodInsn(INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "o"."(I)V".false); }}}Copy the code
  1. If it is an Activity and there is no onWindowFocusChanged method, insert it
private class TraceClassAdapter extends ClassVisitor {

    @Override
    public void visitEnd(a) {
        // If it is an Activity and the onWindowFocusChanged method does not exist, this method is inserted to count the Activity start time
        if(! hasWindowFocusMethod && isActivityOrSubClass && isNeedTrace) { insertWindowFocusChangeMethod(cv, className); }super.visitEnd(); }}Copy the code
  1. Trace the onWindowFocusChanged method and appMethodBeat. at on exit to calculate the startup time
public final static String MATRIX_TRACE_CLASS = "com/tencent/matrix/trace/core/AppMethodBeat";

private void traceWindowFocusChangeMethod(MethodVisitor mv, String classname) {
    mv.visitMethodInsn(Opcodes.INVOKESTATIC, TraceBuildConstants.MATRIX_TRACE_CLASS, "at"."(Landroid/app/Activity; Z)V".false);
}
Copy the code
public class AppMethodBeat implements BeatLifecycle {

    public static void at(Activity activity, boolean isFocus) {
        for(IAppMethodBeatListener listener : listeners) { listener.onActivityFocused(activityName); }}}Copy the code

StartupTracer is the implementation class for IAppMethodBeatListener.

conclusion

Matrix Gradle plugin implements the MatrixPlugin class, which does three things:

  1. Add Extension to provide user-defined configuration options
  2. Read the Extension configuration and, if Trace is enabled, perform MatrixTraceTransform, count the methods and pile
  3. Read the extension configuration, if enabled removeUnusedResources function, the execution RemoveUnusedResourcesTask, remove unwanted resources

It is important to note that the piling task is performed at compile time to avoid impacting the obfuscation operation. Because the ProGuard operation is done before this task, it means that the class file at the time of staking has been confused. However, pile insertion after ProGuard is selected, because if pile insertion is carried out in advance, part of the methods do not conform to the inline rules and cannot be optimized during proguard, which ultimately leads to the number of program methods cannot be reduced, thus causing the problem of too many methods

Transform is mainly executed in three steps:

  1. According to configuration files (mapping. TXT, Blackmethodlist. TXT, and baseMethodMapFile), analyze method statistics rules, such as the mapping between the confused class name and the original class name, and the blacklist of methods that do not need to be inserted
  2. Use ASM to access all Class files, record their ids, and write them to the file (methodmapping.txt)
  3. Insert the pile

The processing process of pile insertion mainly includes four steps:

  1. To enter the method, execute AppMethodBeat. I, pass in the method ID, and record the timestamp
  2. Appmethodbeat. o is executed when exiting the method, passing in the method ID and logging the timestamp
  3. If it is an Activity and there is no onWindowFocusChanged method, insert it
  4. Trace the onWindowFocusChanged method and appMethodBeat. at on exit to calculate the startup time

Notable details include:

  1. Methods of counting include the application’s own, JAR dependencies, and an additional dispatchMessage method with a fixed ID
  2. Abstract or interface classes do not require statistics
  3. Simple methods such as empty methods and GET & set methods do not require statistics
  4. The methods specified in BlackmethodList.txt do not require statistics