前言
繼續上一章節自定義Gradle插件,利用plugin進一步做一些事情
本章節利用Google提供的Transform API 在編譯的過程中操作.class文件。
先說一下Transform是什麼
gradle從1.5開始,gradle插件包含了一個叫Transform的API,這個API允許第三方插件在class文件轉爲爲dex文件前操作編譯好的class文件,這個API的目標是簡化自定義類操作,而不必處理Task,並且在操作上提供更大的靈活性。並且可以更加靈活地進行操作。
官方文檔:http://google.github.io/android-gradle-dsl/javadoc/
我們接着在上面的demo中繼續完成使用Transform API,
在我們自定義的gradle插件的build.gradle中引入transform的包,下面會進行代碼注入,就一起引入的其他包
compile 'com.android.tools.build:transform-api:1.5.0'
compile 'javassist:javassist:3.12.1.GA'
compile 'commons-io:commons-io:2.5'
項目地址:TransformPlugin
接下來創建一個類繼承Transform 並實現其方法
package zxy.com.plugin
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.gradle.api.Project
public class MyClassTransform extends Transform {
private Project mProject;
public MyClassTransform(Project p) {
this.mProject = p;
}
//transform的名稱
//transformClassesWithMyClassTransformForDebug 運行時的名字
//transformClassesWith + getName() + For + Debug或Release
@Override
public String getName() {
return "MyClassTransform";
}
//需要處理的數據類型,有兩種枚舉類型
//CLASSES和RESOURCES,CLASSES代表處理的java的class文件,RESOURCES代表要處理java的資源
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
// 指Transform要操作內容的範圍,官方文檔Scope有7種類型:
// EXTERNAL_LIBRARIES 只有外部庫
// PROJECT 只有項目內容
// PROJECT_LOCAL_DEPS 只有項目的本地依賴(本地jar)
// PROVIDED_ONLY 只提供本地或遠程依賴項
// SUB_PROJECTS 只有子項目。
// SUB_PROJECTS_LOCAL_DEPS 只有子項目的本地依賴項(本地jar)。
// TESTED_CODE 由當前變量(包括依賴項)測試的代碼
@Override
public Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
//指明當前Transform是否支持增量編譯
@Override
public boolean isIncremental() {
return false;
}
// Transform中的核心方法,
// inputs中是傳過來的輸入流,其中有兩種格式,一種是jar包格式一種是目錄格式。
// outputProvider 獲取到輸出目錄,最後將修改的文件複製到輸出目錄,這一步必須做不然編譯會報錯
@Override
public void transform(Context context,
Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
System.out.println("你愁啥----------------進入transform了--------------")
//遍歷input
inputs.each { TransformInput input ->
//遍歷文件夾
input.directoryInputs.each { DirectoryInput directoryInput ->
//注入代碼
MyInjects.inject(directoryInput.file.absolutePath, mProject)
// 獲取output目錄
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
// 將input的目錄複製到output指定目錄
FileUtils.copyDirectory(directoryInput.file, dest)
}
////遍歷jar文件 對jar不操作,但是要輸出到out路徑
input.jarInputs.each { JarInput jarInput ->
// 重命名輸出文件(同目錄copyFile會衝突)
def jarName = jarInput.name
println("jar = " + jarInput.file.getAbsolutePath())
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)
}
}
System.out.println("瞅你咋地--------------結束transform了----------------")
}
}
在我們自定義的gradle插件的apply方法中註冊自定義的Transform,上一章節已經有介紹過apply入口
def android = project.extensions.getByType(AppExtension)
//註冊一個Transform
def classTransform = new MyClassTransform(project);
android.registerTransform(classTransform);
BuildConfig這個類大家並不陌生,在項目裏會用到,大家知道這個類可以增加我們自定義的屬性嗎,可是你知道怎麼生成的麼?
//我們自定義的
testCreatJavaConfig{
str = "動態生成java類的字符串"
}
然後回到我們的自定義的Plugin中,貼一下整個代碼
import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* @author:xinyu.zhou
*/
public class MyPlugin implements Plugin<Project> {
void apply(Project project) {
System.out.println("------------------開始----------------------");
System.out.println("這是我們的自定義插件!");
//AppExtension就是build.gradle中android{...}這一塊
def android = project.extensions.getByType(AppExtension)
//註冊一個Transform
def classTransform = new MyClassTransform(project);
android.registerTransform(classTransform);
//創建一個Extension,名字叫做testCreatJavaConfig 裏面可配置的屬性參照MyPlguinTestClass
project.extensions.create("testCreatJavaConfig", MyPlguinTestClass)
//生產一個類
if (project.plugins.hasPlugin(AppPlugin)) {
//獲取到Extension,Extension就是 build.gradle中的{}閉包
android.applicationVariants.all { variant ->
//獲取到scope,作用域
def variantData = variant.variantData
def scope = variantData.scope
//拿到build.gradle中創建的Extension的值
def config = project.extensions.getByName("testCreatJavaConfig");
//創建一個task
def createTaskName = scope.getTaskName("CeShi", "MyTestPlugin")
def createTask = project.task(createTaskName)
//設置task要執行的任務
createTask.doLast {
//生成java類
createJavaTest(variant, config)
}
//設置task依賴於生成BuildConfig的task,然後在生成BuildConfig後生成我們的類
String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
if (generateBuildConfigTask) {
createTask.dependsOn generateBuildConfigTask
generateBuildConfigTask.finalizedBy createTask
}
}
}
System.out.println("------------------結束了嗎----------------------");
}
static def void createJavaTest(variant, config) {
//要生成的內容
def content = """package com.zxy.plugin;
public class MyPlguinTestClass {
public static final String str = "${config.str}";
}
""";
//獲取到BuildConfig類的路徑
File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
def javaFile = new File(outputDir, "MyPlguinTestClass.java")
javaFile.write(content, 'UTF-8');
}
}
class MyPlguinTestClass {
def str = "默認值";
}
編譯一下看一下效果
可以看到我在app目錄下的build.gradle文件裏配置的testCreatJavaConfig 生效了,可以取到str的值
接下來要使用javassist,簡單介紹下
- Javassist是一個動態類庫,可以用來檢查、”動態”修改以及創建 Java類。其功能與jdk自帶的反射功能類似,但比反射功能更強大
- ClassPool:javassist的類池,使用ClassPool 類可以跟蹤和控制所操作的類,它的工作方式與 JVM 類裝載器非常相似,
CtClass: CtClass提供了檢查類數據(如字段和方法)以及在類中添加新字段、方法和構造函數、以及改變類、父類和接口的方法。不過,Javassist 並未提供刪除類中字段、方法或者構造函數的任何方法。
CtField:用來訪問域
CtMethod :用來訪問方法
CtConstructor:用來訪問構造器
想了解更多請自行查閱資料
下面我們利用Transform在MainActivity中動態的插入代碼,先看一下現在的MainAcitivity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView= findViewById(R.id.tv);
textView.setText(com.zxy.plugin.MyPlguinTestClass.str);
}
}
可以看到上面的setText中使用的是我們上面動態生成的類中的字段,看一下怎麼利用Transform插入代碼,先看一下Transform中代碼
// Transform中的核心方法,
// inputs中是傳過來的輸入流,其中有兩種格式,一種是jar包格式一種是目錄格式。
// outputProvider 獲取到輸出目錄,最後將修改的文件複製到輸出目錄,這一步必須做不然編譯會報錯
@Override
public void transform(Context context,
Collection<TransformInput> inputs,
Collection<TransformInput> referencedInputs,
TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
System.out.println("你愁啥----------------進入transform了--------------")
//遍歷input
inputs.each { TransformInput input ->
//遍歷文件夾
input.directoryInputs.each { DirectoryInput directoryInput ->
//注入代碼
MyInjects.inject(directoryInput.file.absolutePath, mProject)
// 獲取output目錄
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
// 將input的目錄複製到output指定目錄
FileUtils.copyDirectory(directoryInput.file, dest)
}
////遍歷jar文件 對jar不操作,但是要輸出到out路徑
input.jarInputs.each { JarInput jarInput ->
// 重命名輸出文件(同目錄copyFile會衝突)
def jarName = jarInput.name
println("jar = " + jarInput.file.getAbsolutePath())
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)
}
}
System.out.println("瞅你咋地--------------結束transform了----------------")
}
生成代碼在MyInjects類中,在這個類中我們傳入了兩個參數,一個是當前變量的文件夾,一個是當前的工程對象,來看一下代碼
public class MyInjects {
//初始化類池
private final static ClassPool pool = ClassPool.getDefault();
public static void inject(String path,Project project) {
//將當前路徑加入類池,不然找不到這個類
pool.appendClassPath(path);
//project.android.bootClasspath 加入android.jar,不然找不到android相關的所有類
pool.appendClassPath(project.android.bootClasspath[0].toString());
//引入android.os.Bundle包,因爲onCreate方法參數有Bundle
pool.importPackage("android.os.Bundle");
File dir = new File(path);
if (dir.isDirectory()) {
//遍歷文件夾
dir.eachFileRecurse { File file ->
String filePath = file.absolutePath
println("filePath = " + filePath)
if (file.getName().equals("MainActivity.class")) {
//獲取MainActivity.class
CtClass ctClass = pool.getCtClass("com.zxy.plugin.MainActivity");
println("ctClass = " + ctClass)
//解凍
if (ctClass.isFrozen())
ctClass.defrost()
//獲取到OnCreate方法
CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
println("方法名 = " + ctMethod)
String insetBeforeStr = """ android.widget.Toast.makeText(this,"WTF emmmmmmm.....我是被插了的Toast代碼~!!",android.widget.Toast.LENGTH_SHORT).show();
"""
//在方法開頭插入代碼
ctMethod.insertBefore(insetBeforeStr);
ctClass.writeFile(path)
ctClass.detach()//釋放
}
}
}
}
}
通過反編譯可以看到我們成功的注入了一個Toast
運行效果
總結
還是那句話,本章節是讓我們瞭解plugin和javassist結合使用入門,很多插件化等技術都會用到javassist,需要我們更多的深入瞭解和探索,無論是自定義gradle還是注入代碼這些技術都是通往大牛之路的必備技能,有描述錯誤的地方歡迎童鞋們指出。