MehodInterceptor

The project address

MehodInterceptor

The preface

MehodInterceptor is a method interceptor that uses ASM to dynamically modify bytecode for method interception. With this framework, you can control whether or not a method executes.

For example, some businesses have some general judgment logic: for example, a confirmation prompt pops up to determine whether the user has logged in, and whether the APP has certain permissions. The method is executed only if these judgments pass. Otherwise, the command is not executed.

This general logic can now be annotated to methods.

Like this:


    @confirm (" Confirm to share ")
    @RequestLogin
    @RequestPermission( {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION})
    public void share(Context context) {
        Toast.makeText(context, "Share the success", Toast.LENGTH_SHORT).show();
    }
Copy the code

No more code to write, and the end result is something like this.

use

The framework has been published to mavenCentral(). Only gradle integration is required in the root directory.

buildscript {
    repositories {
        google()
        // The frame is located in the central warehouse
        mavenCentral()
    }
    dependencies {
        classpath "Com. Android. Tools. Build: gradle: 4.1.0." "
        / / this framework
        classpath 'the IO. Making. Zhuguohui: method - interceptor: 1.0.1'}}Copy the code

In the required Module, do as follows


apply plugin:"com.zhuguohui.methodinterceptor"

methodInterceptor {
    include= ["com.example.myapplication"]
    handlers= [
            // Handle reminders
            "com.example.myapplication.handler.confirm.Confirm":"com.example.myapplication.handler.confirm.ConfirmUtil".// Process the login
            "com.example.myapplication.handler.login.RequestLogin":"com.example.myapplication.handler.login.LoginRequestHandler".// Process permission
           "com.example.myapplication.handler.permission.RequestPermission":"com.example.myapplication.handler.permission.PermissionRequestHandler".//test
           "com.example.myapplication.handler.test.Test":"com.example.myapplication.handler.test.TestHandler"]}Copy the code

Parameters that

include Passing in the package name of the class you want to process is essentially a set, and you can pass in more than one
handlers Pass in a map, the key is the annotation you define, and the value is the handler for the annotation. If a method is annotated by the annotation, the bytecode is modified while the modified method is being executed. Pass the method to the processor. Executed by process control

The sample

annotations

Annotations define values that are formatted as JSON data to be returned to the handler when the method is called.


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Confirm {
    String value(a);
}

Copy the code

The processor must have two static methods. OnMethodIntercepted and onMethodInterceptedError

The method parameters

annotationJson Values in annotations formatted to JSON
caller The owner of the method, the class in which the method is declared, passes the this pointer to that class
method Methods annotated by annotations
objects Method to execute

The processor

public class ConfirmUtil {

  static class ConfirmValue{
      String value;
      boolean showToast;

      public String getValue(a) {
          return value;
      }

      public void setValue(String value) {
          this.value = value;
      }

      public boolean isShowToast(a) {
          return showToast;
      }

      public void setShowToast(boolean showToast) {
          this.showToast = showToast; }}public static void onMethodIntercepted(String annotationJson, Object caller, Method method, Object... objects) {

        Context context = (Context) caller;
        ConfirmValue cv=new Gson().fromJson(annotationJson,ConfirmValue.class);
        new AlertDialog.Builder(context)
                .setTitle(cv.value)
                .setPositiveButton("Sure".new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        try {
                            method.setAccessible(true);
                            method.invoke(caller,objects);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        dialog.dismiss();
                    }
                }).setNegativeButton("Cancel".new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        }).create().show();
    }


    public static void onMethodInterceptedError(Object caller,Exception e){
        e.printStackTrace();
        Toast.makeText((Context) caller,"Failed to handle annotation :"+e.getMessage(),Toast.LENGTH_SHORT).show(); }}Copy the code

Matters needing attention

  1. Annotations cannot currently modify static methods (support for later consideration)
  2. Method parameters currently do not support basic types (support will be considered later)
  3. The function cannot have a return value. The callback that needs to be returned can be implemented via the pass interface (depending on the usage scenario).

If it is a basic type, use the corresponding wrapper type. Such as

  / / does not support
    public void add(int a,int b){}/ / support
    public void add(Integer a,Integer b){}Copy the code

debugging

Because of the bytecode involved, you can build and view the generated code in this location later if you feel the generated method is not correct.

The principle of

Why ASM

To implement annotation to change method execution. There are few options. But the first thing that came to mind was Java’s dynamic proxy.

But the downside of dynamic proxies is that only interfaces can be proxy. That is, you must encapsulate an interface. It’s too expensive to use.

Then came the idea of using Cglib for proxy. But there’s a problem. Cglib implements proxies by dynamically creating a subclass of a class. But if our business code is declared in the activity, there is no way. Because the initialization of an activity is not up to us.

Then AOP was considered. AspectJ comes to mind first. AspectJX was used.AspectJXHowever, AspectJX does not support MutilDex. A lot of people said they couldn’t launch the app. I also have the following errors, feel the author also did not maintain. I abandoned the library.I had to use ASM later

Methods to replace

Briefly, if a method is commented as follows

  @RequestLogin
    public void comment(Context context) {
        Toast.makeText(context, "Review success", Toast.LENGTH_SHORT).show();
    }
Copy the code

With ASM, the ongoing method comment is copied to a method of another name.

  private void _confirm_index46_commit(Context context) {
        Toast.makeText(context, "Issued successfully".0).show();
    }
Copy the code

And the old method will be modified to look like this

public void comment(Context context) {
        try {
            Method var9 = null;
            String var3 = "_requestlogin_index48_comment";
            String var4 = "{}";
            Method[] var5 = this.getClass().getDeclaredMethods(a);for(int var6 = 0; var6 < var5.length; ++var6) {
                if (var5[var6].getName().equals(var3)) {
                    var9 = var5[var6];
                    break; }}if (var9 == null) {
                throw new RuntimeException("don't find method [" + var3 + "] in class [" + this.getClass().getName() + "]");
            }

            LoginRequestHandler.onMethodIntercepted(var4, this, var9, new Object[]{context});
        } catch (Exception var8) {
            Exception var2 = var8;

            try {
                LoginRequestHandler.onMethodInterceptedError(this, var2);
            } catch (Exception var7) {
                var7.printStackTrace(a); }}}Copy the code

Of course, the above logic can be superimposed, that is, annotations can be used superimposed.

   @confirm (" Confirm to share ")
    @RequestLogin
    @RequestPermission( {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION})
    public void share(Context context) {
        Toast.makeText(context, "Share the success", Toast.LENGTH_SHORT).show();
    }
Copy the code

Finally, the real method is this

private void _requestpermission_index54__requestlogin_index53__confirm_index52_share(Context context) {
        Toast.makeText(context, "Share the success".0).show();
    }
Copy the code

Method reinstallation will not be a problem

No, there is an index in the aspect name to solve this problem. This index is a static variable, and iterates through methods with the same name to get different indexes.

The bytecode

By defining a Gradle plugin, we register a Transform in the Android plugin. The Transform performs processing between the class and dex.

The ASM framework is used to modify the class.

Using the following plug-ins, you can generate ASM code.Plug-in effect

And finally, slow work makes good work. The Android Studio comparison tool is used to compare the differences between ASM code of different methods to achieve the function.

Method replication

ASM provides two API frameworks, one for parsing classes into a Node tree and the other for parsing classes into events. If you don’t understand, think of XML parsing.

In an event-based API we just copy the instructions that we encounter into another method.