Transform詳解

推薦先看幾篇文章:

1.Android 熱修復(fù)使用Gradle Plugin1.5改造Nuwa插件

2.GradleTransformAPI的基本使用

3.Transform官方文檔

4.一起玩轉(zhuǎn)Android項(xiàng)目中的字節(jié)碼(強(qiáng)烈推薦)

5.Android編譯流程和Gradle使用

引用說(shuō)明

本文章主要作個(gè)人總結(jié)記錄,感謝CSDNAndroid高級(jí)架構(gòu)的資料和文章,如有侵權(quán),請(qǐng)聯(lián)系刪除。

知識(shí)點(diǎn)

1. 打包過(guò)程

要了解Transform,首先我們需要知道Gradle構(gòu)建一個(gè)安卓應(yīng)用,會(huì)經(jīng)過(guò)哪些步驟。

1.png

上圖是谷歌官網(wǎng)給出的一個(gè)典型的apk構(gòu)建的過(guò)程,比較概括。主要包括兩個(gè)過(guò)程,首先是編譯過(guò)程,編譯的內(nèi)容包括本工程的文件以及依賴(lài)的各種庫(kù)文件,編譯的輸出包括dex文件和編譯后的資源文件。然后是打包過(guò)程。配合Keystore對(duì)第一步的輸出進(jìn)行簽名對(duì)齊,生成最終的apk文件。

2.png

上面這張圖對(duì)上面的步驟以及每步用到的工具進(jìn)行了細(xì)分,概括如下:

  1. Java編譯器對(duì)工程本身的java代碼進(jìn)行編譯,這些java代碼有三個(gè)來(lái)源:app的源代碼,由資源文件生成的R文件(aapt工具),以及有aidl文件生成的java接口文件(aidl工具)。產(chǎn)出為.class文件。
  2. .class文件和依賴(lài)的三方庫(kù)文件通過(guò)dex工具生成Delvik虛擬機(jī)可執(zhí)行的.dex文件,可能有一個(gè)或多個(gè),包含了所有的class信息,包括項(xiàng)目自身的class和依賴(lài)的class。產(chǎn)出為.dex文件。
  3. apkbuilder工具將.dex文件和編譯后的資源文件生成未經(jīng)簽名對(duì)齊的apk文件。這里編譯后的資源文件包括兩部分,一是由aapt編譯產(chǎn)生的編譯后的資源文件,二是依賴(lài)的三方庫(kù)里的資源文件。產(chǎn)出為未經(jīng)簽名的.apk文件。
  4. 分別由Jarsigner和zipalign對(duì)apk文件進(jìn)行簽名和對(duì)齊,生成最終的apk文件。

2. 什么是Transform

簡(jiǎn)單介紹:
從android-build-tool:gradle:1.5開(kāi)始,gradle插件包含了一個(gè)叫Transform的API,這個(gè)API允許第三方插件在class文件轉(zhuǎn)為為dex文件前操作編譯好的class文件,這個(gè)API的目標(biāo)就是簡(jiǎn)化class文件的自定義的操作而不用對(duì)Task進(jìn)行處理。

作用域:
Transform是作用在.class編譯后,打包成.dex前,可以對(duì).class和resource進(jìn)行再處理的部分。
為了印證,我們隨便建立一個(gè)項(xiàng)目Build的一次。

image


可以很清楚的看到,原生就帶了一系列Transform供使用。那么這些Transform是怎么組織在一起的呢,我們用一張圖表示:

image


每個(gè)Transform其實(shí)都是一個(gè)gradle task,Android編譯器中的TaskManager將每個(gè)Transform串連起來(lái),第一個(gè)Transform接收來(lái)自javac編譯的結(jié)果,以及已經(jīng)拉取到在本地的第三方依賴(lài)(jar. aar),還有resource資源,注意,這里的resource并非android項(xiàng)目中的res資源,而是asset目錄下的資源。 這些編譯的中間產(chǎn)物,在Transform組成的鏈條上流動(dòng),每個(gè)Transform節(jié)點(diǎn)可以對(duì)class進(jìn)行處理再傳遞給下一個(gè)Transform。我們常見(jiàn)的混淆,Desugar等邏輯,它們的實(shí)現(xiàn)如今都是封裝在一個(gè)個(gè)Transform中,而我們自定義的Transform,會(huì)插入到這個(gè)Transform鏈條的最前面。

但其實(shí),上面這幅圖,只是展示Transform的其中一種情況。而Transform其實(shí)可以有兩種輸入,一種是消費(fèi)型的,當(dāng)前Transform需要將消費(fèi)型型輸出給下一個(gè)Transform,另一種是引用型的,當(dāng)前Transform可以讀取這些輸入,而不需要輸出給下一個(gè)Transform,比如Instant Run就是通過(guò)這種方式,檢查兩次編譯之間的diff的。

最終,我們定義的Transform會(huì)被轉(zhuǎn)化成一個(gè)個(gè)TransformTask,在Gradle編譯時(shí)調(diào)用。

TransformManager.class

  /**
     * Adds a Transform.
     *
     * <p>This makes the current transform consumes whatever Streams are currently available and
     * creates new ones for the transform output.
     *
     * <p>his also creates a {@link TransformTask} to run the transform and wire it up with the
     * dependencies of the consumed streams.
     *
     * @param taskFactory the task factory
     * @param scope the current scope
     * @param transform the transform to add
     * @param callback a callback that is run when the task is actually configured
     * @param <T> the type of the transform
     * @return {@code Optional<AndroidTask<Transform>>} containing the AndroidTask for the given
     *     transform task if it was able to create it
     */
    @NonNull
    public <T extends Transform> Optional<TransformTask> addTransform(
            @NonNull TaskFactory taskFactory,
            @NonNull TransformVariantScope scope,
            @NonNull T transform,
            @Nullable TransformTask.ConfigActionCallback<T> callback) {

        if (!validateTransform(transform)) {
            // validate either throws an exception, or records the problem during sync
            // so it's safe to just return null here.
            return Optional.empty();
        }

        List<TransformStream> inputStreams = Lists.newArrayList();
        String taskName = scope.getTaskName(getTaskNamePrefix(transform));

        // get referenced-only streams
        List<TransformStream> referencedStreams = grabReferencedStreams(transform);

        // find input streams, and compute output streams for the transform.
        IntermediateStream outputStream = findTransformStreams(
                transform,
                scope,
                inputStreams,
                taskName,
                scope.getGlobalScope().getBuildDir());

        if (inputStreams.isEmpty() && referencedStreams.isEmpty()) {
            // didn't find any match. Means there is a broken order somewhere in the streams.
            issueReporter.reportError(
                    Type.GENERIC,
                    new EvalIssueException(
                            String.format(
                                    "Unable to add Transform '%s' on variant '%s': requested streams not available: %s+%s / %s",
                                    transform.getName(),
                                    scope.getFullVariantName(),
                                    transform.getScopes(),
                                    transform.getReferencedScopes(),
                                    transform.getInputTypes())));
            return Optional.empty();
        }

        //noinspection PointlessBooleanExpression
        if (DEBUG && logger.isEnabled(LogLevel.DEBUG)) {
            logger.debug("ADDED TRANSFORM(" + scope.getFullVariantName() + "):");
            logger.debug("\tName: " + transform.getName());
            logger.debug("\tTask: " + taskName);
            for (TransformStream sd : inputStreams) {
                logger.debug("\tInputStream: " + sd);
            }
            for (TransformStream sd : referencedStreams) {
                logger.debug("\tRef'edStream: " + sd);
            }
            if (outputStream != null) {
                logger.debug("\tOutputStream: " + outputStream);
            }
        }

        transforms.add(transform);

        // create the task...
        TransformTask task =
                taskFactory.create(
                        new TransformTask.ConfigAction<>(
                                scope.getFullVariantName(),
                                taskName,
                                transform,
                                inputStreams,
                                referencedStreams,
                                outputStream,
                                recorder,
                                callback));

        return Optional.ofNullable(task);
    }

3.Transform解讀

我們首先先定義一個(gè)自定義的Transform,需要實(shí)現(xiàn)如下方法。

class AspectJTransform extends Transform {

    final String NAME =  "AjcTransform"

    @Override
    String getName() {
        return NAME
    }

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

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

    @Override
    boolean isIncremental() {
        return true
    }

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

首先我們一項(xiàng)項(xiàng)分析:

(1)Name
  @Override
    String getName() {
        return NAME
    }

Name顧名思義,就是我們的Transform名稱(chēng),再回到我們剛剛Build的流程里:

image

這個(gè)最終的名字是如何構(gòu)成的呢?好像跟我們這邊的定義的名字有區(qū)別。

在gradle plugin的源碼中有一個(gè)叫TransformManager的類(lèi),這個(gè)類(lèi)管理著所有的Transform的子類(lèi),里面有一個(gè)方法叫g(shù)etTaskNamePrefix,在這個(gè)方法中就是獲得Task的前綴,以transform開(kāi)頭,之后拼接ContentType,這個(gè)ContentType代表著這個(gè)Transform的輸入文件的類(lèi)型,類(lèi)型主要有兩種,一種是Classes,另一種是Resources,ContentType之間使用And連接,拼接完成后加上With,之后緊跟的就是這個(gè)Transform的Name,name在getName()方法中重寫(xiě)返回即可。代碼如下:

    @NonNull
    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();
    }
(2)getInputTypes()

先來(lái)看代碼注釋?zhuān)⑨寣?xiě)的很清晰了,必須是CLASSES(0x01),RESOURCES(0x02)之一,相當(dāng)于Transform需要處理的類(lèi)型。

    /**
     * Returns the type(s) of data that is consumed by the Transform. This may be more than
     * one type.
     *
     * <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>
     */
    @NonNull
    public abstract Set<ContentType> getInputTypes();
    
    ----------------------------------
    
     /**
     * The type of of the content.
     */
    enum DefaultContentType implements ContentType {
        /**
         * The content is compiled Java code. This can be in a Jar file or in a folder. If
         * in a folder, it is expected to in sub-folders matching package names.
         */
        CLASSES(0x01),

        /** The content is standard Java resources. */
        RESOURCES(0x02);

        private final int value;

        DefaultContentType(int value) {
            this.value = value;
        }

        @Override
        public int getValue() {
            return value;
        }
    }

其實(shí)還有一些另外的,還沒(méi)研究


CotentType
(3)getScopes()

先來(lái)看源碼注釋?zhuān)@個(gè)的作用相當(dāng)于用來(lái)Transform表明作用域

    /**
     * Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.
     */
    @NonNull
    public abstract Set<Scope> getScopes();

開(kāi)發(fā)一共可以選如下幾種:

  /**
     * The scope of the content.
     *
     * <p>
     * This indicates what the content represents, so that Transforms can apply to only part(s)
     * of the classes or resources that the build manipulates.
     */
    enum Scope implements ScopeType {
        /** Only the project (module) content */
        PROJECT(0x01),
        /** Only the sub-projects (other modules) */
        SUB_PROJECTS(0x04),
        /** Only the external libraries */
        EXTERNAL_LIBRARIES(0x10),
        /** Code that is being tested by the current variant, including dependencies */
        TESTED_CODE(0x20),
        /** Local or remote dependencies that are provided-only */
        PROVIDED_ONLY(0x40),

        /**
         * Only the project's local dependencies (local jars)
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        PROJECT_LOCAL_DEPS(0x02),
        /**
         * Only the sub-projects's local dependencies (local jars).
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(0x08);
        
        

一般來(lái)說(shuō)如果是要處理所有class字節(jié)碼,Scope我們一般使用TransformManager.SCOPE_FULL_PROJECT。即

public static final Set<Scope> SCOPE_FULL_PROJECT =
            Sets.immutableEnumSet(
                    Scope.PROJECT,
                    Scope.SUB_PROJECTS,
                    Scope.EXTERNAL_LIBRARIES);

還有一些其他的可以參考下。


Scope
(4)isIncremental()

增量編譯開(kāi)關(guān)。

/**
     * Returns whether the Transform can perform incremental work.
     *
     * <p>If it does, then the TransformInput may contain a list of changed/removed/added files, unless
     * something else triggers a non incremental run.
     */
    public abstract boolean isIncremental();

當(dāng)我們開(kāi)啟增量編譯的時(shí)候,相當(dāng)input包含了changed/removed/added三種狀態(tài),實(shí)際上還有notchanged。需要做的操作如下:

  • NOTCHANGED: 當(dāng)前文件不需處理,甚至復(fù)制操作都不用;
  • ADDED、CHANGED: 正常處理,輸出給下一個(gè)任務(wù);
  • REMOVED: 移除outputProvider獲取路徑對(duì)應(yīng)的文件。
(5)transform()

先來(lái)看一下源碼注釋?zhuān)荰ransform處理文件的核心代碼:

    /**
     * Executes the Transform.
     *
     * <p>The inputs are packaged as an instance of {@link TransformInvocation}
     * <ul>
     *     <li>The <var>inputs</var> collection of {@link TransformInput}. These are the inputs
     *     that are consumed by this Transform. A transformed version of these inputs must
     *     be written into the output. What is received is controlled through
     *     {@link #getInputTypes()}, and {@link #getScopes()}.</li>
     *     <li>The <var>referencedInputs</var> collection of {@link TransformInput}. This is
     *     for reference only and should be not be transformed. What is received is controlled
     *     through {@link #getReferencedScopes()}.</li>
     * </ul>
     *
     * A transform that does not want to consume anything but instead just wants to see the content
     * of some inputs should return an empty set in {@link #getScopes()}, and what it wants to
     * see in {@link #getReferencedScopes()}.
     *
     * <p>Even though a transform's {@link Transform#isIncremental()} returns true, this method may
     * be receive <code>false</code> in <var>isIncremental</var>. This can be due to
     * <ul>
     *     <li>a change in secondary files ({@link #getSecondaryFiles()},
     *     {@link #getSecondaryFileOutputs()}, {@link #getSecondaryDirectoryOutputs()})</li>
     *     <li>a change to a non file input ({@link #getParameterInputs()})</li>
     *     <li>an unexpected change to the output files/directories. This should not happen unless
     *     tasks are improperly configured and clobber each other's output.</li>
     *     <li>a file deletion that the transform mechanism could not match to a previous input.
     *     This should not happen in most case, except in some cases where dependencies have
     *     changed.</li>
     * </ul>
     * In such an event, when <var>isIncremental</var> is false, the inputs will not have any
     * incremental change information:
     * <ul>
     *     <li>{@link JarInput#getStatus()} will return {@link Status#NOTCHANGED} even though
     *     the file may be added/changed.</li>
     *     <li>{@link DirectoryInput#getChangedFiles()} will return an empty map even though
     *     some files may be added/changed.</li>
     * </ul>
     *
     * @param transformInvocation the invocation object containing the transform inputs.
     * @throws IOException if an IO error occurs.
     * @throws InterruptedException
     * @throws TransformException Generic exception encapsulating the cause.
     */
    public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {
        // Just delegate to old method, for code that uses the old API.
        //noinspection deprecation
        transform(transformInvocation.getContext(), transformInvocation.getInputs(),
                transformInvocation.getReferencedInputs(),
                transformInvocation.getOutputProvider(),
                transformInvocation.isIncremental());
    }

大致意思如下,具體大家一定要仔細(xì)看注釋?zhuān)?br>

  1. 如果拿取了getInputs()的輸入進(jìn)行消費(fèi),則transform后必須再輸出給下一級(jí)
  2. 如果拿取了getReferencedInputs()的輸入,則不應(yīng)該被transform。
  3. 是否增量編譯要以transformInvocation.isIncremental()為準(zhǔn)。
(6)getSecondaryFiles()

上面transform函數(shù)這里還提到了一個(gè)東西叫

secondary files ({@link #getSecondaryFiles()}

網(wǎng)上Transform的講解對(duì)它的提及比較少,先看看注釋?zhuān)臀覀冎敖榻B的一樣,有一系列API:

  /**
     * Returns a list of additional file(s) that this Transform needs to run. Preferably, use
     * {@link #getSecondaryFiles()} API which allow eah secondary file to indicate if changes
     * can be handled incrementally or not. This API will treat all additional file change as
     * a non incremental event.
     *
     * <p>Changes to files returned in this list will trigger a new execution of the Transform
     * even if the qualified-content inputs haven't been touched.
     *
     * <p>Any changes to these files will trigger a non incremental execution.
     *
     * <p>The default implementation returns an empty collection.
     *
     * @deprecated replaced by {@link #getSecondaryFiles()}
     */
    @Deprecated
    @NonNull
    public Collection<File> getSecondaryFileInputs() {
        return ImmutableList.of();
    }

    /**
     * Returns a list of additional file(s) that this Transform needs to run.
     *
     * <p>Changes to files returned in this list will trigger a new execution of the Transform
     * even if the qualified-content inputs haven't been touched.
     *
     * <p>Each secondary input has the ability to be declared as necessitating a non incremental
     * execution in case of change. This Transform can therefore declare which secondary file
     * changes it supports in incremental mode.
     *
     * <p>The default implementation returns an empty collection.
     */
    @NonNull
    public Collection<SecondaryFile> getSecondaryFiles() {
        return ImmutableList.of();
    }

    /**
     * Returns a list of additional (out of streams) file(s) that this Transform creates.
     *
     * <p>These File instances can only represent files, not directories. For directories, use
     * {@link #getSecondaryDirectoryOutputs()}
     *
     *
     * <p>Changes to files returned in this list will trigger a new execution of the Transform
     * even if the qualified-content inputs haven't been touched.
     *
     * <p>Changes to these output files force a non incremental execution.
     *
     * <p>The default implementation returns an empty collection.
     */
    @NonNull
    public Collection<File> getSecondaryFileOutputs() {
        return ImmutableList.of();
    }

    /**
     * Returns a list of additional (out of streams) directory(ies) that this Transform creates.
     *
     * <p>These File instances can only represent directories. For files, use
     * {@link #getSecondaryFileOutputs()}
     *
     * <p>Changes to directories returned in this list will trigger a new execution of the Transform
     * even if the qualified-content inputs haven't been touched.
     *
     * <p>Changes to these output directories force a non incremental execution.
     *
     * <p>The default implementation returns an empty collection.
     */
    @NonNull
    public Collection<File> getSecondaryDirectoryOutputs() {
        return ImmutableList.of();
    }

跟據(jù)字面意思理解,除了主輸入/輸出流之外,Transform還可以額外定義另外的流供下個(gè)使用,不過(guò)我們平時(shí)用到的不多,可以根據(jù)系統(tǒng)自帶的Transform源碼看看它輸出了啥,比如ProguardTransform:

public class ProGuardTransform extends BaseProguardAction {
  ......
  private final ImmutableList<File> secondaryFileOutputs;
  ......
   public ProGuardTransform(@NonNull VariantScope variantScope) {
        ......
        secondaryFileOutputs = ImmutableList.of(printMapping, printSeeds, printUsage);
    }
    @NonNull
    @Override
    public Collection<SecondaryFile> getSecondaryFiles() {
        final List<SecondaryFile> files = Lists.newArrayList();

        if (testedMappingFile != null && testedMappingFile.isFile()) {
            files.add(SecondaryFile.nonIncremental(testedMappingFile));
        } else if (testMappingConfiguration != null) {
            files.add(SecondaryFile.nonIncremental(testMappingConfiguration));
        }

        // the config files
        files.add(SecondaryFile.nonIncremental(getAllConfigurationFiles()));

        return files;
    }

    @NonNull
    @Override
    public Collection<File> getSecondaryFileOutputs() {
        return secondaryFileOutputs;
    }

可以看到,它實(shí)際上是對(duì)mapping文件額外的配置,相當(dāng)于如注釋一樣,是相對(duì)于主流額外新一個(gè)流,實(shí)際開(kāi)發(fā)中我們用的較少。

(7)isCacheable()

按照慣例,先看注釋?zhuān)?/p>

/**
     * Returns if this transform's outputs should be cached. Please read {@link
     * org.gradle.api.tasks.CacheableTask} Javadoc if you would like to make your transform
     * cacheable.
     */
    public boolean isCacheable() {
        return false;
    }

如果我們的transform需要被緩存,則為true。
它被TransformTask所用到:

@CacheableTask
public class TransformTask extends StreamBasedTask implements Context {

@Override
public void execute(@NonNull TransformTask task) {
        task.transform = transform;
        task.consumedInputStreams = consumedInputStreams;
        task.referencedInputStreams = referencedInputStreams;
        task.outputStream = outputStream;
        task.setVariantName(variantName);
        task.recorder = recorder;
        if (configActionCallback != null) {
            configActionCallback.callback(transform, task);
        }
        //這一句,如果設(shè)置為true,則緩存輸出
        task.getOutputs().cacheIf(t -> transform.isCacheable());
        
        task.registerConsumedAndReferencedStreamInputs();
        }

4.Transform編寫(xiě)模板

(1)無(wú)增量編譯:
class AspectJTransform extends Transform {

    final String NAME =  "AjcTransform"

    @Override
    String getName() {
        return NAME
    }

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

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

    @Override
    boolean isIncremental() {
        return false
    }

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

        //OutputProvider管理輸出路徑,如果消費(fèi)型輸入為空,你會(huì)發(fā)現(xiàn)OutputProvider == null
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        transformInvocation.inputs.each { TransformInput input ->
            input.jarInputs.each { JarInput jarInput ->
                //處理Jar
                processJarInput(jarInput, outputProvider)
            }

            input.directoryInputs.each { DirectoryInput directoryInput ->
                //處理源碼文件
                processDirectoryInputs(directoryInput, outputProvider)
            }
        }
    }

    void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR)
        //TODO do some transform
        //將修改過(guò)的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了        
        FileUtils.copyFiley(jarInput.getFile(), dest)
    }

    void processDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        File dest = outputProvider.getContentLocation(directoryInput.getName(),
                directoryInput.getContentTypes(), directoryInput.getScopes(),
                Format.DIRECTORY)
        //建立文件夾        
        FileUtils.forceMkdir(dest)
        //TODO do some transform
        //將修改過(guò)的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了        
        FileUtils.copyDirectory(directoryInput.getFile(), dest)
    }
}

這里只是實(shí)現(xiàn)了簡(jiǎn)單的拷貝,具體怎么處理可以根據(jù)需求出發(fā)

(2)帶增量編譯(推薦):
class AspectJTransform extends Transform {

    final String NAME = "AjcTransform"

    @Override
    String getName() {
        return NAME
    }

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

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

    @Override
    boolean isIncremental() {
        return true
    }

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

        boolean isIncremental = transformInvocation.isIncremental()

        //OutputProvider管理輸出路徑,如果消費(fèi)型輸入為空,你會(huì)發(fā)現(xiàn)OutputProvider == null
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()

        if (!isIncremental) {
            //不需要增量編譯,先清除全部
            outputProvider.deleteAll()
        }

        transformInvocation.getInputs().each { TransformInput input ->
            input.jarInputs.each { JarInput jarInput ->
                //處理Jar
                processJarInputWithIncremental(jarInput, outputProvider, isIncremental)
            }

            input.directoryInputs.each { DirectoryInput directoryInput ->
                //處理文件
                processDirectoryInputWithIncremental(directoryInput, outputProvider, isIncremental)
            }
        }
    }

    void processJarInputWithIncremental(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR)
        if (isIncremental) {
            //處理增量編譯
            processJarInputWhenIncremental(jarInput, dest)
        } else {
            //不處理增量編譯
            processJarInput(jarInput, dest)
        }
    }

    void processJarInput(JarInput jarInput, File dest) {
        transformJarInput(jarInput, dest)
    }

    void processJarInputWhenIncremental(JarInput jarInput, File dest) {
        switch (jarInput.status) {
            case Status.NOTCHANGED:
                break
            case Status.ADDED:
            case Status.CHANGED:
                //處理有變化的
                transformJarInputWhenIncremental(jarInput.getFile(), dest, jarInput.status)
                break
            case Status.REMOVED:
                //移除Removed
                if (dest.exists()) {
                    FileUtils.forceDelete(dest)
                }
                break
        }
    }

    void transformJarInputWhenIncremental(JarInput jarInput, File dest, Status status) {
        if (status == Status.CHANGED) {
            //Changed的狀態(tài)需要先刪除之前的
            if (dest.exists()) {
                FileUtils.forceDelete(dest)
            }
        }
        //真正transform的地方
        transformJarInput(jarInput, dest)
    }

    void transformJarInput(JarInput jarInput, File dest) {
        //TODO do some transform
        //將修改過(guò)的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了
        FileUtils.copyFile(jarInput.getFile(), dest)
    }

    void processDirectoryInputWithIncremental(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {
        File dest = outputProvider.getContentLocation(
                directoryInput.getFile().getAbsolutePath(),
                directoryInput.getContentTypes(),
                directoryInput.getScopes(),
                Format.DIRECTORY)
        if (isIncremental) {
            //處理增量編譯
            processDirectoryInputWhenIncremental(directoryInput, dest)
        } else {
            processDirectoryInput(directoryInput, dest)
        }
    }

    void processDirectoryInputWhenIncremental(DirectoryInput directoryInput, File dest) {
        FileUtils.forceMkdir(dest)
        String srcDirPath = directoryInput.getFile().getAbsolutePath()
        String destDirPath = dest.getAbsolutePath()
        Map<File, Status> fileStatusMap = directoryInput.getChangedFiles()
        fileStatusMap.each { Map.Entry<File, Status> entry ->
            File inputFile = entry.getKey()
            Status status = entry.getValue()
            String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath)
            File destFile = new File(destFilePath)
            switch (status) {
                case Status.NOTCHANGED:
                    break
                case Status.REMOVED:
                    if (destFile.exists()) {
                        FileUtils.forceDelete(destFile)
                    }
                    break
                case Status.ADDED:
                case Status.CHANGED:
                    FileUtils.touch(destFile)
                    transformSingleFile(inputFile, destFile, srcDirPath)
                    break
            }
        }
    }

    void processDirectoryInput(DirectoryInput directoryInput, File dest) {
        transformDirectoryInput(directoryInput, dest)
    }

    void transformDirectoryInput(DirectoryInput directoryInput, File dest) {
        //TODO do some transform
        //將修改過(guò)的字節(jié)碼copy到dest,就可以實(shí)現(xiàn)編譯期間干預(yù)字節(jié)碼的目的了
        FileUtils.copyDirectory(directoryInput.getFile(), dest)
    }

    void transformSingleFile(File inputFile, File destFile, String srcDirPath) {
        FileUtils.copyFile(inputFile, destFile)
    }
}

5.Transform注冊(cè)和使用

定義一個(gè)插件

class AspectJWeaverPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        boolean hasApp = project.getPlugins().hasPlugin(AppPlugin.class)
        if (hasApp) {
            def appExtension = project.getExtensions().getByType(AppExtension.class)
            appExtension.registerTransform(new AspectJTransform(), Collections.EMPTY_LIST)
        }
    }
}

6.Transform優(yōu)化

一般就三種:

  1. 增量編譯
  2. 并發(fā)編譯
  3. include... exclude...縮小transform范圍

這里講一下并發(fā)編譯,簡(jiǎn)單實(shí)現(xiàn)如下:

  WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()
  
  ......
  transformInvocation.getInputs().each { TransformInput input ->
            input.jarInputs.each { JarInput jarInput ->
                //多線程處理Jar
                waitableExecutor.execute(new Callable<Object>() {
                    @Override
                    Object call() throws Exception {
                        processJarInputWithIncremental(jarInput, outputProvider, isIncremental)
                        return null
                    }
                })
            }

            input.directoryInputs.each { DirectoryInput directoryInput ->
                //多線程處理文件
                waitableExecutor.execute(new Callable<Object>() {
                    @Override
                    Object call() throws Exception {
                        processDirectoryInputWithIncremental(directoryInput, outputProvider, isIncremental)
                        return null
                    }
                })
            }
        }

        //等待所有任務(wù)結(jié)束
        waitableExecutor.waitForTasksWithQuickFail(true)

7.Transform用途

結(jié)合ASM、AspectJ、javassit等字節(jié)碼處理框架進(jìn)行AOP編程,具體后面會(huì)講到

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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