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