Demo address: github.com/ClericYi/As…

preface

The recent work is not mainly about pile insertion, but the use of Lancet pile insertion has brought great benefits to the project, which is related to the design of the project. The original design was to complete the access of my new functions with the minimal modification of an original component in Douyin. The project sank from SPI -> Main project Lancet -> Lancet into a custom component, and the repeated attempts to grasp the horrors of this dark technology are indeed true.

Here’s what happened:

First, compare the advantages and disadvantages of phase I and Phase II: Practice finds that the advantages of phase I compared with phase II only do not affect the main project, and the disadvantages are mainly manifested in three aspects:

  1. apiBe altered,implandcomponentYou need to modify it in linkage.
  2. The circumstances at that time decided to useSPIThis will result in a large amount of data being acquired that does not need to be acquired prematurely, resulting in run-time engineering performance degradation, and reflection in performance loss.

We also said that the timing of the implementation of the Lancet needs to be controlled. It is impossible to make it globally effective, because it will affect the compilation speed under certain circumstances. In addition, it will also increase the maintenance cost in the later stage.

The above summary finally leads to the third scheme, which does not affect the main project and does not need to grasp the effective time. The work can be easily completed only when a component gives Hook points.

This article will only explore how to implement AOP schemes such as AscpectJ.

Popular piling scheme explored

A look at some of the most popular piling schemes on Github shows that AspectJ and Lancet are widely used, while AspectJX, an extension of AspectJ, is widely used for its good compatibility.

AspectJXUsing method of

AspectJX is based on gradle Android plugin version 1.5 and above.

Plug-in to introduce

// root -> build.gradle
dependencies {
        classpath 'com. Hujiang. Aspectjx: gradle - android plugin - aspectjx: mid-atlantic moved'        
}
// app -> build.gradle
apply plugin: 'android-aspectjx'
Copy the code

How to use

Android_Permission_AspectjX is a permission request library, but there is a Bug that does not apply to the base class Activity annotation. The base class method is fine.

// 1. app --> build.gradle
compile 'com. Firefly1126. Permissionaspect: permissionaspect: 1.0.1'
// 2. Customize Application
onCreate(){
	PermissionCheckSDK.init(Application);
}
// 3. Add @needPermission as an annotation
@NeedPermission(permissions = {Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
public class BActivity extends Activity {}

// The method applied to the class
@NeedPermission(permissions = {Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
private void startBActivity(String name, long id) {
        startActivity(new Intent(MainActivity.this, BActivity.class));
    }
Copy the code

It is very simple to apply for permission using two annotations.

Some pits in this library

This has completed the import of the library, but access to some Baidu data will find that such a problem occurs library conflict. For example, if there is a conflict with alipay SDK, the following is a code for reproduction.

PayTask alipay = new PayTask(this);
Copy the code

This is due to AspectJX itself, which handles all binaries and libraries by default. AspectJX provides include and exclude commands to filter files that need to be processed and exclude certain files (including class files and JAR files). Of course, to solve this problem, the developers also provide a solution, namely whitelist.

aspectjx {
	// Exclude all class files and libraries (jar files) containing 'android.support' in package path
	exclude 'android.support'
    // exclude '*'
    // Disable AspectJX. This function is enabled by default
    enabled false
}
Copy the code

LancetThe use of

This article is only a brief introduction, for more specific use please see the warehouse: github.com/eleme/lance…

  1. Plug-in to introduce
// root --> build.gradle
dependencies {
    classpath 'com. Android. Tools. Build: gradle: 3.3.2 rainfall distribution on 10-12'
    classpath 'me. Ele: lancet - plugin: 1.0.6'
}
// build.gralde
apply plugin: 'me.ele.lancet'
dependencies {
    compileOnly 'me. Ele: lancet - base: 1.0.6'
}
Copy the code
  1. LancetThe use of
public class LancetHooker {
    @Insert(value = "eat", mayCreateSuper = true)
    @TargetClass(value = "com.example.lancet.Cat", scope = Scope.SELF)
    public void _eat(a) {
        ((Cat)This.get()).bark();
        // We can use this to access members of the current Cat class, only for Insert hooks.(for now)
        System.out.println("> > > > > > >" + this);
        Origin.callVoid();
    }

    @Insert(value = "bark", mayCreateSuper = true)
    @TargetClass(value = "com.example.lancet.Cat", scope = Scope.SELF)
    public void _bark(a){
        System.out.println("Bark is called."); Origin.callVoid(); }}Copy the code

When a Hook point is defined and searched at compile time, the result will look like this when the compile is complete.

public class Cat {

    class _lancet {
        private _lancet(a) {}Call com_example_lancet_LancetHooker__bark
        // If a method such as origine.call () exists internally, the procedure is performed on the original method at its Call point
        @Insert(mayCreateSuper = true, value = "bark")
        @TargetClass(scope = Scope.SELF, value = "com.example.lancet.Cat")
        static void com_example_lancet_LancetHooker__bark(Cat cat) {
            System.out.println("Bark is called.");
            cat.bark$___twin___();
        }

        @Insert(mayCreateSuper = true, value = "eat")
        @TargetClass(scope = Scope.SELF, value = "com.example.lancet.Cat")
        static void com_example_lancet_LancetHooker__eat(Cat cat) {
            cat.bark();
            PrintStream printStream = System.out;
            printStream.println("> > > > > > >"+ cat); cat.eat$___twin___(); }}public void bark(a) {
        _lancet.com_example_lancet_LancetHooker__bark(this);
    }

    public void eat(a) {
        _lancet.com_example_lancet_LancetHooker__eat(this);
    }

    /* access modifiers changed from: private */
    public void eat$___twin___() {
        System.out.println("Cats eat mice.");
    }

    public String toString(a) {
        return "Cat";
    }

    /* access modifiers changed from: private */
    public void bark$___twin___() {
        System.out.println("The cat meow."); }}Copy the code

You can see that what it does is it makes modifications to the source code, and the modifications are to build a static inner class, and the corresponding inner method, and reconfigure the call chain to accomplish the result. What about AspectJ, did he do it that way?

AspectJHow is it implemented?

The permission request can be done with just a few comments, so how does he do it? We can decomcompile the code to see this through JadX-GUI.

Because AspectJX works on all files by default, adding annotations is hijacked unless you use the open whitelist described above

public final class MainActivity extends BaseActivity {
    private static final /* synthetic */ JoinPoint.StaticPart ajc$tjp_0 = null;
    private HashMap _$_findViewCache;

    /* compiled from: MainActivity.kt */
    public class AjcClosure1 extends AroundClosure {
        public AjcClosure1(Object[] objArr) {
            super(objArr);
        }

        public Object run(Object[] objArr) {
            Object[] objArr2 = this.state;
            MainActivity.onCreate_aroundBody0((MainActivity) objArr2[0], (Bundle) objArr2[1], (JoinPoint) objArr2[2]);
            return null; }}static {
        ajc$preClinit();
    }

    private static /* synthetic */ void ajc$preClinit() {
        Factory factory = new Factory("MainActivity.kt", MainActivity.class);
        ajc$tjp_0 = factory.makeSJP(JoinPoint.METHOD_EXECUTION, (Signature) factory.makeMethodSig("4"."onCreate"."com.example.stub.MainActivity"."android.os.Bundle"."savedInstanceState".""."void"), 12);
    }

    public void _$_clearFindViewByIdCache() {
        HashMap hashMap = this._$_findViewCache;
        if(hashMap ! =null) { hashMap.clear(); }}public View _$_findCachedViewById(int i) {
        if (this._$_findViewCache == null) {
            this._$_findViewCache = new HashMap();
        }
        View view = (View) this._$_findViewCache.get(Integer.valueOf(i));
        if(view ! =null) {
            return view;
        }
        View findViewById = findViewById(i);
        this._$_findViewCache.put(Integer.valueOf(i), findViewById);
        return findViewById;
    }

    static final /* synthetic */ void onCreate_aroundBody0(MainActivity ajc$this, Bundle savedInstanceState, JoinPoint joinPoint) {
        super.onCreate(savedInstanceState);
        ajc$this.setContentView((int) R.layout.activity_main);
    }

    /* access modifiers changed from: protected */
    public void onCreate(Bundle savedInstanceState) {
        JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, (Object) this, (Object) this, (Object) savedInstanceState);
        PermissionAspect.aspectOf().adviceOnActivityCreate(new AjcClosure1(new Object[]{this, savedInstanceState, makeJP}).linkClosureAndJoinPoint(69648)); }}Copy the code

A review of the compiled source code shows that the code you have written has been modified in some special way, so we should have our own goal, annotation + automated code modification to complete the task.

How do I automate code changes

The first capability we need to borrow here is traversal from the Gradle Transform Api, which Android Studio naturally integrates with you when you create an Android project.

The capabilities of this Api are only available with Gradle Version 1.5+

So how does it work? Mistress, pictured above.

The above is the complete packaging process of Apk, but using the Transform Api will have more parts in the red box. Of course, if there are annotations in the.class Files of the third party, you can also get caught. So here we know that a target is the compiled.class files, and the logic to change the code must be related to the logic we want to implement.

Having seen the decompilation of a code modification pattern above, we can first consider how this code modification can be carried out. For instance

public void fun(Login login){
	login.on();
}
Copy the code

But we want to directly hijack a method like this, because this method just does a login operation, but I want to do authentication? It’s fine if there’s only one place in the code, but what about multiple places? Maybe my code would look like this

public void fun(Login login){
	if(login.check()) login.on();
    else login.close()
}
Copy the code

The code above is fairly simple, but there are times when repetitive writing of this logic is common, and as the size of the code increases, it becomes more difficult to maintain, and if the authentication method changes one day, it will be cold. This is where staking is often used – AOP is faceted, and all you need to do when implementing code is to annotate the corresponding method and process logic in unison.

Insert pile implementation

The first link: how to implant the ability of piling

Here really really read a lot of information on the Internet, quality is uneven, it took a whole day, finally put the whole thing running 🤣 🤣 🤣, the following article will give me the easiest project to create the scheme.

If you just want to test locally, here’s the easiest way to do it: use buildSrc(case-sensitive!). Naming your Android Library can save you 99% of the hassle.

At the end of the article will be given a version can be used for the implementation of the introduction.

That starts with the first step, the use of plug-ins.

In order to introduce Gradle’s capabilities, change the contents of build. Gradle in your repository to the following form.

apply plugin: 'groovy'

dependencies {
    implementation gradleApi(a)//gradle sdkImplementation 'com. Android. Tools. Build: gradle: 3.5.4' implementation 'com. Android. View the build: gradle - API: 3.5.4'/ / ASM dependencyImplementation 'org.ow2.asm:asm:8.0' implementation 'org.ow2.asm: 8.0' implementation 'org. Ow2. Asm: asm - Commons: 8.0'} repositories{
    google()
    jcenter()
}
Copy the code

Once you’ve done sync, you need to generate a plug-in that you can use.

/** * Create by yiyonghao on 2020-08-08 * Email: [email protected] */
public class AsmPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        System.out.println("=========== doing ============"); }}Copy the code

And in the main project of the app – > build. Gradle add statements apply plugin: com. Example. Buildsrc. AsmPlugin (package name. Plug-in name).

Many projects say to use Groovy to do, in fact, there is no need to direct Java can be.

At this point, if the data =========== doing ============ is printed in the build process, the plug-in is working, and now it’s time to move on to the next step, how to complete the staking of the code.

What capabilities does the overall Gradle Transform API provide without introducing ASM? If we want to pin the code, we must do the following steps:

  1. Source code file acquisition (possibly.classOr it could be.jar)
  2. File modification

Source file acquisition

To get the path to the file, we use the Transform class provided by the Gradle Transform API. The variables in the Transform () method already automatically provide us with many of their own capabilities, such as file traversal.

public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        // Consumer input, from which you can get the JAR package and the class folder path. Need to output to the next task
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //OutputProvider manages the output path. If the consumer input is empty, you will find OutputProvider == null
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        for (TransformInput input : inputs) {
            for (JarInput jarInput : input.getJarInputs()) {
                File dest = outputProvider.getContentLocation(
                        jarInput.getFile().getAbsolutePath(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
            }
            for(DirectoryInput directoryInput : input.getDirectoryInputs()) { File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY); transformDir(directoryInput.getFile(), dest); }}}Copy the code

Through the above way, we can scan our file, then we should access the second step, how to modify the file?

File modification

I’ve never mentioned the logic of the Gradle Transform API for modifying code. Why is that?

Because it does not provide such special functionality, we will introduce the most commonly heard of ASM to do bytecode modification. Here we start by putting our attention to our two classes AsmClassAdapter and AsmMethodVisitor and asmTransform.weave ().

aboutASMVery, very often involved are the following core classes.

Of course, the Demo I’m giving you right now has two classes, the AsmClassAdapter is the one that inherits the ClassVisitor to access the Class which is our Class by Class, The AsmMethodVisitor is passed through the data of the ClassVisitor and then used to access methods that exist in the class.

private static void weave(String inputPath, String outputPath) {
        try {
        	//...
        	// Access to file structures is identified by ASM based capabilities
            ClassReader cr = new ClassReader(is);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            AsmClassAdapter adapter = new AsmClassAdapter(cw);
            cr.accept(adapter, 0);
            //...
        } catch(IOException e) { e.printStackTrace(); }}Copy the code

Essentially, ASM analyzes a file, lets focus on what we want to insert, and how we want to insert it, and then he uses the corresponding scheme to modify the bytecode.

AsmClassAdapterandAsmMethodVisitorA simple implementation of
public class AsmClassAdapter extends ClassVisitor implements Opcodes {
    public AsmClassAdapter(ClassVisitor classVisitor) {
        super(ASM7, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        return (mv == null)?null : new AsmMethodVisitor(mv); / / 1 - >}}Copy the code

And the MethodVisitor method, for us, is just a pegging scheme for the method.

public class AsmMethodVisitor extends MethodVisitor{
    public AsmMethodVisitor(MethodVisitor methodVisitor) {
        super(ASM7, methodVisitor);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
        // Prints before the method is executed
        mv.visitLdcInsn(" before method exec");
        mv.visitLdcInsn(" [ASM 测试] method in " + owner + " ,name=" + name);
        mv.visitMethodInsn(INVOKESTATIC,
                "android/util/Log"."i"."(Ljava/lang/String; Ljava/lang/String;) I".false);
        mv.visitInsn(POP);
		// The original method
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);

        // Prints after the method is executed
        mv.visitLdcInsn(" after method exec");
        mv.visitLdcInsn(" method in " + owner + " ,name=" + name);
        mv.visitMethodInsn(INVOKESTATIC,
                "android/util/Log"."i"."(Ljava/lang/String; Ljava/lang/String;) I".false); mv.visitInsn(POP); }}Copy the code

You can implement more methods like this. And having done that, are we done with the so-called bytecode modification?

Step 2: File overwrite

You may not run, here directly give an answer, and not complete!! We have modified the bytecode, but have you finished overwriting the file?

So you can find code like this in the Demo:

  1. weave()methods
private static void weave(String inputPath, String outputPath) {
        try {
        	// A new file has been created
            FileInputStream is = new FileInputStream(inputPath);
            ClassReader cr = new ClassReader(is);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            AsmClassAdapter adapter = new AsmClassAdapter(cw);
            cr.accept(adapter, 0);
            FileOutputStream fos = new FileOutputStream(outputPath);
            fos.write(cw.toByteArray());
            fos.close();
        } catch(IOException e) { e.printStackTrace(); }}Copy the code
  1. FileUtils.copyFile(jarInput.getFile(), dest);There arejarPackage location migration, all in order to store new code

With that done, let’s take a look at what the resulting code looks like. (File path: app > build > intermediates > transform –> package name –> debug –> all the way to your file.) For example, I generated mainActivity.java locally.

public class MainActivity extends AppCompatActivity {
    public MainActivity(a) {
        Log.i(" before method exec"."[] ASM test method in androidx appcompat/app/AppCompatActivity, name = < init >");
        super(a); Log.i(" after method exec"." method in androidx/appcompat/app/AppCompatActivity ,name=<init>");
    }

    protected void onCreate(Bundle savedInstanceState) {
        Log.i(" before method exec"."[] ASM test method in androidx appcompat/app/AppCompatActivity, name = onCreate");
        super.onCreate(savedInstanceState);
        Log.i(" after method exec"." method in androidx/appcompat/app/AppCompatActivity ,name=onCreate");
        Log.i(" before method exec"."[] ASM test method in com/example/ASM/MainActivity, name = the setContentView." ");
        this.setContentView(2131361820);
        Log.i(" after method exec"." method in com/example/asm/MainActivity ,name=setContentView");
        Log.i(" before method exec".[ASM test] method in Android /util/Log,name=e");
        Log.e("aa"."aa");
        Log.i(" after method exec"." method in android/util/Log ,name=e"); }}Copy the code

If you find it troublesome, you can also use a tool called ASM Bytecode Outline to complete the code view after staking

Each method ends up inserting the code we want to insert, so ok, that’s a big step towards our goal of staking through annotations.

How is this done through annotations

Since we are going to use annotations to accomplish the event, we will create an annotation at this point, but note that the @Retention annotation needs to take effect at compile time.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface ASM {}
Copy the code

You can then add the method to mainActivity.java and add this annotation. So what’s next? This must be the annotation that you’re scanning, which is the visitAnnotation() method.

@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        return super.visitAnnotation(descriptor, visible);
    }
Copy the code

But looking at the inherited method, it’s not obvious that it can’t modify the annotation itself, so our final compromise is to add a flag that tells visitMethodInsn() when to insert a method that it needs to insert.

@Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        if(ANNOTATION_TRACK_METHOD.equals(descriptor)) isMatch = true;
        return super.visitAnnotation(descriptor, visible);
    }
Copy the code

VisitMethodInsn (), however, needs to be determined before inserting, so that piling can be carried out when necessary. Here are the results:

public class MainActivity extends AppCompatActivity {
    public MainActivity(a) {}protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(2131361820);
        Log.e("aa"."aa");
    }

    @Cat
    public void fun(a) {
        Log.d("tag"."onCreate start");
        Log.d("tag"."onCreate end");
    }

    @ASM
    public void fun1(a) {}}Copy the code

Publish a plugin that can be used by others

Don’t worry about the Module name at this point. Define the name you want. For convenience, you can choose to copy the code you wrote in buildSrc earlier. The first thing we need to do is to use Gradle for the upload operation.

// Add the following code to your new Module --> build.gradle
uploadArchives {
    repositories.mavenDeployer {
        repository(url: uri('.. /repo'))
        pom.groupId = 'com.example.asm'
        pom.artifactId = 'asm_plugin'
        pom.version = '1.0.0'}}Copy the code

But if we were released at this point and introduced in the main project, we wouldn’t be foundPluginThe plug-in.

Because he needs to do one more step, to create the following directory, this is so that the files we publish can be found

implementation-class = com.example.asm_plugin.AsmPlugin // Plugins are given in the package location
Copy the code

Finally, the repo is introduced in root –> build.gralde, which works just as buildSrc does.

buildscript {
    repositories {
        google()
        jcenter()
        maven {
            url uri("repo")
        }
    }
    dependencies {
        classpath 'com. Android. Tools. Build: gradle: 3.5.4'
        classpath 'com. Example. Asm: asm_plugin: 1.0.0'}}Copy the code

The resources

  • Android AOP AspectJX conflicts with third-party librarieswww.jianshu.com/p/3899f0431…
  • Work with me to implement compile-time bytecode weaving with ASM:Juejin. Cn/post / 684490…
  • ASM for Android Fully embedded Solutions:www.sensorsdata.cn/blog/201812…