使用ASM實(shí)現(xiàn)方法攔截框架,再也不用寫重復(fù)代碼了

MehodInterceptor

項(xiàng)目地址

MehodInterceptor

序言

MehodInterceptor是一個(gè)使用ASM來(lái)動(dòng)態(tài)修改字節(jié)碼,以達(dá)到方法攔截。通過(guò)該框架,可以控制某個(gè)方法是否執(zhí)行。

比如某些業(yè)務(wù)有一些通用的判斷邏輯:比如彈出確認(rèn)提示,判斷用戶是否登錄,判斷APP是否具有某些權(quán)限。只有這些判斷通過(guò),才會(huì)執(zhí)行該方法。否則不執(zhí)行。

這些通用的邏輯,現(xiàn)在可以通過(guò)注解的方式添加到方法上。

比如這樣:


    @Confirm("確定執(zhí)行分享操作")
    @RequestLogin
    @RequestPermission( {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.ACCESS_FINE_LOCATION})
    public void share(Context context) {
        Toast.makeText(context, "分享成功", Toast.LENGTH_SHORT).show();
    }
在這里插入圖片描述

不再需要寫其他的代碼,最后的效果是這樣的。

在這里插入圖片描述

使用

該框架已經(jīng)發(fā)布到 mavenCentral()了。只需要在根目錄的gradle集成。

buildscript {
    repositories {
        google()
        //框架所在的中央倉(cāng)庫(kù)
        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",
            //處理權(quán)限
           "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"]
}

參數(shù)說(shuō)明

include 傳入你想處理的類所在的包名本質(zhì)上是一個(gè)set,可以傳入多個(gè)
handlers 傳入一個(gè)map,key是你定義的注解,value是注解的處理器。如果一個(gè)方法被該注解注釋,就修改字節(jié)碼,在改方法被執(zhí)行的時(shí)候。將該方法傳遞給處理器。由處理控制執(zhí)行

示例

注解

注解中定義值,這些值在方法被調(diào)用的時(shí)候會(huì)被格式化成JSON數(shù)據(jù)返回給處理器。


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

處理器必須有兩個(gè)靜態(tài)方法。onMethodInterceptedonMethodInterceptedError

方法參數(shù)

annotationJson 被格式化成JSON的注解中的值
caller 方法的擁有者,方法聲明在那個(gè)類中,就會(huì)傳遞該類的this指針
method 被注解注釋的方法
objects 方法執(zhí)行的所有參數(shù)

處理器

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




}

注意事項(xiàng)

  1. 目前注解不能修飾靜態(tài)方法 (后續(xù)考慮支持)
  2. 目前方法參數(shù)不支持基本類型 (后續(xù)會(huì)考慮支持)
  3. 函數(shù)不能有返回值,需要返回的可以通過(guò)傳遞接口利用回調(diào)實(shí)現(xiàn) (使用場(chǎng)景決定的)

如果是基本類型,需要使用對(duì)應(yīng)的包裝類型。比如

  //不支持
    public void add(int a,int b){
      
    }

    //支持
    public void add(Integer a,Integer b){

    }

調(diào)試

因?yàn)樯婕暗阶止?jié)碼,如果感覺(jué)生成的方法不對(duì)可以build以后再這個(gè)位置查看生成的代碼。


在這里插入圖片描述

在這里插入圖片描述

原理

為什么是ASM

要想實(shí)現(xiàn)通過(guò)注解來(lái)改變方法的執(zhí)行??梢赃x擇的途徑不多。但是最開(kāi)始想到的就是java的動(dòng)態(tài)代理。

但是動(dòng)態(tài)代理有個(gè)不好的地方就是只能代理接口。也就是說(shuō)必須封裝一個(gè)接口出來(lái)。這樣的使用成本太高。

后來(lái)想到了使用Cglib來(lái)實(shí)行代理。但是也有一個(gè)問(wèn)題。Cglib通過(guò)動(dòng)態(tài)創(chuàng)建一個(gè)類的子類來(lái)實(shí)現(xiàn)代理。但是如果我們的業(yè)務(wù)代碼是在activity中聲明的那么就沒(méi)有辦法了。因?yàn)閍ctivity的初始化不是我們能決定的。

后來(lái)就考慮AOP。最先想到的就是AspectJ。用了大名鼎鼎的AspectJX。
AspectJX
但是AspectJX對(duì)MutilDex支持的不好。很多人都反應(yīng)無(wú)法啟動(dòng)app。我也出現(xiàn)了以下錯(cuò)誤,感覺(jué)作者也沒(méi)有維護(hù)了。就放棄了這個(gè)庫(kù)。

在這里插入圖片描述

后面不得不用了ASM

方法替換

簡(jiǎn)單的說(shuō)一下,如果一個(gè)方法被如下注釋

  @RequestLogin
    public void comment(Context context) {
        Toast.makeText(context, "評(píng)論成功", Toast.LENGTH_SHORT).show();
    }

通過(guò)ASM 會(huì)把正在的方法 comment 復(fù)制成一個(gè)其他名字的方法。

  private void _confirm_index46_commit(Context context) {
        Toast.makeText(context, "簽發(fā)成功", 0).show();
    }

而原來(lái)的方法會(huì)被改造成這樣

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

    }

當(dāng)然以上的邏輯是可以疊加的,也就是注解可以疊加使用。

   @Confirm("確定執(zhí)行分享操作")
    @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();
    }

方法重裝會(huì)不會(huì)出問(wèn)題

不會(huì),方面名稱中有一個(gè)index就是為了解決這個(gè)問(wèn)題的。這個(gè)index是一個(gè)靜態(tài)變量,遍歷相同名字方法的時(shí)候所得到的index不同。

字節(jié)碼

通過(guò)定義一個(gè)gradle插件,在Android 的插件中注冊(cè)了一個(gè)Transform ,Transform的作用就是在class 到dex之間進(jìn)行處理。

而修改class使用的是ASM框架。

使用一下以下插件,可以生成ASM代碼。


在這里插入圖片描述

插件效果


在這里插入圖片描述

最后就是慢工出細(xì)活。通過(guò)Android Studio的對(duì)比工具來(lái)對(duì)比不同方法的ASM代碼的差異,實(shí)現(xiàn)功能。


在這里插入圖片描述

在這里插入圖片描述

方法的復(fù)制

ASM 提供了兩套API框架,一套把class解析成一顆Node樹(shù),一套把class解析成各種事件。不明白的可以想想xml的解析。

在基于事件的API中我們只需要把遇到的指令,拷貝到另一個(gè)方法中就行了。

在這里插入圖片描述
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容