以前在极客时间上面学习张绍文老师的《Android开发高手课》的时候,有一章节讲了android中编译插桩的三种方法:AspectJ、ASM、Redex。以为这个东西好厉害,就想着要弄懂它,在后面章节的Sample练习中也详细讲解了ASM与TransForm结合在android插桩中的运用。可是这个知识点仍是有点难度的,想要弄懂这个知识点仍是须要不少储备知识的。html
要想理解android中字节码插桩的运用,须要掌握如下几个知识点:java
AOP(Aspect Oriented Program)是一种面向切面编程的思想。这种思想是相对于OOP(Object Oriented Programming)来讲的。这里能够参考邓凡平老师的深刻理解Android之AOP。Java中的面向对象编程的特色是继承、多态和封装。这就使功能被划分到一个一个模块中,模块之间经过设计好的接口交互。OOP的精髓就使把功能或者问题模块化。
可是在现实中,咱们会有一些这样的需求,好比:在项目中全部模块都添加日志统计模块,统计每一个方法的运行时间等。这个若是用OOP的思想来实现的话,须要在每一个模块的每一个方法中添加须要的代码。而经过AOP就能很好的解决这个问题,AOP能够理解为在代码运行期间,动态地将代码切入到类中的指定方法、指定位置上的编程思想。注意这是一种编程思想,它的具体实现方式有不少,好比java中的动态代理,aspectj以及咱们今天要讲的经过asm来实现。android
原本想先讲Transform相关的知识的,可是,Transform通常在自定义插件中使用,因此若是不先介绍自定义插件的话,可能看不懂要讲的Transform,这里就简单介绍一下自定义插件。
这里推荐在AndroidStudio中自定义Gradle插件,这篇文章详细讲解了如何在android studio中建立Gradle插件,这里就再也不细述。建立好了以后咱们会在groovy文件夹下面建立一个继承Plugin类的子类,以下:数据库
package com.soulmate.plugin.lifecycle
import org.gradle.api.Plugin
import org.gradle.api.Project
class CustomPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.task("testTask"){
doLast{
println "hello from the CustomPlugin"
}
}
}
}
复制代码
当咱们在Terminal中输入gradle testTask的时候,会看到输出“hello from the CustomPlugin”,后面经过Transform来处理时,也是在apply方法中进行处理的。编程
在官方文档中是这么形容Transform:设计模式
Starting with 1.5.0-beta1,the Gradle Plugin includes a Transform API allowing
3rd party plugins to manipulate compiled class files before they are converted to dex files
The goal of this API is to simplify injecting custom class manipulations without having to deal with tasks,
and to offer more flexibility on what is manipulated. The internal code processing (jacoco, progard, multi-dex)
have all moved to this new mechanism already in 1.5.0-beta1
复制代码
简单翻译一下就是Gradle工具从1.5.0版本开始提供Transform API,在编译后的class文件转换成dex文件以前,经过Transform API来处理编译后的class文件。api
Transform API的目标是不须要经过处理任务来简化注入自定义类的操做,在处理上面提供了更大的灵活性。包括(proguard、multi-dex等)都在1.5.0中迁移到这个新机制中。数组
简单总结就是Transform API是操做编译后的.class文件,而咱们知道.class文件中是java编译后的字节码,因此Transform至关于提供了一个操做字节码的入口。(具体java中的字节码相关知识能够网上搜索,这里我强烈推荐一下《深刻理解Java虚拟机》这本书,这本书上面对字节码有很详细的讲解)。而因为字节码的操做比较复杂,咱们通常须要借助工具来处理java字节码,ASM工具就是一个很是好的字节码处理工具,后面咱们会介绍ASM在处理字节码方面的运用。bash
咱们写一个TestTransform继承Transform而后看一些重写的方法。数据结构
public class TestTransform extends Transform {
private static Project project
TestTransform(Project project) {
this.project = project
}
@Override
public String getName() {
return “TestTransform”;
}
/**
* 须要处理的数据类型,有两种枚举类型
* CLASSES 表明处理的编译后的class文件,RESOURCES 表明要处理的java资源
*/
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
/**
* 值Transform 的做用范围,有一下7种类型:
* 1.EXTERNAL_LIBRARIES 只有外部库
* 2.PROJECT 只有项目内容
* 3.PROJECT_LOCAL_DEPS 只有项目的本地依赖(本地jar)
* 4.PROVIDED_ONLY 只提供本地或远程依赖项
* 5.SUB_PROJECTS 只有子项目
* 6.SUB_PROJECTS_LOCAL_DEPS 只有子项目的本地依赖项(本地jar)
* 7.TESTED_CODE 由当前变量(包括依赖项)测试的代码
*/
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
//是否支持增量编译
@Override
public boolean isIncremental() {
return false;
}
//这个方法用来进行具体的输入输出处理,这里能够获取输入的目录文件以及jar包文件
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
}
}
复制代码
这里要补充一下,如今自定义的Transform有了,自定义的plugin也有了,如何将二者关联起来了。这时咱们须要用到一个类AppExtension,这个类继承自BaseExtension。咱们在TestPlugin类中改写apply方法:
package com.soulmate.plugin.lifecycle
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class CustomPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
AppExtension appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new TestTransform(project))
}
}
复制代码
这样咱们就将自定义的插件和Transform关联起来了。接下来咱们介绍一下ASM相关的知识,而后最后在讲解在transform()方法中使用ASM来处理相应的需求
ASM是一个java字节码操控框架。它能被用来 动态生成类或者加强既有类的功能。ASM采用的是Visitor设计模式对字节码进行访问和修改,核心类主要有如下几个:
这里主要给的是巴巴巴巴巴巴掌
的文章手摸手增长字节码往方法体内插代码,这个例子对于理解asm中具体的插入代码方式有很是直观的理解。这里我就不贴出具体代码了,我只是将main()方法中的
FileOutputStream fos = new FileOutputStream("out/Bazhang223.class");
fos.write(code);
fos.close();
复制代码
替换成了
FileOutputStream fos = new FileOutputStream("Bazhang223.class");
fos.write(code);
fos.close();
复制代码
运行main方法后,会在as的根目录下面生成Bazhang223.class文件。打开这个class文件,你会发现你想要添加的两个输出已经添加成功了。
前面咱们已经对每个都进行了介绍,如今咱们对这三者的概念应该有了清晰的认识,接下来就要看看如何将三者结合起来使用了。
自定义plugin这个不用说,确定是首先须要作的事。
而后咱们须要作的是重写自定义自定义的Transform子类中的transform()
方法,这个方法很是重要,这个方法是全部业务逻辑的入口,在这个方法里面你能够遍历全部目录和jar包,获取全部的class文件,而后作须要的处理。具体遍历的代码以下。
//Transform 的 inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
inputs.each { TransformInput input ->
//遍历directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
//do Something
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
//do Something
}
}
复制代码
既然咱们能够获取全部的class文件了,那么如今咱们就能够对每一个class文件进行修改了,修改class文件就用到了ASM。这里就以《android高手开发课》上面的例子讲一下,将每一个class文件转换成字节数组,而后传给下面的方法:
public static run(InputStream is) throws IOException {
ClassReader classReader = new ClassReader(is);
//COMPUTE_MAXS 说明使用 ASM 自动计算本地变量表最大值和操做数栈的最大值
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
//EXPAND_FRAMES 说明在读取 class 的时候同时展开栈映射帧 (StackMap Frame),在使用 AdviceAdapter 里这项是必须打开的
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
}
复制代码
具体在ASM中如何修改这里就不详细说了,能够参考《android高手开发课》中的代码。
好了,到这里咱们终于将这三者的关系讲完了,这样你应该对字节码插桩的实现有了清晰的认识了。后面你就能够结合网上的一些案例来本身实现字节码插桩了。
编译插桩技术仍是很是重要的,咱们平时用到的不少框架包括butterknife
、Dagger
以及数据库ORM
框架都会在编译过程当中生成代码。因此对于一名开发人员来讲仍是要很好的掌握这门技术的。