如何理解 Transform API

概述

Starting with 1.5.0-beta1, the Gradle plugin includes a Transform API allowing 3rd party plugins to manipulate compiled class files before they are converted to dex files.
(The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)

摘自 Android Studio Project Site

Android Gradle 工具在 1.5.0 版本后提供了 Transfrom API, 允許第三方 Plugin 在打包 dex 文件之前的編譯過(guò)程中操作 .class 文件。目前 jarMerge、proguard、multi-dex、Instant-Run 都已經(jīng)換成 Transform 實(shí)現(xiàn)。

分析

從官方的描述中得知:

  1. Transform API 是新引進(jìn)的操作 class 的方式
  2. Transform API 在編譯之后,生成 dex 之前起作用

在翻查文檔以及結(jié)合之前自己實(shí)現(xiàn) Plugin 的經(jīng)驗(yàn),想到的幾個(gè)問(wèn)題:

  1. Transform 是如何拿到 class 文件的?
  2. Transform 與 Gradle Task 之間的關(guān)系?
  3. 為什么 Transform 的作用域在編譯之后, 生成 Dex 之前,Gradle 是如何控制的?
  4. 既然 Instant-Run 使用 Transform 實(shí)現(xiàn),那 Transform 是如何得到變更的內(nèi)容的?
  5. Transform 之間的依賴關(guān)系是怎樣的?

Transform

在解答問(wèn)題之前,先看下 Transform 長(zhǎng)什么樣:

public class TestTransform extends Transform {
    @Override
    public String getName() {
        return null;
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return null;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return null;
    }

    @Override
    public boolean isIncremental() {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
    }
}

name: 給 transform 起個(gè)名字。 這個(gè) name 并不是最終的名字, 在 TransformManager 中會(huì)對(duì)名字再處理:


    static String getTaskNamePrefix(Transform transform) {
        StringBuilder sb = new StringBuilder(100);
        sb.append("transform");
        sb.append((String)transform.getInputTypes().stream().map((inputType) -> {
            return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, inputType.name());
        }).sorted().collect(Collectors.joining("And"))).append("With").append(StringHelper.capitalize(transform.getName())).append("For");
        return sb.toString();
    }

inputTypes: transform 要處理的數(shù)據(jù)類型。

  • CLASSES 表示要處理編譯后的字節(jié)碼,可能是 jar 包也可能是目錄

  • RESOURCES 表示處理標(biāo)準(zhǔn)的 java 資源

scopes:transform 的作用域

type Des
PROJECT 只處理當(dāng)前項(xiàng)目
SUB_PROJECTS 只處理子項(xiàng)目
PROJECT_LOCAL_DEPS 只處理當(dāng)前項(xiàng)目的本地依賴,例如jar, aar
EXTERNAL_LIBRARIES 只處理外部的依賴庫(kù)
PROVIDED_ONLY 只處理本地或遠(yuǎn)程以provided形式引入的依賴庫(kù)
TESTED_CODE 測(cè)試代碼

ContentType 和 Scopes 都返回集合,TransformManager 中封裝了默認(rèn)的幾種集中類型

** isIncremental** : 當(dāng)前 Transform 是否支持增量編譯

Transform 的工作流程

image.png

Transform 將輸入進(jìn)行處理,然后寫(xiě)入到指定的目錄下作為下一個(gè) Transform 的輸入源。

獲取輸出路徑:

destDir = transformInvocation.outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)

案例解讀

Metis 是一個(gè) Android 的 SPI 實(shí)現(xiàn),解決運(yùn)行時(shí)獲取指定的服務(wù)類型。
主要原理是用注解標(biāo)記指定的類型,插件在編譯過(guò)程中掃描所有的 class;對(duì)被注解標(biāo)記過(guò)的類動(dòng)態(tài)生成一個(gè) java 源文件,再將 java 文件編譯之后會(huì)被打包進(jìn) dex; 運(yùn)行時(shí)只要調(diào)用工具類的方法執(zhí)行查詢操作即可。
動(dòng)態(tài)生成的源文件:

final class MetisRegistry {
  private static final Map<Class<?>, HashSet<Class<?>>> sServices = new LinkedHashMap<Class<?>, HashSet<Class<?>>>();

  static {
    register(io.github.yangxiaolei.sub.TestAction.class, io.github.yangxiaolei.sub.TestAction1.class);
    register(io.github.yangxiaolei.sub.TestAction.class, io.github.yangxlei.TestAction3.class);
    register(io.github.yangxlei.TestAction3.class, io.github.yangxlei.MainActivity.class);
  }

  static final Set<Class<?>> get(Class<?> key) {
    Set<Class<?>> result = sServices.get(key);
    return null == result ? Collections.<Class<?>>emptySet() : Collections.unmodifiableSet(result);
  }

  private static final void register(Class key, Class<?> value) {
    HashSet<Class<?>> result = sServices.get(key);
    if (result == null) {
      result = new HashSet<Class<?>>();
      sServices.put(key, result);
    }
    result.add(value);
  }
}

1. 如何獲取 class 文件

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental() {
        return true;
    }

配置 Transform 的輸入類型為 Class, 作用域?yàn)槿こ獭?這樣在 transform(TransformInvocation transformInvocation) 方法中, transformInvocation.inputs 會(huì)傳入工程內(nèi)所有的 class 文件。

inputs 包含兩個(gè)部分:

public interface TransformInput {
    Collection<JarInput> getJarInputs();

    Collection<DirectoryInput> getDirectoryInputs();
}

看接口方法可知,包含了 jar 包和目錄。子 module 的 java 文件在編譯過(guò)程中也會(huì)生成一個(gè) jar 包然后編譯到主工程中。
app/build 的目錄下可以看到 class 分別在 folders 和 jars 兩個(gè)目錄下:

2. Transform 與 Gradle Task 之間的關(guān)系?

Gradle 包中有一個(gè) TransformManager 的類,用來(lái)管理所有的 Transform。 在里面找到了這樣的代碼:


   public <T extends Transform> Optional<AndroidTask<TransformTask>> addTransform(TaskFactory taskFactory, TransformVariantScope scope, T transform, ConfigActionCallback<T> callback) {
               ...
               this.transforms.add(transform);
               AndroidTask task1 = this.taskRegistry.create(taskFactory, new ConfigAction(scope.getFullVariantName(), taskName, transform, inputStreams, referencedStreams, outputStream, this.recorder, callback));
               ...
               return Optional.ofNullable(task1);
           }
       }
   }

addTransform 方法在執(zhí)行過(guò)程中,會(huì)將 Transform 包裝成一個(gè) AndroidTask 對(duì)象。
所以可以理解為一個(gè) Transform 就是一個(gè) Task

3. Gradle 是如何控制 Transform 的作用域的?

還是在 Gradle 的包中有一個(gè) TaskManager 類,管理所有的 Task 執(zhí)行。 其中有一個(gè)方法:


    public void createPostCompilationTasks(TaskFactory tasks, VariantScope variantScope) {
        ...
        List customTransforms = extension.getTransforms();
        List customTransformsDependencies = extension.getTransformsDependencies();
        int preColdSwapTask = 0;

        for(int multiDexClassListTask = customTransforms.size(); preColdSwapTask < multiDexClassListTask; ++preColdSwapTask) {
            Transform dexOptions = (Transform)customTransforms.get(preColdSwapTask);
            List dexTransform = (List)customTransformsDependencies.get(preColdSwapTask);
            transformManager.addTransform(tasks, variantScope, dexOptions).ifPresent((t) -> {
                if(!dexTransform.isEmpty()) {
                    t.dependsOn(tasks, dexTransform);
                }

                if(dexOptions.getScopes().isEmpty()) {
                    variantScope.getAssembleTask().dependsOn(tasks, t);
                }

            });
        }
        ...
    }

該方法在 javaCompile 之后調(diào)用, 會(huì)遍歷所有的 transform,然后一一添加進(jìn) TransformManager。 加完自定義的 Transform 之后,再添加 Proguard, JarMergeTransform, MultiDex, Dex 等 Transform。

postCompilation 的調(diào)用:

   if(jackOptions1.isEnabled().booleanValue()) {
            javacTask = this.createJackTask(tasks, variantScope, true);
            setJavaCompilerTask(javacTask, tasks, variantScope);
        } else {
            javacTask = this.createJavacTask(tasks, variantScope);
            addJavacClassesStream(variantScope);
            setJavaCompilerTask(javacTask, tasks, variantScope);
            this.createPostCompilationTasks(tasks, variantScope);
        }

調(diào)用時(shí)判斷是使用 jack 編譯還是 javac 編譯。 javac 編譯完之后再組裝 Transform。
看了源碼之后,也可以回答 Transform 之間的依賴關(guān)系:

  • 因?yàn)槭潜闅v List 順序添加的,所以可以在 Plugin 中通過(guò)先后順序一一添加
  • registerTransform 方法第二個(gè)參數(shù)是 dependsOn, 可以手動(dòng)設(shè)置依賴關(guān)系

4. 如何得到文件的增量

再回到 TransformInput 這個(gè)接口,輸入源分為 JarInput 和 DirectoryInput


public interface JarInput extends QualifiedContent {
    Status getStatus();
}

Status 是一個(gè)枚舉:

public enum Status {
    NOTCHANGED,
    ADDED,
    CHANGED,
    REMOVED;
}

所以在輸入源中, 獲取了 JarInput 的對(duì)象時(shí),可以同時(shí)得到每個(gè) jar 的變更狀態(tài)。
需要注意的是:比如先 clean 再編譯時(shí), jar 的狀態(tài)是 NOTCHANGED

再看看 DirectoryInput:

public interface DirectoryInput extends QualifiedContent {
    Map<File, Status> getChangedFiles();
}

changedFiles 是一個(gè) Map,其中會(huì)包含所有變更后的文件,以及每個(gè)文件對(duì)應(yīng)的狀態(tài)。
同樣需要注意的是:先 clean 再編譯時(shí), changedFiles 是空的。

所以在處理增量時(shí),只需要根據(jù)每個(gè)文件的狀態(tài)進(jìn)行相應(yīng)的處理即可,不需要每次所有流程都重新來(lái)一遍。

踩了的坑

Transform 是用來(lái)處理 class 文件的, 但是在 Metis 的實(shí)現(xiàn)時(shí),需要生成 java 源文件,再將 java 文件編譯一下。
之前的實(shí)現(xiàn)方式是:

  • 創(chuàng)建一個(gè) generateSourceCode 的 task,依賴 JavaCompile, 這樣可以在整體編譯完成之后拿到所有的 class 文件
  • 再創(chuàng)建一個(gè) compileSourceCode 的 task,在 generateSourceCode
    執(zhí)行完成后編譯動(dòng)態(tài)生成的 java 源碼

但是現(xiàn)在 Transform 并不是原生的 task, 沒(méi)有找到合適的辦法讓 task 依賴 transfrom(誰(shuí)要是有好辦法告訴我~~ )。

現(xiàn)在的解決辦法是在 MetisTransform 生成完 java 源文件之后,主動(dòng)調(diào)用 javac 來(lái)編譯文件。

然后開(kāi)始了踩坑之旅。。

1. 怎么得到 sourceCompatibility & targetCompatibility 版本

調(diào)用 javac 需要兼容指定的版本,sourceCompatibility 和 targetCompatibility 有時(shí)候會(huì)配置,有時(shí)候不會(huì)配置會(huì)有默認(rèn)值。但是在 Transform 如何得到這兩個(gè)值呢?
翻查源碼時(shí)找到了 JavaCompile 包含這兩個(gè)屬性,所以只要能找到 JavaCompile 這個(gè) task,就能得到這兩個(gè)值:

        def sourceCompatibility
        def targetCompatibility
        def bootClasspath
        mProject.tasks.each { task ->
            if (AbstractCompile.isAssignableFrom(task.class)) {
                sourceCompatibility = task.sourceCompatibility
                targetCompatibility = task.targetCompatibility
            }

            if (JavaCompile.isAssignableFrom(task.class)) {
                bootClasspath = task.options.bootClasspath
            }
        }

bootClassPath 的值獲取采用同樣的方法。

2. javac 在哪?

不同的系統(tǒng) javac 的配置是不一樣的。在 bash 環(huán)境下可以通過(guò)

which javac

獲取到 javac 的路徑。
在 Project 類中找到一個(gè) exec 的方法,用來(lái)執(zhí)行命令

    def getJavac() {
        def stdOut = new ByteArrayOutputStream()
        mProject.exec {
            commandLine 'which'
            args 'javac'
            standardOutput = stdOut
        }

        return stdOut.toString().trim()
    }

**一定要 trim() !!! **

3. commandLine 的坑

到這正常應(yīng)該已經(jīng)沒(méi)有問(wèn)題了,只需要再調(diào)用 exec 執(zhí)行 javac 命令就可以了。但是...
javac 的命令在程序中是一個(gè)變量, 正常代碼會(huì)是這樣:

def javac = getJavac()
 mProject.exec {
            commandLine javac
            args "xxx", "xxx", "xxx"
        }

然后就報(bào)異常: command property is null!
但是 commandLine 后面直接配置 '/usr/bin/javac' 能編譯成功。我也不知道為什么。。 誰(shuí)要是知道一定要告訴我?。?/p>

最后通過(guò)曲線救國(guó), 將 javac 命令寫(xiě)入到一個(gè) shell 文件中,然后再 exec 中執(zhí)行一個(gè) shell 腳本。

  def generateCompileShell(tempDir, javac, sourceCompatibility, targetCompatibility, sourceFile, destDir, bootClasspath, classpaths) {
       def shellFile = new File(tempDir, "compileMetisShell.sh")
       if (shellFile.exists()) shellFile.delete()

       shellFile.append("#!/bin/sh")

       shellFile.append("\n")

       shellFile.append("${javac} -source ${sourceCompatibility} -target ${targetCompatibility} ${sourceFile} -d ${destDir}")

       shellFile.append(" -bootclasspath ${bootClasspath}")

       shellFile.append(" -classpath ")

       classpaths.each { classpath ->
           shellFile.append("${classpath}:")
       }

       return shellFile
   }
 ExecResult result = mProject.exec {
            executable 'sh'
            args shell.absolutePath
        }

后記

這次使用 Transform api 重新實(shí)現(xiàn) Metis 的插件工具,翻查了很多文檔,但是很少有對(duì) transform 講的很詳細(xì)。一步一步摸索出來(lái)感覺(jué)收獲良多。

下一步準(zhǔn)備將一個(gè) AppRouter 的庫(kù)使用 tranform 重構(gòu)一下,比 Metis 要更復(fù)雜一點(diǎn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容