因为要做一个无埋点收集数据的功能,需要自定义一个Plugin,搜到的方法大部分都是打印一个HelloWorld,没有任何的参考价值,所以详细记录一下过程。
如果想对编译的class文件进行字节码注入,hook是一种方式,但是gradle1.5之后android gradle插件也可以通过自定义一个Plugin,调用这段代码来注册一个Transform。
class GatherPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def android = project.extensions.findByType(AppExtension)
android.registerTransform(new GatherTransform(project))
}
}
Transform是一个抽象类,通过继承这个类可以对字节码进行修改。为了弄这个,经过有些麻烦,踩了一些gradle的坑,特意记录一下。
整个过程分为下面几步
创建一个Groovy模块
创建一个GatherPlugin
创建一个GatherTransform
利用ASM扫描所有的类文件,然后在指定地方插入代码
这个是Gradle的API,方便查看
创建一个Groovy模块
-
创建一个Groovy项目
可以通过创建一个lib项目把里面的文件都删了,处理build.gradle和放源码的目录。
这里的如果创建本工程自己用的插件文件的目录名字必须是buildSrc,先以本工程用的插件为例。
修改build.gradle文件脚本代码
apply plugin: 'groovy'
//上传插件到仓库需要 非必要
apply plugin: 'maven'
dependencies {
compile gradleApi()//gradle sdk
compile localGroovy()//groovy sdk
compile 'com.android.tools.build:gradle:2.3.1'
compile 'org.ow2.asm:asm:5.0.3'
compile 'org.ow2.asm:asm-commons:5.0.3'
}
repositories {
jcenter()
mavenCentral()
}
有个坑
- jackOptions 为true 会导致自定义的Transform 不能执行
- 创建的文件必须要以.groovy 为后缀,否则在其他文件中引用会语法错误
创建GatherPlugin和GatherTransform
这个很简单
GatherPlugin.groovy文件,文件后缀一定要有groovy
class GatherPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def android = project.extensions.findByType(AppExtension)
android.registerTransform(new GatherTransform(project))
}
}
在项目的gradle.build文件里引用插件
apply plugin: 'com.android.application'
apply plugin: com.cyy.gather.GatherPlugin
.....
GatherTransform.groovy文件
public class GatherTransform extends Transform{
Project project
// 构造函数,我们将Project保存下来备用
public GatherTransform(Project project) {
this.project = project
}
// 设置我们自定义的Transform对应的Task名称
@Override
String getName() {
return "GatherTransform"
}
// 指定输入的类型,通过这里的设定,可以指定我们要处理的文件类型
//这样确保其他类型的文件不会传入
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
// 指定Transform的作用范围
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println(" transform transform ")
}
@Override
void transform(Context context, Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider, boolean isIncremental)
throws IOException, TransformException, InterruptedException {
/**
* Transform的inputs有两种类型,
* 一种是目录, DirectoryInput
* 一种是jar包,JarInput
* 要分开遍历
*/
inputs.each { TransformInput input ->
/**
* 对类型为“文件夹”的input进行遍历
*/
input.directoryInputs.each {
/**
* 文件夹里面包含的是
* 我们手写的类
* R.class、
* BuildConfig.class
* R$XXX.class
* 等
* 根据自己的需要对应处理
*/
println("it == ${it}")
//注入代码
Inject.injectOnClick(it.file.absolutePath)
// 获取output目录
def dest = outputProvider.getContentLocation(it.name,
it.contentTypes, it.scopes,
Format.DIRECTORY)
// 将input的目录复制到output指定目录
FileUtils.copyDirectory(it.file, dest)
}
//对类型为jar文件的input进行遍历
input.jarInputs.each { JarInput jarInput ->
//jar文件一般是第三方依赖库jar文件
// 重命名输出文件(同目录copyFile会冲突)
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
//生成输出路径
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
//将输入内容复制到输出
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}
这样整个插件就可以运行了。
利用ASM扫描所有的类文件,然后在指定地方插入代码
在制定的代码区域注入指定代码主要在Inject,groovy中完成的,这个代码主要就是怎么用Groovy,所以没有贴。
我是第一次用ASM,对ASM的语法一点不懂,出了很多问题。看了很多的例子代码,基本上都是注入一个输出HelloWorld,属于没有一点参考价值的。
当然我们只是做一个插件没有必要去花时间去学习ASM,这个东西要学习也不是一天两天的事,踩很多坑之后找到一个工具,非常好用。一个Studio插件 ASM Bytecode Outline , 下载后解压,将复制Studio的图片中的目录,然后重启Studo
这个插件使用很简单,重启后Studio左边会出现如图所示
鼠标右击你的某一个类。
然后就会把你这个类的代码全部转化成ASM语法格式的。66666。如果不会写ASM的语法,把你的代码在一个测试类中先写好,然后利用ASM生成出对应的ASM语法,在把代码copy到Inject.groovy中即可。
例如GatherClassVisitor.groovy文件中这些代码都是通过这个工具生产的
methodList.each {
if (it == "onResume" || it == "onPause"){
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC , it, "()V", null, null)
mv.visitVarInsn(ALOAD, 0)
mv.visitMethodInsn(INVOKESPECIAL, superName, it, "()V", false)
mv.visitVarInsn(ALOAD, 0);
mv.visitInsn(it == "onResume" ? ICONST_1 : ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentResumeOrPause", "(Landroid/support/v4/app/Fragment;Z)V", false);
mv.visitInsn(RETURN)
mv.visitMaxs(1, 1)
mv.visitEnd()
} else if (it == "onHiddenChanged"){
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "onHiddenChanged", "(Z)V", null, null)
mv.visitCode()
mv.visitVarInsn(ALOAD, 0)
mv.visitVarInsn(ILOAD, 1)
mv.visitMethodInsn(INVOKESPECIAL, superName, "onHiddenChanged", "(Z)V", false)
mv.visitVarInsn(ALOAD, 0)
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onHiddenChanged", "(Landroid/support/v4/app/Fragment;Z)V", false);
mv.visitInsn(RETURN)
mv.visitMaxs(2, 2)
mv.visitEnd()
}else if (it == "onViewCreated"){
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "onViewCreated", "(Landroid/view/View;Landroid/os/Bundle;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKESPECIAL, superName, "onViewCreated", "(Landroid/view/View;Landroid/os/Bundle;)V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentCreatedView", "(Landroid/support/v4/app/Fragment;Landroid/view/View;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(3, 3);
mv.visitEnd();
}else if (it == ""){
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "setUserVisibleHint", "(Z)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESPECIAL, superName, "setUserVisibleHint", "(Z)V", false);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentSettUserVisibleHint", "(Landroid/support/v4/app/Fragment;Z)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
}