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:
- 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
- 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
- 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:
- Scope for the entire project (including the current project, subprojects, dependent libraries, etc.)
- 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:
- 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
- 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
- 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:
- Methods of counting include the application’s own, JAR dependencies, and an additional dispatchMessage method with a fixed ID
- Abstract or interface classes do not require statistics
- Simple methods such as empty methods and GET & set methods do not require statistics
- 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:
- 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
- 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
- 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
- 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:
- Add Extension to provide user-defined configuration options
- Read the Extension configuration and, if Trace is enabled, perform MatrixTraceTransform, count the methods and pile
- 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:
- 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
- Use ASM to access all Class files, record their ids, and write them to the file (methodmapping.txt)
- Insert the pile
The processing process of pile insertion mainly includes four steps:
- To enter the method, execute AppMethodBeat. I, pass in the method ID, and record the timestamp
- Appmethodbeat. o is executed when exiting the method, passing in the method ID and logging the timestamp
- If it is an Activity and there is no onWindowFocusChanged method, insert it
- Trace the onWindowFocusChanged method and appMethodBeat. at on exit to calculate the startup time
Notable details include:
- Methods of counting include the application’s own, JAR dependencies, and an additional dispatchMessage method with a fixed ID
- Abstract or interface classes do not require statistics
- Simple methods such as empty methods and GET & set methods do not require statistics
- The methods specified in BlackmethodList.txt do not require statistics