In fact, this has been involved in AOP(Aspect Oriented Programming), that is, Aspect Oriented Programming, during compilation of the code for dynamic management, in order to achieve unified maintenance purposes.

# 2, the Transform

Java file ->.class file ->.dex file. Just intercept it in the red circle and get all the methods to modify it before releasing it. Google officially provides the Transfrom API for Android Gradle after version 1.5.0, which allows third-party plugins to manipulate.class files during compilation prior to packaging dex files. So what we do is we implement Transform and we go through the.class file and we get all the methods, we modify them and we replace the original file.

*/ public class AutoTransform extends Transform {@override StringgetName() {
        return "AutoTrack"
    }
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false} @Override public void transform( @NonNull Context context, @NonNull Collection<TransformInput> inputs, @NonNull Collection<TransformInput> referencedInputs, @Nullable TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {// All files will be passed through /** All input files */ inputs. Each {TransformInput input -> /** * all jar */ input.jarinputs  { JarInput jarInput -> ... } / directory traversal * * * * / input. DirectoryInputs. Each {DirectoryInput DirectoryInput - >... }}}Copy the code

The Transform API allows you to iterate over all files. The Transform API allows you to iterate over all files. The Transform API allows you to iterate over all files using Gradle. Writing Gradle plugins may require some knowledge of Goovy. You can also write Gradle plugins directly in Java. Goovy is fully Java compatible, and only pluginentry.groovy is implemented by intercepting the pluginentry.groovy

class PluginEntry implements Plugin<Project> { @Override void apply(Project project) { ... / / using the Transform to traverse the def android = project. Extensions. GetByType (AppExtension) registerTransform (android)... } def static registerTransform(BaseExtension android) { AutoTransform transform = new AutoTransform() android.registerTransform(transform) }Copy the code

The only thing left to do is to get the.class file. As we all know, class files are in bytecode format, which is quite difficult to operate, so we need a bytecode library to ease the difficulty, that is ASM. ASM can generate binary class files directly or enhance existing classes. Java classes are stored in rigorously formatted.class files that have enough metadata to parse all the elements of the class: class names, methods, attributes, and Java bytecodes (instructions). The core classes in the ASM framework are as follows:

  • ClassReader: This class is used to parse compiled class bytecode files.
  • ClassWriter: This class is used to rebuild compiled classes, such as changing class names, attributes, and methods, and even generating bytecode files for new classes.
  • ClassVisitor: Responsible for visitor member information. These include annotations marked on a class, class constructors, class fields, class methods, and static code blocks.
  • AdviceAdapter: Implements the MethodVisitor interface, which is responsible for “visiting” method information for specific method bytecode operations.

The entire method of the ClassVisitor is as follows, iterating through the members of the class in some order.

class AutoClassVisitor extends ClassVisitor { AutoClassVisitor(final ClassVisitor cv) { super(Opcodes.ASM4, cv) } @Override void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {// Perform the condition filtering required to satisfy the class... super.visit(version, access, name, signature, superName, interfaces) } @Override void visitInnerClass(String name, String outerName, String innerName, int access) {// Inner class information... super.visitInnerClass(name, outerName, innerName, access) } @Override MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { Visitvisitmethod = cv.visitMethod(Access, name, desc, signature, exceptions) MethodVisitor adapter = null ... adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) ...return methodVisitor
    }

    @Override
    void visitEnd() {// Class member information traversal introduction... super.visitEnd() } }Copy the code

In the MethodVisitor, the changes are made based on the methods you’ve already got.

MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
        boolean isAnnotation = false
        @Override
        protected void onMethodEnter() {super.onmethodenter () {super.onmethodenter (); } @override protected void onMethodExit(int opcode) {super.onmethodexit (opcode); } @override AnnotationVisitor visitAnnotation(String des, Boolean visible) {/** * Override AnnotationVisitor visitAnnotation(String des, Boolean visible) {...return super.visitAnnotation(des, visible)
        }
    }
Copy the code

The above is the general idea, now through Luffy according to the specific needs of actual practice, such as the onClick method click time (automatic burying point is the same reason, but changed the method of inserting piles). Build. Gradle: build. Gradle: build

dependencies {
    classpath 'com. Xixi. Plugin: plugin: - the SNAPSHOT 1.0.1'
}

Copy the code

In your app build.gradle

apply plugin: 'apk.move.plugin'

xiaoqingwa{
    name = "Xx"
    isDebug = trueMatchData = [// whether to use annotations to find the corresponding method'isAnotation': false// Methods can be matched by class name or implementation interface name'ClassFilter': [['ClassName': null, 'InterfaceName':null,
                     'MethodName':null, 'MethodDes'VisitAnnotation ->onMethodEnter->onMethodExit :null]], // Insert bytecode, method execution order'MethodVisitor':{
                MethodVisitor methodVisitor, int access, String name, String desc ->
                    MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
                        boolean isAnnotation = false
                        @Override
                        protected void onMethodEnter} @override protected void onMethodExit(int opcode) {Override protected void onMethodExit(int opcode) { Super.onmethodexit (opcode) /** * Override AnnotationVisitor to Override AnnotationVisitor visitAnnotation(String des, boolean visible) {return super.visitAnnotation(des, visible)
                        }
                    }
                    return adapter
            }
    ]
}
Copy the code

If you are using the demo, you will have to package the plugin locally because you have not uploaded it to the JCenter library. Remember to comment out all the dependencies first and then enable the plugin after it is packaged, otherwise it will not compile. Xiaoqingwa {} configuration information is not addressed, as will be discussed later, mainly in order to be able to dynamically replace the peg method without modifying the plug-in.

Timecache.java (); timecache.java (); timecache.java ()

/** * file :xishuang * Date:2018.01.10 * Des */ public class TimeCache {public static Map<String, Long> sStartTime = new HashMap<>(); public static Map<String, Long> sEndTime = new HashMap<>(); public static voidsetStartTime(String methodName, long time) {
        sStartTime.put(methodName, time);
    }

    public static void setEndTime(String methodName, long time) {
        sEndTime.put(methodName, time);
    }

    public static String getCostTime(String methodName) {
        long start = sStartTime.get(methodName);
        long end = sEndTime.get(methodName);
        long dex = end - start;
        return "method: " + methodName + " cost " + dex + " ns"; }}Copy the code

The idea is to use a HashMap to temporarily store the time of the corresponding method and get the time difference when exiting the method. In a method before and after the time statistics method, this specific process how to operate, because the class file is bytecode format, ASM is also bytecode operation, so must be inserted into the code into bytecode first. Java Bytecode Editor (Java Bytecode Editor) is recommended for viewing bytecodes by importing the.class file. For example, we want to insert the following code:

private void countTime() {
    TimeCache.setStartTime("newFunc", System.currentTimeMillis());
    
    TimeCache.setEndTime("newFunc", System.currentTimeMillis());
    Log.d("Take", TimeCache.getCostTime("newFunc"));
}
Copy the code

Compile the.java file into a.class file and open it with the Java Bytecode Editor

/ / method before joining methodVisitor. VisitMethodInsn methodVisitor. VisitLdcInsn (name) methodVisitor. VisitMethodInsn (INVOKESTATIC,"java/lang/System"."currentTimeMillis"."()J".false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."setStartTime"."(Ljava/lang/String; J)V".false) / / method to join methodVisitor. VisitLdcInsn (name) methodVisitor. VisitMethodInsn (INVOKESTATIC,"java/lang/System"."currentTimeMillis"."()J".false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."setEndTime"."(Ljava/lang/String; J)V".false)
methodVisitor.visitLdcInsn("Take")
methodVisitor.visitLdcInsn(name)
methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."getCostTime"."(Ljava/lang/String;) Ljava/lang/String;".false)
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false)

Copy the code

Build. Gradle in the app to configure the bytecode, finally set the filter conditions, the final code is as follows: build. Gradle

xiaoqingwa{
    name = "Xx"
    isDebug = trueMatchData = [// whether to use annotations to find the corresponding method'isAnotation': false// Methods can be matched by class name or implementation interface name'ClassFilter': [['ClassName': 'com.xishuang.plugintest.MainActivity'.'InterfaceName': 'android/view/View$OnClickListener'.'MethodName':'onClick'.'MethodDes':'(Landroid/view/View;) V'VisitAnnotation ->onMethodEnter->onMethodExit'MethodVisitor':{
                MethodVisitor methodVisitor, int access, String name, String desc ->
                    MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
                        boolean isAnnotation = false
                        @Override
                        protected void onMethodEnter() {super.onmethodenter () {super.onmethodenter ()if(! isAnnotation){ //return
//                            }

                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity"."notifyInsert"."()V".false)
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."setStartTime"."(Ljava/lang/String; J)V".false} @override protected void onMethodExit(int opcode) {super.onmethodexit (opcode)if(! isAnnotation){ //return
//                            }

                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."setEndTime"."(Ljava/lang/String; J)V".false)
                            methodVisitor.visitLdcInsn("Take")
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."getCostTime"."(Ljava/lang/String;) Ljava/lang/String;".false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false*/ @override AnnotationVisitor visitAnnotation(String des, Boolean visible) {//if (des.equals("Lcom/xishuang/annotation/AutoCount;")) {
//                                println "Annotation match:" + des
//                                isAnnotation = true
//                            }
                            return super.visitAnnotation(des, visible)
                        }
                    }
                    return adapter
            }
    ]
}
Copy the code

‘isAnotation’ indicates whether the corresponding method is found using annotations, false because we are now judging by the concrete class information. ‘ClassFilter’ indicates the filter criteria, where ‘ClassName’ and ‘InterfaceName’ are used to determine which methods in a class can be traversed for matching. Otherwise, the method name will not be matched. These interested children can modify the extension. ‘MethodName’ and ‘MethodDes’ are method names and method descriptors. You can uniquely determine a MethodName, and methods that meet class filtering criteria are matched, such as the click event onClick(View v) we are counting. Means to inherit from android/view/view $an OnClickListener class or class name is’ com. Xishuang. Plugintest. MainActivity ‘can be traversal method, Then the method meets onClick(View V) and the code is inserted.

After setting, you can check the details in logs. IsDebug = true enables log printing.

It can be seen from the log that the bytecode we set up was actually inserted successfully. Now take a look at the compiled file to verify: app\ Build \intermediates\transforms\AutoTrack\debug\folders

In addition to the above method to find the modified method, you can also use annotations to find the method, simple switch, just need to change the app build.gradle file can be done, the project also has a reference, add a annotation class.

/** * Author:xishuang * Date:2018.1.9 * Des */ @target (ElementType.METHOD) public @interface AutoCount {}Copy the code

Then add your own custom annotations to the corresponding methods

@AutoCount
    private void onClick() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    @AutoCount
    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.button) {
            Toast.makeText(this, "I am the button.", Toast.LENGTH_SHORT).show(); }}Copy the code

Modify the configuration file in build.gradle

xiaoqingwa{
    name = "Xx"
    isDebug = trueMatchData = [// whether to use annotations to find the corresponding method'isAnotation': true// Methods can be matched by class name or implementation interface name'ClassFilter': [['ClassName': 'com.xishuang.plugintest.MainActivity'.'InterfaceName': 'android/view/View$OnClickListener'.'MethodName':'onClick'.'MethodDes':'(Landroid/view/View;) V'VisitAnnotation ->onMethodEnter->onMethodExit'MethodVisitor':{
                MethodVisitor methodVisitor, int access, String name, String desc ->
                    MethodVisitor adapter = new AutoMethodVisitor(methodVisitor, access, name, desc) {
                        boolean isAnnotation = false
                        @Override
                        protected void onMethodEnter() {super.onmethodenter () {super.onmethodenter ()if(! isAnnotation){return
                            }

                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/MainActivity"."notifyInsert"."()V".false)
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."setStartTime"."(Ljava/lang/String; J)V".false} @override protected void onMethodExit(int opcode) {super.onmethodexit (opcode)if(! isAnnotation){return
                            }

                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."setEndTime"."(Ljava/lang/String; J)V".false)
                            methodVisitor.visitLdcInsn("Take")
                            methodVisitor.visitLdcInsn(name)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "com/xishuang/plugintest/TimeCache"."getCostTime"."(Ljava/lang/String;) Ljava/lang/String;".false)
                            methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log"."d"."(Ljava/lang/String; Ljava/lang/String;) I".false*/ @override AnnotationVisitor visitAnnotation(String des, Boolean visible) {if (des.equals("Lcom/xishuang/annotation/AutoCount;")) {
                                println "Annotation match:" + des
                                isAnnotation = true
                            }
                            return super.visitAnnotation(des, visible)
                        }
                    }
                    return adapter
            }
    ]
}
Copy the code

Key code is the ‘isAnotation’ is set to true, then add your annotations in visitAnnotation method class match, namely the des. Equals (” Lcom/xishuang/annotation/AutoCount;” ) code, the descriptor for the annotation class, works much the same as above, but does not print a log, because finding a method by annotation will iterate over every method, printing too much information will explode the computer.

6, plug-in extension automatic burying point function

For the following method for buried point listening, and realize the View of the unique distinction chain

  • View onClick(View v
  • 2, Fragment onResume() method
  • Fragment onPause()
  • Fragment setUserVisibleHint(Boolean b
  • Fragment onHiddenChanged(Boolean B
  • 6, Manually set listening conditions in app module: specified method or annotation method

Add-ons to the automatic burying point processing class are mainly ChoiceUtil

The class in your App that handles listening methods is AutoHelper.java

/** * Author:xishuang * Date:2018.03.01 * Des: AutoHelper class */ public class AutoHelper {private static final String TAG = AutoHelper.class.getSimpleName(); private static Context context = AutoApplication.getInstance().getApplicationContext(); Public static void onClick(View View) {String path = autoutil. getPath(context, View); String activityName = AutoUtil.getActivityName(view); path = activityName +":onClick:"+ path; Log.d(TAG, path); } /** * public static void */onClick() {
        Log.d(TAG, "onClick()");
    }

    public static void onFragmentResume(Fragment fragment) {
        Log.d(TAG, "onFragmentResume" + fragment.getClass().getSimpleName());
    }

    public static void onFragmentPause(Fragment fragment) {
        Log.d(TAG, "onFragmentPause"  + fragment.getClass().getSimpleName());
    }

    public static void setFragmentUserVisibleHint(Fragment fragment, boolean isVisibleToUser) {
        Log.d(TAG, "setFragmentUserVisibleHint->" + isVisibleToUser + "- >" + fragment.getClass().getSimpleName());
    }

    public static void onFragmentHiddenChanged(Fragment fragment, boolean hidden) {
        Log.d(TAG, "onFragmentHiddenChanged->" + hidden + "- >" + fragment.getClass().getSimpleName());
    }
Copy the code

Specific information can see the source code, has been shared to Github, here about the general idea and code framework, the blogger has initially extended the basic function of automatic burying point, more interesting gameplay you can modify the plug-in to achieve. Github address: Luffy.