上篇文章學習了下如何自定義gradle插件《自定義gradle》,在實際應用中一般都會結合自定義的Transform和字節碼操作框架(像ASM、Javassist)通過操作字節碼來實現自己的業務邏輯,Transform是Android Gradle 在1.5.0 版本後提供的, 它允許第三方的Plugin插件在打包成 dex 文件之前的編譯過程中操作 class 文件,對於一些重複性的操作,程序員可以統一修改class文件來實現自己的功能,從而避免在項目中出現大量的重複代碼。在Android項目中常用的場景有統計打點、打印方法執行的耗時時長等,本文中以統計打印Activity的打開和關閉事件爲例,來學習實踐下整個過程。
1、創建自定義的Plugin插件
在custom_plugin的src/main/groovy/com/znh/plugin下創建一個用於頁面統計的自定義插件PagePlugin,代碼如下:
package com.znh.plugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Created by znh on 2020-04-21
* <p>
* 用於頁面統計的Plugin
*/
class PagePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//註冊自定義的PageTransform
def androidConfig = project.extensions.getByType(AppExtension)
PageTransform transform = new PageTransform()
androidConfig.registerTransform(transform)
}
}
注:不要忘記在resources/META-INF/gradle-plugins下爲PagePlugin配置對應的properties文件
2、引入ASM依賴並創建Transform
這裏選用的操作字節碼的框架是ASM,所以需要引入ASM的依賴包,由於Transform是android gradle下面的api,所以也需要引入android gradle環境,在custom_plugin的build.gradle文件中添加以上配置,配置代碼如下:
//引入android環境
implementation 'com.android.tools.build:gradle:3.6.3'
//ASM依賴
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
在custom_plugin的src/main/groovy/com/znh/plugin下創建一個用於頁面統計的Transform類PageTransform,代碼如下:
package com.znh.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.znh.gradle.custom.plugin.PageClassVisitor
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
/**
* Created by znh on 2020-04-21
* <p>
* 處理頁面統計的Transform
*/
class PageTransform extends Transform {
@Override
String getName() {
return "PageTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super 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)
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
inputs.each {
TransformInput transformInput ->
//Gradle3.6.0之後需要單獨複製jar包
transformInput.jarInputs.each { JarInput jarInput ->
File jarFile = jarInput.file
def destJar = outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes,
jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarFile, destJar)
}
//操作class文件
transformInput.directoryInputs.each {
DirectoryInput directoryInput ->
File dir = directoryInput.file
if (dir) {
dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
//讀取和解析class文件
ClassReader classReader = new ClassReader(file.bytes)
//對class文件的寫入
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
//訪問class文件的內容
ClassVisitor classVisitor = new PageClassVisitor(classWriter)
//調用classVisitor的各個方法
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
//將修改的字節碼以byte數組返回
byte[] bytes = classWriter.toByteArray()
//通過文件流寫入方式覆蓋原先的內容,完成class文件的修改
FileOutputStream outputStream = new FileOutputStream(file.path)
outputStream.write(bytes)
outputStream.close()
}
}
//處理完輸入文件後,把輸出傳遞給下一個transform
def destDir = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, destDir)
}
}
}
}
- getName方法:執行這個Transform的task時,會以這個名字爲基礎生成task名稱( 比如這裏的任務是Task :app:transformClassesWithPageTransformForDebug)
- getInputTypes方法:表示要處理的數據類型是什麼,CLASSES 表示要處理編譯後的字節碼(可能是 jar 包也可能是目錄),RESOURCES 表示要處理的是標準的 java 資源
- getScopes方法:表示Transform 的作用域,這裏設置的SCOPE_FULL_PROJECT代表作用域是全工程
- isIncremental方法:表示是否支持增量編譯,false不支持
- transform方法:在這個方法中獲取到文件輸入源,對裏面的class文件做一些自定義的修改後,將文件複製到指定目錄中做爲下一個Transform的輸入源,多個Transfrom之間是一條執行鏈,上一個Transfrom的輸出作爲下一個Transfrom的輸入,由於transform方法中的父類是空實現,所以當自定義一個Transfrom時,就算對class不進行任何操作,在這個方法中仍然要將文件複製到指定目錄作爲下一個Transfrom的輸入,如果不做複製操作直接調用super方法,那整個執行鏈將斷開而導致程序異常
上述代碼中的PageClassVisitor類是自定義的操作class文件的類,它實現了ASM框架中的ClassVisitor類
3、基於ASM創建class文件的操作類
需要根據ASM框架提供的api和規則來創建操作字節碼文件的操作類,這裏需要修改字節碼來實現自己的業務邏輯,所以爲了方便這裏使用java類來實現,在/custom_plugin的src/main/java/com/znh/gradle/custom/plugin下創建PageClassVisitor和PageMethodVisitor用來修改class字節碼,插入自己的統計指令,以完成自己的業務需求。PageClassVisitor類用於對class字節碼的觀察並監聽類的信息,PageClassVisitor代碼如下:
package com.znh.gradle.custom.plugin;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* Created by znh on 2020-04-21
* <p>
* ASM操作class的類
*/
public class PageClassVisitor extends ClassVisitor {
//當前類的類名稱
//本例:com/znh/gradle/plugin/demo/MainActivity
private String className;
//className類的父類名稱
//本例:androidx/appcompat/app/AppCompatActivity
private String superName;
public PageClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public void visit(int version, int access, String className, String signature, String superName, String[] interfaces) {
super.visit(version, access, className, signature, superName, interfaces);
this.className = className;
this.superName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String methodName, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(access, methodName, descriptor, signature, exceptions);
//判斷是否自己要匹配的方法(這裏匹配onCreate和onDestroy方法)
if ("android/support/v7/app/AppCompatActivity".equals(superName)//support包
|| "androidx/appcompat/app/AppCompatActivity".equals(superName)) {//androidx包
if (methodName.startsWith("onCreate")) {
return new PageMethodVisitor(methodVisitor, className, methodName);
}
if (methodName.startsWith("onDestroy")) {
return new PageMethodVisitor(methodVisitor, className, methodName);
}
}
return methodVisitor;
}
}
PageMethodVisitor類用於對方法的觀察,可以在visitCode的方法中修改字節碼,插入自己的字節碼指令,從而植入自己的業務邏輯,PageMethodVisitor類代碼如下:
package com.znh.gradle.custom.plugin;
import org.objectweb.asm.MethodVisitor;
import groovyjarjarasm.asm.Opcodes;
/**
* Created by znh on 2020-04-21
* <p>
* ASM操作Method方法的類
*/
public class PageMethodVisitor extends MethodVisitor {
//當前的類名
private String className;
//當前的方法名
private String methodName;
PageMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
super(Opcodes.ASM5, methodVisitor);
this.className = className;
this.methodName = methodName;
}
@Override
public void visitCode() {
super.visitCode();
//在這裏插入自己的字節碼指令
//可以插入頁面打開關閉的打點上報代碼,這裏用log打印代替了
mv.visitLdcInsn("Page_TAG");//可以用來過濾log日誌的tag
mv.visitLdcInsn(className + "--->" + methodName);//插入要打印的內容
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
}
4、測試打印結果
上述代碼編寫完成後,需要將插件重新上傳到maven上,然後在app的build.gradle中引入新上傳的插件,運行項目並查看log日誌如下:
項目地址:https://github.com/huihuigithub/blog_demo_projects.git(gradle_plugin_demo項目)