美文网首页Android开发经验谈Android开发
Android字节码的手术刀插桩初体验

Android字节码的手术刀插桩初体验

作者: python草莓 | 来源:发表于2020-06-22 21:04 被阅读0次

本文有对其他博客的一些借鉴。

现创建了一个Android开发水友圈,圈内会不定时更新一些Android中高级的进阶资料,欢迎大家带着技术问题来讨论,共同成长进步!(包含资深UI工程师,Android底层开发工程师,Android架构师,原生性能优化及混合优化,flutter专精);希望有技术的大佬加入,水圈内解决的问题越多获得的权利越大!

我们都知道Dalvik虚拟机运行的是.dex文件。.dex文件又是通过.class文件通过dx工具编译而来。今天要体验的就是一个非常有意思的技术,字节码的插桩。

大部分时候都会用埋点来介绍这个技术。原理就是,通过Transform这个类去获取项目中的.class文件。然后使用AMS提供的几个类去解析.class文件。通过对类名,方法名的判断,筛选出你需要修改的.class文件。然后在需要修改的地方插入你想要的被转成字节码的代码。

最复杂的部分是:.class文件有着自己很严格的格式,如果我们想注入代码时,不是直接插入相关的指令即可。我们还需要去找到相应的StackMapFrame,换句话说就是要找到对应的帧栈,因为我们插入的方法可能和已有的方法中的对象有引用关系,所以需要对帧栈进行计算,最后还要压缩剩下的帧。不过好在这步AMS已经处理完了,我们只需要进行调用就行。

首先要使用使用Transform就需要使用自定义插件。那么先去自定义一个插件。新建一个android library。把除了src/main/java和.gradle文件外的其他所有文件都删除了。

image

这样就行了。

然后我们需要用groovy语言去写插件所以需要一个groovy文件夹。在此之前先去把gradle重新写一下。把之前的都删了,然后插入下面的就行,这个写法基本上是固定了。因为我们要把插件发布到本地。

apply plugin: 'groovy'
apply plugin: 'maven'
 
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
 
    implementation  gradleApi()
    implementation  localGroovy()
 
    implementation 'com.android.tools.build:gradle:3.6.1'
 
    //ASM相关依赖
    implementation 'org.ow2.asm:asm-commons:7.1'
    implementation 'org.ow2.asm:asm:7.1'

然后在groovy文件夹下面自己新建一个.groovy文件。用AS编写groovy文件需要相当注意,因为这玩意大部分时候都不会报错。里面的代码意思是将自定义的transform注册到任务里,而且打印了一句话。

package my.test.lifecycle
 
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class LifeCyclePlugin implements Plugin<Project>{
 
    @Override
    void apply(Project project) {
        System.out.println("register_LifeCyclePlugin")
        def android =project.extensions.getByType(AppExtension);
        LifeCycleTransForm lifeCycleTransForm=new LifeCycleTransForm();
        android.registerTransform(lifeCycleTransForm)
    }

然后在main文件夹下在新建一个resources文件夹

image

文件名一定不能错。在这个文件夹下新建一个.properties文件。my.test.lifecycle前面这段就是你的插件名了。

gradle文件中在写上group与version,然后直接运行uploadArchives这个任务。你会看到在工程下出现一个新的文件夹asm_lifecycle。

group='my.test.lifecycle'
version='1.0.0'
 
uploadArchives{
    repositories{
        mavenDeployer {
            //本地的Maven地址设置
            repository(url: uri('../asm_lifecycle'))
        }
    }

然后在app的gradle里把插件给导进来。

apply plugin: 'my.test.lifecycle'
buildscript {
    repositories {
        google()
        jcenter()
        maven { url '../asm_lifecycle' }
        //自定义插件maven地址
    }
    dependencies {
        //加载自定义插件 group + module + version
        classpath 'my.test.lifecycle:my_lifecycle_plugin:1.0.0'
    }

这个时候,我们的APP就可以使用自己的插件了。现在开始写我们的自定义Transform。

package my.test.lifecycle
 
import asm.test.plugin.LifecycleClassVisitor
import asm.test.plugin.LifecycleMethodVisitor
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
 
 
class LifeCycleTransForm extends Transform {
 
    //自定义的TransForm名称
    @Override
    String getName() {
        return "LifeCycleTransForm"
    }
 
    //设置自定义TransForm接收的文件类型
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
 
    //设置自定义TransForm检索范围
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY
    }
 
    //是否支持增量编译
    @Override
    boolean isIncremental() {
        return false
    }
 
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        //拿到所有的class文件
        Collection<TransformInput> transformInputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
 
        transformInputs.each { TransformInput transformInput ->
            // 遍历directoryInputs(文件夹中的class文件) directoryInputs代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
            // 比如我们手写的类以及R.class、BuildConfig.class以及MainActivity.class等
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        def name = file.name;
                        if (name.endsWith(".class") && !name.startsWith("R\$")
                                && !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
                            System.out.println("find class: " + file.name)
 
                            //对class文件进行读取与解析
                            ClassReader classReader = new ClassReader(file.bytes)
                            //对class文件的写入
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                            //访问class文件相应的内容,解析到某一个结构就会通知到ClassVisitor的相应方法
                            ClassVisitor classVisitor = new LifecycleClassVisitor(classWriter)
                            //依次调用 ClassVisitor接口的各个方法
                            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                            //toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
                            byte[] bytes = classWriter.toByteArray()
 
                            //通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
//                            FileOutputStream outputStream = new FileOutputStream( file.parentFile.absolutePath + File.separator + fileName)
                            FileOutputStream outputStream = new FileOutputStream(file.path)
                            outputStream.write(bytes)
                            outputStream.close()
 
                        }
                    }
                }
 
                //处理完输入文件后把输出传给下一个文件
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
                        directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }
    }

注释写的还是很详细的。

ClassReader是用于解析class文件
ClassWriter是用于写入你要插入的字节码,并以流的形式返回
ClassVisitor用于访问class文件的类,需要自定义去继承

所以我们新建一个class的访问类,与一个方法的访问类

package asm.test.plugin;
 
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
 
public class LifecycleClassVisitor extends ClassVisitor {
    private String className;
    private String superName;
 
    public LifecycleClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }
 
    /**
     * 
     * @param version JDK版本
     * @param access 类修饰信息 public private
     * @param name 全类名
     * @param signature 泛型信息
     * @param superName 继承的类名
     * @param interfaces 接口信息
     */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
    }
 
    /**
     * 
     * 
     * @param access 方法修饰 private public
     * @param name 方法名
     * @param desc 返回类型 int boolean
     * @param signature 泛型信息
     * @param exceptions 异常信息
     * @return
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("ClassVisitor visitMethod name-------" + name + ", superName is " + superName);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
 
        if(superName.equals("android/app/Activity")){
            if(name.startsWith("onCreate")){
                return new LifecycleMethodVisitor(mv,className,name);
            }
        }
        return mv;
    }
 
    @Override
    public void visitEnd() {
        super.visitEnd();
    }

注释写的很详细了,就不在解释

package asm.test.plugin;
 
 
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
 
public class LifecycleMethodVisitor extends MethodVisitor {
    private String className;
    private String methodName;
 
    public LifecycleMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
        super(Opcodes.ASM5, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    }
 
    /**
     * 方法执行前
     */
    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitLdcInsn("TAG");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        mv.visitLdcInsn("Activity=");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
        mv.visitLdcInsn(" method=onCreate");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }
 
    /**
     * 方法执行后
     * @param opcode
     */
    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }

看到visitCode是不是感觉很难,没关系他们提供了一个工具

image.png

插件里安装这个插件

image.png

右边会多一块区域

image.png
先写好你想插入的代码,然后右击鼠标
image.png
然后他就会把相应的字节码写法展示给你 image.png
拷贝这段就行。
这样在oncreate方法前插入一个日志的事儿就完成了。
插桩可以做的事情太多了,各种监控,插件化,或者当做一个过滤器。而且这个技术相当好玩,因为不会去修改源码,你就可以实现自己想做的事情。

https://shimo.im/docs/vgg6PjDvxDK9YKj6/ 《Android学习、面试、文档及进阶视频免费领》,可复制链接后用石墨文档 App 或小程序打开

相关文章

  • Android字节码的手术刀插桩初体验

    本文有对其他博客的一些借鉴。 现创建了一个Android开发水友圈,圈内会不定时更新一些Android中高级的进阶...

  • android字节码插桩

    自定义插件 目前,Android项目基本都是使用Gradle去构建,在学习插桩之前先对Gradle插件知识有基本的...

  • Android AOP之字节码插桩

    title: Android AOP之字节码插桩author: 陶超description: 实现数据收集SDK时...

  • Android AOP之字节码插桩

    title: Android AOP之字节码插桩author: 陶超description: 实现数据收集SDK时...

  • 自定义Gradle插件

      最近在学习字节码插桩技术,利用字节码插桩技术,我们可以在编译时期对字节码进行修改,达到完成一些特殊需求,比如埋...

  • 注解 - 插桩,编译后处理筛选

    什么是插桩? 插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Cla...

  • 注解的使用(二):插桩,编译后处理筛选

    什么是插桩? 插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Cla...

  • android字节码插桩研究

    背景 之前在极客时间上面学习张绍文老师的《Android开发高手课》的时候,有一章节讲了android中编译插桩的...

  • Android字节码插桩demo

    1. 基本概念 1.1 java字节码 Java字节码是Java虚拟机执行的一种虚拟指令格式。可通过javac 编...

  • Android字节码插桩学习

    AGP 7.0之后建议使用Transform Actionhttps://mp.weixin.qq.com/s/-...

网友评论

    本文标题:Android字节码的手术刀插桩初体验

    本文链接:https://www.haomeiwen.com/subject/rvbffktx.html