首先需要熟悉APK打包流程,字节码知识,Gradle,才有可能把下面的内容看懂。
@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()); } 复制代码
创建plugin调试任务 ![image-20200609173703523](/Users/jackie/Library/Application Support/typora-user-images/image-20200609173703523.png)
终端输入:
./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true
然后终端界面会停留在这里![image-20200609173445941](/Users/jackie/Library/Application Support/typora-user-images/image-20200609173445941.png)
点击AS上面的Debug按钮,打上断点,开始调试
参考: www.jianshu.com/p/ea3e00c5e… gralde文件件调试等等(fucknmb.com/2017/07/05/…
断点无法进入Transform的transform方法
先定义好Extension
在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 } 复制代码
使用
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'} 复制代码
学习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(); } 复制代码
trycatch返回值的问题
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等等。
MainActivity.java生成MainActivity.class文件,用javac MainActivity.java无法生成,会报找不到各种包,在app/build/intermediates/javac目录下面寻找.class文件
查看代码是否添加成功,apk编译过程会有中间产物,生成jar包,可以在这里查看,不用反编译查看,/Users/jackie/Desktop/WorkPlace/AsmDemo/app/build/intermediates/transforms/TimeTransform
AsmDemo中含有多个实例 github.com/ljzyljc/Asm… github.com/dikeboy/Dhj…
模仿Hunter框架写基础处理流程,transform等流程无法调试,一定要记得打日志,新认识好多个异常,构造器问题,方法调用参数的问题,ASM插件的show difference使用,一个类中引用一个其他类,其他类需要先生成字节码,然后该类才能生成。R文件相关无法生成字节码。
插入代码遇到的问题,记住android等开头的包,kotlin等一些库,无法进行插入代码,记得要忽略。
log中的打印有时候进行查找类的时候竟然无法查到,可能是打印的日志过长,需大致的模糊搜索,看一遍。
这些坑在实际项目开发中肯定难以避免,所以一定要有的真正的代码实践,才算是真正的入门。