V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tianxiangyu1688
V2EX  ›  Android

[Android 开发]推荐一个专属 Android 端 AOP 切面框架,没有 AspectJ,只需一个注解就可以请求权限、切换线程、禁止多点、监测生命周期等等,甚至可以切入三方库

  •  
  •   tianxiangyu1688 · 346 天前 · 6484 次点击
    这是一个创建于 346 天前的主题,其中的信息可能已经有所发展或是发生改变。

    AndroidAOP 是专属于 Android 端 Aop 框架,只需一个注解就可以请求权限、切换线程、禁止多点、监测生命周期等等,本库不是基于 AspectJ 实现的 Aop,当然你也可以定制出属于你的 Aop 代码

    AndroidAOP - Github 链接

    特色功能

    1 、本库内置了开发中常用的一些切面注解供你使用

    2 、本库支持让你自己做切面,语法简单易上手

    3 、本库同步支持 Java 和 Kotlin 代码

    4 、本库支持切入三方库

    5 、本库支持切点方法为 Lambda 表达式的情况

    6 、本库支持生成所有切点信息 Json 文件,方便一览所有切点位置在此配置

    7 、本库不是基于 AspectJ 实现的,织入代码量极少,侵入性极低

    点此下载 apk

    版本限制

    最低 Gradle 版本:8.0

    最低 SDK 版本:minSdkVersion >= 21

    使用步骤

    在开始之前可以给项目一个 Star 吗?非常感谢,你的支持是我唯一的动力。欢迎 Star 和 Issues!

    一、在项目根目录下的 build.gradle 添加(必须)

    buildscript {
        dependencies {
            //必须项 👇
            classpath 'io.github.FlyJingFish.AndroidAop:android-aop-plugin:1.2.3'
        }
    }
    plugins {
        //非必须项 👇,如果需要自定义切面,并且使用 android-aop-ksp 这个库的话需要配置 ,下边版本号根据你项目的 Kotlin 版本决定
        id 'com.google.devtools.ksp' version '1.8.0-1.0.9' apply false
    }
    

    Kotlin 和 KSP Github 的匹配版本号列表

    二、在 app 的 build.gradle 添加(此步为必须项)

    ⚠️注意:👆此步为必须项👇

    //必须项 👇
    plugins {
        ...
        id 'android.aop'//最好放在最后一行
    }
    

    三、引入依赖库

    plugins {
        //非必须项 👇,如果需要自定义切面,并且使用 android-aop-ksp 这个库的话需要配置 
        id 'com.google.devtools.ksp'
    }
    
    dependencies {
        //必须项 👇
        implementation 'io.github.FlyJingFish.AndroidAop:android-aop-core:1.2.3'
        implementation 'io.github.FlyJingFish.AndroidAop:android-aop-annotation:1.2.3'
        //非必须项 👇,如果你想自定义切面需要用到,⚠️支持 Java 和 Kotlin 代码写的切面
        ksp 'io.github.FlyJingFish.AndroidAop:android-aop-ksp:1.2.3'
        //非必须项 👇,如果你想自定义切面需要用到,⚠️只适用于 Java 代码写的切面
        annotationProcessor 'io.github.FlyJingFish.AndroidAop:android-aop-processor:1.2.3'
        //⚠️上边的 android-aop-ksp 和 android-aop-processor 二选一
    }
    

    提示:ksp 或 annotationProcessor 只是在当前 module 起作用,在哪个 module 中有自定义切面代码就加在哪个 module ,必须依赖项可以通过 api 方式只加到公共 module 上

    四、在 app 的 build.gradle 添加 androidAopConfig 配置项(此步为可选配置项)

    plugins {
        ...
    }
    androidAopConfig {
        // enabled 为 false 切面不再起作用,默认不写为 true
        enabled true 
        // include 不设置默认全部扫描,设置后只扫描设置的包名的代码
        include '你项目的包名','自定义 module 的包名','自定义 module 的包名'
        // exclude 是扫描时排除的包
        // 可排除 kotlin 相关,提高速度
        exclude 'kotlin.jvm', 'kotlin.internal','kotlinx.coroutines.internal', 'kotlinx.coroutines.android'
        
        // verifyLeafExtends 是否开启验证叶子继承,默认打开,如果没有设置 @AndroidAopMatchClassMethod 的 type = MatchType.LEAF_EXTENDS ,可以关闭
        verifyLeafExtends true
        //默认关闭,开启在 Build 或 打包后 将会生成切点信息 json 文件在 app/build/tmp/cutInfo.json
        cutInfoJson false
    }
    android {
        ...
    }
    

    提示:合理使用 include 和 exclude 可提高编译速度,建议直接使用 include 设置你项目的相关包名(包括 app 和自定义 module 的)

    另外设置此处之后由于 Android Studio 可能有缓存,建议重启 AS 并 clean 下项目再继续开发

    本库内置了一些功能注解可供你直接使用

    注解名称 参数说明 功能说明
    @SingleClick value = 快速点击的间隔,默认 1000ms 单击注解,加入此注解,可使你的方法只有单击时才可进入
    @DoubleClick value = 两次点击的最大用时,默认 300ms 双击注解,加入此注解,可使你的方法双击时才可进入
    @IOThread ThreadType = 线程类型 切换到子线程的操作,加入此注解可使你的方法内的代码切换到子线程执行
    @MainThread 无参数 切换到主线程的操作,加入此注解可使你的方法内的代码切换到主线程执行
    @OnLifecycle value = Lifecycle.Event 监听生命周期的操作,加入此注解可使你的方法内的代码在对应生命周期内才去执行
    @TryCatch value = 你自定义加的一个 flag 加入此注解可为您的方法包裹一层 try catch 代码
    @Permission value = 权限的字符串数组 申请权限的操作,加入此注解可使您的代码在获取权限后才执行
    @Scheduled initialDelay = 延迟开始时间
    interval = 间隔
    repeatCount = 重复次数
    isOnMainThread = 是否主线程
    id = 唯一标识
    定时任务,加入此注解,可使你的方法每隔一段时间执行一次,调用 AndroidAop.shutdownNow(id)或 AndroidAop.shutdown(id)可停止
    @Delay delay = 延迟时间
    isOnMainThread = 是否主线程
    id = 唯一标识
    延迟任务,加入此注解,可使你的方法延迟一段时间执行,调用 AndroidAop.shutdownNow(id)或 AndroidAop.shutdown(id)可取消
    @CustomIntercept value = 你自定义加的一个字符串数组的 flag 自定义拦截,配合 AndroidAop.setOnCustomInterceptListener 使用,属于万金油

    上述注解使用示例都在这,还有这

    这块强调一下 @OnLifecycle

    • 1 、 @OnLifecycle 加到的方法所属对象必须是属于直接或间接继承自 FragmentActivity 或 Fragment 的方法才有用,或者注解方法的对象实现 LifecycleOwner 也可以
    • 2 、如果第 1 点不符合的情况下,可以给切面方法第一个参数设置为第 1 点的类型,在调用切面方法传入也是可以的,例如:
    public class StaticClass {
        @SingleClick(5000)
        @OnLifecycle(Lifecycle.Event.ON_RESUME)
        public static void onStaticPermission(MainActivity activity, int maxSelect , ThirdActivity.OnPhotoSelectListener back){
            back.onBack();
        }
    
    }
    

    下面再着重介绍下 @TryCatch @Permission @CustomIntercept

    • @TryCatch 使用此注解你可以设置以下设置(非必须)
    AndroidAop.INSTANCE.setOnThrowableListener(new OnThrowableListener() {
        @Nullable
        @Override
        public Object handleThrowable(@NonNull String flag, @Nullable Throwable throwable,TryCatch tryCatch) {
            // TODO: 2023/11/11 发生异常可根据你当时传入的 flag 作出相应处理,如果需要改写返回值,则在 return 处返回即可
            return 3;
        }
    });
    
    • @Permission 使用此注解必须配合以下设置(⚠️此步为必须设置的,否则是没效果的)
    AndroidAop.INSTANCE.setOnPermissionsInterceptListener(new OnPermissionsInterceptListener() {
        @SuppressLint("CheckResult")
        @Override
        public void requestPermission(@NonNull ProceedJoinPoint joinPoint, @NonNull Permission permission, @NonNull OnRequestPermissionListener call) {
            Object target =  joinPoint.getTarget();
            if (target instanceof FragmentActivity){
                RxPermissions rxPermissions = new RxPermissions((FragmentActivity) target);
                rxPermissions.request(permission.value()).subscribe(call::onCall);
            }else if (target instanceof Fragment){
                RxPermissions rxPermissions = new RxPermissions((Fragment) target);
                rxPermissions.request(permission.value()).subscribe(call::onCall);
            }else{
                // TODO: target 不是 FragmentActivity 或 Fragment ,说明注解所在方法不在其中,请自行处理这种情况
                // 建议:切点方法第一个参数可以设置为 FragmentActivity 或 Fragment ,然后 joinPoint.args[0] 就可以拿到
            }
        }
    });
    
    • @CustomIntercept 使用此注解你必须配合以下设置(⚠️此步为必须设置的,否则还有什么意义呢?)
    AndroidAop.INSTANCE.setOnCustomInterceptListener(new OnCustomInterceptListener() {
        @Nullable
        @Override
        public Object invoke(@NonNull ProceedJoinPoint joinPoint, @NonNull CustomIntercept customIntercept) {
            // TODO: 2023/11/11 在此写你的逻辑 在合适的地方调用 joinPoint.proceed(),
            //  joinPoint.proceed(args)可以修改方法传入的参数,如果需要改写返回值,则在 return 处返回即可
    
            return null;
        }
    });
    

    👆上边三个监听,最好放到你的 application 中

    此外本库也同样支持让你自己做切面,实现起来非常简单!

    本库通过 @AndroidAopPointCut 和 @AndroidAopMatchClassMethod 两种注解,实现自定义切面

    一、**@AndroidAopPointCut** 是在方法上通过注解的形式做切面的,上述中注解都是通过这个做的,详细使用请看 wiki 文档

    ⚠️注意:自定义的注解(也就是被 @AndroidAopPointCut 注解的注解类)如果是 Kotlin 代码请用 android-aop-ksp 那个库

    下面以 @CustomIntercept 为例介绍下该如何使用

    • 创建注解
    @AndroidAopPointCut(CustomInterceptCut.class)
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CustomIntercept {
        String[] value() default {};
    }
    
    • 创建注解处理切面的类(需要实现 BasePointCut 接口,它的泛型填上边的注解)
    class CustomInterceptCut : BasePointCut<CustomIntercept> {
        override fun invoke(
            joinPoint: ProceedJoinPoint,
            annotation: CustomIntercept //annotation 就是你加到方法上的注解
        ): Any? {
            // 在此写你的逻辑
            // joinPoint.proceed() 表示继续执行切点方法的逻辑,不调用此方法不会执行切点方法里边的代码
            // 关于 ProceedJoinPoint 可以看 wiki 文档,详细点击下方链接
            return joinPoint.proceed()
        }
    }
    

    关于 ProceedJoinPoint 使用说明,下文的 ProceedJoinPoint 同理

    • 使用

    直接将你写的注解加到任意一个方法上,例如加到了 onCustomIntercept() 当 onCustomIntercept() 被调用时首先会进入到上文提到的 CustomInterceptCut 的 invoke 方法上

    @CustomIntercept("我是自定义数据")
    fun onCustomIntercept(){
        
    }
    
    

    二、**@AndroidAopMatchClassMethod** 是做匹配某类及其对应方法的切面的

    匹配方法支持精准匹配,点此看 wiki 详细使用文档

    ⚠️注意:自定义的匹配类方法切面(也就是被 @AndroidAopMatchClassMethod 注解的代码)如果是 Kotlin 代码请用 android-aop-ksp 那个库

    • 例子一
    package com.flyjingfish.test_lib;
    
    public class TestMatch {
        public void test1(int value1,String value2){
    
        }
    
        public String test2(int value1,String value2){
            return value1+value2;
        }
    }
    
    

    假如 TestMatch 是要匹配的类,而你想要匹配到 test2 这个方法,下边是匹配写法:

    package com.flyjingfish.test_lib.mycut;
    
    @AndroidAopMatchClassMethod(
            targetClassName = "com.flyjingfish.test_lib.TestMatch",
            methodName = ["test2"],
            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 设置的 type 是 MatchType.SELF 表示只匹配 TestMatch 这个类自身,不考虑其子类

    • 例子二

    假如想 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()
        }
    }
    

    可以看到上方 AndroidAopMatchClassMethod 设置的 type 是 MatchType.EXTENDS 表示匹配所有继承自 OnClickListener 的子类,另外更多继承方式,请参考 Wiki 文档

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

    匹配切面实用场景:

    • 例如你想做退出登陆逻辑时可以使用上边这个,只要在页面内跳转就可以检测是否需要退出登陆

    • 又或者你想在三方库某个方法上设置切面,可以直接设置对应类名,对应方法,然后 type = MatchType.SELF ,这样可以侵入三方库的代码,当然这么做记得修改上文提到的 androidAopConfig 的配置

    详细使用请看 wiki 文档

    常见问题

    1 、Build 时报错 "ZipFile invalid LOC header (bad signature)"

    • 请重启 Android Studio ,然后 clean 项目

    2 、 同一个方法存在多个注解或匹配切面时,怎么处理的

    • 多个切面叠加到一个方法上时注解优先于匹配切面(上文的匹配切面),注解切面之间从上到下依次执行
    • 调用 proceed 才会执行下一个切面,多个切面中最后一个切面执行 proceed 才会调用切入方法内的代码
    • 在前边切面中调用 proceed(args) 可更新方法传入参数,并在下一个切面中也会拿到上一层更新的参数
    • 存在异步调用proceed时,第一个异步调用 proceed 切面的返回值(就是 invoke 的返回值)就是切入方法的返回值;

    混淆规则

    下边是涉及到本库的一些必须混淆规则

    # AndroidAop 必备混淆规则 -----start-----
    
    -keep class * {
        @androidx.annotation.Keep <fields>;
    }
    
    -keepnames class * implements com.flyjingfish.android_aop_annotation.base.BasePointCut
    -keepnames class * implements com.flyjingfish.android_aop_annotation.base.MatchClassMethod
    -keep class * implements com.flyjingfish.android_aop_annotation.base.BasePointCut{
        public <init>();
    }
    -keep class * implements com.flyjingfish.android_aop_annotation.base.MatchClassMethod{
        public <init>();
    }
    
    # AndroidAop 必备混淆规则 -----end-----
    
    1 条回复    2024-02-06 17:31:34 +08:00
    iflint
        1
    iflint  
       292 天前
    去看看
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2729 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 19ms · UTC 10:16 · PVG 18:16 · LAX 02:16 · JFK 05:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.