使用ASM實(shí)現(xiàn)方法攔截框架官撼,再也不用寫(xiě)重復(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();
    }
在這里插入圖片描述

不再需要寫(xiě)其他的代碼漱办,最后的效果是這樣的这刷。

在這里插入圖片描述

使用

該框架已經(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í)行客叉∷薪撸可以選擇的途徑不多。但是最開(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)系作者
  • 序言:七十年代末犀被,一起剝皮案震驚了整個(gè)濱河市椅您,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌寡键,老刑警劉巖掀泳,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡员舵,警方通過(guò)查閱死者的電腦和手機(jī)脑沿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)马僻,“玉大人捅伤,你說(shuō)我怎么就攤上這事∥撞#” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵祠汇,是天一觀的道長(zhǎng)仍秤。 經(jīng)常有香客問(wèn)我,道長(zhǎng)可很,這世上最難降的妖魔是什么诗力? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮我抠,結(jié)果婚禮上苇本,老公的妹妹穿的比我還像新娘。我一直安慰自己菜拓,他們只是感情好瓣窄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著纳鼎,像睡著了一般俺夕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贱鄙,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天劝贸,我揣著相機(jī)與錄音,去河邊找鬼逗宁。 笑死映九,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瞎颗。 我是一名探鬼主播件甥,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼言缤!你這毒婦竟也來(lái)了嚼蚀?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤管挟,失蹤者是張志新(化名)和其女友劉穎轿曙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡导帝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年守谓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片您单。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斋荞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出虐秦,到底是詐尸還是另有隱情平酿,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布悦陋,位于F島的核電站蜈彼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俺驶。R本人自食惡果不足惜幸逆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望暮现。 院中可真熱鬧还绘,春花似錦、人聲如沸栖袋。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)塘幅。三九已至菇怀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間晌块,已是汗流浹背爱沟。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匆背,地道東北人呼伸。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像钝尸,于是被迫代替她去往敵國(guó)和親括享。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容