使用ASM实现方法拦截框架,再也不用写重复代码了

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();
}

处理器必须有两个静态方法。onMethodInterceptedonMethodInterceptedError

方法参数

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();
    }




}

注意事项

  1. 目前注解不能修饰静态方法 (后续考虑支持)
  2. 目前方法参数不支持基本类型 (后续会考虑支持)
  3. 函数不能有返回值,需要返回的可以通过传递接口利用回调实现 (使用场景决定的)

如果是基本类型,需要使用对应的包装类型。比如

  //不支持
    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中我们只需要把遇到的指令,拷贝到另一个方法中就行了。

在这里插入图片描述
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容