MehodInterceptor
项目地址
MehodInterceptor
序言
MehodInterceptor是一个使用ASM来动态修改字节码,以达到方法拦截。通过该框架,可以控制某个方法是否执行。
比如某些业务有一些通用的判断逻辑:比如弹出确认提示,判断用户是否登录,判断APP是否具有某些权限。只有这些判断通过,才会执行该方法。否则不执行。
这些通用的逻辑,现在可以通过注解的方式添加到方法上。
比如这样:
@Confirm("确定执行分享操作")
@RequestLogin
@RequestPermission( {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION})
public void share(Context context) {
Toast.makeText(context, "分享成功", Toast.LENGTH_SHORT).show();
}
不再需要写其他的代码,最后的效果是这样的。
使用
该框架已经发布到 mavenCentral()了。只需要在根目录的gradle集成。
buildscript {
repositories {
google()
//框架所在的中央仓库
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.0"
//本框架
classpath 'io.github.zhuguohui:method-interceptor:1.0.1'
}
}
在需要使用的module中如下配置即可
apply plugin:"com.zhuguohui.methodinterceptor"
methodInterceptor {
include= ["com.example.myapplication"]
handlers= [
//处理提醒
"com.example.myapplication.handler.confirm.Confirm":"com.example.myapplication.handler.confirm.ConfirmUtil",
//处理登录
"com.example.myapplication.handler.login.RequestLogin":"com.example.myapplication.handler.login.LoginRequestHandler",
//处理权限
"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"]
}
参数说明
include | 传入你想处理的类所在的包名本质上是一个set,可以传入多个 |
handlers | 传入一个map,key是你定义的注解,value是注解的处理器。如果一个方法被该注解注释,就修改字节码,在改方法被执行的时候。将该方法传递给处理器。由处理控制执行 |
示例
注解
注解中定义值,这些值在方法被调用的时候会被格式化成JSON数据返回给处理器。
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Confirm {
String value();
}
处理器必须有两个静态方法。onMethodIntercepted 和 onMethodInterceptedError
方法参数
annotationJson | 被格式化成JSON的注解中的值 |
caller | 方法的拥有者,方法声明在那个类中,就会传递该类的this指针 |
method | 被注解注释的方法 |
objects | 方法执行的所有参数 |
处理器
public class ConfirmUtil {
static class ConfirmValue{
String value;
boolean showToast;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public boolean isShowToast() {
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("确定", 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("取消", 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,"处理注解失败:"+e.getMessage(),Toast.LENGTH_SHORT).show();
}
}
注意事项
- 目前注解不能修饰静态方法 (后续考虑支持)
- 目前方法参数不支持基本类型 (后续会考虑支持)
- 函数不能有返回值,需要返回的可以通过传递接口利用回调实现 (使用场景决定的)
如果是基本类型,需要使用对应的包装类型。比如
//不支持
public void add(int a,int b){
}
//支持
public void add(Integer a,Integer b){
}
调试
因为涉及到字节码,如果感觉生成的方法不对可以build以后再这个位置查看生成的代码。
原理
为什么是ASM
要想实现通过注解来改变方法的执行。可以选择的途径不多。但是最开始想到的就是java的动态代理。
但是动态代理有个不好的地方就是只能代理接口。也就是说必须封装一个接口出来。这样的使用成本太高。
后来想到了使用Cglib来实行代理。但是也有一个问题。Cglib通过动态创建一个类的子类来实现代理。但是如果我们的业务代码是在activity中声明的那么就没有办法了。因为activity的初始化不是我们能决定的。
后来就考虑AOP。最先想到的就是AspectJ。用了大名鼎鼎的AspectJX。
AspectJX
但是AspectJX对MutilDex支持的不好。很多人都反应无法启动app。我也出现了以下错误,感觉作者也没有维护了。就放弃了这个库。
后面不得不用了ASM
方法替换
简单的说一下,如果一个方法被如下注释
@RequestLogin
public void comment(Context context) {
Toast.makeText(context, "评论成功", Toast.LENGTH_SHORT).show();
}
通过ASM 会把正在的方法 comment 复制成一个其他名字的方法。
private void _confirm_index46_commit(Context context) {
Toast.makeText(context, "签发成功", 0).show();
}
而原来的方法会被改造成这样
public void comment(Context context) {
try {
Method var9 = null;
String var3 = "_requestlogin_index48_comment";
String var4 = "{}";
Method[] var5 = this.getClass().getDeclaredMethods();
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();
}
}
}
当然以上的逻辑是可以叠加的,也就是注解可以叠加使用。
@Confirm("确定执行分享操作")
@RequestLogin
@RequestPermission( {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION})
public void share(Context context) {
Toast.makeText(context, "分享成功", Toast.LENGTH_SHORT).show();
}
最后真正的方法是这样
private void _requestpermission_index54__requestlogin_index53__confirm_index52_share(Context context) {
Toast.makeText(context, "分享成功", 0).show();
}
方法重装会不会出问题
不会,方面名称中有一个index就是为了解决这个问题的。这个index是一个静态变量,遍历相同名字方法的时候所得到的index不同。
字节码
通过定义一个gradle插件,在Android 的插件中注册了一个Transform ,Transform的作用就是在class 到dex之间进行处理。
而修改class使用的是ASM框架。
使用一下以下插件,可以生成ASM代码。
插件效果
最后就是慢工出细活。通过Android Studio的对比工具来对比不同方法的ASM代码的差异,实现功能。
方法的复制
ASM 提供了两套API框架,一套把class解析成一颗Node树,一套把class解析成各种事件。不明白的可以想想xml的解析。
在基于事件的API中我们只需要把遇到的指令,拷贝到另一个方法中就行了。