ModuleA should jump to a certain interface of moduleB. If you do not configure moduleB in the corresponding build.gradle, AS will tell you that you cannot jump. A route is needed to distribute the jump operation. Secondly, with the slow iteration of time, I found that the required functions had been written, and slowly began to various optimization. The common optimization is speed optimization, which naturally needs to check the time consuming situation of the method, so when freeing hands, I need a correct posture to count the time consuming of the method.

Attached is Github project address: github.com/Neacy/Neacy…

Train of thought

1. Use annotations to add corresponding protocols to the interface to jump to and where statistics are needed. 2. Implement a Transform Gradle plugin in Groovy to parse the corresponding annotations. 3. Use ASM framework to generate the corresponding code is mainly to write or insert class bytecode. 4. The routing framework needs reflection to get the routing table generated by ASM and then call it in the code to realize the jump.

============== with these ideas in mind, the next step is to write hard code ………… .

I’m going to start with the two comments that I’m going to use, and I’m going to write them all in a minute. Remember that we’re using class so I’m going to use @Retention(retentionPolicy.class).

*/ @retention (retentionPolicy.class) @target (elementType.type) public @interface NeacyProtocol {String value(); */ @retention (retentionPolicy.class) @target (elementType.method) public @interface NeacyCost {String value(); }Copy the code

Change the position to write a gradle plug-in, how to write the main reference district blog.csdn.net/sbsujjbcy/a… Just follow the steps. Assuming we’re done and set up, we have a prototype:

public class NeacyPlugin extends Transform implements Plugin<Project> { private static final String PLUGIN_NAME = "NeacyPlugin" private Project project @Override void apply(Project project) { this.project = project def android = project.extensions.getByType(AppExtension); android.registerTransform(this) } @Override String getName() { return PLUGIN_NAME } @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } @Override Set<QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } @Override boolean isIncremental()  { return true } @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { } }Copy the code

All we need to do is scan the corresponding annotation in the Transform and use ASM to write the class bytecode. We know that TransformInput has two possibilities: one is a directory and the other is a JAR package, so we need to traverse it separately:

inputs.each { TransformInput input -> input.directoryInputs.each { DirectoryInput directoryInput -> if (directoryInput.file.isDirectory()) { println "==== directoryInput.file = " + directoryInput.file directoryInput.file.eachFileRecurse { File file -> // ... // After processing the input file, The output to the next task def dest = outputProvider. GetContentLocation (directoryInput. Name, directoryInput contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, dest) } input.jarInputs.each { JarInput jarInput -> println "------=== jarInput.file === " + jarInput.file.getAbsolutePath() File tempFile = null if (jarInput.file.getAbsolutePath().endsWith(".jar")) { // ... */ def jarName = jarinput. name def md5Name = */ jarName = jarInput DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, JarName. Length () - (4)} / / processing jar bytecode injection processing def dest = outputProvider. GetContentLocation (jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) FileUtils.copyFile(jarInput.file, dest) } }Copy the code

For unfamiliar code style of code can refer to this article: blog.csdn.net/innost/arti… Guaranteed to understand everything after reading, good article highly recommended.

Then, the most troublesome part of the bytecode injection function, first look at the main call code:

ClassReader classReader = new ClassReader(file.bytes)
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                            NeacyAsmVisitor classVisitor = new NeacyAsmVisitor(Opcodes.ASM5, classWriter)
                            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)Copy the code

The main amount of code involved in the call is still relatively small, and it’s mostly about customizing a ClassVisitor. And in every ClassVisitor it’s going to differentiate between visitAnnotation and visitMethod

@Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { NeacyLog.log("=====---------- NeacyAsmVisitor visitAnnotation ----------====="); NeacyLog.log("=== visitAnnotation.desc === " + desc); AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible); If (type.getDescriptor (neacyProtocol.class).equals(desc)) {// mProtocolAnnotation = new if the annotation is not null NeacyAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc); return mProtocolAnnotation; } return annotationVisitor; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { NeacyLog.log("=====---------- visitMethod ----------====="); MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); mMethodVisitor = new NeacyMethodVisitor(Opcodes.ASM5, mv, access, name, desc); return mMethodVisitor; }Copy the code

So in visitAnnotation that’s where we scan the corresponding annotations like type.getDescriptor (NeacyProtocol.class).equals(desc) to see if it’s the annotation that we need to process, In this case, we’re dealing with the annotations NeacyProtocol and NeacyCost that we defined earlier.

Here I’ll show what the code looks like in the injected class: generate a good routing table:

Time consuming code injected successfully:

A glance at the logcat print of the elapsed time suggests success is close. First, take a look at the class structure. IntelliJ IDEA is recommended and then install a plugin called Bytecode Outline. Here, take a look at the time-consuming generation of class files Bytecode.

On the left is our corresponding Java file, and on the right is the class bytecode generated after compilation. For the right side of the general is not to understand but the magic ASM can see the understand and provide a series of API for us to call, we just write it, according to the above operation greatly reduce the huge difficulty of work, thank you again.

So the code byte generation of our routing framework, let me post the whole class, is not a lot of code:

/** * public class NeacyRouterWriter implements Opcodes {public byte[] generateClass(String PKG, HashMap<String, String> metas) { ClassWriter cw = new ClassWriter(0); FieldVisitor fv; MethodVisitor mv; V1_7, opcodes. ACC_PUBLIC + opcodes. ACC_SUPER, PKG, null, "Java /lang/Object", null); // Declare a static variable fv = cw.visitField(opcodes.acc_public + opcodes.acc_static, "map", "Ljava/util/HashMap;" , "Ljava/util/HashMap<Ljava/lang/String; Ljava/lang/String; >;" , null); fv.visitEnd(); // The default constructor <init> mv = cw.visitMethod(opcodes.acc_public, "<init>", "()V", null, null); mv.visitCode(); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false); mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(1, 1); // generate a getMap method mv = cw.visitMethod(opcodes.acc_public, "getMap", "()Ljava/util/HashMap;" , "()Ljava/util/HashMap<Ljava/lang/String; Ljava/lang/String; >;" , null); mv.visitCode(); mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;" ); mv.visitInsn(Opcodes.ARETURN); mv.visitMaxs(1, 1); mv.visitEnd(); Mv = cw.visitMethod(opcodes.acc_static, "<clinit>", "()V", null, null); mv = cw.visitMethod("<clinit>", "()V", null, null); mv.visitCode(); mv.visitTypeInsn(Opcodes.NEW, "java/util/HashMap"); mv.visitInsn(Opcodes.DUP); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false); mv.visitFieldInsn(Opcodes.PUTSTATIC, pkg, "map", "Ljava/util/HashMap;" ); for (Map.Entry<String, String> entrySet : metas.entrySet()) { String key = entrySet.getKey(); String value = entrySet.getValue(); NeacyLog.log("=== key === " + key); NeacyLog.log("=== value === " + value); mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;" ); mv.visitLdcInsn(key); mv.visitLdcInsn(value); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/HashMap", "put", "(Ljava/lang/Object; Ljava/lang/Object;) Ljava/lang/Object;" , false); mv.visitInsn(Opcodes.POP); } mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(3, 0); mv.visitEnd(); cw.visitEnd(); return cw.toByteArray(); }}Copy the code

Then, the main code inserts for method time-consuming are as follows:

@override protected void onMethodEnter() {if (isInject) {neacylog.log ("====== start insert method = "+ methodName); /** NeacyCostManager.addStartTime("xxxx", System.currentTimeMillis()); */ mv.visitLdcInsn(methodName); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addStartTime", "(Ljava/lang/String; J)V", false); } } @Override protected void onMethodExit(int opcode) { if (isInject) { /** NeacyCostManager.addEndTime("xxxx", System.currentTimeMillis()); */ mv.visitLdcInsn(methodName); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addEndTime", "(Ljava/lang/String; J)V", false); /** NeacyCostManager.startCost("xxxx"); */ mv.visitLdcInsn(methodName); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "startCost", "(Ljava/lang/String;) V", false); Neacylog. log("==== end of insertion ===="); }}Copy the code

Gradle plugins can be used to insert code into the routing table, and then they can be used to call gradle plugins. There is no problem in overwriting the current class when traversing directories:

If (isDebug) {// Scan is performed only for Debug const Time // Scan time Comments NeacyCost byte[] bytes = classWriter.tobytearray () File destFile = new File(file.parentFile.absoluteFile, Name) project.logger.debug "========== rewrite location ->lastFilePath = "+ destfile.getabsolutePath () FileOutputStream fileOutputStream = new FileOutputStream(destFile) fileOutputStream.write(bytes) fileOutputStream.close() }Copy the code

For jar traversal, all we need to do is disassemble the JAR first and then produce another JAR after the injection code is completed, so we need to create a temporary address to store the new JAR.

Separator + "neacy_conconst. Jar ") if (isDebug) {tempFile = new File(jarint.file.getparent () + file.separator +" neacy_conconst. Jar ") if (isDebug) {tempFile = new File(jarint.file.getparent () + file.separator + "neacy_conconst  (tempFile.exists()) { tempFile.delete() } fos = new FileOutputStream(tempFile) jarOutputStream = new JarOutputStream(fos) // Omit some code.... ZipEntry ZipEntry = new ZipEntry (entryName) jarOutputStream. PutNextEntry (ZipEntry) / / scan time consuming annotations NeacyCost byte [] bytes = classWriter.toByteArray() jarOutputStream.write(bytes) }Copy the code

It is necessary to insert a plugin configuration, because for method time statistics, it is good to use it in debug mode during development, which is why there is an if(debugOn) check. First define an Extension:

Public class NeacyExtension {Boolean debugOn = true public NeacyExtension(Project Project) {}Copy the code

Then read from transFROM:

    void apply(Project project) {
        this.project = project
        project.extensions.create("neacy", NeacyExtension, project)

        def android = project.extensions.getByType(AppExtension);
        android.registerTransform(this)


        project.afterEvaluate {
            def extension = project.extensions.findByName("neacy") as NeacyExtension
            def debugOn = extension.debugOn

            project.logger.error '========= debugOn = ' + debugOn

            project.android.applicationVariants.each { varient ->
                project.logger.error '======== varient Name = ' + varient.name
                if (varient.name.contains(DEBUG) && debugOn) {
                    isDebug = true
                }
            }
        }
    }Copy the code

Build. Gradle and you can use it happily.

apply plugin: com.neacy.plugin.NeacyPlugin
neacy {
    debugOn true
}Copy the code

Of course, more code can be found in the Git library of Demo.

And then finally, how do we get our library code to call it? That’s the reflection that we talked about earlier because it’s a compile-generated class and you can’t call it directly except for reflection, and reflection is a little bit of a performance drag so we’re just going to do this initialization right from the start.

/ * * * initializes the routing * / public void initRouter () {try {Class clazz = Class. Class.forname (" com. Neacy. The router. NeacyProtocolManager "); Object newInstance = clazz.newInstance(); Field field = clazz.getField("map"); field.setAccessible(true); HashMap<String, String> temps = (HashMap<String, String>) field.get(newInstance); if (temps ! = null && ! temps.isEmpty()) { mRouters.putAll(temps); Log.w("Jayuchou", "=== mRouters.Size === " + mRouters.size()); } } catch (Exception e) { e.printStackTrace(); Public void startIntent(Context Context, String protocol, Bundle bundle) { if (TextUtils.isEmpty(protocol)) return; String protocolValue = mRouters.get(protocol); try { Class destClass = Class.forName(protocolValue); Intent intent = new Intent(context, destClass); if (bundle ! = null) { intent.putExtras(bundle); } context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); }}Copy the code

At the very end, how do you use it?

@NeacyProtocol("Neacy://app/MainActivity")
public class MainActivity extends AppCompatActivity {

    @Override
    @NeacyCost("MainActivity.onCreate")
    protected void onCreate(Bundle savedInstanceState) {Copy the code

So once you’ve identified it with the annotations above, the method takes time and of course you need to route where you need to pass the protocol to jump to, and of course it’s a matter of code.

NeacyRouterManager.getInstance().startIntent(TestActivity.this, "Neacy://neacymodule/NeacyModuleActivity", bundle);Copy the code

Such a complete routing framework and method time statistics V1.0 version is finished.

Thanks………………… Thank the gods of the article: www.wangyuwei.me/2017/03/05/…