本篇章里分析的AGP源码都是基于3.4.2版本的,很老的版本,也没办法,因为公司里用的就是3.4.2. 。。
简介
在AGP里面,aapt(Android Asset Packaging Tool)担任着资源编译的角色,aapt前后经历了两个版本,aapt以及aapt2,aapt2在AGP3.0之后就已经是默认的资源编译工具了,因此我们现在接触到的都是aapt2。
aapt 1.0
aapt 1.0的年代,资源编译并不支持增量,这意味着修改一个资源文件,项目里所有资源都得被重新编译打包。随着项目变得庞大,资源越来越多的同时,编译速度也会变得越来越慢了。aapt 2.0
因为aapt 1.0版本存在着明显的缺陷,对此谷歌对它进行了升级改造,主要是把资源的编译拆解为两个阶段:编译阶段,链接阶段。编译阶段会把资源文件编译成文件后缀为.arsc.flat
的二进制文件,而链接阶段会把编译好的.arsc.flat
文件链接成资源包(在AGP里就是resources-debug.ap_
),当有资源被修改了,只需要重新编译修改资源,然后重新链接就可以了。编译时会检查语法,链接时会检查符号,过程是跟c++类似的。
因为aapt2把资源编译过程分解成两个步骤,这也使得资源的增量编译变得可行了,关于aapt2的更详细介绍大家可以看官方给这篇文档AAPT2。下面我将用两篇博客来浅析下AGP资源的编译跟链接过程,大神过路莫笑。
资源compile
之前我的文章介绍过java的编译是由compileDebugJavaXXX
提供的,kotlin的编译是由compileDebugKotlin
提供的,在惯性思维作用下,期初我在研究资源编译时,也是在找compileDebugResources之类的任务,找半天没找着,最后才发现,资源编译居然是在mergeResource
任务里完成的,以前以为它只是检查资源合并资源作用,没想到合并完资源后顺便的也把编译资源也给做了(library模块不会编译资源,而是copy resource)
在AGP里,资源的编译过程大概可以分解为以下三部分
- 读取依赖并且解析出resources
- 进行资源的merge并且保存本地
- 编译所有资源文件
下面我们来一一的分析每个部分细节。
读取解析resources资源
MergeResources的任务入口函数有doFullTaskAction
跟doIncrementalTaskAction
,顾名思义的,一个是全量编译入口,一个是增量编译入口,但仔细看增量编译方法,其内部的实现跟全量编译方法实现是差不多,里面只是做了些是否支持增量编译之类的检测工作。这里我们只分析全量编译的入口函数。
protected void doFullTaskAction() throws IOException, JAXBException {
//省略掉部分代码。。。
//1. 获取所有依赖资源
List resourceSets = getConfiguredResourceSets(preprocessor);
//2. 创建资源合并工作类ResourceMerger
ResourceMerger merger = new ResourceMerger(minSdk.get());
//3.创建资源编译器,对于library其实仅仅是个文件拷贝器
try (ResourceCompilationService resourceCompiler =
getResourceProcessor(
getBuilder(),
aapt2FromMaven,
workerExecutorFacade,
flags,
processResources)) {
//4读取本地资源并且添加到ResourceMerger准备进行资源合并.
for (ResourceSet resourceSet : resourceSets) {
resourceSet.loadFromFiles(getILogger());
merger.addDataSet(resourceSet);
}
}
//省略掉部分代码。。。
}
先是从configuration拿到依赖module资源,接着是创建出ResourceMerger用作来做资源合并,getResourceProcessor
方法会返回真正编译资源的类对象,它的实现如下:
private static ResourceCompilationService getResourceProcessor(
@NonNull AndroidBuilder builder,
@Nullable FileCollection aapt2FromMaven,
@NonNull WorkerExecutorFacade workerExecutor,
ImmutableSet flags,
boolean processResources) {
//对于library模块返回的是个文件拷贝器
if (!processResources) {
return CopyToOutputDirectoryResourceCompilationService.INSTANCE;
}
Aapt2ServiceKey aapt2ServiceKey =
Aapt2DaemonManagerService.registerAaptService(
aapt2FromMaven, builder.getBuildToolInfo(), builder.getLogger());
//对于application模块返回的是WorkerExecutorResourceCompilationService对象
return new WorkerExecutorResourceCompilationService(workerExecutor, aapt2ServiceKey);
}
getResourceProcessor
对于不同模块会返回不同的对象,对于library模块来说返回的是个文件拷贝器,作用是把merge后的资源拷贝到application模块下,由application来编译。对于application模块返回的是WorkerExecutorResourceCompilationService
对象,作用是资源合并完马上进行编译,这个对象是如何进行资源编译的我们先放一放,先接着往下分析。
创建出资源编译类对象后,开始把所有依赖资源文件解析加载到内存中,ResourceSet
继承了DataSet
,loadFromFiles
由后者提供,代码如下:
public void loadFromFiles(ILogger logger) throws MergingException {
List errors = new ArrayList<>();
for (File file : mSourceFiles) {
if (file.isDirectory()) {
try {
readSourceFolder(file, logger);
} catch (MergingException e) {
errors.addAll(e.getMessages());
}
} else if (file.isFile()) {
// TODO support resource bundle
loadFile(file, file, logger);
}
}
}
这段代码很简单,就是遍历所有资源文件读取文件,我们先分析从目录读取资源的逻辑代码readSourceFolder
protected void readSourceFolder(File sourceFolder, ILogger logger)
throws MergingException {
File[] folders = sourceFolder.listFiles();
if (folders != null) {
for (File folder : folders) {
if (folder.isDirectory() && !isIgnored(folder)) {
//1.先返回资源类型.
FolderData folderData = getFolderData(folder);
if (folderData != null) {
try {
//2.开始解析目录下面的资源
parseFolder(sourceFolder, folder, folderData, logger);
} catch (MergingException e) {
errors.addAll(e.getMessages());
}
}
}
}
先是解析出资源的类型,譬如是drawable
资源还是layout
资源colors
资源等等这些,解析的过程也十分的简单粗暴,就是根据目录名称来判断的,如目录为values
,就认定为是values
类型,代码这里就不贴了,效果如下:
解析完资源类型后,接着parseFolder
方法便开始解析目录下的所有资源文件,首先也是会遍历目录下的所有资源文件,然后根据前面解析出来的不同的资源类型会有不同的处理逻辑。
针对于
layout
drawable
资源,由于这类资源通常情况下一个xml文件仅描述一种资源,所以简单的返回一个ResourceFile
对象就可以,这个ResourceFile
对象就是当前资源文件的描述。对于values目录下面的资源,譬如strings.xml colors.xml,由于一个xml文件里面可以存在着多条的资源记录,为了把所有资源item给读取出来,会创建出
ValueResourceParser2
来解析xml内容,后者会为每一条资源item创建出与之对应的ResourceMergerItem
对象用作来描述资源,最终也是返回ResourceFile
对象用来描述此xml资源文件,代码如下:
private void parseFolder(File sourceFolder, File folder, FolderData folderData, ILogger logger)
throws MergingException {
File[] files = folder.listFiles();
if (files != null && files.length > 0) {
for (File file : files) {
if (!file.isFile() || isIgnored(file)) {
continue;
}
ResourceFile resourceFile = createResourceFile(file, folderData, logger);
processNewResourceFile(sourceFolder, resourceFile);
}
}
}
private ResourceFile createResourceFile(@NonNull File file,
@NonNull FolderData folderData, @NonNull ILogger logger) throws MergingException {
if (folderData.type != null) {
//对于layout drawable这类资源由于一个xml通常仅描述着一个单独资源.
//因此这里直接返回ResourceFile对象 ResourceMergerItem就是当前资源.
return new ResourceFile(
file,
new ResourceMergerItem(
getNameForFile(file),
mNamespace,
folderData.type,
null,
mLibraryName),
folderData.folderConfiguration);
} else {
try {
//对于values目录下的资源 譬如string.xml, colors.xml这类资源由于一个xml
//里面会有多条资源item, ValueResourceParser2会把所有资源项给解析出来
//因此这里返回的ResourceFile对象是对应的xml资源文件 而ResourceMergerItem
//是里面的所有资源项的描述
ValueResourceParser2 parser =
new ValueResourceParser2(file, mNamespace, mLibraryName);
parser.setTrackSourcePositions(mTrackSourcePositions);
parser.setCheckDuplicates(mCheckDuplicates);
List items = parser.parseFile();
return new ResourceFile(file, items, folderData.folderConfiguration);
} catch (MergingException e) {
logger.error(e, "Failed to parse %s", file.getAbsolutePath());
throw e;
}
}
}
ValueResourceParser2
会解析xml文件格式,把xml文件里定义的资源内容读取出来,并且里面会检查资源是否有重复。实际上就是一些xml的读取逻辑,这里就不再仔细的分析下去了,有兴趣的读者可以自行分析。
解析出来的每一条资源数据会用ResourceMergerItem
对象来保存,里面会记录了资源的一些信息,譬如有以下资源文件
#ff0092
那么ValueResourceParser2
会给这条资源信息生成一个与之对应的ResourceMergerItem
对象,这个对象会把资源类型、资源名称、资源内容等等信息保存下来,给下面的资源merge过程使用
在解析完所有资源文件后,最后会调用processNewResourceFile
方法把解析出来的结果存到map里面,代码如下:
private void processNewResourceFile(File sourceFolder, ResourceFile resourceFile)
throws MergingException {
if (resourceFile != null) {
if (resourceFile.getType() == DataFile.FileType.GENERATED_FILES
&& mGeneratedSet != null) {
mGeneratedSet.processNewDataFile(sourceFolder, resourceFile, true);
} else {
processNewDataFile(sourceFolder, resourceFile, true /*setTouched*/);
}
}
}
protected void processNewDataFile(@NonNull File sourceFolder,
@NonNull F dataFile,
boolean setTouched) throws MergingException {
//这里的item其实就是前面解析出来的ResourceMergerItem对象
Collection dataItems = dataFile.getItems();
addDataFile(sourceFolder, dataFile);
for (I dataItem : dataItems) {
//以map结构保存解析出来的内容
mItems.put(dataItem.getKey(), dataItem);
if (setTouched) {
dataItem.setTouched();
}
}
}
代码也比较的简单,遍历解析出来的所有资源并且存储到map里面,map的key通过调用ResourceMergerItem::getKey
方法获取,map的value就是ResourceMergerItem对象,这里我们需要关心下key的生成规则,后面merge的时候需要用到。
public String getKey() {
String qualifiers = getQualifiers();
//省略部分代码。。。
if (!qualifiers.isEmpty()) {
return typeName + "-" + qualifiers + "/" + getName();
}
return typeName + "/" + getName();
}
如果qualifiers是空的话,key就是类型/资源名
这样的格式,譬如上面我们提到的test_white_color
资源,它的类型是color
,那么与之对应的key便是color/test_white_color
了。
merge资源并保存到本地
回到MergeResources::doFullTaskAction
继续往下看,接着是创建出MergedResourceWriter对象,并调用了ResourceMerger的mergeData
方法,传入了前面创建出来的MergedResourceWriter对象。mergeData
方法比较复杂,我们拆解出来分析。
public void mergeData(@NonNull MergeConsumer consumer, boolean doCleanUp)
throws MergingException {
consumer.start(mFactory);
try {
// get all the items keys.
Set dataItemKeys = new HashSet<>();
//1. 遍历所有资源把key存起来。
for (S dataSet : mDataSets) {
// quick check on duplicates in the resource set.
dataSet.checkItems();
ListMultimap map = dataSet.getDataMap();
dataItemKeys.addAll(map.keySet());
}
// loop on all the data items.
for (String dataItemKey : dataItemKeys) {
//2. 如果是styleable资源的话进行styleable的merge.
if (requiresMerge(dataItemKey)) {
// get all the available items, from the lower priority, to the higher
// priority
List items = new ArrayList<>(mDataSets.size());
for (S dataSet : mDataSets) {
// look for the resource key in the set
ListMultimap itemMap = dataSet.getDataMap();
if (itemMap.containsKey(dataItemKey)) {
List setItems = itemMap.get(dataItemKey);
items.addAll(setItems);
}
}
mergeItems(dataItemKey, items, consumer);
continue;
}
}
}
//省略部分代码...
}
这里的dataSet其实就是前面add进去的ResourceSet对象,首先是调用getDataMap
获取ResourceSet里面的所有资源,返回的map结构前面我们已经介绍过了,它的key是:类型/资源名,这种结构,value是ResourceMergerItem对象。先是把所有资源的key值存储起来,接着判断资源是否是styleable
资源,如果是的话会通过mergeItems
方法来mergestyleable
资源,这里我们跳过直接往下分析。mergeData
后半部分的代码逻辑比较复杂,我剔除了大部分的逻辑整理出以下代码:
public void mergeData(@NonNull MergeConsumer consumer, boolean doCleanUp)
throws MergingException {
consumer.start(mFactory);
try {
// loop on all the data items.
for (String dataItemKey : dataItemKeys) {
//省略部分代码。。。
setLoop: for (int i = mDataSets.size() - 1 ; i >= 0 ; i--) {
S dataSet = mDataSets.get(i);
// look for the resource key in the set
ListMultimap itemMap = dataSet.getDataMap();
if (!itemMap.containsKey(dataItemKey)) {
continue;
}
List items = itemMap.get(dataItemKey);
if (items.isEmpty()) {
continue;
}
for (int ii = items.size() - 1 ; ii >= 0 ; ii--) {
I item = items.get(ii);
//省略部分代码。。。
if (toWrite == null && !item.isRemoved()) {
toWrite = item;
}
if (toWrite != null && previouslyWritten != null) {
break setLoop;
}
}
}
//省略部分代码。。。。
if (toWrite == null) {
// nothing to write? delete only then.
assert previouslyWritten.isRemoved();
consumer.removeItem(previouslyWritten, null /*replacedBy*/);
} else if (previouslyWritten == null || previouslyWritten == toWrite) {
// easy one: new or updated res
consumer.addItem(toWrite);
}
}
} finally {
consumer.end();
}
}
收集完所有key之后,下面就是遍历所有ResourceSet对象,找到对应的资源并且调addItem
方法把资源回调给consumer,consumer其实就是前面创建的MergedResourceWriter对象,我们跟进去看下它的addItem
方法,代码如下:
public void addItem(@NonNull final ResourceMergerItem item) throws ConsumerException {
final ResourceFile.FileType type = item.getSourceType();
if (type == ResourceFile.FileType.XML_VALUES) {
//省略部分代码。。。
mValuesResMap.put(item.getQualifiers(), item);
} else {
//省略部分代码。。。
if (item.isTouched()) {
mCompileResourceRequests.add(
new CompileResourceRequest(file, getRootFolder(), folderName));
}
}
}
如果是values目录下面的xml资源会先保存到map里,map的key是getQualifiers
方法返回,它对应的值有好几种类型,譬如version类型,如果我们的新建的资源目录时带-v23这种版本号的,那么getQualifiers
返回的就是对应的版本号值,又譬如对于string资源,涉及到多语言的话getQualifiers
返回的就是对应的语言类型,如en
zh
zh-rCN
等等。
如果是非values目录下的资源,如layout
资源,drawable
等等这些会构造出CompileResourceRequest
对象把需要编译的资源信息保存起来,对于全量编译来讲,item.isTouched
方法恒返回true,这个是在前面介绍的解析资源那part的ResourceSet::processNewResourceFile
方法里面把isTouched强行设置为true了。
前面的mergeData
处理完资源后,接着就开始把该合并的资源合并后写到本地,然后进行资源的编译。
在mergeData
处理完资源后接着会调用MergedResourceWriter对象的end
方法,这个方法做了两件事情,第一,先把前面合并的资源保存到本地,第二,调用资源编译API编译资源文件。我们先来看第一部分
首先是调用了父类的end
方法,后者会调用postWriteAction
抽象方法,并且等它执行结束,MergedResourceWriter实现了这个抽象方法
@Override
protected void postWriteAction() throws ConsumerException {
//本地保存目录,合并后的资源将会保存到这个目录下
///Users/nls/Desktop/job/abooster/installer/build/intermediates/incremental/mergeDebugResources/merged.dir
File tmpDir = new File(mTemporaryDirectory, "merged.dir");
try {
FileUtils.cleanOutputDir(tmpDir);
} catch (IOException e) {
throw new ConsumerException(e);
}
// now write the values files.
for (String key : mValuesResMap.keySet()) {
String folderName = key.isEmpty() ?
ResourceFolderType.VALUES.getName() :
ResourceFolderType.VALUES.getName() + RES_QUALIFIER_SEP + key;
//创建出合并资源文件
File valuesFolder = new File(tmpDir, folderName);
File outFile = new File(valuesFolder, folderName + DOT_XML);
FileUtils.mkdirs(valuesFolder);
DocumentBuilder builder = mFactory.newDocumentBuilder();
Document document = builder.newDocument();
final String publicTag = ResourceType.PUBLIC.getName();
List publicNodes = null;
Node rootNode = document.createElement(TAG_RESOURCES);
document.appendChild(rootNode);
Collections.sort(items);
for (ResourceMergerItem item : items) {
Node nodeValue = item.getValue();
if (nodeValue != null && publicTag.equals(nodeValue.getNodeName())) {
if (publicNodes == null) {
publicNodes = Lists.newArrayList();
}
publicNodes.add(nodeValue);
continue;
}
// add a carriage return so that the nodes are not all on the same line.
// also add an indent of 4 spaces.
rootNode.appendChild(document.createTextNode("\n "));
ResourceFile source = item.getSourceFile();
Node adoptedNode = NodeUtils.adoptNode(document, nodeValue);
if (source != null) {
XmlUtils.attachSourceFile(
adoptedNode, new SourceFile(source.getFile()));
}
rootNode.appendChild(adoptedNode);
}
//省略部分代码。。。。
CompileResourceRequest request =
new CompileResourceRequest(
outFile,
getRootFolder(),
folderName,
pseudoLocalesEnabled,
crunchPng,
blame != null ? blame : ImmutableMap.of());
//编译合并后的资源文件
mResourceCompiler.submitCompile(request);
//后面的代码全被我省略了。。
}
}
postWriteAction
方法也是比较复杂的,让了方便读者理解,我已经把核心代码逻辑抽出来,不相关的其他代码逻辑先剔除掉了。
首先是创建出merged.dir
目录,合并后的资源会被保存到这里,接着是创建出资源保存的目录跟保存资源文件名。目录名就是values-qualifier
这样的格式,譬如values-v23
values-zh-CN
等等这些,文件名跟目录名格式一样,只是会加上.xml文件后缀。
把目录跟文件创建出来后接着是遍历所有ResourceMergerItem对象,把它写进values.xml
里面,这个资源文件就是合并所有资源后的资源文件,最后就是编译这个资源文件。
在前面我们绍过,如果是非values
资源的话,会构建出CompileResourceRequest对象用来保存要编译的资源信息,回到end
方法,在保存并且编译完合并资源后就开始编译其他资源,代码如下:
@Override
public void end() throws ConsumerException {
// Make sure all PNGs are generated first.
super.end();
//省略部分代码。。。
while (!mCompileResourceRequests.isEmpty()) {
CompileResourceRequest request = mCompileResourceRequests.poll();
//省略部分代码。。。
if (notCompiledOutputDirectory != null) {
File typeDir =
new File(
notCompiledOutputDirectory,
request.getInputDirectoryName());
FileUtils.mkdirs(typeDir);
FileUtils.copyFileToDirectory(fileToCompile, typeDir);
}
//开始编译资源
mResourceCompiler.submitCompile(
new CompileResourceRequest(
fileToCompile,
request.getOutputDirectory(),
request.getInputDirectoryName(),
pseudoLocalesEnabled,
crunchPng,
ImmutableMap.of(),
request.getInputFile()));
}
//省略部分代码。。。
}
同样的为了方便读者理解,大部分不相关的代码逻辑已被我剔除了。整理后的代码逻辑已经是相当的清晰了,就是遍历队列,把所有等待编译的资源文件统统提交到mResourceCompiler
去进行编译。
资源编译过程
关于mResourceCompiler
对象,前面我们已经是介绍过了,对于library模块来说返回的是个文件拷贝器,它会把merge后的资源文件拷贝到application模块去进行编译,因为过程比较简单,这里我们就不再做太多的分析了,对于application来说就是mResourceCompiler
其实就是个WorkerExecutorResourceCompilationService对象,它的代码如下:
override fun submitCompile(request: CompileResourceRequest) {
// b/73804575
requests.add(request)
}
override fun close() {
//省略部分代码。。
// b/73804575
workerExecutor.submit(Aapt2CompileWithBlameRunnable::class.java,
Aapt2CompileWithBlameRunnable.Params(aapt2ServiceKey, bucketRequests))
//省略部分代码。。。
}
WorkerExecutorResourceCompilationService的代码显然比它的名字简单得多了,submitCompile
也只是简单的保存下编译请求,close
的时候会把这些编译请求任务提交给线程池去做。
Aapt2CompileWithBlameRunnable代码如下:
class Aapt2CompileWithBlameRunnable @Inject constructor(
private val params: Params
) : Runnable {
override fun run() {
val logger = LoggerWrapper(Logging.getLogger(this::class.java))
useAaptDaemon(params.aapt2ServiceKey) { daemon ->
params.requests.forEach { request ->
try {
daemon.compile(request, logger)
} catch (e: Aapt2Exception) {
throw rewriteCompileException(e, request)
}
}
}
}
}
代码也是比较简单,useAaptDaemon
方法返回了aapt的包装对象,内部会调用getAaptDaemon
方法,后者会通过WorkerActionServiceRegistry拿到前面注册进去的RegisteredAaptService对象以及对象里比较重要的成员变量:aapt守护进程管理器Aapt2DaemonManager
Aapt2DaemonManager内部维护了aapt进程池,当调用leaseDaemon
方法时,会首先判断池子里有没有空闲aapt进程,有就直接返回,没有的话会重新创建一个新的aapt守护进程,代码如下:
fun leaseDaemon(): LeasedAaptDaemon {
val daemon =
pool.find { !it.busy } ?: newAaptDaemon()
daemon.busy = true
return LeasedAaptDaemon(daemon, this::returnProcess)
}
private fun newAaptDaemon(): LeasableAaptDaemon {
val displayId = latestDisplayId++
val process = daemonFactory.invoke(displayId)
val daemon = LeasableAaptDaemon(process, timeSource.read())
if (pool.isEmpty()) {
listener.firstDaemonStarted(this)
}
pool.add(daemon)
return daemon
}
daemonFactory
其实就是在Aapt2DaemonManagerService的registerAaptService方法里面设置进去的回调,代码如下:
serviceRegistry.registerService(key, {
val manager = Aapt2DaemonManager(logger = logger,
daemonFactory = { displayId ->
Aapt2DaemonImpl(
displayId = "#$displayId",
aaptExecutable = aaptExecutablePath,
daemonTimeouts = daemonTimeouts,
logger = logger)
},
expiryTime = daemonExpiryTimeSeconds,
expiryTimeUnit = TimeUnit.SECONDS,
listener = Aapt2DaemonManagerMaintainer())
RegisteredAaptService(manager)
})
这里可以清楚看到daemonFactory
其实就是kt block,因此newAaptDaemon
最终实例化的是Aapt2DaemonImpl对象。实例化出来的Aapt2DaemonImpl对象没有直接返回给外面使用,而是通过LeasedAaptDaemon对象又包了一层,LeasedAaptDaemon只是用作来判断一些状态,本质上最终的编译任务还是由Aapt2DaemonImpl来完成的。
回到Aapt2CompileWithBlameRunnable类,
override fun run() {
//省略部分代码。。。
//这里的daemon其实就是LeasedAaptDaemon,LeasedAaptDaemon只是做一些
//状态的检测,真正的资源编译是由Aapt2DaemonImpl来完成
daemon.compile(request, logger)
}
现在我们已经知道了这里的daemon
其实是LeasedAaptDaemon对象,但是LeasedAaptDaemon并不参与资源的编译,最终资源的编译是由Aapt2DaemonImpl来完成的,我们直接看Aapt2DaemonImpl的代码
Aapt2DaemonImpl继承Aapt2Daemon,并且实现了它的doCompile
doLink
以及startProcess
stopProcess
等抽象方法,前者是资源的编译跟链接相关的,后者是进程相关的。
compile
方法代码实现如下:
override fun compile(request: CompileResourceRequest, logger: ILogger) {
checkStarted()
//省略部分代码。。。
doCompile(request, logger)
}
先是调用了checkStarted
检查当前的进程状态,需要开启进程的话会调用startProcess
方法来创建新的aapt进程
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")
}
}
Aapt2DaemonImpl重写了startProcess,代码如下:
override fun startProcess() {
val waitForReady = WaitForReadyOnStdOut(displayName, logger)
processOutput.delegate = waitForReady
val processBuilder = ProcessBuilder(aaptCommand)
process = processBuilder.start()
writer = try {
GrabProcessOutput.grabProcessOutput(
process,
GrabProcessOutput.Wait.ASYNC,
processOutput)
process.outputStream.bufferedWriter(Charsets.UTF_8)
}
//省略部分代码。。。
}
可以看到,其实最终的aapt进程是通过ProcessBuilder来创建,aaptCommand指向了aapt的可执行文件,我的Mac电脑就是/Users/nls/.gradle/caches/transforms-2/files-2.1/2808f45549b8e37bfeb50699ed4844d4/aapt2-3.4.2-5326820-osx/aapt2
进程创建成功后,接着会通过GrabProcessOutput类来设置进程的输入输出句柄,用作来跟进程打交道的,譬如往进程写入命令,从进程里读取执行结果等等。没了解过Java ProcessBuilder的可以先看下这篇文章 Java ProcessBuilder
进程创建完了接着会调用doCompile
开始编译资源,代码如下:
override fun doCompile(request: CompileResourceRequest, logger: ILogger) {
val waitForTask = WaitForTaskCompletion(displayName, logger)
try {
processOutput.delegate = waitForTask
//往appt进程写入编译命令
Aapt2DaemonUtil.requestCompile(writer, request)
//省略部分代码。。。
//等待进程执行完毕并且读取编译结果.
val result = waitForTask.future.get(daemonTimeouts.compile, daemonTimeouts.compileUnit)
when (result) {
is WaitForTaskCompletion.Result.Succeeded -> {}
is WaitForTaskCompletion.Result.Failed -> {
val args = makeCompileCommand(request).joinToString(" \\\n ")
throw Aapt2Exception.create(
logger = logger,
description = "Android resource compilation failed",
output = result.stdErr,
processName = displayName,
command = "$aaptPath compile $args"
)
}
is WaitForTaskCompletion.Result.InternalAapt2Error -> {
throw result.failure
}
}
} finally {
processOutput.delegate = noOutputExpected
}
}
Aapt2DaemonUtil 的requestCompile
方法负责构造aapt的执行命令跟参数,并且把命令行push到aapt进程去,这里的writer便是前面创建的aapt进程写句柄,代码如下:
public static void requestLink(@NonNull Writer writer, @NonNull AaptPackageConfig command)
throws IOException {
ImmutableList args;
try {
args = AaptV2CommandBuilder.makeLinkCommand(command);
} catch (AaptException e) {
throw new IOException("Unable to make AAPT link command.", e);
}
request(writer, "l", args);
}
fun makeCompileCommand(request: CompileResourceRequest): ImmutableList {
val parameters = ImmutableList.Builder()
if (request.isPseudoLocalize) {
parameters.add("--pseudo-localize")
}
if (!request.isPngCrunching) {
// Only pass --no-crunch for png files and not for 9-patch files as that breaks them.
val lowerName = request.inputFile.path.toLowerCase(Locale.US)
if (lowerName.endsWith(SdkConstants.DOT_PNG)
&& !lowerName.endsWith(SdkConstants.DOT_9PNG)) {
parameters.add("--no-crunch")
}
}
if (request.partialRFile != null) {
parameters.add("--output-text-symbols", request.partialRFile!!.absolutePath)
}
parameters.add("--legacy")
//指定编译后的文件输出路径,譬如我的项目下这个路径是:
///Users/nls/Desktop/job/abooster/app/build/intermediates/res/merged/debug
parameters.add("-o", request.outputDirectory.absolutePath)
//需要进行编译的文件
parameters.add(request.inputFile.absolutePath)
return parameters.build()
}
最后request
会把构造好的命令push给aapt进程执行,
private static void request(Writer writer, String command, Iterable args)
throws IOException {
writer.write(command);
writer.write('\n');
for (String s : args) {
writer.write(s);
writer.write('\n');
}
// Finish the request
writer.write('\n');
writer.write('\n');
writer.flush();
}
我们到build/intermediates/res/merged/debug
目录下面就能看到刚才构建好的资源文件了
结语
实际上MergeResources的过程比我们上面分析的复杂多了,只不过我们只管全量编译过程,很多不相关的条件分支代码都被我删掉忽略了。实际上也不是每次都需要通过configuration来获取依赖moduel资源的,也不是每次都要通过创建ValueResourceParser2对象来解析资源,合并过的资源信息会被保存一份build/intermediates/incremental/mergeDebugResources/merger.xml
来,下次只需要解析merger.xml 文件。这些都是在增量编译的时候才会有的逻辑,至于增量编译的流程这里就不再分析了。