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
- Annotations cannot currently modify static methods (support for later consideration)
- Method parameters currently do not support basic types (support will be considered later)
- 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.