Android Gradle 資源編譯 - MergeResource

問題背景

在客戶端插件實現中, 有一種插件依賴宿主資源的方案。即插件打包時並不包含與宿主相同的資源,而在運行期直接讀取宿主的資源,這就要求在插件編譯時,要將宿主的資源包作爲輸入進行鏈接,就如同依賴系統基礎資源framework res包一樣。在開發上也需要變更資源引用的方式。

在平時開發中引用資源的方式:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <android.support.design.widget.AppBarLayout
        android:background="?attr/colorPrimary"
        app:elevation="0dp">
    </android.support.design.widget.AppBarLayout>
</LinearLayout>

上面會涉及到兩個問題:自定義屬性 和 資源引用。由於 AppBarLayout 是定義在宿主中的資源,插件中並沒有,那麼在寫法上就會有所不同。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:host="http://schemas.android.com/apk/packageName">
    <android.support.design.widget.AppBarLayout
        android:background="?packageName:attr/colorPrimary"
        host:elevation="0dp">
    </android.support.design.widget.AppBarLayout>
</LinearLayout>

雖然可以通過人工的方式修改,但layout目錄下近2K個文件,要全部修改顯然不現實,並且還有第三方依賴,且都是Glide在線依賴的方式更沒得改。此時需要找到一種方式在資源編譯時通過腳本批量化修改並且不改變原文件,即對開發人員來說是無感知的,在插件中開發和普通的app開發無異。

新版是基於 APG 3.5.3 版本 和 AAPT2,有別原來的 APG2.3.3 + AAPT的模式,在資源的編譯上作出了不小的調整。在新版中Android Gradle Plugin 打包的過程中,MergeResource任務同時完成 合併資源 和 編譯資源,並且對於非values類的資源不在落地,這就需要在 MergeResource任務尋找一個時機,進行任務的攔截和修改。


任務的創建

MergeResource的任務是在ApplicationTaskManager.createTasksForVariantScope()中創建創建,方法實現只是簡單的調用父類TaskManager.createMergeResourcesTask()方法。

    @Override
    public void createTasksForVariantScope(
            @NonNull final VariantScope variantScope,
            @NonNull List<VariantScope> variantScopesForLint) {

        // Add a task to merge the resource folders
        createMergeResourcesTask(
                variantScope,
                 // processResources 是否編譯資源 
                 // ApplicationTaskManager true 
                 // LibratyTaskManager false
                true, 
                Sets.immutableEnumSet(MergeResources.Flag.PROCESS_VECTOR_DRAWABLES));
    }

TaskManager.createMergeResourcesTask()方法中判斷下是否生成非編譯的資源,接着調用basicCreateMergeResourcesTask()
    public TaskProvider<MergeResources> createMergeResourcesTask(
            @NonNull VariantScope scope,
            boolean processResources,
            ImmutableSet<MergeResources.Flag> flags) {

        boolean unitTestRawResources =
                globalScope.getExtension().getTestOptions().getUnitTests().isIncludeAndroidResources()
                        && !projectOptions.get(BooleanOption.ENABLE_UNIT_TEST_BINARY_RESOURCES);

        boolean alsoOutputNotCompiledResources =
                scope.getType().isApk()&& !scope.getType().isForTesting()
                        && (scope.useResourceShrinker() || unitTestRawResources);

        return basicCreateMergeResourcesTask(...);
    }  

basicCreateMergeResourcesTask方法首先構造編譯文件的輸出目錄mergedOutputDir 、任務的前綴名稱等信息,接着通過初始化MergeResources.CreationAction對象,然後調用taskFactory.register()進行任務的註冊。

    public TaskProvider<MergeResources> basicCreateMergeResourcesTask(
            @NonNull VariantScope scope,
            @NonNull MergeType mergeType,
            @Nullable File outputLocation,
            final boolean includeDependencies,
            final boolean processResources,
            boolean alsoOutputNotCompiledResources,
            @NonNull ImmutableSet<MergeResources.Flag> flags,
            @Nullable PreConfigAction preConfigCallback,
            @Nullable TaskConfigAction<MergeResources> configCallback) {

        //資源編譯後.flat文件存放的目錄
        File mergedOutputDir = MoreObjects
                .firstNonNull(outputLocation, scope.getDefaultMergeResourcesOutputDir());
        //任務名稱前綴merge
        String taskNamePrefix = mergeType.name().toLowerCase(Locale.ENGLISH);
        //進行任務配置和註冊
        TaskProvider<MergeResources> mergeResourcesTask =
                taskFactory.register(new MergeResources.CreationAction(...), preConfigCallback, configCallback, null);

        scope.getArtifacts()
                .appendArtifact(
                        mergeType.getOutputType(),
                        ImmutableList.of(mergedOutputDir),
                        mergeResourcesTask.getName());
        return mergeResourcesTask;
    }

通過MergeResources.CreationAction()來進行任務的配置。

首先通過Aapt2MavenUtils.getAapt2FromMavenAndVersion()獲取候選的AAPT2的可執行文件,方法會優先判斷是否有配置 android.aapt2FromMavenOverride屬性,如果有的話會優先使用自定義的AAPT2文件,這樣我們就可以遇到一些bug的時候,先通過設置這個屬性來自定義AAPT2的版本。

接着初始化ResourcesComputer, 實現類爲 DependencyResourcesComputer, 在initFromVariantScope()方法中會初始化兩個重要的變量 libraries(外部第三方依賴的res目錄集合) 和resources(項目SourceSet資源配置信息, 如: src/main, src/debug),最後設置任務執行的Task依賴。

@Override
        public void configure(@NonNull MergeResources task) {
            super.configure(task);

            VariantScope variantScope = getVariantScope();
            GlobalScope globalScope = variantScope.getGlobalScope();
            BaseVariantData variantData = variantScope.getVariantData();

            task.minSdk =  TaskInputHelper.memoize( () -> variantData
                                                         .getVariantConfiguration()
                                                         .getMinSdkVersion()
                                                         .getApiLevel());

            //選擇執行指定版本的AAPT2
            Pair<FileCollection, String> aapt2AndVersion =   
                                              Aapt2MavenUtils.getAapt2FromMavenAndVersion(globalScope);
            task.getAapt2FromMaven().from(aapt2AndVersion.getFirst());
            task.aapt2Version = aapt2AndVersion.getSecond();
            task.setIncrementalFolder(variantScope.getIncrementalDir(getName()));

            //初始化需要編譯的資源信息: 項目res目錄 和 第三方依賴res目錄
            task.getResourcesComputer().initFromVariantScope(variantScope, includeDependencies);
            // app\src\main\res, app\src\debug\res 等資源目錄
            task.sourceFolderInputs = () -> variantData.getVariantConfiguration()
                                           .getSourceFiles(SourceProvider::getResDirectories);

            task.outputDir = outputLocation;
            if (!task.disableVectorDrawables) {
                task.generatedPngsOutputDir = variantScope.getGeneratedPngsOutputDir();
            }

            task.dependsOn(variantScope.getTaskContainer().getResourceGenTask());
        }

任務的執行


方案的優化

舊版的MergeResource任務會拷貝項目 res目錄和所有依賴的資源文件到build\intermediates\res\merged\flavor\debug\目錄中,並會對values*目錄中的文件進行合併(即該目錄下的 colors.xml、strings.xml、styles.xml等合併成一個values.xml文件),後續的ProcessAndroidResources任務會將該目錄下的文件編譯成一個.ap_文件。

新版的AAPT2將資源的編譯拆分成兩個階段:編譯鏈接。MergeResource任務不在是簡單的拷貝源文件和values資源合併。任務執行時將values類資源合併保存在\intermediates\incremental\mergeDebugResources\merged.dir目錄中,但不再拷貝.xml、.png、.jpg等資源到merged目錄,而是在任務執行時直接引用緩存文件進行編譯,如直接引用: C:\Gradle\caches\transforms-2\files-2.1\2ad9bf\appcompat-v7-28.0.0\res\drawable-xxxhdpi-v4\abc_light.png,通過AAPT2編譯生成中間文件abc_light.png.flat,然後保存在build\intermediates\res\merged\debug\中,爲後續的資源鏈接做準備。


執行過程

增量任務

MergeResource繼承至IncrementalTask支持增量編譯,當符合增量編譯條件時會調用doIncrementalTaskAction,否則全量編譯時調用doFullTaskAction

    /**
     * Gradle's entry-point into this task. Determines whether or not it's possible to do this task
     * incrementally and calls either doIncrementalTaskAction() if an incremental build is possible,
     * and doFullTaskAction() if not.
     */
    @TaskAction
    internal fun taskAction(inputs: IncrementalTaskInputs) {
        recordTaskAction { handleIncrementalInputs(inputs) }
    }

    private fun handleIncrementalInputs(inputs: IncrementalTaskInputs) {
        if (!incremental || !inputs.isIncremental) {
            project.logger.info("Unable do incremental execution: full task run")
            doFullTaskAction()
            return
        }
        doIncrementalTaskAction(getChangedInputs(inputs))
    }
    private fun getChangedInputs(inputs: IncrementalTaskInputs): Map<File, FileStatus> {}

全量編譯

MergeResource任務在執行全量編譯時,會執行doFullTaskAction()方法,

    @Override
    protected void doFullTaskAction() throws IOException, JAXBException {
        
        // 全量編譯時刪除上一次的輸出內容
        File destinationDir = getOutputDir();
        FileUtils.cleanOutputDir(destinationDir);
        if (dataBindingLayoutInfoOutFolder != null) {
            FileUtils.deleteDirectoryContents(dataBindingLayoutInfoOutFolder);
        }
        
        ResourcePreprocessor preprocessor = getPreprocessor();
        // 項目所有的res目錄信息: app\src\main\res, app\src\debug\res
        // 外部依賴的res目錄信息: gradle\caches\transforms-2\files-2.1\5...188\appcompat-v7-27.1.1\res
        // 自動生成的res目錄信息: app\build\generated\res
        // 項目依賴的res目錄信息: libraryModule\build\intermediates\packaged_res
        List<ResourceSet> resourceSets = getConfiguredResourceSets(preprocessor);

        // create a new merger and populate it with the sets.
        ResourceMerger merger = new ResourceMerger(minSdk.get());

        //  getAapt2FromMaven() 用於獲取可執行的AAPT2文件所在目錄的相關信息, 
        //  caches\transforms-2\files-2.1\8***e\aapt2-3.5.3-5435860-windows
        try (ResourceCompilationService resourceCompiler = getResourceProcessor(...)) {

            for (ResourceSet resourceSet : resourceSets) {
                 //解析res目錄下的所有文件, 並添加到merger中
                 resourceSet.loadFromFiles(new LoggerWrapper(getLogger()));
                 merger.addDataSet(resourceSet);
            }

            MergedResourceWriter writer = new MergedResourceWriter(...);
                            
            //mergeData方法中會執行values.xml文件內容的合併,構造資源編譯的CompileResourceRequest的對象, 然後添加到
            //ResourceCompilationService的任務隊列中
           merger.mergeData(writer, false /*doCleanUp*/)

        } catch (Exception e) {
        } finally {
            cleanup();
        }
    }

方法開始時會先清空上一次編譯的輸出內容。接着構造一個ResourcePreprocessor 對象,主要作用是用於根據VectorDrawable生成對應的PNG圖片和xml資源。

然後調用getConfiguredResourceSets()來獲得需要編譯的資源集合ResourceSet列表,這個列表包含了項目中所有res目錄信息 、 外部第三方依賴的res目錄信息、generated下的res目錄信息。

getResourceProcessor()方法會首先構造一個WorkerExecutorResourceCompilationService對象,對象中包含一個任務隊列 requests: MutableList< CompileResourceRequest> 用於保存編譯請求,每一個資源文件會對應生成一個request對象。

資源準備

遍歷上一步得到的resourceSets集合,調用每一個ResourceSet的loadFromFiles來加載和解析res資源目錄下的所有資源文件,根據資源文件類型的不同會解析成 “SINGLE_FILE”、“XML_VALUES”、"GENERATED_FILES"等不同類型的ResourceFile,ResourceSet中包含一個mItems< String, ResourceMergerItem> 集合,用於保存上一步解析後的得到的ResourceFile。

  • SINGLE_FILE: 對應 drawable、anim、layout等不用單文件資源,在ResourceFile.mItems集合中僅包含一個節點指向這個資源文件;
    在這裏插入圖片描述
  • XML_VALUES: 對應values文件夾下的資源,該類型還會解析xml文件中的每一個節點, 如"string/app_name", “color/app_title”, "style/app_default"等信息,併爲每一個節點對應生成一個ResourceMergerItem對象,保存在ResourceFile的mItems 中。

“string/app_name” => ResourceMergerItem()
“color/app_title” => ResourceMergerItem()

解析完成後,調用ResourceMerger的addDataSet將ResourceSet對象添加到ResourceMerger.mDataSets集合中等待合併。


資源合併

資源準備完成後,會調用ResourceMerger.mergeData()方法開始資源的合併。方法中遍歷上一步收集的ResourceMerger.mDataSets集合,將所有ResourceSet.mItems< String, ResourceMergerItem>Map中的key放到一個HashSet中進行去重。然後遍歷這個Set的keySet(),根據key取得對應的對應的ResourceMergerItem對象,接着調用MergedResourceWriter的addItem方法,傳入上一步得到的ResourceMergerItem對象。
addItem方法方法中會根據不同的資源類型進行處理:

  • "SINGLE_FILE"類型的資源:生成一個CompileResourceRequest對象,添加到mCompileResourceRequests集合中
  • "XML_VALUES"類型的資源:則將ResourceMergerItem暫存到mValuesResMap集合中

資源全部遍歷結束後,調用MergedResourceWriter.end()方法,該方法會先調用super.end()執行父類的邏輯,接着再次調用postWriteAction()方法,用來將上一個階段中生成的mValuesResMap集合數據寫入values.xml文件,AAPT 到 AAPT2 文件路徑由原來的 “intermediates/res/merged/productFlavor/values/values.xml” 變更爲 “intermediates/incremental/mergedProductFlavorResources/merged.dir/values/values.xml”。文件寫入完成後,會生成一個對應的CompileResourceRequest對象, 這時不在添加到mCompileResourceRequests集合中,而是直接調用WorkerExecutorResourceCompilationService.submitCompile()提交任務中。到這裏ResourceMerger.mergeData()方法就執行結束了。


資源編譯

資源合併結束後回到doFullTaskAction()方法中, 由於使用try-with-resource的方式初始化的ResourceCompilationService, 當塊執行結束後會調用close方法,方法中會將資源合併步驟中提交的編譯任務提交給WorkerExecutorFacade去執行。

    override fun close() {
        if (requests.isEmpty()) {
            return
        }
        val buckets = minOf(requests.size, 8) // Max 8 buckets
        for (bucket in 0 until buckets) {
            val bucketRequests = requests.filterIndexed { i, _ ->
                i.rem(buckets) == bucket
            }
            workerExecutor.submit(
                Aapt2CompileRunnable::class.java,
                Aapt2CompileRunnable.Params(aapt2ServiceKey, bucketRequests, errorFormatMode, true)
            )
        }
        requests.clear()
        workerExecutor.close()
    }

任務最多分成8組同時執行, 每組任務使用同一個AAPT2進程, 通過outputStream不斷的給AAPT2傳遞編譯的資源信息。編譯指令的寫入是在Aapt2DeamonUtil類中的request方法實現的。


攔截方案

通過上面的分析我們知道,資源最終的編譯都是要將任務提交給WorkerExecutorFacade,我們只要在MergeResources任務初始化配置後,將workerExecutorFacade變量替換成實現了WorkerExecutorFacade接口的一個代理,攔截submit(actionClass: Class<out Runnable>, parameter: Serializable)方法,對提交編譯的資源文件進 拷貝 => 修改/替換 => 再提交編譯的處理,以達到資源引用方式的整體替換。

總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章