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,让应用不再崩溃

相关推荐
晓宜3 小时前
Java25 新特性介绍
java·python·算法
Seven973 小时前
SpringIOC、DI及Bean线程安全面试题解析
java
TitosZhang3 小时前
BIO、NIO、AIO详解
java·redis·nio
Arva .3 小时前
Spring Boot 配置文件
java·spring boot·后端
IT_Octopus3 小时前
https私人证书 PKIX path building failed 报错解决
java·spring boot·网络协议·https
程序员清风4 小时前
网易三面:Java中默认使用的垃圾回收器及特点分版本说说?
java·后端·面试
这周也會开心4 小时前
本地部署javaweb项目到Tomcat的三种方法
java·tomcat
小蒜学长4 小时前
jsp基于JavaWeb的原色蛋糕商城的设计与实现(代码+数据库+LW)
java·开发语言·数据库·spring boot·后端
摇滚侠4 小时前
Spring Boot中使用线程池来优化程序执行的效率!笔记01
java·spring boot·多线程