深刻理解Transform

前言

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

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

Transform的使用及原理

什麼是Transform

自從1.5.0-beta1版本開始, android gradle插件就包含了一個Transform API, 它容許第三方插件在編譯後的類文件轉換爲dex文件以前作處理操做. 而使用Transform API, 咱們徹底能夠不用去關注相關task的生成與執行流程, 它讓咱們能夠只聚焦在如何對輸入的類文件進行處理java

Transform的使用

Transform的註冊和使用很是易懂, 在咱們自定義的plugin內, 咱們能夠經過android.registerTransform(theTransform)或者android.registerTransform(theTransform, dependencies).就能夠進行註冊.android

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處理模板git

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都聲明它的做用域, 做用對象以及具體的操做以及操做後輸出的內容.github

做用域

經過Transform#getScopes方法咱們能夠聲明自定義的transform的做用域, 指定做用域包括以下幾種算法

QualifiedContent.Scope
EXTERNAL_LIBRARIES 只包含外部庫
PROJECT 只做用於project自己內容
PROVIDED_ONLY 支持compileOnly的遠程依賴
SUB_PROJECTS 子模塊內容
TESTED_CODE 當前變體測試的代碼以及包括測試的依賴項

做用對象

經過Transform#getInputTypes咱們能夠聲明其的做用對象, 咱們能夠指定的做用對象只包括兩種api

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對象來傳遞app

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來驗證

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中的插件瀏覽器就能夠找到

惟一的遺憾在於它沒法轉換kotlin文件爲經過ASM建立的類文件 而後咱們就能夠經過打開一份java未編譯文件, 經過右鍵選擇Show Bytecode Outline轉爲對應的字節碼, 並能夠看到對應的經過ASM建立的類格式
譬如咱們新建了一個類, 能夠經過asm插件獲得經過core api生成的對應方法.

@RouteModule
public class ASMTest {

}

複製代碼

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

組件通訊中的做用

Transform API在組件化工程中有不少應用方向, 目前咱們項目中在自開發的路由框架中, 經過其去作了模塊的自動化靜態註冊, 同時考慮到路由經過協議文檔維護的不肯定性(頁面路由地址的維護不及時致使對應開發沒法及時更新對應代碼), 咱們作了路由的常量管理, 首先經過掃描整個工程項目代碼收集路由信息, 創建符合必定規則的路由原始基礎信息文件, 經過variant#registerJavaGeneratingTask註冊 經過對應原始信息文件生成對應常量Java文件下沉在基礎通用組件中的task, 這樣上層依賴於這個基礎組件的項目均可以經過直接調用常量來使用路由.在各組件代碼隔離的狀況下, 能夠經過由組件aar傳遞原始信息文件, 仍然走上面的步驟生成對應的常量表, 而存在的類重複的問題, 經過自定義Transform處理合並

業務監控中的做用

在應用工程中, 咱們一般有關於網絡監控,應用性能檢測(包括頁面加載時間, 甚至包括各個方法調用所耗時間, 可能存在超過閾值須要警告)的需求, 這些需求咱們都不可能嵌入在業務代碼中, 都是能夠基於Transform API進行處理. 而針對於埋點, 咱們也能夠經過Transform實現自動化埋點的功能, 經過ASM CoreASM Tree將盡量多的字段信息造成記錄傳遞, 這裏有些咱們項目中已經實現了, 有一些則是咱們須要去優化或者去實現的.

其餘

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

相關參考

相關文章
相關標籤/搜索