MehodInterceptor
項(xiàng)目地址
序言
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)方法联予。onMethodIntercepted 和 onMethodInterceptedError
方法參數(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)
- 目前注解不能修飾靜態(tài)方法 (后續(xù)考慮支持)
- 目前方法參數(shù)不支持基本類型 (后續(xù)會(huì)考慮支持)
- 函數(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è)方法中就行了。