本篇章里分析的AGP源碼都是基于3.4.2版本的洼冻,很老的版本崭歧,也沒辦法,因為公司里用的就是3.4.2. 撞牢。率碾。
簡介
在AGP里面,aapt(Android Asset Packaging Tool)擔(dān)任著資源編譯的角色屋彪,aapt前后經(jīng)歷了兩個版本所宰,aapt以及aapt2,aapt2在AGP3.0之后就已經(jīng)是默認的資源編譯工具了畜挥,因此我們現(xiàn)在接觸到的都是aapt2仔粥。
aapt 1.0
aapt 1.0的年代,資源編譯并不支持增量蟹但,這意味著修改一個資源文件躯泰,項目里所有資源都得被重新編譯打包。隨著項目變得龐大华糖,資源越來越多的同時麦向,編譯速度也會變得越來越慢了。aapt 2.0
因為aapt 1.0版本存在著明顯的缺陷客叉,對此谷歌對它進行了升級改造磕蛇,主要是把資源的編譯拆解為兩個階段:編譯階段景描,鏈接階段。編譯階段會把資源文件編譯成文件后綴為.arsc.flat
的二進制文件秀撇,而鏈接階段會把編譯好的.arsc.flat
文件鏈接成資源包(在AGP里就是resources-debug.ap_
),當(dāng)有資源被修改了向族,只需要重新編譯修改資源呵燕,然后重新鏈接就可以了。編譯時會檢查語法件相,鏈接時會檢查符號再扭,過程是跟c++類似的。
因為aapt2把資源編譯過程分解成兩個步驟夜矗,這也使得資源的增量編譯變得可行了泛范,關(guān)于aapt2的更詳細介紹大家可以看官方給這篇文檔AAPT2。下面我將用兩篇博客來淺析下AGP資源的編譯跟鏈接過程紊撕,大神過路莫笑罢荡。
資源compile
之前我的文章介紹過java的編譯是由compileDebugJavaXXX
提供的,kotlin的編譯是由compileDebugKotlin
提供的对扶,在慣性思維作用下区赵,期初我在研究資源編譯時,也是在找compileDebugResources之類的任務(wù)浪南,找半天沒找著笼才,最后才發(fā)現(xiàn),資源編譯居然是在mergeResource
任務(wù)里完成的络凿,以前以為它只是檢查資源合并資源作用骡送,沒想到合并完資源后順便的也把編譯資源也給做了(library模塊不會編譯資源,而是copy resource)
在AGP里絮记,資源的編譯過程大概可以分解為以下三部分
- 讀取依賴并且解析出resources
- 進行資源的merge并且保存本地
- 編譯所有資源文件
下面我們來一一的分析每個部分細節(jié)摔踱。
讀取解析resources資源
MergeResources的任務(wù)入口函數(shù)有doFullTaskAction
跟doIncrementalTaskAction
,顧名思義的到千,一個是全量編譯入口昌渤,一個是增量編譯入口,但仔細看增量編譯方法憔四,其內(nèi)部的實現(xiàn)跟全量編譯方法實現(xiàn)是差不多膀息,里面只是做了些是否支持增量編譯之類的檢測工作。這里我們只分析全量編譯的入口函數(shù)了赵。
protected void doFullTaskAction() throws IOException, JAXBException {
//省略掉部分代碼潜支。。柿汛。
//1. 獲取所有依賴資源
List<ResourceSet> resourceSets = getConfiguredResourceSets(preprocessor);
//2. 創(chuàng)建資源合并工作類ResourceMerger
ResourceMerger merger = new ResourceMerger(minSdk.get());
//3.創(chuàng)建資源編譯器冗酿,對于library其實僅僅是個文件拷貝器
try (ResourceCompilationService resourceCompiler =
getResourceProcessor(
getBuilder(),
aapt2FromMaven,
workerExecutorFacade,
flags,
processResources)) {
//4讀取本地資源并且添加到ResourceMerger準備進行資源合并.
for (ResourceSet resourceSet : resourceSets) {
resourceSet.loadFromFiles(getILogger());
merger.addDataSet(resourceSet);
}
}
//省略掉部分代碼埠对。。裁替。
}
先是從configuration拿到依賴module資源项玛,接著是創(chuàng)建出ResourceMerger用作來做資源合并,getResourceProcessor
方法會返回真正編譯資源的類對象弱判,它的實現(xiàn)如下:
private static ResourceCompilationService getResourceProcessor(
@NonNull AndroidBuilder builder,
@Nullable FileCollection aapt2FromMaven,
@NonNull WorkerExecutorFacade workerExecutor,
ImmutableSet<Flag> 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
對象,作用是資源合并完馬上進行編譯遭商,這個對象是如何進行資源編譯的我們先放一放固灵,先接著往下分析。
創(chuàng)建出資源編譯類對象后劫流,開始把所有依賴資源文件解析加載到內(nèi)存中巫玻,ResourceSet
繼承了DataSet
,loadFromFiles
由后者提供困介,代碼如下:
public void loadFromFiles(ILogger logger) throws MergingException {
List<Message> 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
資源等等這些徒扶,解析的過程也十分的簡單粗暴,就是根據(jù)目錄名稱來判斷的根穷,如目錄為values
姜骡,就認定為是values
類型,代碼這里就不貼了屿良,效果如下:
解析完資源類型后圈澈,接著parseFolder
方法便開始解析目錄下的所有資源文件,首先也是會遍歷目錄下的所有資源文件尘惧,然后根據(jù)前面解析出來的不同的資源類型會有不同的處理邏輯康栈。
針對于
layout
drawable
資源,由于這類資源通常情況下一個xml文件僅描述一種資源喷橙,所以簡單的返回一個ResourceFile
對象就可以啥么,這個ResourceFile
對象就是當(dāng)前資源文件的描述。對于values目錄下面的資源贰逾,譬如strings.xml colors.xml悬荣,由于一個xml文件里面可以存在著多條的資源記錄,為了把所有資源item給讀取出來疙剑,會創(chuàng)建出
ValueResourceParser2
來解析xml內(nèi)容氯迂,后者會為每一條資源item創(chuàng)建出與之對應(yīng)的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就是當(dāng)前資源.
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對象是對應(yīng)的xml資源文件 而ResourceMergerItem
//是里面的所有資源項的描述
ValueResourceParser2 parser =
new ValueResourceParser2(file, mNamespace, mLibraryName);
parser.setTrackSourcePositions(mTrackSourcePositions);
parser.setCheckDuplicates(mCheckDuplicates);
List<ResourceMergerItem> 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文件里定義的資源內(nèi)容讀取出來禁灼,并且里面會檢查資源是否有重復(fù)。實際上就是一些xml的讀取邏輯轿曙,這里就不再仔細的分析下去了匾二,有興趣的讀者可以自行分析。
解析出來的每一條資源數(shù)據(jù)會用ResourceMergerItem
對象來保存拳芙,里面會記錄了資源的一些信息,譬如有以下資源文件
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="test_white_color">#ff0092</color>
</resources>
那么ValueResourceParser2
會給這條資源信息生成一個與之對應(yīng)的ResourceMergerItem
對象皮璧,這個對象會把資源類型舟扎、資源名稱、資源內(nèi)容等等信息保存下來悴务,給下面的資源merge過程使用
在解析完所有資源文件后睹限,最后會調(diào)用processNewResourceFile
方法把解析出來的結(jié)果存到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<I> dataItems = dataFile.getItems();
addDataFile(sourceFolder, dataFile);
for (I dataItem : dataItems) {
//以map結(jié)構(gòu)保存解析出來的內(nèi)容
mItems.put(dataItem.getKey(), dataItem);
if (setTouched) {
dataItem.setTouched();
}
}
}
代碼也比較的簡單讯檐,遍歷解析出來的所有資源并且存儲到map里面羡疗,map的key通過調(diào)用ResourceMergerItem::getKey
方法獲取,map的value就是ResourceMergerItem對象别洪,這里我們需要關(guān)心下key的生成規(guī)則叨恨,后面merge的時候需要用到。
public String getKey() {
String qualifiers = getQualifiers();
//省略部分代碼挖垛。痒钝。。
if (!qualifiers.isEmpty()) {
return typeName + "-" + qualifiers + "/" + getName();
}
return typeName + "/" + getName();
}
如果qualifiers是空的話痢毒,key就是類型/資源名
這樣的格式送矩,譬如上面我們提到的test_white_color
資源,它的類型是color
哪替,那么與之對應(yīng)的key便是color/test_white_color
了栋荸。
merge資源并保存到本地
回到MergeResources::doFullTaskAction
繼續(xù)往下看,接著是創(chuàng)建出MergedResourceWriter對象凭舶,并調(diào)用了ResourceMerger的mergeData
方法晌块,傳入了前面創(chuàng)建出來的MergedResourceWriter對象。mergeData
方法比較復(fù)雜库快,我們拆解出來分析摸袁。
public void mergeData(@NonNull MergeConsumer<I> consumer, boolean doCleanUp)
throws MergingException {
consumer.start(mFactory);
try {
// get all the items keys.
Set<String> dataItemKeys = new HashSet<>();
//1. 遍歷所有資源把key存起來。
for (S dataSet : mDataSets) {
// quick check on duplicates in the resource set.
dataSet.checkItems();
ListMultimap<String, I> 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<I> items = new ArrayList<>(mDataSets.size());
for (S dataSet : mDataSets) {
// look for the resource key in the set
ListMultimap<String, I> itemMap = dataSet.getDataMap();
if (itemMap.containsKey(dataItemKey)) {
List<I> setItems = itemMap.get(dataItemKey);
items.addAll(setItems);
}
}
mergeItems(dataItemKey, items, consumer);
continue;
}
}
}
//省略部分代碼...
}
這里的dataSet其實就是前面add進去的ResourceSet對象义屏,首先是調(diào)用getDataMap
獲取ResourceSet里面的所有資源靠汁,返回的map結(jié)構(gòu)前面我們已經(jīng)介紹過了蜂大,它的key是:類型/資源名,這種結(jié)構(gòu)蝶怔,value是ResourceMergerItem對象奶浦。先是把所有資源的key值存儲起來,接著判斷資源是否是styleable
資源踢星,如果是的話會通過mergeItems
方法來mergestyleable
資源澳叉,這里我們跳過直接往下分析。mergeData
后半部分的代碼邏輯比較復(fù)雜沐悦,我剔除了大部分的邏輯整理出以下代碼:
public void mergeData(@NonNull MergeConsumer<I> 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<String, I> itemMap = dataSet.getDataMap();
if (!itemMap.containsKey(dataItemKey)) {
continue;
}
List<I> 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對象本砰,找到對應(yīng)的資源并且調(diào)addItem
方法把資源回調(diào)給consumer碴裙,consumer其實就是前面創(chuàng)建的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
方法返回,它對應(yīng)的值有好幾種類型珠十,譬如version類型料扰,如果我們的新建的資源目錄時帶-v23這種版本號的,那么getQualifiers
返回的就是對應(yīng)的版本號值,又譬如對于string資源,涉及到多語言的話getQualifiers
返回的就是對應(yīng)的語言類型计贰,如en
zh
zh-rCN
等等。
如果是非values目錄下的資源拯钻,如layout
資源帖努,drawable
等等這些會構(gòu)造出CompileResourceRequest
對象把需要編譯的資源信息保存起來,對于全量編譯來講粪般,item.isTouched
方法恒返回true拼余,這個是在前面介紹的解析資源那part的ResourceSet::processNewResourceFile
方法里面把isTouched強行設(shè)置為true了。
前面的mergeData
處理完資源后亩歹,接著就開始把該合并的資源合并后寫到本地匙监,然后進行資源的編譯。
在mergeData
處理完資源后接著會調(diào)用MergedResourceWriter對象的end
方法小作,這個方法做了兩件事情亭姥,第一,先把前面合并的資源保存到本地顾稀,第二致份,調(diào)用資源編譯API編譯資源文件。我們先來看第一部分
首先是調(diào)用了父類的end
方法础拨,后者會調(diào)用postWriteAction
抽象方法,并且等它執(zhí)行結(jié)束绍载,MergedResourceWriter實現(xiàn)了這個抽象方法
@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;
//創(chuàng)建出合并資源文件
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<Node> 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
方法也是比較復(fù)雜的,讓了方便讀者理解矫夯,我已經(jīng)把核心代碼邏輯抽出來鸽疾,不相關(guān)的其他代碼邏輯先剔除掉了。
首先是創(chuàng)建出merged.dir
目錄训貌,合并后的資源會被保存到這里制肮,接著是創(chuàng)建出資源保存的目錄跟保存資源文件名。目錄名就是values-qualifier
這樣的格式递沪,譬如values-v23
values-zh-CN
等等這些豺鼻,文件名跟目錄名格式一樣,只是會加上.xml文件后綴款慨。
把目錄跟文件創(chuàng)建出來后接著是遍歷所有ResourceMergerItem對象儒飒,把它寫進values.xml
里面,這個資源文件就是合并所有資源后的資源文件檩奠,最后就是編譯這個資源文件桩了。
在前面我們紹過附帽,如果是非values
資源的話,會構(gòu)建出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()));
}
//省略部分代碼洁段。应狱。。
}
同樣的為了方便讀者理解祠丝,大部分不相關(guān)的代碼邏輯已被我剔除了疾呻。整理后的代碼邏輯已經(jīng)是相當(dāng)?shù)那逦耍褪潜闅v隊列写半,把所有等待編譯的資源文件統(tǒng)統(tǒng)提交到mResourceCompiler
去進行編譯岸蜗。
資源編譯過程
關(guān)于mResourceCompiler
對象,前面我們已經(jīng)是介紹過了叠蝇,對于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
的時候會把這些編譯請求任務(wù)提交給線程池去做股囊。
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的包裝對象更啄,內(nèi)部會調(diào)用getAaptDaemon
方法稚疹,后者會通過WorkerActionServiceRegistry拿到前面注冊進去的RegisteredAaptService對象以及對象里比較重要的成員變量:aapt守護進程管理器Aapt2DaemonManager
Aapt2DaemonManager內(nèi)部維護了aapt進程池,當(dāng)調(diào)用leaseDaemon
方法時,會首先判斷池子里有沒有空閑aapt進程内狗,有就直接返回怪嫌,沒有的話會重新創(chuàng)建一個新的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方法里面設(shè)置進去的回調(diào)柳沙,代碼如下:
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只是用作來判斷一些狀態(tài),本質(zhì)上最終的編譯任務(wù)還是由Aapt2DaemonImpl來完成的数初。
回到Aapt2CompileWithBlameRunnable類找爱,
override fun run() {
//省略部分代碼。泡孩。车摄。
//這里的daemon其實就是LeasedAaptDaemon,LeasedAaptDaemon只是做一些
//狀態(tài)的檢測仑鸥,真正的資源編譯是由Aapt2DaemonImpl來完成
daemon.compile(request, logger)
}
現(xiàn)在我們已經(jīng)知道了這里的daemon
其實是LeasedAaptDaemon對象吮播,但是LeasedAaptDaemon并不參與資源的編譯,最終資源的編譯是由Aapt2DaemonImpl來完成的眼俊,我們直接看Aapt2DaemonImpl的代碼
Aapt2DaemonImpl繼承Aapt2Daemon薄料,并且實現(xiàn)了它的doCompile
doLink
以及startProcess
stopProcess
等抽象方法,前者是資源的編譯跟鏈接相關(guān)的泵琳,后者是進程相關(guān)的。
compile
方法代碼實現(xiàn)如下:
override fun compile(request: CompileResourceRequest, logger: ILogger) {
checkStarted()
//省略部分代碼誊役。获列。。
doCompile(request, logger)
}
先是調(diào)用了checkStarted
檢查當(dāng)前的進程狀態(tài)蛔垢,需要開啟進程的話會調(diào)用startProcess
方法來創(chuàng)建新的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來創(chuàng)建艺玲,aaptCommand指向了aapt的可執(zhí)行文件括蝠,我的Mac電腦就是/Users/nls/.gradle/caches/transforms-2/files-2.1/2808f45549b8e37bfeb50699ed4844d4/aapt2-3.4.2-5326820-osx/aapt2
進程創(chuàng)建成功后,接著會通過GrabProcessOutput類來設(shè)置進程的輸入輸出句柄饭聚,用作來跟進程打交道的忌警,譬如往進程寫入命令,從進程里讀取執(zhí)行結(jié)果等等秒梳。沒了解過Java ProcessBuilder的可以先看下這篇文章 Java ProcessBuilder
進程創(chuàng)建完了接著會調(diào)用doCompile
開始編譯資源法绵,代碼如下:
override fun doCompile(request: CompileResourceRequest, logger: ILogger) {
val waitForTask = WaitForTaskCompletion(displayName, logger)
try {
processOutput.delegate = waitForTask
//往appt進程寫入編譯命令
Aapt2DaemonUtil.requestCompile(writer, request)
//省略部分代碼箕速。。朋譬。
//等待進程執(zhí)行完畢并且讀取編譯結(jié)果.
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
方法負責(zé)構(gòu)造aapt的執(zhí)行命令跟參數(shù)盐茎,并且把命令行push到aapt進程去,這里的writer便是前面創(chuàng)建的aapt進程寫句柄徙赢,代碼如下:
public static void requestLink(@NonNull Writer writer, @NonNull AaptPackageConfig command)
throws IOException {
ImmutableList<String> 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<String> {
val parameters = ImmutableList.Builder<String>()
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
會把構(gòu)造好的命令push給aapt進程執(zhí)行字柠,
private static void request(Writer writer, String command, Iterable<String> 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
目錄下面就能看到剛才構(gòu)建好的資源文件了
結(jié)語
實際上MergeResources的過程比我們上面分析的復(fù)雜多了,只不過我們只管全量編譯過程犀忱,很多不相關(guān)的條件分支代碼都被我刪掉忽略了募谎。實際上也不是每次都需要通過configuration來獲取依賴moduel資源的,也不是每次都要通過創(chuàng)建ValueResourceParser2對象來解析資源阴汇,合并過的資源信息會被保存一份build/intermediates/incremental/mergeDebugResources/merger.xml
來数冬,下次只需要解析merger.xml 文件。這些都是在增量編譯的時候才會有的邏輯搀庶,至于增量編譯的流程這里就不再分析了拐纱。