Transform API
從 1.5.0-beta1 開始,Gradle 插件包含一個 Transform API,允許第三方插件在將已編譯的類文件轉(zhuǎn)換為 dex 文件之前對其進(jìn)行操作。(該 API 已存在于 1.4.0-beta2 中,但已在 1.5.0-beta1 中進(jìn)行了徹底修改)
Transform API 的目標(biāo)是簡化注入自定義類的操作而不必處理任務(wù),并為操作內(nèi)容提供更大的靈活性。內(nèi)部代碼處理(jacoco,progard,multi-dex)已經(jīng)在 1.5.0-beta1 中轉(zhuǎn)移到了這一新機(jī)制。
Transform 的注冊和使用非常簡單,在我們自定義的 Gradle 插件中,只需創(chuàng)建一個實(shí)現(xiàn) Transform 接口的類,然后將其注冊到 android.registerTransform(theTransform) 或 android.registerTransform(theTransform, dependencies) 中即可。
Transform 是一個鏈?zhǔn)浇Y(jié)構(gòu),每個 Transform 都是一個 Gradle 的 Task,Android 編譯器通過 TaskManager 將每個 Transform 串聯(lián)起來。
Gradle Transform 是 Android 官方提供給開發(fā)者在項(xiàng)目構(gòu)建階段,即由 .class 到 .dex 轉(zhuǎn)換期間修改 .class 文件的一套 API。目前比較經(jīng)典的應(yīng)用是字節(jié)碼插樁、代碼注入技術(shù)。
主要方法
Transform.java 是一個抽象類,我們在使用是,需要實(shí)現(xiàn)它,它的主要方法有哪些呢?
getName()
用于指定 Transform 的名字,對應(yīng)了該 Transform 的 Task 的名稱。
isIncremental()
該方法指明是否支持增量編譯,增量編譯用于加快編譯速度。
getInputTypes()
指定 Transform 要處理的數(shù)據(jù)類型,可以作為輸入過濾的一種手段。
在 TransformManager 中定義了很多類型:
CONTENT_CLASS // 代表 javac 編譯成的 class 文件,可能是 jar 也可能是目錄。
CONTENT_JARS
CONTENT_RESOURCES // 表示處理標(biāo)準(zhǔn)的 java 資源
CONTENT_NATIVE_LIBS
CONTENT_DEX
CONTENT_DEX_WITH_RESOURCES
DATA_BINDING_BASE_CLASS_LOG_ARTIFACT
getScopes()
用于指定 Transform 的作用域。同樣在 TransformManager 中定義了很多類型。
常見的作用域有:
PROJECT:只處理當(dāng)前項(xiàng)目。
SUB_PROJECT:只處理子項(xiàng)目。
PROJECT_LOCAL_DEPS:只處理當(dāng)前項(xiàng)目的本地依賴,例如:jar、aar。
SUB_PROJECT_LOCAL_DEPS:只處理子項(xiàng)目的本地依賴,例如:jar、aar。
EXTERNAL_LIBRARIES:只處理外部依賴庫。
PROVIDED_ONLY:只處理本地或遠(yuǎn)程以 provided 形式引入的依賴庫。
TESTED_CODE:測試代碼。
SCOPE_FULL_PROJECT:即代表所有 Project。
確定了 ContentType 和 Scope 后就確定了該自定義 Transform 需要處理的資源流。
例如,上面提到的常用輸入類型(CONTENT_CLASS)和常用作用域(SCOPE_FULL_PROJECT)表示的就是 所有項(xiàng)目中 java 編譯成的 class 組成的資源流。
transform()
transform 方法來處理中間轉(zhuǎn)換過程,主要邏輯在該方法中實(shí)現(xiàn)。
它的定義:
public void transform(@NonNull TransformInvocation transformInvocation)
該方法的參數(shù)是 TransformInvocation。
我們可以在 transform 方法中,實(shí)現(xiàn)對字節(jié)碼的修改、處理等操作。
TransformInvocation
我們可以通過 TransformInvocation 來獲取輸入,同時也獲得了輸出的功能。
TransformInvocation 接口定義如下:
public interface TransformInvocation {
@NonNull Context getContext();
//TransformInput 是輸入文件的抽象,包括 jar 和目錄格式。
@NonNull Collection<TransformInput> getInputs();
@NonNull Collection<TransformInput> getReferencedInputs();
@NonNull Collection<SecondaryInput> getSecondaryInputs();
// Transform 的輸出,通過它可以獲取輸出路徑。
@Nullable TransformOutputProvider getOutputProvider();
boolean isIncremental();
}
TransformInvocation 的使用,我們舉例說明一下:
@Override
void transform(@NonNull TransformInvocation transformInvocation) {
def startTime = System.currentTimeMillis()
//如果是非增量編譯,則刪除之前的輸出
if (!transformInvocation.isIncremental) {
transformInvocation.outputProvider.deleteAll()
}
//TransformInvocation 來獲取輸入
Collection<TransformInput> inputs = transformInvocation.inputs
//TransformInvocation 來獲取輸出
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//遍歷inputs
inputs.each { TransformInput input ->
//遍歷directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍歷jarInputs
input.jarInputs.each { JarInput jarInput ->
handleJarInputs(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
}
TransformInput
TransformInput 是指這些輸入文件的抽象。它包括兩部分:
DirectoryInput 集合
是指以源碼方式參與項(xiàng)目編譯的所有目錄結(jié)構(gòu)及其目錄下 的源碼文件。JarInput 集合
是指以 jar 包方式參與項(xiàng)目編譯的所有本地 jar 包和遠(yuǎn)程 jar 包。
TransformOutputProvider
是 Transform 的輸出的抽象,通過它可以獲取輸出路徑。TransformOutputProvider 通過調(diào)用 getContentLocation 來獲取輸出目錄:
@NonNull
File getContentLocation(
@NonNull String name,
@NonNull Set<QualifiedContent.ContentType> types,
@NonNull Set<? super QualifiedContent.Scope> scopes,
@NonNull Format format);
實(shí)戰(zhàn)
Transform 的注冊和使用非常易懂, 在我們自定義的 plugin 內(nèi), 我們可以通過 android.registerTransform(theTransform) 或者 android.registerTransform(theTransform, dependencies) 就可以完成注冊。
使用 Transform API 主要是寫一個類繼承 Transform,并把該 Transform 注入到打包過程中。
注入 Transform 很簡單,先獲取 com.android.build.gradle.AppExtension 對象,然后調(diào)用它的registerTransform() 方法。
這個方法實(shí)際上是屬于 BaseExtension 的,AppExtension 繼承自 BaseExtension:
#com.android.build.gradle.BaseExtension
public void registerTransform(@NonNull Transform transform, Object... dependencies) {
transforms.add(transform);
transformDependencies.add(Arrays.asList(dependencies));
}
注冊我們自定義的 Transform:
void apply(Project project) {
AppExtension android = project.extensions.getByType(AppExtension)
android.registerTransform(new MethodTimeTransform(project))
}
通過獲取 module 的 Project 的 AppExtension,通過它的 registerTransform 方法完成 Transform 的注冊。
這里注冊之后,會在編譯過程中的 TransformManager#addTransform 中生成一個 task,然后在執(zhí)行這個 task 的時候會執(zhí)行到我們自定義的 Transform 的 transform 方法。這個 task 的執(zhí)行時機(jī)就是 .class 文件轉(zhuǎn)換成 .dex文 件的時候。
我們來看完整的實(shí)例代碼。
自定義的 Transform 類,AsmTransform:
class AsmTransform extends Transform{
private Project mProject;
AsmTransform(Project project){
mProject = project
}
@Override
String getName() {
return "budaye_transform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return true
}
@Override
void transform(@NonNull TransformInvocation transformInvocation) {
def startTime = System.currentTimeMillis()
//如果是非增量編譯,則刪除之前的輸出
if (!transformInvocation.isIncremental) {
transformInvocation.outputProvider.deleteAll()
}
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
//遍歷inputs
inputs.each { TransformInput input ->
//遍歷directoryInputs
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍歷jarInputs
input.jarInputs.each { JarInput jarInput ->
handleJarInputs(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
}
/**
* 處理文件目錄下的class文件
*/
static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
//是否是目錄
if (directoryInput.file.isDirectory()) {
//列出目錄所有文件(包含子文件夾,子文件夾內(nèi)文件)
directoryInput.file.eachFileRecurse { File file ->
def name = file.name
if (checkClassFile(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}
//處理完輸入文件之后,要把輸出給下一個任務(wù)
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}
/**
* 處理Jar中的class文件
*/
static void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名輸出文件,因?yàn)榭赡芡?會覆蓋
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的緩存被重復(fù)插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插樁class
if (checkClassFile(entryName)) {
//class文件處理
println '----------- deal with "jar" class file <' + entryName + '> -----------'
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new LifecycleClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//結(jié)束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
/**
* 檢查class文件是否需要處理
* @param fileName
* @return
*/
static boolean checkClassFile(String name) {
//只處理需要的class文件
return (name.endsWith(".class") && !name.startsWith("R\$")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)
&& "android/support/v4/app/FragmentActivity.class".equals(name))
}
}
AsmTransform 的注冊:
@Override
void apply(Project project) {
if (project.plugins.hasPlugin(AppPlugin)){
//registerTransform
def android = project.extensions.getByType(AppExtension)
android.registerTransform(new AsmTransform(project))
}
}
到了這里,就實(shí)現(xiàn)了整個 Transform 的定義和注冊過程了。
**PS:更多性能優(yōu)化相關(guān)文章,請查看 --> 《Android 性能優(yōu)化》
————————————————
版權(quán)聲明:本文為CSDN博主「卜大爺」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u011578734/article/details/114262419