Android ASM流程全打通

3,829 阅读6分钟

首先需要熟悉APK打包流程,字节码知识,Gradle,才有可能把下面的内容看懂。

1.Transform关键方法

@Override
String getName() {
    return "try-catch transform"
}

//CLASSES 处理编译后的字节码,可能是jar包也可能是目录
//RESOURCES 处理标准的java资源
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
    return TransformManager.CONTENT_CLASS
}

//Transform的作用域,比如当前项目,子项目,子项目的本地依赖等等
@Override
Set<? super QualifiedContent.Scope> getScopes() {
    return TransformManager.SCOPE_FULL_PROJECT
}

//是否支持增量编译
@Override
boolean isIncremental() {
    return false
}

//处理编译后的class文件
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation)
}
//之前使用的是
	void transform(Context context, Collection<TransformInput> inputs,
                   Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider, boolean isIncremental)
                   
//但是,该方法已经被废弃,使用的是transform(transformInvocation),点入源码一看实际调用的还是旧方法
//public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        // Just delegate to old method, for code that uses the old API.
        //noinspection deprecation
        transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                transformInvocation.getOutputProvider(),
                transformInvocation.isIncremental());
    }

2.Gradle调试

  1. 创建plugin调试任务 ![image-20200609173703523](/Users/jackie/Library/Application Support/typora-user-images/image-20200609173703523.png)

  2. 终端输入:

    ./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true

    然后终端界面会停留在这里![image-20200609173445941](/Users/jackie/Library/Application Support/typora-user-images/image-20200609173445941.png)

  3. 点击AS上面的Debug按钮,打上断点,开始调试

    参考: www.jianshu.com/p/ea3e00c5e… gralde文件件调试等等(fucknmb.com/2017/07/05/…

  4. 断点无法进入Transform的transform方法

3.Android Extension

  1. 先定义好Extension

  2. 在plugin中创建extension,然后创建一个task来输入我们设置好的值

    //1. TryCatchPlugin
    
    project.extensions.create("tryCatchInfo",TryCatchExtension)
    println("============create TryCatchInfo Extension")
    project.afterEvaluate {
        println("============afterEvaluate=========")
        project.task("tryCatchTask",type:TryCatchTask)
    }
    
    //2. TryCatchTask
    
    class TryCatchTask extends DefaultTask{
    
        TryCatchExtension tryCatchExtension
    
    		//注意,这里用无参构造器,然后在构造器给tryCatchExtension赋值
        TryCatchTask(){
            tryCatchExtension = project.tryCatchInfo
            println("====TryCatchTask======Constructor======"+tryCatchExtension.toString())
        }
    
        @TaskAction
        void run(){
            println("====Task run====")
            println(tryCatchExtension.toString())
        }
    }
    
    
    tryCatchInfo {
        pathName = "com.jackie.testlib.MyClass"
        methodName = "testCrash"
        exceptionName = "java.lang.Exception"
        returnValue = 10
    }
    
    
  3. 使用

    apply plugin: 'com.jackie.trycatch'
    
    classpath 'com.jackie.trycatch:trycatchplugin:1.0'
    
    tryCatchInfo {
        pathName = "com.jackie.testlib.MyClass"
        methodName = "testCrash"
        exceptionName = "java.lang.Exception"
        returnValue = 10
    }
    
    ./gradlew tryCatchTask
    
    输出:
    
    > Task :app:tryCatchTask
    ====Task run====
    TryCatchExtension.groovy{pathName=com.jackie.testlib.MyClassmethodName=testCrashexceptionName=java.lang.Exception, returnValue='10'}
    
    

4.ASM

学习ASM没有什么技巧,就是看API,使用一些插件方便查看字节码,多练习,然后你才能入门,最后达到精通。

可以结合前一篇的ASM学习笔记来学习ASM的内容

ASM设计了两种类型,一种是基于Tree API,一种是基于Visitor API(visitor pattern)

Tree API将class的结构读取到内存,构建一个树形结构,然后需要处理Method、Field等元素时,到树形结构中定位到某个元素,进行操作,然后把操作再写入新的class文件。

Visitor API则将通过接口的方式,分离读class和写class的逻辑,一般通过一个ClassReader负责读取class字节码,然后ClassReader通过一个ClassVisitor接口,将字节码的每个细节按顺序通过接口的方式,传递给ClassVisitor(你会发现ClassVisitor中有多个visitXXXX接口),这个过程就像ClassReader带着ClassVisitor游览了class字节码的每一个指令。

上面这两种解析文件结构的方式在很多处理结构化数据时都常见,一般得看需求背景选择合适的方案,而我们的需求是这样的,出于某个目的,寻找class文件中的一个hook点,进行字节码修改,这种背景下,我们选择Visitor API的方式比较合适。

下面这段代码,通过Visitor API读取一个class的内容,保存到另一个文件

private void copy(String inputPath, String outputPath) {
    try {
        FileInputStream is = new FileInputStream(inputPath);
        ClassReader cr = new ClassReader(is);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        cr.accept(cw, 0);
        FileOutputStream fos = new FileOutputStream(outputPath);
        //ClassWriter.toByteArray,将ClassReader传递到ClassWriter的字节码导出,写入新的文件。
        fos.write(cw.toByteArray());
        fos.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

首先,我们通过ClassReader文件读取某个class文件,然后定义一个ClassWriter,这个ClassWriter我们可以看它源码,其实就是一个ClassVisitor的实现,负责将ClassReader传递过来的数据写到一个字节流中,而真正触发这个逻辑就是通过ClassReader的accept方式。

public void accept(ClassVisitor classVisitor, Attribute[] attributePrototypes, int 		  parsingOptions) {
    
    // 读取当前class的字节码信息
    int accessFlags = this.readUnsignedShort(currentOffset);
    String thisClass = this.readClass(currentOffset + 2, charBuffer);
    String superClass = this.readClass(currentOffset + 4, charBuffer);
    String[] interfaces = new String[this.readUnsignedShort(currentOffset + 6)];

    
    
    //classVisitor就是刚才accept方法传进来的ClassWriter,每次visitXXX都负责将字节码的信息存储起来
    classVisitor.visit(this.readInt(this.cpInfoOffsets[1] - 7), accessFlags, thisClass, signature, superClass, interfaces);
    
    /**
        略去很多visit逻辑
    */

    //visit Attribute
    while(attributes != null) {
        Attribute nextAttribute = attributes.nextAttribute;
        attributes.nextAttribute = null;
        classVisitor.visitAttribute(attributes);
        attributes = nextAttribute;
    }

    /**
        略去很多visit逻辑
    */

    classVisitor.visitEnd();
}

5.ASM遇到的问题

  1. trycatch返回值的问题

  2. getCommonSuperClass(),寻找两个类的共同父类。

    ​ 源码中使用了classloader,但是编译器使用的classloader并没有加载Android项目中的代码,所以我们需要自定义一个ClassLoader,将前面提到的Transform中接收到的所有的jar以及class,还有android.jar都添加到自定义的ClassLoader中。(其实上面这个方法注释中已经暗示了这个方法存在的一些问题)

    ​ 但是,如果只是替换了getCommonSuperClass中的Classloader,依然还有一个更深的坑,我们可以看看前面getCommonSuperClass的实现,它是如何寻找父类的呢?它是通过Class.forName加载某个类,然后再去寻找父类,但是,但是,android.jar中的类可不能随随便便加载的呀,android.jar对于Android工程来说只是编译时依赖,运行时是用Android机器上自己的android.jar。而且android.jar所有方法包括构造函数都是空实现,其中都只有一行代码。

    throw new RuntimeException("Stub!");
    

    ​ 这样加载某个类时,它的静态域就会被触发,而如果有一个static的变量刚好在声明时被初始化,而初始化中只有一个RuntimeException,此时就会抛异常。

    ​ 所以,我们不能通过这种方式来获取父类,能否通过不需要加载class就能获取它的父类的方式呢?谜底就在眼前,父类其实也是一个class的字节码中的一项数据,那么我们就从字节码中查询父类即可。利用ClassWriter,ClassReader等等。

  3. MainActivity.java生成MainActivity.class文件,用javac MainActivity.java无法生成,会报找不到各种包,在app/build/intermediates/javac目录下面寻找.class文件

  4. 查看代码是否添加成功,apk编译过程会有中间产物,生成jar包,可以在这里查看,不用反编译查看,/Users/jackie/Desktop/WorkPlace/AsmDemo/app/build/intermediates/transforms/TimeTransform

  5. AsmDemo中含有多个实例 github.com/ljzyljc/Asm… github.com/dikeboy/Dhj…

    1. 统计方法耗时,可以跨方法
    2. 添加try-catch
    3. onClick添加埋点
    4. new Thread统一替换成CustomThread
    5. 本来想实现全局string加密,但是要是继续研究下去实在成本太大,也苦于缺乏支撑,等下次项目中有遇到再深入研究吧,已经有过一番研究,有了字节码基础/gradle等,下次开始就会很快了。
  6. 模仿Hunter框架写基础处理流程,transform等流程无法调试,一定要记得打日志,新认识好多个异常,构造器问题,方法调用参数的问题,ASM插件的show difference使用,一个类中引用一个其他类,其他类需要先生成字节码,然后该类才能生成。R文件相关无法生成字节码。

  7. 插入代码遇到的问题,记住android等开头的包,kotlin等一些库,无法进行插入代码,记得要忽略。

  8. log中的打印有时候进行查找类的时候竟然无法查到,可能是打印的日志过长,需大致的模糊搜索,看一遍。

  9. 这些坑在实际项目开发中肯定难以避免,所以一定要有的真正的代码实践,才算是真正的入门。