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<SymbolTable> 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关于资源部分的内容就已经讲完了,感谢诸位的支持。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容