字节码插桩技术---Android项目实操(二)

字节码插桩技术---ASM的使用(一)

字节码插桩技术---Android项目实操(二)

字节码插桩技术---Transform配合ASM进行插桩(三)

上篇博客简单介绍了使用ASM进行字节码插桩的过程,但是仅仅依靠上篇博客的技术点,是无法在Android项目中使用的,有一个阻碍点就是由于class文件最后被打包到了dex文件中,无法像上篇文章那样,拿到准确的class文件路径。这篇文章,我会详细介绍如何在Android项目中进行字节码插桩

一、Android打包流程

android开发的同学应该都知道这张图,这是Android打包的流程图

 图中的每一个过程,都对应了一个Task,我们可以通过Task的doFirst方法和doLast方法分别监听任务执行的开始结束过程。

字节码插桩是对class文件进行的,所以图中的两个标记分别是源文件编译为class文件class文件打包为dex。按理说,源文件编译为class文件之后(doLast)和class文件打包为dex之前(doFirst)都是很好的切入点,但是源文件编译为class文件这个点会存在以下的问题:

(1)class文件生成有多个切入点

我们的项目中可能会有两种语言:Java和Kotlin。Debug模式下,Java文件编译为class文件的指令为:compileDebugJavaWithJavac;Kotlin文件编译为class文件的指令为:compileDebugKotlin。所以如果我们选用源文件编译为class文件这个点的话,需要监听多条指令

(2)多依赖库问题

如果我们项目中有app和otherlibrary,app依赖otherlibrary,执行编译工作时,会有如下的指令输出(以Java文件编译成class为例):

app:compileDebugJavaWithJavac
otherlibrary:compileDebugJavaWithJavac

多个库,会有多条指令输出。而我们执行字节码插桩时,相关的插桩代码我们会写在app的build中,所以我们只能监听到app module的指令输出。而它所依赖库的指令,并监听不到。 

综上所述,源文件编译为class文件的过程不可取。而class文件打包为dex这个点不会有上面的问题,指令只有一条:

app:dexBuilderDebug

注意:
低版本的gradle,指令为:
app:transformClassesWithDexBuilderForDebug

二、在app的build.gradle中进行插桩

先做准备工作,在项目的build.gradle添加gradle plugin依赖,因为在这个库里,有ASM的库,这样我们在app的build.gradle中就可以使用ASM进行插桩了

    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.3'
    }

首先,我们先对class文件打包为dex的任务进行监听(以下的方法都是和android指令同级)

afterEvaluate {
    android.getApplicationVariants().all {
        variant ->
            String variantName = variant.name
            //首字母大写 debug变为Debug/release变为Release
            String capitalizeName = variantName.capitalize()
            //jar和class打包成dex的任务
            Task task = project.getTasks().findByName("dexBuilder"+capitalizeName)
            if(task!=null){
                //打包之前执行插桩
                task.doFirst {
                    execute(task)
                }
            }
    }
}

variantName:这个值就是我们打包的类型debug或者release

task:我们所说的class打包成dex的任务,要注意的是dex中不仅仅只包含了class,还有jar包。所以这个任务的输入参数为class和jar文件

doFirst:这个方法我们上面说过了,是执行这个任务前的回调

具体的执行在execute方法中,我们看一下execute方法,内容如下:

static void execute(Task task){
    FileCollection files = task.getInputs().getFiles()
    filesIterator(files.iterator())
}

//注释1
static void filesIterator(Iterator<File>files){
    while (files.hasNext()){
        File file = files.next()
        //注释2
        if(file.isDirectory()){
            filesIterator(file.listFiles().iterator())
        }else{
            String filePath = file.getAbsoluteFile()
            if(filePath.endsWith(".class")){
                //注释3
                executeClass(filePath)
            }else if(filePath.endsWith(".jar")){
                //注释4
                executeJava(filePath)
            }
        }
    }
}

注释1这个方法就是就是遍历每一个文件,

注释2这里会判断文件是否为文件夹,如果是文件夹的话,继续进行遍历;否则的话,判断文件是否为class,如果是class的话,执行注释3的executeClass,如果是jar包,执行executeJava

这里需要注意一下:在旧的gradle版本中,执行打包的指令输出为app:transformClassesWithDexBuilderForDebug,这时候的class包含了各个依赖库的class,比如app依赖了自己的otherlibrary,这时候的class也包含了otherlibrary的class文件。但是在新的版本中,otherlibrary会先打包成jar文件,执行dexBuilderDebug时,再将jar打包进dex。所以,如果我们想在自己项目的class文件中进行字节码修改,那么需要在处理jar文件时,需要筛选出其他库的jar,这个我们介绍executeJava的时候再说。我们先看一下executeClass方法:

static void executeClass(String filePath){
    try{
        FileInputStream is = new FileInputStream(filePath)
        //注释1
        byte[] byteCode =asm(is)
        is.close()

        FileOutputStream os = new FileOutputStream(filePath)
        os.write(byteCode)
        os.close()

    }catch(Exception e){
        e.printStackTrace()
    }
}

这里的处理过程:先拿到class文件的输入流,然后使用asm工具进行改造,之后再重新写入覆盖源文件。核心的操作时asm方法:

static byte[] asm(InputStream inputStream){
    ClassReader classReader = new ClassReader(inputStream)
    ClassWriter classWriter = new ClassWriter(classReader,0)
    ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7,classWriter){
        @Override
        MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
            return new MyMethodVisitor(Opcodes.ASM7,methodVisitor,access,name,descriptor)
        }
    }
    classReader.accept(classVisitor,0)
    return classWriter.toByteArray()
}

static class MyMethodVisitor extends AdviceAdapter{
    //方法名称
    String name
    protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor)
        this.name = name
    }

    @Override
    protected void onMethodExit(int opcode) {
        //注释1
        if("<init>".equals(name)&&opcode==Opcodes.RETURN){
            Type type1 = Type.getType("Ljava/lang/System;");
            Type type2 = Type.getType("Ljava/io/PrintStream;");
            //对应字节码指令getstatic,
            getStatic(type1,"out",type2);
            //对应字节码指令ldc
            visitLdcInsn("ASMTest=====>test");
            //对应字节码指令invokevirtual
            invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V"));
        }
        super.onMethodExit(opcode)
    }
}

这里的内容上篇文章字节码插桩技术---ASM的使用(一)_紫气东来_life的博客-CSDN博客我有说过,感兴趣的同学可以看一看

注释1这里的逻辑为:向构造方法的末尾插入一句输出代码

我们再说一下关于jar包的处理,我看一下executeJava方法:

static void executeJava(String filePath){
    //注释1
    if(!filePath.contains("build/intermediates/runtime_library_classes_jar/debug/classes.jar")){
        return
    }
    try {
        File srcFile = new File(filePath)
        //注释2
        File distFile = new File(srcFile.getParent(), srcFile.getName() + ".bak")
        JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(distFile))
        JarFile jarFile = new JarFile(srcFile)
        Enumeration<JarEntry> entries = jarFile.entries()
        //注释3
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            // 读jar包中的class
            jarOutputStream.putNextEntry(new JarEntry(jarEntry.getName()))
            InputStream is = jarFile.getInputStream(jarEntry)
            //注释4
            byte[] byteCode = asm(is)
            jarOutputStream.write(byteCode)
            jarOutputStream.closeEntry()
        }
        jarOutputStream.close()
        jarFile.close()
        srcFile.delete()
        //注释5
        distFile.renameTo(srcFile)

    }catch(IOException e){
        e.printStackTrace()
    }
}

注释1这里就是我上面所说的筛选条件,如果是自己项目的依赖库,那么jar的路径会包含上面的路径信息;注释2这里根据原jar创建了一个备份文件,改造的信息会重新写入到这个备份文件中,之后会将原jar包删除,再将备份文件重新命名为原jar包的名称;注释3的while循环就是遍历jar中的class文件,因为一个jar中可能会包含多个文件;注释4上面说过了;注释就是备份文件重新命名为原jar包的名称。其他的都是一些基本的IO操作。

至此,class文件的改造就已经完成了。可以使用dex2jar,jd-GUI等工具去查看,这里我就不贴结果了。项目下载

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值