Android Gradle学习(十二)- ASM对于字节码的增删改查

一:信息采集

1.1 Class信息采集

类的基本信息

kotlin 复制代码
class XXClassCollectVisitor(
    api: Int,
    classVisitor: ClassVisitor) : ClassVisitor(api, classVisitor) {

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
    }
}
  • access:类的访问修饰符(如 ACC_PUBLICACC_FINALACC_RECORD 等) - name:类的内部名称(格式为 包名/类名,如 java/lang/String

  • signature:类的泛型签名(非泛型类为 null

  • superName:父类的内部名称(如 java/lang/Object,接口的父类为 null

  • interfaces:实现的接口内部名称数组(如 [java/io/Serializable]

类的注解信息

kotlin 复制代码
class XXClassCollectVisitor( api: Int, classVisitor: ClassVisitor) : ClassVisitor(api, classVisitor) {
    override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
        return super.visitAnnotation(descriptor, visible)
    }
}
  • descriptor:注解类型的描述符(如 Lcom/xxx/MyAnnotation;

  • visible:是否为运行时可见注解(true 对应 @Retention(RUNTIME)

类的方法信息

kotlin 复制代码
class XXClassCollectVisitor( api: Int, classVisitor: ClassVisitor) : ClassVisitor(api, classVisitor) {
    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        var mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        return mv
    }

}
  • access:方法的访问修饰符(如 ACC_PUBLICACC_STATICACC_ABSTRACT 等)

  • name:方法名(构造方法为 <init>,静态初始化方法为 <clinit>

  • descriptor:方法的描述符(如 (Ljava/lang/String;)I 表示参数为 String、返回 int

  • signature:方法的泛型签名(非泛型为 null

  • exceptions:方法抛出的异常类内部名称数组(如 [java/lang/IOException]

返回值:MethodVisitor 实例,用于访问方法的指令、局部变量等

2.2 Method信息采集

方法的名称、签名等信息

ClassVisitor#visitMethod采集

方法的注解信息

ClassVisitor#visitAnnotation

方法体指令信息

kotlin 复制代码
class PrivacyMethodProxyVisitor(
    ...
) : AdviceAdapter(api, methodVisitor, access, methodName, descriptor) {

    override fun visitMethodInsn(
        opcode: Int,
        owner: String?,
        name: String?,
        descriptor: String?,
        isInterface: Boolean
    ) {
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
    }
}
  • opcode:指令操作码(如 INVOKEVIRTUALINVOKESTATICINVOKESPECIAL
  • owner:方法所属类的内部名称
  • name:方法名
  • descriptor:方法描述符
  • isInterface:是否为接口方法(ASM 5+ 新增参数)

3.1 Annotation信息采集

注解参数信息

kotlin 复制代码
class PrivacyAnnotationCollectVisitor(
    ....) : AnnotationVisitor(api, annotationVisitor) {

    override fun visit(name: String?, value: Any?) {
        super.visit(name, value)
        when (name) {
            "name" -> {
                val routerName = value.toString()
            }
            else -> Unit
        }
    }
}

二:查找

2.1 查找对应的class

2.1.1 根据接口查找

给所有按钮的点击事件插入防抖功能,需要使用ClassVisitor查找所有实现了OnClickListener接口的类。(lamda表达式无法通过此方式匹配,后续再说)

java 复制代码
btnAgree.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
//      if (DoubleClickController.isDoubleClick()) return; // 要插入的代码
        xxx
    }
});
java 复制代码
class DoubleClickClassVisitor extends ClassVisitor {

    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
        if(interfaces != null && interfaces.length > 0){
            for(int i = 0 ; i < interfaces.length ; i++){
                String mInterfaceStr = interfaces[i];
                if (mInterfaceStr.equals("android/view/View$OnClickListener")) {
                    isImplOnClickListener = true;
                    // do something
                }
            }
        }
    }
}
  1. visit遍历class时,遍历其实现的所有接口,通过类名匹配

2.1.2 根据注解查找

实现一个动态路由,编译期查找到所有的标记了Router的注解,实现String -> Activity的映射关系

scala 复制代码
@Router(name = "logoActivity")
public class LogoActivity extends Activity {
}
kotlin 复制代码
class MyClassVisitor extends ClassVisitor {

    private var className: String? = null

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        className = name
    }
    
    override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
        if ("Lcom/kongge/Router;" == descriptor) {
            // 使用AnnotationVisitor找到Router注解的name字段值,并将name和className做映射
            return XXAnnotationVisitor(...)
        }
        return super.visitAnnotation(descriptor, visible)
    }
}

2.2 查找对应的方法

2.2.1 根据名称、方法签名、接口等信息查找

比如按钮防抖,需要在onClick方法体加上代码。

groovy 复制代码
class DoubleClickClassVisitor extends ClassVisitor {
 
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        if (mv != null && name == ("onClick")
                && isImplOnClickListener
                && descriptor.contains("(Landroid/view/View;)V")) {
            boolean isAbstractMethod = (access & Opcodes.ACC_ABSTRACT) != 0
            boolean isNativeMethod = (access & Opcodes.ACC_NATIVE) != 0
            if (!isAbstractMethod && !isNativeMethod) {
                // 找到方法后,使用自定义MethodVisitor处理后续
                return new DoubleClickMethodVisitor(api, mv, access, name, descriptor)
            }
        }
        return mv
    }
}

2.2.2 根据注解信息查找

kotlin 复制代码
class XXClassVisitor(
    private val context: Context,
    api: Int,
    classVisitor: ClassVisitor) : ClassVisitor(api, classVisitor) {

    private var isHasTargetAnnotation = false
    private var className: String? = null

    override fun visit(
        version: Int,
        access: Int,
        name: String?,
        signature: String?,
        superName: String?,
        interfaces: Array<out String>?
    ) {
        super.visit(version, access, name, signature, superName, interfaces)
        className = name
    }

    override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
        if ("Lcom/kongge/annotation/TargetClass;" == descriptor) {
            isHasTargetAnnotation = true
        }
        return super.visitAnnotation(descriptor, visible)
    }

    override fun visitMethod(
        access: Int,
        name: String?,
        descriptor: String?,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        var mv = super.visitMethod(access, name, descriptor, signature, exceptions)
        if (isHasTargetAnnotation) {
            mv = XXMethodCollectVisitor(api, mv, access, name, descriptor, className)
        }
        return mv
    }

}

2.3 查找指定的代码段

隐私合规如今越来越严格,需要找到项目里面所有的隐私相关调用,比如获取AndroidId的地方,在主项目里面可以通过搜索代码来查找,但是三方SDK就不行了,这时候就可以通过查找字节码的方式来实现。

java 复制代码
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
kotlin 复制代码
class PrivacyMethodProxyVisitor(
private val className: String?,
private val methodName: String?,
...
) : AdviceAdapter(api, methodVisitor, access, methodName, descriptor) {

    override fun visitMethodInsn(
        opcode: Int,
        owner: String?,
        name: String?,
        descriptor: String?,
        isInterface: Boolean
    ) {
        if (opcode and Opcodes.ACC_STATIC == Opcodes.ACC_STATIC && owner == "android/provider/Settings$Secure" && name == "getString"
            && descriptor == "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;") {
            // 记录className和methodName,即可知道是哪个类的哪个方法调用了获取AndroidId的方法
        } else {
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
        }
    }
}

三:字节码修改

3.1 方法体新增代码

ButtononClick方法体的第一行插入双击判断,如果是双击则直接return

typescript 复制代码
btnAgree.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (DoubleClickController.isDoubleClick()) return; // 要插入的代码
        HardwareUtils.setIsAgree(true);
    }
});
scala 复制代码
class DoubleClickMethodVisitor extends AdviceAdapter {
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter()
        visitMethodInsn(INVOKESTATIC, "com/kongge/commonsdk/doubleclick/DoubleClickController", "isDoubleClick", "()Z", false)
        Label label0 = new Label()
        visitJumpInsn(IFEQ, label0)
        visitInsn(RETURN)
        visitLabel(label0)
    }
    
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode)
        // 方法末尾插入代码
    }
}

3.2 方法体删除代码

删除所有System.out.println()(使用混淆规则来去掉日志输出更方便,这里仅做字节码删除演示)

scala 复制代码
public class PrintlnFilterVisitor extends MethodVisitor {

    public PrintlnFilterVisitor(MethodVisitor mv) {
        super(Opcodes.ASM9, mv);
    }

    // 拦截方法调用指令(检测 PrintStream.println 调用)
    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
        // 如果是之前标记的 println 调用,则跳过不写入
        if (opcode == Opcodes.INVOKEVIRTUAL
                && "java/io/PrintStream".equals(owner)
                && "println".equals(name)) {
            return; // 不调用父类方法,即删除该指令
        }

        // 非目标方法调用,正常写入
        super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    }

}

3.3 方法体修改代码

kotlin 复制代码
// 需要被替换的代码
Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)

class Proxy {

    companion object {
        // 替换的代码
        fun getAndroidId(resolver: ContentResolver, name: String): String {
            return "aaaa"
        }
    }
}
kotlin 复制代码
class PrivacyMethodProxyVisitor(
...
) : AdviceAdapter(api, methodVisitor, access, methodName, descriptor) {

    override fun visitMethodInsn(
        opcode: Int,
        owner: String?,
        name: String?,
        descriptor: String?,
        isInterface: Boolean
    ) {
        if (owenr == "android/provider/Settings$Secure" && name == "getString" && descriptor == "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;") {
            super.visitMethodInsn(opcode, "com/kongge/plugindemo/proxy/Proxy", "getAndroidId", "(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;", isInterface)
        } else {
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
        }
    }

做的通用一点,可以通过自定义注解的方式,标记相关的类和方法

less 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface TargetClass {
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface TargetMethod {

    Class originClass();

    String originMethod();

}
kotlin 复制代码
@TargetClass
class Proxy {

    companion object {
        @JvmStatic
        @TargetMethod(
            originClass = Settings.Secure::class,
            originMethod = "getString"
        )
        fun getAndroidId(resolver: ContentResolver, name: String): String {
            return "aaaa"
        }
    }


}
  • @TargetClass标记需要处理的类,避免每个类都遍历处理,提高编译效率
  • @TargetMethod标记需要处理的方法,originClass标记被替换的类,originMethod标记被替换的方法,替换成当前类Proxy和方法getAndroidId,方法签名共用getAndroidId的方法签名,即(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;
  • 使用CollectionTransform来收集标记了注解的类
  • 使用ProxyTransform来替换这些类
kotlin 复制代码
class PrivacyAnnotationCollectVisitor(
    ...) : AnnotationVisitor(api, annotationVisitor) {

    override fun visit(name: String?, value: Any?) {
        super.visit(name, value)
        when (name) {
            "originClass" -> {
                var originClass = value.toString()
                originClass = originClass.substring(1, originClass.length - 1)
                privacyAnnotationCollectInfoBean.originClass = originClass
            }
            "originMethod" -> {
                privacyAnnotationCollectInfoBean.originMethod = value.toString()
            }
            else -> Unit
        }
    }


    override fun visitEnd() {
        super.visitEnd()
        privacyCollectManager.registerCollectInfoBean(privacyAnnotationCollectInfoBean)
    }

}
kotlin 复制代码
class PrivacyMethodProxyVisitor(
    ...
) : AdviceAdapter(api, methodVisitor, access, methodName, descriptor) {

    override fun visitMethodInsn(
        opcode: Int,
        owner: String?,
        name: String?,
        descriptor: String?,
        isInterface: Boolean
    ) {
        val privacyAnnotationCollectInfoBean = privacyCollectManager.findCollectInfoBean(owner, name, descriptor)
        if (privacyAnnotationCollectInfoBean != null) {
            super.visitMethodInsn(opcode, privacyAnnotationCollectInfoBean.targetClass, privacyAnnotationCollectInfoBean.targetMethod, descriptor, isInterface)
        } else {
            super.visitMethodInsn(opcode, owner, name, descriptor, isInterface)
        }
    }
}

四:小结

本章节示例仅作为参考,还有很多细节需要打磨。

需要深入学习的可以琢磨琢磨饿了么的开源库Lancet,使用说明可以参考鸿洋大神的Android 无所不能的 hook,让应用不再崩溃

相关推荐
一叶飘零_sweeeet5 分钟前
从测试小白到高手:JUnit 5 核心注解 @BeforeEach 与 @AfterEach 的实战指南
java·junit
摇滚侠21 分钟前
Spring Boot3零基础教程,Reactive-Stream 四大核心组件,笔记106
java·spring boot·笔记
Z3r4y22 分钟前
【代码审计】RuoYi-3.0 三处安全问题分析
java·web安全·代码审计·ruoyi-3.0
与遨游于天地38 分钟前
Spring解决循环依赖实际就是用了个递归
java·后端·spring
陈果然DeepVersion41 分钟前
Java大厂面试真题:Spring Boot+微服务+AI智能客服三轮技术拷问实录(六)
java·spring boot·redis·微服务·面试题·rag·ai智能客服
BeingACoder1 小时前
【SAA】SpringAI Alibaba学习笔记(一):SSE与WS的区别以及如何注入多个AI模型
java·笔记·学习·saa·springai
DolphinScheduler社区1 小时前
真实迁移案例:从 Azkaban 到 DolphinScheduler 的选型与实践
java·大数据·开源·任务调度·azkaban·海豚调度·迁移案例
zhangkaixuan4562 小时前
Apache Paimon 写入流程
java·大数据·apache·paimon
Java爱好狂.2 小时前
分布式ID|从源码角度深度解析美团Leaf双Buffer优化方案
java·数据库·分布式·分布式id·es·java面试·java程序员
胡桃夹夹子2 小时前
存档111111111
java·开发语言