Skip to content

@AndroidAopMatchClassMethod

FlyJingFish edited this page May 7, 2024 · 76 revisions

简述

@AndroidAopMatchClassMethod 是做匹配某类及其对应方法的切面的,这个切面方式关注的是方法的执行(Method Execution) 注意区分和@AndroidAopReplaceClass的区别

这个方式不同于 @AndroidAopPointCut 的地方就是不用在代码内添加任何代码,包括注解

@AndroidAopMatchClassMethod(
    targetClassName = 目标类名(包含包名),
    methodName = 方法名数组,
    type = 匹配类型,非必须,默认 EXTENDS
    excludeClasses = 排除继承关系中的一些类的数组(type 不是 SELF 才有效),非必须
)

type 有四种类型(不设置默认 EXTENDS):

  • SELF 表示匹配的是 targetClassName 所设置类的自身
  • EXTENDS 表示匹配的是所有继承于 targetClassName 所设置的类
  • DIRECT_EXTENDS 表示匹配的是 直接继承于 targetClassName 所设置的类
  • LEAF_EXTENDS 表示匹配的是 末端继承(就是没有子类了) targetClassName 所设置的类

简单来说,LEAF_EXTENDSDIRECT_EXTENDS是两个极端,前者关注的是继承关系中最后一个节点,后者关注的是继承关系中第一个节点。

另外注意 EXTENDS 这种匹配类型范围比较大,所有继承的中间类也可能会加入切面代码

⚠️⚠️⚠️ 不是所有类都可以Hook进去,type 类型为 SELF 时,targetClassName 所设置的类必须是安装包里的代码。例如:如果这个类(如:Toast)在 android.jar 里边是不行的

excludeClasses 是排除掉继承关系中的一些类,可以设置多个,且 type 不是 SELF 才有效

创建切面处理类

切面处理类需要实现 MatchClassMethod 接口,在 invoke 中处理切面逻辑

interface MatchClassMethod {
    fun invoke(joinPoint: ProceedJoinPoint, methodName:String): Any?
}

举例

例一

想要监测所有继承自 AppCompatActivity 类的所有 startActivity 跳转

@AndroidAopMatchClassMethod(
   targetClassName = "androidx.appcompat.app.AppCompatActivity",
   methodName = {"startActivity"},
   type = MatchType.EXTENDS
)
public class MatchActivityMethod implements MatchClassMethod {
    @Nullable
    @Override
    public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull String methodName) {
        // 在此写你的逻辑 
        return joinPoint.proceed();
    }
}

⚠️注意:对于匹配子类方法的,如果子类没有重写匹配的方法,是无效的,另外对同一个类的同一个方法不要做多次匹配,否则只有一个会生效

例二

假如想 Hook 所有的 android.view.View.OnClickListener 的 onClick,说白了就是想全局监测所有的设置 OnClickListener 的点击事件,代码如下:

@AndroidAopMatchClassMethod(
    targetClassName = "android.view.View.OnClickListener",
    methodName = ["onClick"],
    type = MatchType.EXTENDS //type 一定是 EXTENDS 因为你想 hook 所有继承了 OnClickListener 的类
)
class MatchOnClick : MatchClassMethod {
//    @SingleClick(5000) //联合 @SingleClick ,给所有点击增加防多点,6不6
    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
        Log.e("MatchOnClick", "=====invoke=====$methodName")
        return joinPoint.proceed()
    }
}

这块提示下,对于使用了 lambda 点击监听的;

ProceedJoinPoint 的 target 不是 android.view.View.OnClickListener

  • 对于Java target 是 设置lambda表达式的那个类的对象
  • 对于Kotlin target 是 null

invoke 回调的 methodName 也不是 onClick 而是编译时自动生成的方法名,类似于这样 onCreate$lambda$14 里边包含了 lambda 关键字

对于 onClick(view:View) 的 view

  • 如果是 Kotlin 的代码 ProceedJoinPoint.args[1]
  • 如果是 Java 的代码 ProceedJoinPoint.args[0]

这块不在继续赘述了,自己用一下就知道了;

总结下:其实对于所有的 lambda 的 ProceedJoinPoint.args

  • 如果是 Kotlin 第一个参数是设置lambda表达式的那个类的对象,后边的参数就是 hook 方法的所有参数
  • 如果是 Java 从第一个参数开始就是 hook 方法的所有参数

例三

目标类有多个重名方法,只想匹配某一个方法(下文有提到精准匹配规则)

package com.flyjingfish.test_lib;

public class TestMatch {
    public void test(int value1){

    }

    public String test(int value1,String value2){
        return value1+value2;
    }
}

例如有 TestMatch 这个类 有两个名为 test 的方法,你只想匹配 test(int value1,String value2) 这个方法,那么写法如下:

package com.flyjingfish.test_lib.mycut;

@AndroidAopMatchClassMethod(
        targetClassName = "com.flyjingfish.test_lib.TestMatch",
        methodName = ["java.lang.String test(int,java.lang.String)"],
        type = MatchType.SELF
)
class MatchTestMatchMethod : MatchClassMethod {
  override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
    Log.e("MatchTestMatchMethod","======"+methodName+",getParameterTypes="+joinPoint.getTargetMethod().getParameterTypes().length);
    // 在此写你的逻辑 
    //不想执行原来方法逻辑,👇就不调用下边这句
    return joinPoint.proceed()
  }
}

例四

继承关系层次较多时不想每层都加入切面

@AndroidAopMatchClassMethod(
    targetClassName = "android.view.View.OnClickListener",
    methodName = ["onClick"],
    type = MatchType.EXTENDS //type 一定是 EXTENDS 因为你想 hook 所有继承了 OnClickListener 的类
)
class MatchOnClick : MatchClassMethod {
//    @SingleClick(5000) //联合 @SingleClick ,给所有点击增加防多点,6不6
    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
        Log.e("MatchOnClick", "=====invoke=====$methodName")
        return joinPoint.proceed()
    }
}
public abstract class MyOnClickListener implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        ...
        //这块有必要的逻辑代码
    }
}
binding.btnSingleClick.setOnClickListener(object :MyOnClickListener(){
    override fun onClick(v: View?) {
        super.onClick(v)//尤其是这句调用父类 onClick 想要保留执行父类方法的逻辑
        onSingleClick()
    }
})

这么写,会导致上边的 MyOnClickListener onClick 也加入切面,如此一来相当于一个点击有回调了两次切面处理类的 invoke ,这可能不是我们想要的,所以可以这么改

@AndroidAopMatchClassMethod(
    targetClassName = "android.view.View.OnClickListener",
    methodName = ["onClick"],
    type = MatchType.EXTENDS,
    excludeClasses = ["com.flyjingfish.androidaop.test.MyOnClickListener"]//加上这个可以排除掉一些类
)
class MatchOnClick : MatchClassMethod {
    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
        Log.e("MatchOnClick", "=====invoke=====$methodName")
        return joinPoint.proceed()
    }
}

或者设置 type 为 LEAF_EXTENDS 直接过滤掉中间类(这让我想起一句广告:没有中间商赚差价)

例五

切入点是伴生对象怎么办?

假如有这样一个代码

package com.flyjingfish.androidaop

class ThirdActivity : BaseActivity() {
    companion object{
        fun start(){
            ...
        }
    }
}

伴生对象修饰符 companion 首字母大写,如下即可

@AndroidAopMatchClassMethod(
    targetClassName = "com.flyjingfish.androidaop.ThirdActivity.Companion",
    methodName = ["start"],
    type = MatchType.SELF
)
class MatchCompanionStart : MatchClassMethod {
    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
        Log.e("MatchCompanionStart", "======$methodName")
        return joinPoint.proceed()
    }
}

例六

切入点是 Kotlin 代码的成员变量,想要监听赋值、取值的操作

package com.flyjingfish.androidaop.test
class TestBean {
    var name:String = "test"
}

在代码中我们会有这样的操作

testBean.name = "1111" //赋值操作
val name = testBean.name //取值操作

您可以这么写

@AndroidAopMatchClassMethod(
    targetClassName = "com.flyjingfish.androidaop.test.TestBean",
    methodName = ["setName","getName"],
    type = MatchType.SELF
)
class MatchTestBean : MatchClassMethod {
    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
        Log.e("MatchTestBean", "======$methodName");
        ToastUtils.makeText(ToastUtils.app,"MatchTestBean======$methodName")
        return joinPoint.proceed()
    }
}

例七

如果切点方法是 suspend 修饰的函数怎么办?

package com.flyjingfish.androidaop

class MainActivity: BaseActivity2() {
    suspend fun getData(num:Int) :Int{
        return withContext(Dispatchers.IO) {
            getDelayResult()
        }
    }
}

精准匹配写法如下,匹配的函数返回值类型无论是哪种,都写 suspend ,具体说明看下文的精准匹配部分

@AndroidAopMatchClassMethod(
    targetClassName = "com.flyjingfish.androidaop.MainActivity",
    methodName = ["suspend getData(int)"],
    type = MatchType.SELF
)
class MatchSuspend : MatchClassMethod {
    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
        Log.e("MatchSuspend", "======$methodName")
        return joinPoint.proceed()
    }
}

例八

想要匹配一个类的所有方法

@AndroidAopMatchClassMethod(
    targetClassName = "com.flyjingfish.androidaop.SecondActivity",
    methodName = ["*"],
    type = MatchType.SELF
)
class MatchAllMethod : MatchClassMethod {
    override fun invoke(joinPoint: ProceedJoinPoint, methodName: String): Any? {
        Log.e("MatchMainAllMethod", "AllMethod======" + methodName);
        return joinPoint.proceed()
    }
}

方法名部分只写一个,并且填 * 即可匹配所有方法

匹配规则

可以看到上述例子中有的设置的方法名就只写了方法名,有的也写上了返回值类型和参数类型,下边介绍下

模糊匹配

  • 1、methodName 有且只有一个方法名为 * 的则匹配类中的所有方法 如上边的例八
  • 2、methodName 只写方法名但不写返回类型和参数类型也是模糊匹配,这将会使目标类中所有同名方法都加入切点

精准匹配

methodName 方法名写上返回类型、参数类型,这样就可以精准到一个方法加入切点(精准匹配异常时会自动退化到模糊匹配)

匹配的写法公式: 返回值类型 方法名(参数类型,参数类型...)

  • 返回值类型 可以不用写
  • 方法名 必须写
  • 参数类型 可以不用写,写的话用 () 包裹起来,多个参数类型用 , 隔开,没有参数就只写 ()
  • 返回值类型 和 方法名 之间用空格隔开
  • 返回值类型 和 参数类型 不写的话就是不验证
  • 返回值类型 和 参数类型 都要用 Java 的类型表示,除了 8 种基本类型之外,其他引用类型都是 包名.类名
  • 如果函数是 suspend 修饰的,那 返回值类型 无论是什么类型都写 suspend ,参数类型 还是按上述几点来写

下边给出类型表示不同的 Kotlin 对 Java 对照表,如果是 Kotlin 代码请对号入座

(有发现不全的可以跟我反馈)

Kotlin 类型 Java 类型
Int int
Short short
Byte byte
Char char
Long long
Float float
Double double
Boolean boolean
Int? java.lang.Integer
Short? java.lang.Short
Byte? java.lang.Byte
Char? java.lang.Character
Long? java.lang.Long
Float? java.lang.Float
Double? java.lang.Double
Boolean? java.lang.Boolean
String java.lang.String
Unit(或不写) void
Unit? java.lang.Void
Any java.lang.Object

其他不在上表中的数据类型,都属于引用类型,写法就是 包名.类名