AGP资源编译过程分析二link

本篇章里分析的AGP源码都是基于3.4.2版本的,很老的版本,也没办法,因为公司里用的就是3.4.2. 。。

在上一篇《AGP资源编译过程分析一compile》文章里,我们已经介绍过aapt2把资源的编译拆解为编译链接两部分,并且已经分析过资源的编译过程,今天我们来聊聊AGP的资源链接部分。

资源的链接比资源的编译简单多了,原因是编译前需要做资源merge,而资源的链接是没有这块内容的,所以链接的部分显得简单多了。

跟资源编译一样,对于资源的链接AGP也并没有提供类似于linkDebugResources的Task任务,在AGP里面,资源的链接是由processDebugResources任务来完成,比较有趣的是在AGP源码里processDebugResources任务是由LinkApplicationAndroidResourcesTask类来提供的,也许是谷歌的开发团队并不想把太多的实现细节暴露出来吧。

我们直接定位的这个任务的入口函数doFullTaskAction,在3.4版本AGP里,资源并不支持增量链接,因此也没有提供类似于doIncrementalTaskAction的增量入口函数。

override fun doFullTaskAction() {
    //收集manifest文件
    val manifestBuildElements = ExistingBuildElements.from(taskInputType, manifestFiles)
    //收集依赖的R.txt文件
    val dependencies = if (dependenciesFileCollection != null)
        dependenciesFileCollection!!.files

    //省略部分代码。。。    
    AaptSplitInvoker(
        AaptSplitInvokerParams(
            mainOutput,
            dependencies,
            imports,
            splitList,
            featureResourcePackages,
            mainOutput.apkData,
            true,
            aapt2ServiceKey,
            this
        )
    ).run()
}

doFullTaskAction方法先是收集链接时所依赖的资源文件,譬如AndroidManifest.xml文件,R.txt文件等等,注意这个AndroidManifest文件也是merge后的AndroidManifest文件,文件路径是/intermediates/merged_manifests/debug/AndroidManifest.xml

收集完所有必须参数后创建出AaptSplitInvoker对象并且调用了它的run方法,后者又直接调用了invokeAaptForSplit,过程如下:

override fun run() {
    try {
        invokeAaptForSplit(params)
    } catch (e: IOException) {
        throw RuntimeException(e)
    }
}

private fun invokeAaptForSplit(params: AaptSplitInvokerParams) {
    //省略前面一堆的链接参数整理。
    val configBuilder = AaptPackageConfig.Builder()
        .setManifestFile(manifestFile)
        .setOptions(params.aaptOptions)
        .setCustomPackageForR(packageForR)
        .setSymbolOutputDir(symbolOutputDir)
        .setSourceOutputDir(srcOut)
        .setResourceOutputApk(resOutBaseNameFile)
        .setProguardOutputFile(proguardOutputFile)
        .setMainDexListProguardOutputFile(mainDexListProguardOutputFile)
        .setVariantType(params.variantType)
        .setDebuggable(params.debuggable)
        .setResourceConfigs(params.resourceConfigs)
        .setSplits(params.multiOutputPolicySplitList)
        .setPreferredDensity(preferredDensity)
        .setPackageId(params.packageId)
        .setAllowReservedPackageId(
            params.packageId != null && params.packageId < FeatureSetMetadata.BASE_ID
        )
        .setDependentFeatures(featurePackagesBuilder.build())
        .setImports(params.imports)
        .setIntermediateDir(params.incrementalFolder)
        .setAndroidJarPath(params.androidJarPath)
        .setUseConditionalKeepRules(params.useConditionalKeepRules)
        //参数太多 还是再省去一些吧。。。

        try {
            getAaptDaemon(params.aapt2ServiceKey!!).use { aaptDaemon ->
                //构造完参数开始进行资源链接
                AndroidBuilder.processResources(
                    aaptDaemon,
                    configBuilder.build(),
                    LoggerWrapper(
                        Logging.getLogger(
                            LinkApplicationAndroidResourcesTask::class.java
                        )
                    )
                )
            }
        } catch (e: Aapt2Exception) {
            throw rewriteLinkException(
                e, MergingLog(params.mergeBlameFolder)
            )
        }

        //再省略掉一部分代码。。。。
        //存储一下链接的结果.
        appendOutput(
            BuildOutput(
                InternalArtifactType.PROCESSED_RES,
                params.apkData,
                resOutBaseNameFile,
                params.manifestOutput.properties
            ),
            params.resPackageOutputFolder
        )
}

invokeAaptForSplit方法特别的长,上面都是我去掉很多代码后精炼出来的逻辑。
首先第一步是通过AaptPackageConfig.Builder把所有链接需要用到的参数序列化成AaptPackageConfig对象,aapt2的link过程需要的参数特别的多。

参数准备好后会调用getAaptDaemon来开始链接资源,这个getAaptDaemon是不是似曾相识的,没错,它就是上一章里讲资源文件编译时详细介绍过的,因此这里的kt block aaptDaemon 也正式LeasableAaptDaemon对象了,前面的编译文章也介绍过了,LeasableAaptDaemon并不负责资源的编译工作,它只是做了一些状态的简单检查,真正的资源编译链接时由Aapt2DaemonImpl来完成的。

这里没有直接调用Aapt2DaemonImpl的link方法,而是先调用了AndroidBuilder.processResources,我们直接跟进去看下它的代码实现:

public static void processResources(
        @NonNull BlockingResourceLinker aapt,
        @NonNull AaptPackageConfig aaptConfig,
        @NonNull ILogger logger)
        throws IOException, ProcessException {

    try {
        aapt.link(aaptConfig, logger);
    } catch (Aapt2Exception | Aapt2InternalException e) {
        throw e;
    } catch (Exception e) {
        throw new ProcessException("Failed to execute aapt", e);
    }

    File sourceOut = aaptConfig.getSourceOutputDir();
    if (sourceOut != null) {
        // Figure out what the main symbol file's package is.
        String mainPackageName = aaptConfig.getCustomPackageForR();
        if (mainPackageName == null) {
            mainPackageName =
                    SymbolUtils.getPackageNameFromManifest(aaptConfig.getManifestFile());
        }

        // Load the main symbol file.
        File mainRTxt = new File(aaptConfig.getSymbolOutputDir(), "R.txt");
        SymbolTable mainSymbols =
                mainRTxt.isFile()
                        ? SymbolIo.readFromAapt(mainRTxt, mainPackageName)
                        : SymbolTable.builder().tablePackage(mainPackageName).build();

        // For each dependency, load its symbol file.
        Set depSymbolTables =
                SymbolUtils.loadDependenciesSymbolTables(
                        aaptConfig.getLibrarySymbolTableFiles());

        boolean finalIds = true;
        if (aaptConfig.getVariantType().isAar()) {
            finalIds = false;
        }
        RGeneration.generateRForLibraries(mainSymbols, depSymbolTables, sourceOut, finalIds);
    }
}

可以看见其实内部也是先调用了Aapt2DaemonImpl的link方法进行资源的链接,成功之后会生成R文件并且保存下来。

资源的链接过程是跟编译类似的,首先link方法会检查进程状态,没有创建的话会先创建出aapt守护进程,代码如下:

override fun link(request: AaptPackageConfig, logger: ILogger) {
    checkStarted()
    try {
        doLink(request, logger)
    } catch (e: Aapt2Exception) {
        // Propagate errors in the users sources directly.
        throw e
    } catch (e: TimeoutException) {
        handleError("Link timed out", e)
    } catch (e: Exception) {
        handleError("Unexpected error during link", e)
    }
}

private fun checkStarted() {
    when (state) {
        State.NEW -> {
            logger.verbose("%1\$s: starting", displayName)
            try {
                startProcess()
            } catch (e: TimeoutException) {
                handleError("Daemon startup timed out", e)
            } catch (e: Exception) {
                handleError("Daemon startup failed", e)
            }
            state = State.RUNNING
        }
        State.RUNNING -> {
            // Already ready
        }
        State.SHUTDOWN -> error("$displayName: Cannot restart a shutdown process")
    }
}

doLink是个抽象方法,Aapt2DaemonImpl实现了这个方法,代码如下:

override fun doLink(request: AaptPackageConfig, logger: ILogger) {
    val waitForTask = WaitForTaskCompletion(displayName, logger)
    try {
        processOutput.delegate = waitForTask
        Aapt2DaemonUtil.requestLink(writer, request)
        val result = waitForTask.future.get(daemonTimeouts.link, daemonTimeouts.linkUnit)
        when (result) {
            is WaitForTaskCompletion.Result.Succeeded -> { }
            is WaitForTaskCompletion.Result.Failed -> {
                val configWithResourcesListed =
                    if (request.intermediateDir != null) {
                        request.copy(listResourceFiles = true)
                    } else {
                        request
                    }
                val args =
                    makeLinkCommand(configWithResourcesListed).joinToString("\\\n        ")

                throw Aapt2Exception.create(
                    logger = logger,
                    description = "Android resource linking failed",
                    output = result.stdErr,
                    processName = displayName,
                    command = "$aaptPath link $args"
                )
            }
            is WaitForTaskCompletion.Result.InternalAapt2Error -> {
                throw result.failure
            }
        }
    } finally {
        processOutput.delegate = noOutputExpected
    }
}

可以看见doLink几乎是跟doCompile一样的逻辑了,如果还没有看上一篇的资源编译分析文章的,这里我建议大家还是先看下,链接的过程我就不再多详细分析了,大概的流程是:
Aapt2DaemonUtil.requestLink内部会通过AaptV2CommandBuilder的makeLinkCommand方法把前面准备好的链接参数拼接成aapt命令参数,makeLinkCommand方法很长,本质上就是参数拼接,代码这里就不贴了
最后会把拼接好的参数push到aapt进程去,等待aapt进程执行完毕读取链接结果返回。
链接后生成的资源包会被保存到intermediates/processed_res/debug/processDebugResources/out/resources-debug.ap_下,R.txt文件会被保存到/build/intermediates/symbols/debug/R.txt下。整个过程大概就是这样,这里顺带提一下,高级版本AGP资源的编译过程虽然依然是通过启动aapt进程,写入aapt命令来完成的,但是链接过程就不太一样了,链接过程没有直接调用aapt进程来完成,而是在代码里直接依赖了aapt的代码,链接时是直接调用aapt相关的链接函数来完成的。

结语

当你看懂了AGP的资源编译过程后,你会发现资源的链接过程实在是太简单了,无非就是启动aapt进程,把编译后的.arsc.flat产物扔过去给aapt进程处理,还有主要就是编译跟链接都共用了一套API,最终实现过程都由Aapt2DaemonImpl来完成。到这里AGP关于资源部分的内容就已经讲完了,感谢诸位的支持。

你可能感兴趣的:(AGP资源编译过程分析二link)