深入理解Transform

toothpickTina發表於2019-04-24

前言

其實Transform API在一個android工程的打包流程中作用非常大, 像是我們熟知的混淆處理, 類檔案轉dex檔案的處理, 都是通過Transform API去完成的. 本篇內容主要圍繞Transform做展開:

  1. Transform API的使用及原理
  2. 位元組碼處理框架ASM使用技巧
  3. Transform API在應用工程上的使用摸索

Transform的使用及原理

什麼是Transform

自從1.5.0-beta1版本開始, android gradle外掛就包含了一個Transform API, 它允許第三方外掛在編譯後的類檔案轉換為dex檔案之前做處理操作. 而使用Transform API, 我們完全可以不用去關注相關task的生成與執行流程, 它讓我們可以只聚焦在如何對輸入的類檔案進行處理

Transform的使用

Transform的註冊和使用非常易懂, 在我們自定義的plugin內, 我們可以通過android.registerTransform(theTransform)或者android.registerTransform(theTransform, dependencies).就可以進行註冊.

class DemoPlugin: Plugin<Project> {
    override fun apply(target: Project) {
        val android = target.extensions.findByType(BaseExtension::class.java)
        android?.registerTransform(DemoTransform())
    }
}
複製程式碼

而我們自定義的Transform繼承於com.android.build.api.transform.Transform, 具體我們可以看javaDoc, 以下程式碼是比較常見的transform處理模板

class DemoTransform: Transform() {
    /**
     * transform 名字
     */
    override fun getName(): String = "DemoTransform"

    /**
     * 輸入檔案的型別
     * 可供我們去處理的有兩種型別, 分別是編譯後的java程式碼, 以及資原始檔(非res下檔案, 而是assests內的資源)
     */
    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> = TransformManager.CONTENT_CLASS

    /**
     * 是否支援增量
     * 如果支援增量執行, 則變化輸入內容可能包含 修改/刪除/新增 檔案的列表
     */
    override fun isIncremental(): Boolean = false

    /**
     * 指定作用範圍
     */
    override fun getScopes(): MutableSet<in QualifiedContent.Scope> = TransformManager.SCOPE_FULL_PROJECT

    /**
     * transform的執行主函式
     */
    override fun transform(transformInvocation: TransformInvocation?) {
      transformInvocation?.inputs?.forEach {
          // 輸入源為資料夾型別
          it.directoryInputs.forEach {directoryInput->
              with(directoryInput){
                  // TODO 針對資料夾進行位元組碼操作
                  val dest = transformInvocation.outputProvider.getContentLocation(
                      name,
                      contentTypes,
                      scopes,
                      Format.DIRECTORY
                  )
                  file.copyTo(dest)
              }
          }

          // 輸入源為jar包型別
          it.jarInputs.forEach { jarInput->
              with(jarInput){
                  // TODO 針對Jar檔案進行相關處理
                  val dest = transformInvocation.outputProvider.getContentLocation(
                      name,
                      contentTypes,
                      scopes,
                      Format.JAR
                  )
                  file.copyTo(dest)
              }
          }
      }
    }
}
複製程式碼

每一個Transform都宣告它的作用域, 作用物件以及具體的操作以及操作後輸出的內容.

作用域

通過Transform#getScopes方法我們可以宣告自定義的transform的作用域, 指定作用域包括如下幾種

QualifiedContent.Scope
EXTERNAL_LIBRARIES 只包含外部庫
PROJECT 只作用於project本身內容
PROVIDED_ONLY 支援compileOnly的遠端依賴
SUB_PROJECTS 子模組內容
TESTED_CODE 當前變體測試的程式碼以及包括測試的依賴項

作用物件

通過Transform#getInputTypes我們可以宣告其的作用物件, 我們可以指定的作用物件只包括兩種

QualifiedContent.ContentType
CLASSES Java程式碼編譯後的內容, 包括資料夾以及Jar包內的編譯後的類檔案
RESOURCES 基於資源獲取到的內容

TransformManager整合了部分常用的Scope以及Content集合, 如果是application註冊的transform, 通常情況下, 我們一般指定TransformManager.SCOPE_FULL_PROJECT;如果是library註冊的transform, 我們只能指定TransformManager.PROJECT_ONLY , 我們可以在LibraryTaskManager#createTasksForVariantScope中看到相關的限制報錯程式碼

            Sets.SetView<? super Scope> difference =
                    Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
            if (!difference.isEmpty()) {
                String scopes = difference.toString();
                globalScope
                        .getAndroidBuilder()
                        .getIssueReporter()
                        .reportError(
                                Type.GENERIC,
                                new EvalIssueException(
                                        String.format(
                                                "Transforms with scopes '%s' cannot be applied to library projects.",
                                                scopes)));
            }
複製程式碼

而作用物件我們主要常用到的是TransformManager.CONTENT_CLASS

TransformInvocation

我們通過實現Transform#transform方法來處理我們的中間轉換過程, 而中間相關資訊都是通過TransformInvocation物件來傳遞

public interface TransformInvocation {

    /**
     * transform的上下文
     */
    @NonNull
    Context getContext();

    /**
     * 返回transform的輸入源
     */
    @NonNull
    Collection<TransformInput> getInputs();

    /**
     * 返回引用型輸入源
     */
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
     * 額外輸入源
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    /**
     * 輸出源
     */
    @Nullable
    TransformOutputProvider getOutputProvider();


    /**
     * 是否增量
     */
    boolean isIncremental();
}
複製程式碼

關於輸入源, 我們可以大致分為消費型和引用型和額外的輸入源

  1. 消費型就是我們需要進行transform操作的, 這類物件在處理後我們必須指定輸出傳給下一級, 我們主要通過getInputs()獲取進行消費的輸入源, 而在進行變換後, 我們也必須通過設定getInputTypes()getScopes()來指定輸出源傳輸給下個transform.
  2. 引用型輸入源是指我們不進行transform操作, 但可能存在檢視時候使用, 所以這類我們也不需要輸出給下一級, 在通過覆寫getReferencedScopes()指定我們的引用型輸入源的作用域後, 我們可以通過TransformInvocation#getReferencedInputs()獲取引用型輸入源
  3. 另外我們還可以額外定義另外的輸入源供下一級使用, 正常開發中我們很少用到, 不過像是ProGuardTransform中, 就會指定建立mapping.txt傳給下一級; 同樣像是DexMergerTransform, 如果開啟了multiDex功能, 則會將maindexlist.txt檔案傳給下一級

Transform的原理

Transform的執行鏈

我們已經大致瞭解它是如何使用的, 現在看下他的原理(本篇原始碼基於gradle外掛3.3.2版本)在去年AppPlugin原始碼解析中, 我們粗略瞭解了android的com.android.application以及com.android.library兩個外掛都繼承於BasePlugin, 而他們的主要執行順序可以分為三個步驟

  1. project的配置
  2. extension的配置
  3. task的建立

BaseExtension內部維護了一個transforms集合物件, android.registerTransform(theTransform)實際上就是將我們自定義的transform例項新增到這個列表物件中. 在3.3.2的原始碼中, 也可以這樣理解. 在BasePlugin#createAndroidTasks中, 我們通過VariantManager#createAndroidTasks建立各個變體的相關編譯任務, 最終通過TaskManager#createTasksForVariantScope(application外掛最終實現方法在TaskManager#createPostCompilationTasks中, 而library外掛最終實現方法在LibraryTaskManager#createTasksForVariantScope中)方法中獲取BaseExtension中維護的transforms物件, 通過TransformManager#addTransform將對應的transform物件轉換為task, 註冊在TaskFactory中.這裡關於一系列Transform Task的執行流程, 我們可以選擇看下application內的相關transform流程, 由於篇幅原因, 可以自行去看相關原始碼, 這裡的transform task流程分別是從Desugar->MergeJavaRes->自定義的transform->MergeClasses->Shrinker(包括ResourcesShrinker和DexSplitter和Proguard)->MultiDex->BundleMultiDex->Dex->ResourcesShrinker->DexSplitter, 由此呼叫鏈, 我們也可以看出在處理類檔案的時候, 是不需要去考慮混淆的處理的.

TransformManager

TransformManager管理了專案對應變體的所有Transform物件, 它的內部維護了一個TransformStream集合物件streams, 每當新增一個transform, 對應的transform會消費掉對應的流, 而後將處理後的流新增會streams

public class TransformManager extends FilterableStreamCollection{
    private final List<TransformStream> streams = Lists.newArrayList();
}
複製程式碼

我們可以看下它的核心方法addTransform

@NonNull
    public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
            @NonNull TaskFactory taskFactory,
            @NonNull TransformVariantScope scope,
            @NonNull T transform,
            @Nullable PreConfigAction preConfigAction,
            @Nullable TaskConfigAction<TransformTask> configAction,
            @Nullable TaskProviderCallback<TransformTask> providerCallback) {

        ...

        List<TransformStream> inputStreams = Lists.newArrayList();
        // transform task的命名規則定義
        String taskName = scope.getTaskName(getTaskNamePrefix(transform));

        // 獲取引用型流
        List<TransformStream> referencedStreams = grabReferencedStreams(transform);

        // 找到輸入流, 並計算通過transform的輸出流
        IntermediateStream outputStream = findTransformStreams(
                transform,
                scope,
                inputStreams,
                taskName,
                scope.getGlobalScope().getBuildDir());

        // 省略程式碼是用來校驗輸入流和引用流是否為空, 理論上不可能為空, 如果為空, 則說明中間有個transform的轉換處理有問題
        ...

        transforms.add(transform);

        // transform task的建立
        return Optional.of(
                taskFactory.register(
                        new TransformTask.CreationAction<>(
                                scope.getFullVariantName(),
                                taskName,
                                transform,
                                inputStreams,
                                referencedStreams,
                                outputStream,
                                recorder),
                        preConfigAction,
                        configAction,
                        providerCallback));
    }
複製程式碼

TransformManager中新增一個Transform管理, 流程可分為以下幾步

  1. 定義transform task名
static String getTaskNamePrefix(@NonNull 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();
    }
複製程式碼

從上面程式碼, 我們可以看到新建的transform task的命名規則可以理解為transform${inputType1.name}And${inputType2.name}With${transform.name}For${variantName}, 對應的我們也可以通過已生成的transform task來驗證

深入理解Transform
2. 通過transform內部定義的引用型輸入的作用域(SCOPE)和作用型別(InputTypes), 通過求取與streams作用域和作用型別的交集來獲取對應的流, 將其定義為我們需要的引用型流

private List<TransformStream> grabReferencedStreams(@NonNull Transform transform) {
        Set<? super Scope> requestedScopes = transform.getReferencedScopes();
        ...

        List<TransformStream> streamMatches = Lists.newArrayListWithExpectedSize(streams.size());

        Set<ContentType> requestedTypes = transform.getInputTypes();
        for (TransformStream stream : streams) {
            Set<ContentType> availableTypes = stream.getContentTypes();
            Set<? super Scope> availableScopes = stream.getScopes();

            Set<ContentType> commonTypes = Sets.intersection(requestedTypes,
                    availableTypes);
            Set<? super Scope> commonScopes = Sets.intersection(requestedScopes, availableScopes);

            if (!commonTypes.isEmpty() && !commonScopes.isEmpty()) {
                streamMatches.add(stream);
            }
        }

        return streamMatches;
    }
複製程式碼
  1. 根據transform內定義的SCOPE和INPUT_TYPE, 獲取對應的消費型輸入流, 在streams內移除掉這一部分消費性的輸入流, 保留無法匹配SCOPE和INPUT_TYPE的流; 構建新的輸出流, 並加到streams中做管理
private IntermediateStream findTransformStreams(
            @NonNull Transform transform,
            @NonNull TransformVariantScope scope,
            @NonNull List<TransformStream> inputStreams,
            @NonNull String taskName,
            @NonNull File buildDir) {

        Set<? super Scope> requestedScopes = transform.getScopes();
        ...

        Set<ContentType> requestedTypes = transform.getInputTypes();
        // 獲取消費型輸入流
        // 並將streams中移除對應的消費型輸入流
        consumeStreams(requestedScopes, requestedTypes, inputStreams);

        // 建立輸出流
        Set<ContentType> outputTypes = transform.getOutputTypes();
        // 建立輸出流轉換的檔案相關路徑
        File outRootFolder =
                FileUtils.join(
                        buildDir,
                        StringHelper.toStrings(
                                AndroidProject.FD_INTERMEDIATES,
                                FD_TRANSFORMS,
                                transform.getName(),
                                scope.getDirectorySegments()));

        // 輸出流的建立
        IntermediateStream outputStream =
                IntermediateStream.builder(
                                project,
                                transform.getName() + "-" + scope.getFullVariantName(),
                                taskName)
                        .addContentTypes(outputTypes)
                        .addScopes(requestedScopes)
                        .setRootLocation(outRootFolder)
                        .build();
        streams.add(outputStream);

        return outputStream;
    }
複製程式碼
  1. 最後, 建立TransformTask, 註冊到TaskManager中

TransformTask

如何觸發到我們實現的Transform#transform方法, 就在TransformTask對應的TaskAction中執行

void transform(final IncrementalTaskInputs incrementalTaskInputs)
            throws IOException, TransformException, InterruptedException {

        final ReferenceHolder<List<TransformInput>> consumedInputs = ReferenceHolder.empty();
        final ReferenceHolder<List<TransformInput>> referencedInputs = ReferenceHolder.empty();
        final ReferenceHolder<Boolean> isIncremental = ReferenceHolder.empty();
        final ReferenceHolder<Collection<SecondaryInput>> changedSecondaryInputs =
                ReferenceHolder.empty();

        isIncremental.setValue(transform.isIncremental() && incrementalTaskInputs.isIncremental());

        GradleTransformExecution preExecutionInfo =
                GradleTransformExecution.newBuilder()
                        .setType(AnalyticsUtil.getTransformType(transform.getClass()).getNumber())
                        .setIsIncremental(isIncremental.getValue())
                        .build();

        // 一些增量模式下的處理, 包括在增量模式下, 判斷輸入流(引用型和消費型)的變化
        ...

        GradleTransformExecution executionInfo =
                preExecutionInfo.toBuilder().setIsIncremental(isIncremental.getValue()).build();

        ...
        transform.transform(
                                new TransformInvocationBuilder(TransformTask.this)
                                        .addInputs(consumedInputs.getValue())
                                        .addReferencedInputs(referencedInputs.getValue())
                                        .addSecondaryInputs(changedSecondaryInputs.getValue())
                                        .addOutputProvider(
                                                outputStream != null
                                                        ? outputStream.asOutput(
                                                                isIncremental.getValue())
                                                        : null)
                                        .setIncrementalMode(isIncremental.getValue())
                                        .build());

                        if (outputStream != null) {
                            outputStream.save();
                        }
    }
複製程式碼

通過上文的介紹, 我們現在應該知道了自定義的Transform執行的時序, 位置, 以及相關原理. 那麼, 我們現在已經拿到了編譯後的所有位元組碼, 我們要怎麼去處理呢? 我們可以瞭解下ASM

ASM的使用

想要處理位元組碼, 常見的框架有AspectJ, Javasist, ASM. 關於框架的選型網上相關的文章還是比較多的, 從處理速度以及記憶體佔用率上, ASM明顯優於其他兩個框架.本篇主要著眼於ASM的使用.

什麼是ASM

ASM是一個通用的Java位元組碼操作和分析框架。它可以用於修改現有類或直接以二進位制形式動態生成類. ASM提供了一些常見的位元組碼轉換和分析演算法,可以從中構建自定義複雜轉換和程式碼分析工具. ASM庫提供了兩個用於生成和轉換編譯類的API:Core API提供基於事件的類表示,而Tree API提供基於物件的表示。由於基於事件的API(Core API)不需要在記憶體中儲存一個表示該類的物件數, 所以從執行速度和記憶體佔用上來說, 它比基於物件的API(Tree API)更優.然後從使用場景上來說, 基於事件的API使用會比基於物件的API使用更為困難, 譬如當我們需要針對某個物件進行調整的時候.由於一個類只能被一種API管理, 所以我們應該要區分場景選取使用對應的API

ASM外掛

ASM的使用需要一定的學習成本, 我們可以通過使用ASM Bytecode Outline外掛輔助瞭解, 對應外掛在AS中的外掛瀏覽器就可以找到

深入理解Transform
唯一的遺憾在於它無法轉換kotlin檔案為通過ASM建立的類檔案 然後我們就可以通過開啟一份java未編譯檔案, 通過右鍵選擇Show Bytecode Outline轉為對應的位元組碼, 並可以看到對應的通過ASM建立的類格式
深入理解Transform
譬如我們新建了一個類, 可以通過asm外掛得到通過core api生成的對應方法.

@RouteModule
public class ASMTest {

}

複製程式碼

深入理解Transform

Transform API在應用工程方面的摸索使用

元件通訊中的作用

Transform API在元件化工程中有很多應用方向, 目前我們專案中在自開發的路由框架中, 通過其去做了模組的自動化靜態註冊, 同時考慮到路由通過協議文件維護的不確定性(頁面路由地址的維護不及時導致對應開發無法及時更新對應程式碼), 我們做了路由的常量管理, 首先通過掃描整個工程專案程式碼收集路由資訊, 建立符合一定規則的路由原始基礎資訊檔案, 通過variant#registerJavaGeneratingTask註冊 通過對應原始資訊檔案生成對應常量Java檔案下沉在基礎通用元件中的task, 這樣上層依賴於這個基礎元件的專案都可以通過直接呼叫常量來使用路由.在各元件程式碼隔離的情況下, 可以通過由元件aar傳遞原始資訊檔案, 仍然走上面的步驟生成對應的常量表, 而存在的類重複的問題, 通過自定義Transform處理合並

業務監控中的作用

在應用工程中, 我們通常有關於網路監控,應用效能檢測(包括頁面載入時間, 甚至包括各個方法呼叫所耗時間, 可能存在超過閾值需要警告)的需求, 這些需求我們都不可能嵌入在業務程式碼中, 都是可以基於Transform API進行處理. 而針對於埋點, 我們也可以通過Transform實現自動化埋點的功能, 通過ASM CoreASM Tree將盡可能多的欄位資訊形成記錄傳遞, 這裡有些我們專案中已經實現了, 有一些則是我們需要去優化或者去實現的.

其他

關於結合Transform+ASM的使用, 我寫了個一個小Demo, 包括瞭如何處理支援增量功能時的轉換, 如何使用ASM Core ApiASM Tree Api, 做了一定的封裝, 可以參閱

相關參考