因為要做一個無埋點收集數(shù)據(jù)的功能,需要自定義一個Plugin扬蕊,搜到的方法大部分都是打印一個HelloWorld,沒有任何的參考價值丹擎,所以詳細(xì)記錄一下過程尾抑。
如果想對編譯的class文件進(jìn)行字節(jié)碼注入,hook是一種方式蒂培,但是gradle1.5之后android gradle插件也可以通過自定義一個Plugin再愈,調(diào)用這段代碼來注冊一個Transform。
class GatherPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
def android = project.extensions.findByType(AppExtension)
android.registerTransform(new GatherTransform(project))
}
}
Transform是一個抽象類护戳,通過繼承這個類可以對字節(jié)碼進(jìn)行修改翎冲。為了弄這個,經(jīng)過有些麻煩媳荒,踩了一些gradle的坑抗悍,特意記錄一下。
整個過程分為下面幾步
創(chuàng)建一個Groovy模塊
創(chuàng)建一個GatherPlugin
創(chuàng)建一個GatherTransform
利用ASM掃描所有的類文件钳枕,然后在指定地方插入代碼
這個是Gradle的API,方便查看
創(chuàng)建一個Groovy模塊
-
創(chuàng)建一個Groovy項目
可以通過創(chuàng)建一個lib項目把里面的文件都刪了缴渊,處理build.gradle和放源碼的目錄。
這里的如果創(chuàng)建本工程自己用的插件文件的目錄名字必須是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 會導(dǎo)致自定義的Transform 不能執(zhí)行
- 創(chuàng)建的文件必須要以.groovy 為后綴,否則在其他文件中引用會語法錯誤
創(chuàng)建GatherPlugin和GatherTransform
這個很簡單
GatherPlugin.groovy文件田柔,文件后綴一定要有g(shù)roovy
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
// 構(gòu)造函數(shù)俐巴,我們將Project保存下來備用
public GatherTransform(Project project) {
this.project = project
}
// 設(shè)置我們自定義的Transform對應(yīng)的Task名稱
@Override
String getName() {
return "GatherTransform"
}
// 指定輸入的類型,通過這里的設(shè)定硬爆,可以指定我們要處理的文件類型
//這樣確保其他類型的文件不會傳入
@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進(jìn)行遍歷
*/
input.directoryInputs.each {
/**
* 文件夾里面包含的是
* 我們手寫的類
* R.class、
* BuildConfig.class
* R$XXX.class
* 等
* 根據(jù)自己的需要對應(yīng)處理
*/
println("it == ${it}")
//注入代碼
Inject.injectOnClick(it.file.absolutePath)
// 獲取output目錄
def dest = outputProvider.getContentLocation(it.name,
it.contentTypes, it.scopes,
Format.DIRECTORY)
// 將input的目錄復(fù)制到output指定目錄
FileUtils.copyDirectory(it.file, dest)
}
//對類型為jar文件的input進(jìn)行遍歷
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)
//將輸入內(nèi)容復(fù)制到輸出
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}
這樣整個插件就可以運行了劣光。
利用ASM掃描所有的類文件袜蚕,然后在指定地方插入代碼
在制定的代碼區(qū)域注入指定代碼主要在Inject,groovy中完成的,這個代碼主要就是怎么用Groovy绢涡,所以沒有貼牲剃。
我是第一次用ASM,對ASM的語法一點不懂雄可,出了很多問題凿傅〔看了很多的例子代碼,基本上都是注入一個輸出HelloWorld聪舒,屬于沒有一點參考價值的辨液。
當(dāng)然我們只是做一個插件沒有必要去花時間去學(xué)習(xí)ASM,這個東西要學(xué)習(xí)也不是一天兩天的事箱残,踩很多坑之后找到一個工具滔迈,非常好用。一個Studio插件 ASM Bytecode Outline , 下載后解壓被辑,將復(fù)制Studio的圖片中的目錄燎悍,然后重啟Studo
這個插件使用很簡單,重啟后Studio左邊會出現(xiàn)如圖所示
鼠標(biāo)右擊你的某一個類盼理。
然后就會把你這個類的代碼全部轉(zhuǎn)化成ASM語法格式的谈山。66666。如果不會寫ASM的語法榜揖,把你的代碼在一個測試類中先寫好勾哩,然后利用ASM生成出對應(yīng)的ASM語法,在把代碼copy到Inject.groovy中即可举哟。
例如GatherClassVisitor.groovy文件中這些代碼都是通過這個工具生產(chǎn)的
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();
}
}