其實 Gradle Transform 就是個紙老虎 —— Gradle 系列(4)
前言
目前,使用 AGP Transform API 進行位元組碼插樁已經非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 聽起來很高大上,其本質就是一個 Gradle Task。在這篇文章裡,我將帶你理解 Transform 的工作機制、使用方法和核心原始碼解析,並通過一個 Demo 幫助你融會貫通。
這篇文章是全面掌握 Gradle 構建系統系列的第 4 篇:
- 1、Gradle 基礎
- 2、Gradle 外掛
- 3、Gradle 依賴管理
- 4、APG Transform
請點贊加關注,你的支援對我非常重要,滿足下我的虛榮心。
:fire: Hi,我是小彭。本文已收錄到 GitHub · Android-NoteBook 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,歡迎跟著我一起成長。(聯絡方式在 GitHub)
1. 認識 Transform
1.1 什麼是 Transform?
Transform API 是 Android Gradle Plugin 1.5 就引入的特性,主要用於在 Android 構建過程中,在 Class→Dex 這個節點修改 Class 位元組碼。利用 Transform API,我們可以拿到所有參與構建的 Class 檔案,藉助 Javassist 或 ASM 等位元組碼編輯工具進行修改,插入自定義邏輯。一般來說,這些自定義邏輯是與業務邏輯無關的。
使用 Transform 的常見的應用場景有:
- 埋點統計: 在頁面展現和退出等生命週期中插入埋點統計程式碼,以統計頁面展現資料;
- 耗時監控: 在指定方法的前後插入耗時計算,以觀察方法執行時間;
- 方法替換: 將方法呼叫替換為呼叫另一個方法。
1.2 Transform 的基本原理
先大概瞭解下 Transform 的工作機制:
- 1、工作時機: Transform 工作在 Android 構建中由 Class → Dex 的節點;
- 2、處理物件: 處理物件包括 Javac 編譯後的 Class 檔案、Java 標準 resource 資源、本地依賴和遠端依賴的 JAR/AAR。Android 資原始檔不屬於 Transform 的操作範圍,因為它們不是位元組碼;
- 3、Transform Task: 每個 Transform 都對應一個 Task,Transform 的輸入和輸出可以理解為對應 Transform Task 的輸入輸出。每個 TransformTask 的輸出都分別儲存在
app/build/intermediates/transform/[Transform Name]/[Variant]
資料夾中; - 4、Transform 鏈: TaskManager 會將每個 TransformTask 串聯起來,前一個 Transform 的輸出會作為下一個 Transform 的輸入。
1.3Transform API
瞭解 Transform 的基本工作機制後,我們先來看 Transform 的核心 API。這裡僅列舉出 Transform 抽象類中最核心的方法,有幾個次要的方法後面再說。
com.android.build.api.transform.java
public abstract class Transform { // 指定 Transform 的名稱,該名稱還會用於組成 Task 的名稱 // 格式為 transform[InputTypes]With[name]For[Configuration] public abstract String getName(); // (孵化中)用於過濾 Variant,返回 false 表示該 Variant 不執行 Transform public boolean applyToVariant(VariantInfo variant) { return true; } // 指定輸入內容型別 public abstract Set<ContentType> getInputTypes(); // 指定輸出內容型別,預設取 getInputTypes() 的值 public Set<ContentType> getOutputTypes() { return getInputTypes(); } // 指定消費型輸入內容範疇 public abstract Set<? super Scope> getScopes(); // 指定引用型輸入內容範疇 public Set<? super Scope> getReferencedScopes() { return ImmutableSet.of(); } // 指定是否支援增量編譯 public abstract boolean isIncremental(); // 核心 API public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { // 分發到過時 API,以相容舊版本的 Transform //noinspection deprecation transform(transformInvocation.getContext(), transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), transformInvocation.getOutputProvider(), transformInvocation.isIncremental()); } // 指定是否支援快取 public boolean isCacheable() { return false; } }
1.4 ContentType 內容型別
ContentType 是一個列舉類介面,表示輸入或輸出內容的型別,在 AGP 中定義了 DefaultContentType
和 ExtendedContentType
兩個列舉類。但是,我們在自定義 Transform 時只能使用 DefaultContentType 中定義的列舉,即 CLASSES
和 RESOURCES
兩種型別,其它型別僅供 AGP 內建的 Transform 使用。
自定義 Transform 需要在兩個位置定義內容型別:
- 1、Set
getInputTypes(): 指定輸入內容型別,允許通過 Set 集合設定輸入多種型別; - 2、Set
getOutputTypes(): 指定輸出內容型別,預設取 getInputTypes() 的值,允許通過 Set 集合設定輸出多種型別。
ExtendedContentType.java
// 加強型別,自定義 Transform 無法使用 public enum ExtendedContentType implements ContentType { // DEX 檔案 DEX(0x1000), // Native 庫 NATIVE_LIBS(0x2000), // Instant Run 加強類 CLASSES_ENHANCED(0x4000), // Data Binding 中間產物 DATA_BINDING(0x10000), // Dex Archive DEX_ARCHIVE(0x40000), ; }
QualifiedContent.java
enum DefaultContentType implements ContentType { // Java 位元組碼,包括 Jar 檔案和由原始碼編譯產生的 CLASSES(0x01), // Java 資源 RESOURCES(0x02); }
在 TransformManager 中,預定義了一部分內容型別集合,常用的是 CONTENT_CLASS 操作 Class。
TransformManager.java
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES); public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES); public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
1.5 ScopeType 作用域
ScopeType 也是一個列舉類介面,表示輸入內容的範疇。在 AGP 中定義了 InternalScope
和 Scope
兩個列舉類。但是,我們在自定義 Transform 只能使用 Scope 中定義的列舉,其它型別僅供 AGP 內建的 Transform 使用。
Transform 需要在兩個位置定義輸入內容範圍:
- 1、Set
getScopes() 消費型輸入內容範疇: 此範圍的內容會被消費,因此當前 Transform 必須將修改後的內容複製到 Transform 的中間目錄中,否則無法將內容傳遞到下一個 Transform 處理; - 2、Set
getReferencedScopes() 指定引用型輸入內容範疇: 預設是空集合,此範圍的內容不會被消費,因此不需要複製傳遞到下一個 Transform,也不允許修改。
InternalScope.java
// 內部使用的作用域,自定義 Transform 無法使用 public enum InternalScope implements QualifiedContent.ScopeType { MAIN_SPLIT(0x10000), LOCAL_DEPS(0x20000), FEATURES(0x40000), ; }
QualifiedContent.java
enum Scope implements ScopeType { // 當前模組 PROJECT(0x01), // 子模組 SUB_PROJECTS(0x04), // 外部依賴,包括當前模組和子模組本地依賴和遠端依賴的 JAR/AAR EXTERNAL_LIBRARIES(0x10), // 當前變體所測試的程式碼(包括依賴項) TESTED_CODE(0x20), // 本地依賴和遠端依賴的 JAR/AAR(provided-only) PROVIDED_ONLY(0x40), }
在 TransformManager 中,預定義了一部分作用域集合,常用的是 SCOPE_FULL_PROJECT 所有模組。 需要注意,Library 模組註冊的 Transform 只能使用 Scope.PROJECT。
TransformManager.java
public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT); public static final Set<ScopeType> SCOPE_FULL_PROJECT = ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);
1.6 transform 方法
transform() 是實現 Transform 的核心方法,方法的引數是 TransformInvocation,它提供了所有與輸入輸出相關的資訊:
public interface TransformInvocation { Context getContext(); // 消費型輸入內容 Collection<TransformInput> getInputs(); // 引用型輸入內容 Collection<TransformInput> getReferencedInputs(); // 額外輸入內容 Collection<SecondaryInput> getSecondaryInputs(); // 輸出資訊 TransformOutputProvider getOutputProvider(); // 是否增量構建 boolean isIncremental(); }
- isIncremental(): 當前 Transform 任務是否增量構建;
- getInputs(): 獲取 TransformInput 物件,它是消費型輸入內容,對應於 Transform#getScopes() 定義的範圍;
- getReferencedInputs(): 獲取 TransformInput 物件,它是引用型輸入內容,對應於 Transform#getReferenceScope() 定義的內容範圍;
- getOutPutProvider(): TransformOutputProvider 是對輸出檔案的抽象。
輸入內容 TransformInput 由兩部分組成:
- DirectoryInput 集合: 以原始碼方式參與構建的輸入檔案,包括完整的原始碼目錄結構及其中的原始碼檔案;
- JarInput 集合: 以 Jar 和 aar 依賴方式參與構建的輸入檔案,包含本地依賴和遠端依賴。
輸入內容資訊 TransformOutputProvider 有兩個功能:
- deleteAll(): 當 Transform 執行在非增量構建模式時,需要刪除上一次構建產生的所有中間檔案,可以直接呼叫 deleteAll() 完成;
- getContentLocation(): 獲得指定範圍+型別的輸出目標路徑。
TransformOutputProvider.java
public interface TransformOutputProvider { // 刪除所有中間檔案 void deleteAll() // 獲取指定範圍+型別的目標路徑 File getContentLocation(String name, Set<QualifiedContent.ContentType> types, Set<? super QualifiedContent.Scope> scopes, Format format); }
獲取輸入內容對應的輸出路徑:
for (input in transformInvocation.inputs) { for (jarInput in input.jarInputs) { // 輸出路徑 val outputJar = outputProvider.getContentLocation( jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR ) } }
1.7 Transform 增量模式
任何構建系統都會盡量避免重複執行相同工作,Transform 也不例外。雖然增量構建並不是必須的,但作為一個合格的 Transform 實現應該具備增量能力。
1、增量模式標記位:Transform API 有兩個增量標誌位,不要混淆:
- Transform#isIncremental(): Transform 增量構建的使能開關,返回 true 才有可能觸發增量構建;
- TransformInvocation#isIncremental(): 當次 TransformTask 是否增量執行,返回 true 表示正在增量模式。
2、Task 增量模式與 Transform 增量模式的區別:Task 增量模式與 Transform 增量模式的區別在於,Task 增量執行時會跳過整個 Task 的動作列表,而 Transform 增量執行依然會執行 TransformTask,但輸入內容會增加變更內容資訊。
3、增量模式的輸入:增量模式下的所有輸入都是帶狀態的,需要根據這些狀態來做不同的處理,不需要每次所有流程都重新來一遍。比如新增的輸入就需要處理,而未修改的輸入就不需要處理。Transform 定義了四個輸入檔案狀態:
com.android.build.api.transform.Status.java
public enum Status { // 未修改,不需要處理,也不需要複製操作 NOTCHANGED, // 新增,正常處理並複製給下一個任務 ADDED, // 已修改,正常處理並複製給下一個任務 CHANGED, // 已刪除,需同步移除 OutputProvider 指定的目標檔案 REMOVED; }
1.8 註冊 Transform
在 BaseExtension 中維護了一個 Transform 列表,自定義 Transform 需要註冊才能生效,而且還支援額外設定 TransformTask 的依賴。
BaseExtension.kt
abstract class BaseExtension { private val _transforms: MutableList<Transform> = mutableListOf() private val _transformDependencies: MutableList<List<Any>> = mutableListOf() ... fun registerTransform(transform: Transform, vararg dependencies: Any) { _transforms.add(transform) _transformDependencies.add(listOf(dependencies)) } }
註冊 Transform:
// 獲取 Android 擴充套件 val androidExtension = project.extensions.getByType(BaseExtension::class.java) // 註冊 Transform,支援額外增加依賴 androidExtension.registerTransform(ToastTransform(project)/* 支援增加依賴*/)
提示:為了提高編譯效率,可以判斷 Variant 為 release 型別才註冊 Transform,也可以通過重寫 Transform#applyToVariant() 來決定是否執行 Transform。
2. Transform 核心原始碼分析
這一節我們來分析 Transform 相關核心原始碼,這裡我們引用的是 Android Gradle Plugin 7.1.0 版本的原始碼。
2.1 Transform 與 Task 的關係
Project 的構建邏輯由一系列 Task 的組成,每個 Task 負責完成一個基本的工作,例如 Javac 編譯 Task。Transform 也是依靠 Task 執行的,在配置階段,Gradle 會為註冊的 Transform 建立對應的 Task。
提示:說 “建立” 可能不太嚴謹,TransformManager 使用 register 懶建立的方式註冊 Task,其實還沒有建立 Task 例項。我們不要複雜化了,就說建立吧。
而 Task 的依賴關係是通過 TransformTask 的輸入輸出關係隱式確定的,TransformManager 通過 TransformStream 連結各個 TransformTask 的輸入輸出,進而控制 Transform 的依賴關係順序。
LibraryTaskManager.java
@Override protected void doCreateTasksForVariant(ComponentInfo<LibraryVariantBuilderImpl, LibraryVariantImpl> variantInfo) { ... // ----- External Transforms ----- // apply all the external transforms. List<Transform> customTransforms = extension.getTransforms(); List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies(); final IssueReporter issueReporter = libraryVariant.getServices().getIssueReporter(); for (int i = 0, count = customTransforms.size(); i < count; i++) { Transform transform = customTransforms.get(i); // Check the transform only applies to supported scopes for libraries: // We cannot transform scopes that are not packaged in the library // itself. Sets.SetView<? super Scope> difference = Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY); if (!difference.isEmpty()) { String scopes = difference.toString(); issueReporter.reportError( Type.GENERIC, String.format( "Transforms with scopes '%s' cannot be applied to library projects.", scopes)); } List<Object> deps = customTransformsDependencies.get(i); transformManager.addTransform( taskFactory, libraryVariant, transform, null, task -> { // (3.2節提到的額外依賴) // 在註冊 Transform 時,可以額外增加依賴 if (!deps.isEmpty()) { task.dependsOn(deps); } }, taskProvider -> { // if the task is a no-op then we make assemble task // depend on it. if (transform.getScopes().isEmpty()) { TaskFactoryUtils.dependsOn( libraryVariant.getTaskContainer().getAssembleTask(), taskProvider); } }); } // Create jar with library classes used for publishing to runtime elements. taskFactory.register(new BundleLibraryClassesJar.CreationAction( libraryVariant, AndroidArtifacts.PublishedConfigType.RUNTIME_ELEMENTS)); ... }
網上很多朋友提到 “自定義 Transform 的執行時機早於系統內建 Transform”,但從 AGP 7.1.0 原始碼看,並不存在系統 Transform。猜測是新版本 AGP 將這部分 “系統內建 Transform” 修改為由 Task 直接實現,畢竟 從 AGP 7.0 開始 Transform 標記為過時了。
2.2 Transform 的建立過程
- 1、註冊 Transform: 註冊 Transform 僅是將物件註冊到 BaseExtension 中的列表中。TransformManager 會通過 Task 的輸入輸出隱式建立 Transform 的依賴順序,另外還支援在註冊時新增額外的依賴。
BaseExtension.kt
abstract class BaseExtension { private val _transforms: MutableList<Transform> = mutableListOf() private val _transformDependencies: MutableList<List<Any>> = mutableListOf() ... fun registerTransform(transform: Transform, vararg dependencies: Any) { _transforms.add(transform) _transformDependencies.add(listOf(dependencies)) } }
- 2、建立 TransformTask 的執行鏈: TransformTask 屬於 Android 構建構成的一部分,所有 Android Task 的建立入口都從 BasePlugin#createAndroidTasks() 開始。其中會為所有 Variant 變體建立相關的 Task,經過一系列呼叫後,會通過抽象方法 TaskManager#doCreateTaskForVariant() 分派到 ApplicationTaskManager 和 LibraryTaskManager 兩個子類中,以區分 App 模組和 Library 模組。
呼叫鏈概要:
BasePlugin#createAndroidTasks() -> TaskManager#createTasks()->遍歷所有變體 -> for { TaskManager#createTasksForVariant(variant) -> abstract TaskManager#doCreateTasksForVariant(variant) // App -> ApplicationTaskManager#doCreateTasksForVariant(variant) -> ApplicationTaskManager#createCommonTask(variant) -> ApplicationTaskManager#createCompileTask(variant) -> TaskManager#createPostCompilationTasks(config) -> for { Transform#addTransform(transform) } // Library -> LibraryTaskManager#doCreateTasksForVariant(variant) -> for { Transform#addTransform(transform) } }
2.3 TransformTask 的命名格式
Transform#getName() 會用於構造 Task Name,命名格式為 transform[InputTypes]With[name]For[Configuration]
,例如 transformClassed。這塊原始碼體現在 TransformManager 中建立 Task 的位置:
TransformManager.java
// 建立 Transform Task public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(...) { ... // TaskName = 字首 + Configuration String taskName = creationConfig.computeTaskName(getTaskNamePrefix(transform), ""); ... } // TaskName 字首 static String getTaskNamePrefix(Transform transform) { StringBuilder sb = new StringBuilder(100); sb.append("transform"); sb.append(transform .getInputTypes() .stream() .map(inputType -> CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, inputType.name())) .sorted() // Keep the order stable. .collect(Collectors.joining("And"))); sb.append("With"); StringHelper.appendCapitalized(sb, transform.getName()); sb.append("For"); return sb.toString(); }
2.4 TransformTask 的輸入輸出
TransformTask 通過 @Input 和 @OutputDirectory 等註解,將 Transform API 關聯到 Task 的輸入輸出上:
TransformTask.java
public abstract class TransformTask extends StreamBasedTask { ... @Input public Set<QualifiedContent.ContentType> getInputTypes() { return transform.getInputTypes(); } @OutputDirectory @Optional public abstract DirectoryProperty getOutputDirectory(); }
2.5 執行 transform() 方法
每個 Task 內部都保持了一個 Action 列表 actions
,執行 Task 就是按順序執行這個列表,對於自定義 Task,可以通過 @TaskAction
註解新增預設 Action。
TransformTask.java
@TaskAction void transform(final IncrementalTaskInputs incrementalTaskInputs) { ... transform.transform(new TransformInvocationBuilder(context) .addInputs(consumedInputs.getValue()) .addReferencedInputs(referencedInputs.getValue()) .addSecondaryInputs(changedSecondaryInputs.getValue()) .addOutputProvider(outputStream != null ? outputStream.asOutput() : null) .setIncrementalMode(isIncremental.getValue()) .build()); ... }
2.6 Library 模組限制
Library 模組僅只支援使用 Scope.PROJECT 作用域:
LibraryTaskManager.java
// Check the transform only applies to supported scopes for libraries: // We cannot transform scopes that are not packaged in the library // itself. Sets.SetView<? super Scope> difference = Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY); if (!difference.isEmpty()) { String scopes = difference.toString(); issueReporter.reportError(Type.GENERIC, String.format("Transforms with scopes '%s' cannot be applied to library projects.",scopes)); }
3. 自定義 Transform 模板
上一節我們探討了 Transform 的基本工作機制,第 3 節和第 4 節我們來實現一個 Transform Demo。Transform 的核心程式碼在 transform() 方法中,我們要做的就是遍歷輸入檔案,再把修改後的檔案複製到目標路徑中,對於 JarInputs 還有一次解壓和壓縮。更進一步,再考慮增量編譯的情況。
因此,整個 Transform 的核心過程是有固定套路,模板流程圖如下:
—— 圖片引用自 https://rebooters.github.io/2020/01/04/Gradle-Transform-ASM-探索/
我們把整個流程圖做成一個抽象模板類,子類需要重寫 provideFunction()
方法,從輸入流讀取 Class 檔案,修改完位元組碼後再寫入到輸出流。甚至不需要考慮 Trasform 的輸入檔案遍歷、加解壓、增量等,舒服!
BaseCustomTransform.kt
abstract class BaseCustomTransform(private val debug: Boolean) : Transform() { abstract fun provideFunction(): ((InputStream, OutputStream) -> Unit)? open fun classFilter(className: String) = className.endsWith(SdkConstants.DOT_CLASS) override fun isIncremental() = true override fun transform(transformInvocation: TransformInvocation) { super.transform(transformInvocation) log("Transform start, isIncremental = ${transformInvocation.isIncremental}.") val inputProvider = transformInvocation.inputs val referenceProvider = transformInvocation.referencedInputs val outputProvider = transformInvocation.outputProvider // 1. Transform logic implemented by subclasses. val function = provideFunction() // 2. Delete all transform tmp files when not in incremental build. if (!transformInvocation.isIncremental) { log("All File deleted.") outputProvider.deleteAll() } for (input in inputProvider) { // 3. Transform jar input. log("Transform jarInputs start.") for (jarInput in input.jarInputs) { val inputJar = jarInput.file val outputJar = outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR) if (transformInvocation.isIncremental) { // 3.1 Transform jar input in incremental build. when (jarInput.status ?: Status.NOTCHANGED) { Status.NOTCHANGED -> { // Do nothing. } Status.ADDED, Status.CHANGED -> { // Do transform. transformJar(inputJar, outputJar, function) } Status.REMOVED -> { // Delete. FileUtils.delete(outputJar) } } } else { // 3.2 Transform jar input in full build. transformJar(inputJar, outputJar, function) } } // 4. Transform dir input. log("Transform dirInput start.") for (dirInput in input.directoryInputs) { val inputDir = dirInput.file val outputDir = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY) if (transformInvocation.isIncremental) { // 4.1 Transform dir input in incremental build. for ((inputFile, status) in dirInput.changedFiles) { val outputFile = concatOutputFilePath(outputDir, inputFile) when (status ?: Status.NOTCHANGED) { Status.NOTCHANGED -> { // Do nothing. } Status.ADDED, Status.CHANGED -> { // Do transform. doTransformFile(inputFile, outputFile, function) } Status.REMOVED -> { // Delete FileUtils.delete(outputFile) } } } } else { // 4.2 Transform dir input in full build. for (inputFile in FileUtils.getAllFiles(inputDir)) { // Traversal fileTree (depthFirstPreOrder). if (classFilter(inputFile.name)) { val outputFile = concatOutputFilePath(outputDir, inputFile) doTransformFile(inputFile, outputFile, function) } } } } } log("Transform end.") } /** * Do transform Jar. */ private fun transformJar(inputJar: File, outputJar: File, function: ((InputStream, OutputStream) -> Unit)?) { // Create parent directories to hold outputJar file. Files.createParentDirs(outputJar) // Unzip. FileInputStream(inputJar).use { fis -> ZipInputStream(fis).use { zis -> // Zip. FileOutputStream(outputJar).use { fos -> ZipOutputStream(fos).use { zos -> var entry = zis.nextEntry while (entry != null && isValidZipEntryName(entry)) { if (!entry.isDirectory && classFilter(entry.name)) { zos.putNextEntry(ZipEntry(entry.name)) // Apply transform function. applyFunction(zis, zos, function) } entry = zis.nextEntry } } } } } } /** * Do transform file. */ private fun doTransformFile(inputFile: File, outputFile: File, function: ((InputStream, OutputStream) -> Unit)?) { // Create parent directories to hold outputFile file. Files.createParentDirs(outputFile) FileInputStream(inputFile).use { fis -> FileOutputStream(outputFile).use { fos -> // Apply transform function. applyFunction(fis, fos, function) } } } private fun concatOutputFilePath(outputDir: File, inputFile: File) = File(outputDir, inputFile.name) private fun applyFunction(input: InputStream, output: OutputStream, function: ((InputStream, OutputStream) -> Unit)?) { try { if (null != function) { function.invoke(input, output) } else { // Copy input.copyTo(output) } } catch (e: UncheckedIOException) { throw e.cause!! } } private fun log(logStr: String) { if (debug) { println("$name - $logStr") } } }
4. Hello Transform 示例
現在,我手把手帶你基於 BaseCustomTransform 實現一個 Transform Demo。示例程式碼我已經上傳到 Github · DemoHall · HelloTransform 。有用請給個免費的 Star 支援下。
Demo 效果很簡單:
- 實現一個 Transform,在編譯時在 Activity#onCreate() 方法末尾織入一個 Toast 語句;
- 僅通過自定義註解 @Hello 修飾的 Activity#onCreate() 方法會生效。
4.1 步驟 1:初始化程式碼框架
首先,我們先搭建工程的整體框架,再來編寫核心的 Transform 邏輯。我們選擇自定義 Gradle 外掛來承載 Transform 的邏輯,可維護性更好。關於自定義 Gradle 外掛的步驟具體見上一篇文章 《手把手帶你自定義 Gradle 外掛》 ,此處不展開。
提示:提醒一下,並不是說一定要由 Gradle 外掛來承載,你直接在 .gradle 檔案中實現也是 OK 的。
外掛實現類如下:
ToastPlugin.kt
class ToastPlugin : Plugin<Project> { override fun apply(project: Project) { // 獲取 Android 擴充套件 val androidExtension = project.extensions.getByType(BaseExtension::class.java) // 註冊 Transform,支援額外增加依賴 androidExtension.registerTransform(ToastTransform(project)/* 支援增加依賴*/) } }
4.2 步驟 2:拷貝 Transform 模板類
將我們實現的 BaseCustomTransform 模板類複製到工程下,再實現一個子類:
ToastTransform.kt
internal class ToastTransform(val project: Project) : BaseCustomTransform(true) { // Transform 名 override fun getName() = "ToastTransform" // 是否支援增量構建 override fun isIncremental() = true /** * 用於過濾 Variant,返回 false 表示該 Variant 不執行 Transform */ @Incubating override fun applyToVariant(variant: VariantInfo?): Boolean { return "debug" == variant?.buildTypeName } // 指定輸入內容型別 override fun getInputTypes() = TransformManager.CONTENT_CLASS // 指定消費型輸入內容範疇 override fun getScopes() = TransformManager.SCOPE_FULL_PROJECT // 轉換方法 override fun provideFunction() = { ios: InputStream, zos: OutputStream -> input.copyTo(output) } }
其中,provideFunction() 是模板程式碼,引數分別表示源 Class 檔案的輸入流和目標 Class 檔案輸出流。子類要做的事,就是從輸入流讀取 Class 資訊,修改後寫入到輸出流。
4.3 步驟 3:使用 Javassist 修改位元組碼
使用 Javassist API 從輸入流載入資料,在匹配到 onCreate() 方法後檢查是否宣告 @Hello 註解。是則在該方法末尾織入一句 Toast:Hello Transform。本文重點不是 Javassist,此處就不展開了。
override fun provideFunction() = { ios: InputStream, zos: OutputStream -> val classPool = ClassPool.getDefault() // 加入android.jar classPool.appendClassPath((project.extensions.getByName("android") as BaseExtension).bootClasspath[0].toString()) classPool.importPackage("android.os.Bundle") // Input val ctClass = classPool.makeClass(ios) try { ctClass.getDeclaredMethod("onCreate").also { println("onCreate found in ${ctClass.simpleName}") val attribute = it.methodInfo.getAttribute(AnnotationsAttribute.invisibleTag) as? AnnotationsAttribute if (null != attribute?.getAnnotation("com.pengxr.hellotransform.Hello")) { println("Insert toast in ${ctClass.simpleName}") it.insertAfter( """android.widget.Toast.makeText(this,"Hello Transform!",android.widget.Toast.LENGTH_SHORT).show(); """ ) } } } catch (e: NotFoundException) { // ignore } // Output zos.write(ctClass.toBytecode()) ctClass.detach() }
4.4 步驟 4:應用外掛
sample 模組 build.gradle
apply plugin: 'com.pengxr.toastplugin'
4.5 步驟 5:宣告 @Hello 註解
HelloActivity.kt
class HelloActivity : AppCompatActivity() { @Hello override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_hello) } }
4.6 步驟 6:執行
完成以上步驟後,編譯執行程式。可以在 Build Output 看到以下輸出,HelloActivity 啟動時會彈出 Toast HelloTransform,說明織入成功。
... Task :sample:mergeDebugJavaResource > Task :sample:transformClassesWithToastTransformForDebug ... onCreate found in HelloActivity Insert toast in HelloActivity ToastTransform - Transform end. > Task :sample:dexBuilderDebug > Task :sample:mergeExtDexDebug > Task :sample:mergeDexDebug > Task :sample:packageDebug > Task :sample:createDebugApkListingFileRedirect > Task :sample:assembleDebug BUILD SUCCESSFUL in 3m 18s 33 actionable tasks: 33 executed Build Analyzer results available
5. Transform 的未來
從 AGP 7.0 開始,Transform API 已經被廢棄了。是的,就是卷,而且這次直接是降維打擊。以前 Transform 是 AGP 的特性,現在 Gradle 也來整 Transform,不過換了個名字,叫 —— TransformAction 。
那麼,我們還有必要學 AGP Transform API 嗎?如果你現在涉足位元組碼插樁這塊,你建議你還是學以下:
- 1、社群沉澱: AGP Transform API 發展多年,目前社群中已經沉澱下非常多優秀的開源元件和部落格,這些資源對你非常有幫助。而 TransformAction 的社群沉澱還非常單薄;
- 2、技術思維: 雖然換了一套 API,但背後的思路 / 套路是相似的。理解 AGP Transform 的工作機制,對你理解 Gradle TransformAction 有事半功倍的效果。
例如,以下是 Gradle 官方文件 的演示程式碼,是不是套路差不多?
abstract class CountLoc implements TransformAction<TransformParameters.None> { @Inject abstract InputChanges getInputChanges() @PathSensitive(PathSensitivity.RELATIVE) @InputArtifact abstract Provider<FileSystemLocation> getInput() @Override void transform(TransformOutputs outputs) { def outputDir = outputs.dir("${input.get().asFile.name}.loc") println("Running transform on ${input.get().asFile.name}, incremental: ${inputChanges.incremental}") inputChanges.getFileChanges(input).forEach { change -> def changedFile = change.file if (change.fileType != FileType.FILE) { return } def outputLocation = new File(outputDir, "${change.normalizedPath}.loc") switch (change.changeType) { case ADDED: case MODIFIED: println("Processing file ${changedFile.name}") outputLocation.parentFile.mkdirs() outputLocation.text = changedFile.readLines().size() case REMOVED: println("Removing leftover output file ${outputLocation.name}") outputLocation.delete() } } } }
6. 總結
本文的示例程式碼已上傳到 https://github.com/pengxurui/DemoHall ,請 Star 支援。關注我,帶你瞭解更多,我們下次見。
參考資料
- Gradle Transform + ASM 探索 —— REBOOTERS 著
- 深入理解 Transform —— toothpickTina 著
- 現在準備好告別 Transform 了嗎? —— 究極逮蝦戶 著
- AGP Transform API 被廢棄意味著什麼? —— johnsonlee 著
- Transforming dependency artifacts on resolution —— Gradle 官方文件
你的點贊對我意義重大!微信搜尋公眾號 [彭旭銳],希望大家可以一起討論技術,找到志同道合的朋友,我們下次見!
- 測試右移:線上質量監控 ELK 實戰
- ArrayList分析2 :Itr、ListIterator以及SubList中的坑
- Cron表示式(七子表示式)
- [自制作業系統] 第10回 認識保護模式之深入淺出特權級
- gitlab和jenkins做持續整合構建教程
- React技巧之中斷map迴圈
- Java 集合常見知識點&面試題總結(上),2022 最新版!
- 實現一個Prometheus exporter
- 位元組跳動資料平臺技術揭祕:基於 ClickHouse 的複雜查詢實現與優化
- 【Java面試】RDB 和 AOF 的實現原理、優缺點
- HMS Core音訊編輯服務3D音訊技術,助力打造沉浸式聽覺盛宴
- Spring框架系列(9) - Spring AOP實現原理詳解之AOP切面的實現
- 【演算法篇】刷了兩道大廠面試題,含淚 ”重學陣列“
- 2022 開源軟體安全狀況報告:超 41% 的企業對開源安全沒有足夠的信心
- JavaScript中async和await的使用以及佇列問題
- Flex & Bison 開始
- Obsidian基礎教程
- 分享自己平時使用的socket多客戶端通訊的程式碼技術點和軟體使用
- iNeuOS工業網際網路作業系統,增加2154個檢視建模(WEB組態)行業向量圖元、大屏背景及相關圖元
- 多臺雲伺服器的 Kubernetes 叢集搭建