其實 Gradle Transform 就是個紙老虎 —— Gradle 系列(4)

請點贊加關(guān)注醒串,你的支持對我非常重要杭朱,滿足下我的虛榮心。
?? Hi将鸵,我是小彭丸升。本文已收錄到 GitHub · Android-NoteBook 中铆农。這里有 Android 進階成長知識體系,有志同道合的朋友狡耻,歡迎跟著我一起成長墩剖。(聯(lián)系方式在 GitHub)

前言

目前,使用 AGP Transform API 進行字節(jié)碼插樁已經(jīng)非常普遍了夷狰,例如 Booster岭皂、神策等框架中都有 Transform 的影子。Transform 聽起來很高大上孵淘,其本質(zhì)就是一個 Gradle Task蒲障。在這篇文章里歹篓,我將帶你理解 Transform 的工作機制瘫证、使用方法和核心源碼解析,并通過一個 Demo 幫助你融會貫通庄撮。

這篇文章是全面掌握 Gradle 構(gòu)建系統(tǒng)系列的第 4 篇:


1. 認識 Transform

1.1 什么是 Transform?

Transform API 是 Android Gradle Plugin 1.5 就引入的特性烙如,主要用于在 Android 構(gòu)建過程中么抗,在 Class→Dex 這個節(jié)點修改 Class 字節(jié)碼。利用 Transform API亚铁,我們可以拿到所有參與構(gòu)建的 Class 文件蝇刀,借助 Javassist 或 ASM 等字節(jié)碼編輯工具進行修改,插入自定義邏輯徘溢。一般來說吞琐,這些自定義邏輯是與業(yè)務(wù)邏輯無關(guān)的。

使用 Transform 的常見的應(yīng)用場景有:

  • 埋點統(tǒng)計: 在頁面展現(xiàn)和退出等生命周期中插入埋點統(tǒng)計代碼然爆,以統(tǒng)計頁面展現(xiàn)數(shù)據(jù)站粟;
  • 耗時監(jiān)控: 在指定方法的前后插入耗時計算,以觀察方法執(zhí)行時間曾雕;
  • 方法替換: 將方法調(diào)用替換為調(diào)用另一個方法奴烙。

1.2 Transform 的基本原理

先大概了解下 Transform 的工作機制:

  • 1、工作時機: Transform 工作在 Android 構(gòu)建中由 Class → Dex 的節(jié)點;
  • 2缸沃、處理對象: 處理對象包括 Javac 編譯后的 Class 文件恰起、Java 標準 resource 資源、本地依賴和遠程依賴的 JAR/AAR趾牧。Android 資源文件不屬于 Transform 的操作范圍检盼,因為它們不是字節(jié)碼;
  • 3翘单、Transform Task: 每個 Transform 都對應(yīng)一個 Task吨枉,Transform 的輸入和輸出可以理解為對應(yīng) Transform Task 的輸入輸出。每個 TransformTask 的輸出都分別存儲在 app/build/intermediates/transform/[Transform Name]/[Variant] 文件夾中哄芜;
  • 4貌亭、Transform 鏈: TaskManager 會將每個 TransformTask 串聯(lián)起來,前一個 Transform 的輸出會作為下一個 Transform 的輸入认臊。

1.3 Transform 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 不執(zhí)行 Transform
    public boolean applyToVariant(VariantInfo variant) {
        return true;
    }

    // 指定輸入內(nèi)容類型
    public abstract Set<ContentType> getInputTypes();

    // 指定輸出內(nèi)容類型书在,默認取 getInputTypes() 的值
    public Set<ContentType> getOutputTypes() {
        return getInputTypes();
    }

    // 指定消費型輸入內(nèi)容范疇
    public abstract Set<? super Scope> getScopes();

    // 指定引用型輸入內(nèi)容范疇
    public Set<? super Scope> getReferencedScopes() {
        return ImmutableSet.of();
    }

    // 指定是否支持增量編譯
    public abstract boolean isIncremental();

    // 核心 API
    public void transform(TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        // 分發(fā)到過時 API,以兼容舊版本的 Transform
        //noinspection deprecation
        transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                transformInvocation.getOutputProvider(),
                transformInvocation.isIncremental());
    }

    // 指定是否支持緩存
    public boolean isCacheable() {
        return false;
    }
}

1.4 ContentType 內(nèi)容類型

ContentType 是一個枚舉類接口拆又,表示輸入或輸出內(nèi)容的類型儒旬,在 AGP 中定義了 DefaultContentTypeExtendedContentType 兩個枚舉類。但是帖族,我們在自定義 Transform 時只能使用 DefaultContentType 中定義的枚舉栈源,即 CLASSESRESOURCES 兩種類型,其它類型僅供 AGP 內(nèi)置的 Transform 使用竖般。

自定義 Transform 需要在兩個位置定義內(nèi)容類型:

  • 1甚垦、Set<ContentType> getInputTypes(): 指定輸入內(nèi)容類型,允許通過 Set 集合設(shè)置輸入多種類型捻激;
  • 2制轰、Set<ContentType> getOutputTypes(): 指定輸出內(nèi)容類型,默認取 getInputTypes() 的值胞谭,允許通過 Set 集合設(shè)置輸出多種類型垃杖。

ExtendedContentType.java

// 加強類型,自定義 Transform 無法使用
public enum ExtendedContentType implements ContentType {

    // DEX 文件
    DEX(0x1000),

    // Native 庫
    NATIVE_LIBS(0x2000),

    // Instant Run 加強類
    CLASSES_ENHANCED(0x4000),

    // Data Binding 中間產(chǎn)物
    DATA_BINDING(0x10000),

    // Dex Archive
    DEX_ARCHIVE(0x40000),
    ;
}

QualifiedContent.java

enum DefaultContentType implements ContentType {

    // Java 字節(jié)碼丈屹,包括 Jar 文件和由源碼編譯產(chǎn)生的
    CLASSES(0x01),

    // Java 資源
    RESOURCES(0x02);
}

在 TransformManager 中调俘,預(yù)定義了一部分內(nèi)容類型集合伶棒,常用的是 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 也是一個枚舉類接口彩库,表示輸入內(nèi)容的范疇肤无。在 AGP 中定義了 InternalScopeScope 兩個枚舉類。但是骇钦,我們在自定義 Transform 只能使用 Scope 中定義的枚舉宛渐,其它類型僅供 AGP 內(nèi)置的 Transform 使用。

Transform 需要在兩個位置定義輸入內(nèi)容范圍:

  • 1眯搭、Set<ScopeType> getScopes() 消費型輸入內(nèi)容范疇: 此范圍的內(nèi)容會被消費窥翩,因此當前 Transform 必須將修改后的內(nèi)容復(fù)制到 Transform 的中間目錄中,否則無法將內(nèi)容傳遞到下一個 Transform 處理鳞仙;
  • 2寇蚊、Set<ScopeType> getReferencedScopes() 指定引用型輸入內(nèi)容范疇: 默認是空集合,此范圍的內(nèi)容不會被消費棍好,因此不需要復(fù)制傳遞到下一個 Transform仗岸,也不允許修改。

InternalScope.java

// 內(nèi)部使用的作用域借笙,自定義 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 中,預(yù)定義了一部分作用域集合提澎,常用的是 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() 是實現(xiàn) Transform 的核心方法,方法的參數(shù)是 TransformInvocation掂墓,它提供了所有與輸入輸出相關(guān)的信息:

public interface TransformInvocation {

    Context getContext();

    // 消費型輸入內(nèi)容
    Collection<TransformInput> getInputs();

    // 引用型輸入內(nèi)容
    Collection<TransformInput> getReferencedInputs();

    // 額外輸入內(nèi)容
    Collection<SecondaryInput> getSecondaryInputs();

    // 輸出信息
    TransformOutputProvider getOutputProvider();

    // 是否增量構(gòu)建
    boolean isIncremental();
}
  • isIncremental(): 當前 Transform 任務(wù)是否增量構(gòu)建谦纱;
  • getInputs(): 獲取 TransformInput 對象,它是消費型輸入內(nèi)容君编,對應(yīng)于 Transform#getScopes() 定義的范圍跨嘉;
  • getReferencedInputs(): 獲取 TransformInput 對象,它是引用型輸入內(nèi)容吃嘿,對應(yīng)于 Transform#getReferenceScope() 定義的內(nèi)容范圍祠乃;
  • getOutPutProvider(): TransformOutputProvider 是對輸出文件的抽象。

輸入內(nèi)容 TransformInput 由兩部分組成:

  • DirectoryInput 集合: 以源碼方式參與構(gòu)建的輸入文件兑燥,包括完整的源碼目錄結(jié)構(gòu)及其中的源碼文件亮瓷;
  • JarInput 集合: 以 Jar 和 aar 依賴方式參與構(gòu)建的輸入文件,包含本地依賴和遠程依賴降瞳。

輸入內(nèi)容信息 TransformOutputProvider 有兩個功能:

  • deleteAll(): 當 Transform 運行在非增量構(gòu)建模式時嘱支,需要刪除上一次構(gòu)建產(chǎn)生的所有中間文件蚓胸,可以直接調(diào)用 deleteAll() 完成;
  • getContentLocation(): 獲得指定范圍+類型的輸出目標路徑除师。

TransformOutputProvider.java

public interface TransformOutputProvider {

    // 刪除所有中間文件
    void deleteAll()

    // 獲取指定范圍+類型的目標路徑
    File getContentLocation(String name,
    Set<QualifiedContent.ContentType> types,
    Set<? super QualifiedContent.Scope> scopes,
    Format format);
}

獲取輸入內(nèi)容對應(yīng)的輸出路徑:

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 增量模式

任何構(gòu)建系統(tǒng)都會盡量避免重復(fù)執(zhí)行相同工作沛膳,Transform 也不例外。雖然增量構(gòu)建并不是必須的汛聚,但作為一個合格的 Transform 實現(xiàn)應(yīng)該具備增量能力锹安。

1、增量模式標記位: Transform API 有兩個增量標志位倚舀,不要混淆:

  • Transform#isIncremental(): Transform 增量構(gòu)建的使能開關(guān)八毯,返回 true 才有可能觸發(fā)增量構(gòu)建;
  • TransformInvocation#isIncremental(): 當次 TransformTask 是否增量執(zhí)行瞄桨,返回 true 表示正在增量模式话速。

2、Task 增量模式與 Transform 增量模式的區(qū)別: Task 增量模式與 Transform 增量模式的區(qū)別在于芯侥,Task 增量執(zhí)行時會跳過整個 Task 的動作列表泊交,而 Transform 增量執(zhí)行依然會執(zhí)行 TransformTask,但輸入內(nèi)容會增加變更內(nèi)容信息柱查。

3廓俭、增量模式的輸入: 增量模式下的所有輸入都是帶狀態(tài)的,需要根據(jù)這些狀態(tài)來做不同的處理唉工,不需要每次所有流程都重新來一遍研乒。比如新增的輸入就需要處理,而未修改的輸入就不需要處理淋硝。Transform 定義了四個輸入文件狀態(tài):

com.android.build.api.transform.Status.java

public enum Status {

    // 未修改雹熬,不需要處理,也不需要復(fù)制操作
    NOTCHANGED,
    
    // 新增谣膳,正常處理并復(fù)制給下一個任務(wù)
    ADDED,
    
    // 已修改竿报,正常處理并復(fù)制給下一個任務(wù)
    CHANGED,
        
    // 已刪除,需同步移除 OutputProvider 指定的目標文件
    REMOVED;
}

1.8 注冊 Transform

在 BaseExtension 中維護了一個 Transform 列表继谚,自定義 Transform 需要注冊才能生效烈菌,而且還支持額外設(shè)置 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() 來決定是否執(zhí)行 Transform诡壁。


2. Transform 核心源碼分析

這一節(jié)我們來分析 Transform 相關(guān)核心源碼济瓢,這里我們引用的是 Android Gradle Plugin 7.1.0 版本的源碼。

2.1 Transform 與 Task 的關(guān)系

Project 的構(gòu)建邏輯由一系列 Task 的組成欢峰,每個 Task 負責完成一個基本的工作葬荷,例如 Javac 編譯 Task涨共。Transform 也是依靠 Task 執(zhí)行的,在配置階段宠漩,Gradle 會為注冊的 Transform 創(chuàng)建對應(yīng)的 Task举反。

提示: 說 “創(chuàng)建” 可能不太嚴謹,TransformManager 使用 register 懶創(chuàng)建的方式注冊 Task扒吁,其實還沒有創(chuàng)建 Task 實例火鼻。我們不要復(fù)雜化了,就說創(chuàng)建吧雕崩。

而 Task 的依賴關(guān)系是通過 TransformTask 的輸入輸出關(guān)系隱式確定的魁索,TransformManager 通過 TransformStream 鏈接各個 TransformTask 的輸入輸出,進而控制 Transform 的依賴關(guān)系順序盼铁。

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節(jié)提到的額外依賴)
                    // 在注冊 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));
    ...
}

網(wǎng)上很多朋友提到 “自定義 Transform 的執(zhí)行時機早于系統(tǒng)內(nèi)置 Transform”,但從 AGP 7.1.0 源碼看饶火,并不存在系統(tǒng) Transform鹏控。猜測是新版本 AGP 將這部分 “系統(tǒng)內(nèi)置 Transform” 修改為由 Task 直接實現(xiàn),畢竟 從 AGP 7.0 開始 Transform 標記為過時了肤寝。

2.2 Transform 的創(chuàng)建過程

  • 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、創(chuàng)建 TransformTask 的執(zhí)行鏈: TransformTask 屬于 Android 構(gòu)建構(gòu)成的一部分义桂,所有 Android Task 的創(chuàng)建入口都從 BasePlugin#createAndroidTasks() 開始找筝。其中會為所有 Variant 變體創(chuàng)建相關(guān)的 Task,經(jīng)過一系列調(diào)用后澡刹,會通過抽象方法 TaskManager#doCreateTaskForVariant() 分派到 ApplicationTaskManager 和 LibraryTaskManager 兩個子類中呻征,以區(qū)分 App 模塊和 Library 模塊耘婚。

調(diào)用鏈概要:

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() 會用于構(gòu)造 Task Name罢浇,命名格式為 transform[InputTypes]With[name]For[Configuration],例如 transformClassed沐祷。這塊源碼體現(xiàn)在 TransformManager 中創(chuàng)建 Task 的位置:

TransformManager.java

// 創(chuàng)建 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 關(guān)聯(lián)到 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 執(zhí)行 transform() 方法

每個 Task 內(nèi)部都保持了一個 Action 列表 actions,執(zhí)行 Task 就是按順序執(zhí)行這個列表赖临,對于自定義 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 模板

上一節(jié)我們探討了 Transform 的基本工作機制兢榨,第 3 節(jié)和第 4 節(jié)我們來實現(xiàn)一個 Transform Demo嗅榕。Transform 的核心代碼在 transform() 方法中顺饮,我們要做的就是遍歷輸入文件,再把修改后的文件復(fù)制到目標路徑中凌那,對于 JarInputs 還有一次解壓和壓縮兼雄。更進一步,再考慮增量編譯的情況帽蝶。

因此赦肋,整個 Transform 的核心過程是有固定套路,模板流程圖如下:

—— 圖片引用自 https://rebooters.github.io/2020/01/04/Gradle-Transform-ASM-探索/

我們把整個流程圖做成一個抽象模板類励稳,子類需要重寫 provideFunction() 方法佃乘,從輸入流讀取 Class 文件,修改完字節(jié)碼后再寫入到輸出流驹尼。甚至不需要考慮 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 示例

現(xiàn)在,我手把手帶你基于 BaseCustomTransform 實現(xiàn)一個 Transform Demo料祠。示例代碼我已經(jīng)上傳到 Github · DemoHall · HelloTransform骆捧。有用請給個免費的 Star 支持下。

Demo 效果很簡單:

  • 實現(xiàn)一個 Transform髓绽,在編譯時在 Activity#onCreate() 方法末尾織入一個 Toast 語句敛苇;
  • 僅通過自定義注解 @Hello 修飾的 Activity#onCreate() 方法會生效。

4.1 步驟 1:初始化代碼框架

首先顺呕,我們先搭建工程的整體框架枫攀,再來編寫核心的 Transform 邏輯。我們選擇自定義 Gradle 插件來承載 Transform 的邏輯株茶,可維護性更好来涨。關(guān)于自定義 Gradle 插件的步驟具體見上一篇文章《手把手帶你自定義 Gradle 插件》,此處不展開启盛。

提示: 提醒一下蹦掐,并不是說一定要由 Gradle 插件來承載,你直接在 .gradle 文件中實現(xiàn)也是 OK 的僵闯。

插件實現(xiàn)類如下:

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 模板類

將我們實現(xiàn)的 BaseCustomTransform 模板類復(fù)制到工程下,再實現(xiàn)一個子類:

ToastTransform.kt

internal class ToastTransform(val project: Project) : BaseCustomTransform(true) {

    // Transform 名
    override fun getName() = "ToastTransform"

    // 是否支持增量構(gòu)建
    override fun isIncremental() = true

    /**
     * 用于過濾 Variant鳖粟,返回 false 表示該 Variant 不執(zhí)行 Transform
     */
    @Incubating
    override fun applyToVariant(variant: VariantInfo?): Boolean {
        return "debug" == variant?.buildTypeName
    }

    // 指定輸入內(nèi)容類型
    override fun getInputTypes() = TransformManager.CONTENT_CLASS

    // 指定消費型輸入內(nèi)容范疇
    override fun getScopes() = TransformManager.SCOPE_FULL_PROJECT

    // 轉(zhuǎn)換方法
    override fun provideFunction() = { ios: InputStream, zos: OutputStream ->
        input.copyTo(output)
    }
}

其中社裆,provideFunction() 是模板代碼,參數(shù)分別表示源 Class 文件的輸入流和目標 Class 文件輸出流向图。子類要做的事泳秀,就是從輸入流讀取 Class 信息标沪,修改后寫入到輸出流。

4.3 步驟 3:使用 Javassist 修改字節(jié)碼

使用 Javassist API 從輸入流加載數(shù)據(jù)嗜傅,在匹配到 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:應(yīng)用插件

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 已經(jīng)被廢棄了拍冠。是的尿这,就是卷,而且這次直接是降維打擊庆杜。以前 Transform 是 AGP 的特性射众,現(xiàn)在 Gradle 也來整 Transform,不過換了個名字晃财,叫 —— TransformAction叨橱。

那么,我們還有必要學 AGP Transform API 嗎断盛?如果你現(xiàn)在涉足字節(jié)碼插樁這塊罗洗,你建議你還是學以下:

  • 1、社區(qū)沉淀: AGP Transform API 發(fā)展多年钢猛,目前社區(qū)中已經(jīng)沉淀下非常多優(yōu)秀的開源組件和博客伙菜,這些資源對你非常有幫助。而 TransformAction 的社區(qū)沉淀還非常單泵酢贩绕;
  • 2、技術(shù)思維: 雖然換了一套 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. 總結(jié)

本文的示例代碼已上傳到 https://github.com/pengxurui/DemoHall迂尝,請 Star 支持。關(guān)注我剪芥,帶你了解更多垄开,我們下次見。

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市益兄,隨后出現(xiàn)的幾起案子锻梳,更是在濱河造成了極大的恐慌,老刑警劉巖净捅,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件疑枯,死亡現(xiàn)場離奇詭異,居然都是意外死亡蛔六,警方通過查閱死者的電腦和手機荆永,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來国章,“玉大人具钥,你說我怎么就攤上這事∫菏蓿” “怎么了氓拼?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抵碟。 經(jīng)常有香客問我桃漾,道長,這世上最難降的妖魔是什么拟逮? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任撬统,我火速辦了婚禮,結(jié)果婚禮上敦迄,老公的妹妹穿的比我還像新娘恋追。我一直安慰自己,他們只是感情好罚屋,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布苦囱。 她就那樣靜靜地躺著,像睡著了一般脾猛。 火紅的嫁衣襯著肌膚如雪撕彤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機與錄音羹铅,去河邊找鬼蚀狰。 笑死,一個胖子當著我的面吹牛职员,可吹牛的內(nèi)容都是我干的麻蹋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼焊切,長吁一口氣:“原來是場噩夢啊……” “哼扮授!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起专肪,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤刹勃,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后牵祟,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體深夯,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年诺苹,在試婚紗的時候發(fā)現(xiàn)自己被綠了咕晋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡收奔,死狀恐怖掌呜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情坪哄,我是刑警寧澤质蕉,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站翩肌,受9級特大地震影響模暗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜念祭,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一兑宇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧粱坤,春花似錦隶糕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至株旷,卻和暖如春再登,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工霎冯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留铃拇,地道東北人钞瀑。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓沈撞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親雕什。 傳聞我的和親對象是個殘疾皇子缠俺,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容