變量修飾符
- 為什么App Module中的R.java文件的變量是final修飾而Lib Module中R.java文件卻不是?
R文件是由編譯器自動生成顾犹,每個模塊中的R文件的id都是從0x7f+resId+0001開始分配的倒庵,所以說多個模塊肯定會有資源沖突的(同名資源文件),其實lib module應該是沒有R.java文件的炫刷,只是as的一個語法支持擎宝,在編譯成apk時,會替換每個資源文件的id為具體的數(shù)值浑玛,而lib的資源文件的id是會變的绍申,故不能用final進行修飾
- 為什么將App Module中的switch-case語句拷貝到Lib Module中需要轉(zhuǎn)換成if-else?
由1可知顾彰,lib module中的資源文件id是可變的极阅,而在Java語法中,switch的參數(shù)必須是常量或者值涨享,否則會報語法錯誤筋搏,只需要修改成if-else即可解決
R.Java文件的生成規(guī)則
- 同一資源文件在不同的module下引用,大概率下資源id不同灰伟,那么最終打包是如何處理的拆又?
結(jié)論:最終打包出來的那些資源文件id,是會重新分配的(同一資源生成的id栏账,就算不在同一R.class中帖族,打包出來的id也是相同的)
下面做一個測試:
- app Module 依賴 test Module
- test模塊中的string.xml有個test_value字符資源
- 在test模塊引用這個資源文件,查看資源id為-1900022
- 在app模塊引用這個資源文件挡爵,查看資源id為-1900023
這個時候可以看出,相同的資源文件確實有可能出現(xiàn)id不相同的情況,打包成apk文件后,把它拖進AndroidStudio中,打開classes.dex
- 首先找到app包名下的R$stirng竖般,右鍵查看字節(jié)碼,發(fā)現(xiàn)test_value的id值由原來的-1900023變成了0x7f10005d
- 接著找到test模塊包名下的R$stirng茶鹃,查看字節(jié)碼發(fā)現(xiàn)id值也變成了0x7f10005d
由此看來涣雕,id值在打包成apk的時候艰亮,確實重新分配了,而且同一資源的id值也統(tǒng)一了
源碼分析:如何重新分配這些資源的id值的
很多任務都是在ApplicationTaskManager里的createTasksForVariantScope方法中創(chuàng)建的挣郭,現(xiàn)在再來看一下這個方法:
@Override
public void createTasksForVariantScope(
@NonNull final VariantScope variantScope, @NonNull List<VariantScope> variantScopesForLint) {
......
// Add a task to create the BuildConfig class
createBuildConfigTask(variantScope);
// Add a task to process the Android Resources and generate source files
createApkProcessResTask(variantScope);
......
}
可以看到在創(chuàng)建了BuildConfigTask任務后迄埃,接著調(diào)用了createApkProcessResTask()方法,查看注釋兑障,可以知道這個任務是用來處理資源和生成源文件的侄非,一步步點進去看看最終處理邏輯:
createApkProcessResTask() ->
createProcessResTask() ->
createNonNamespacedResourceTasks() ->
GenerateLibraryRFileTask.doFullTaskAction() ->
GenerateLibRFileRunnable.run() ->
SymbolExportUtils.processLibraryMainSymbolTable()
查看SymbolExportUtils.processLibraryMainSymbolTable()方法:
fun processLibraryMainSymbolTable() {
......
val tablesToWrite = processLibraryMainSymbolTable()
// Generate R.java files for main and dependencies
tablesToWrite.forEach { SymbolIo.exportToJava(it, sourceOut, false) }
......
}
看中間的的注釋,可以確定下一句代碼就是用來生成R.java文件的流译,它會為tablesToWrite里面的每一個item都生成一個R文件逞怨,看下tablesToWrite是怎么來的:
internal fun processLibraryMainSymbolTable(): List<SymbolTable> {
// Merge all the symbols together.
// We have to rewrite the IDs because some published R.txt inside AARs are using the
// wrong value for some types, and we need to ensure there is no collision in the
// file we are creating.
val allSymbols: SymbolTable = mergeAndRenumberSymbols(
finalPackageName, librarySymbols, depSymbolTables, platformSymbols
)
val mainSymbolTable = if (namespacedRClass) allSymbols.filter(librarySymbols) else allSymbols
// Generate R.txt file.
Files.createDirectories(symbolFileOut.parent)
SymbolIo.writeForAar(mainSymbolTable, symbolFileOut)
val tablesToWrite =
RGeneration.generateAllSymbolTablesToWrite(allSymbols, mainSymbolTable, depSymbolTables)
return tablesToWrite
}
看開頭的注釋:“We have to rewrite the IDs ”,就知道是必須重寫這些id的意思福澡,再看一下接下來調(diào)用的mergeAndRenumberSymbols()方法:
fun mergeAndRenumberSymbols(): SymbolTable {
......
// the ID value provider.
val idProvider = IdProvider.sequential()
......
}
可以看到調(diào)用了IdProvider.sequential()方法叠赦,這個方法是用來提供id的,看下它里面是怎樣實現(xiàn)的:
fun sequential(): IdProvider {
return object : IdProvider {
private val next = ShortArray(ResourceType.values().size)
override fun next(resourceType: ResourceType): Int {
val typeIndex = resourceType.ordinal
return 0x7f shl 24 or (typeIndex + 1 shl 16) or (++next[typeIndex]).toInt()
}
}
}
可以發(fā)現(xiàn)革砸,產(chǎn)生的id都會0x7f開頭的除秀,我們剛開始打包后資源文件的id值也是0x7f開頭的,到這里基本可以確定业岁,最終打包的資源id鳞仙,就是通過這個IdProvider的匿名子類來重新創(chuàng)建的,而且同一個資源所對應的id也是一樣的
項目中同名資源笔时,會不會覆蓋棍好,規(guī)則是怎么樣的?
我們來做一個測試:
- app Module中有test_value 資源允耿,依賴了同樣有test_value資源的module1
- app Module中有test_value 資源借笙,依賴了同樣有test_value資源的module1,module2
- app Module中無test_value 資源较锡,先后依賴了有test_value資源的module1业稼,module2
- app Module中無test_value 資源,先后依賴了有test_value資源的module2蚂蕴,module1
然后打包apk低散,拖進AndroidStudio,點開resources.arsc文件骡楼,定位到test_value熔号,會看到以下對應結(jié)果:
- test_value的值是app中的值
- test_value的值是app中的值
- test_value的值是module1中的值
- test_value的值是module2中的值
結(jié)論:多模塊開發(fā)中,不同模塊間如果有同名資源鸟整,那么最終采納的優(yōu)先級為:app的優(yōu)先級要高于依賴的module引镊,而module之間的優(yōu)先級則由app/build.gradle
文件中dependencies的implementation順序決定的
那具體是怎么做到的呢?源碼分析
打開ApplicationTaskManager,找到createTasksForVariantScope()方法弟头,會發(fā)現(xiàn):
public void createTasksForVariantScope() {
......
createGenerateResValuesTask(variantScope);
createMergeResourcesTask(variantScope);
......
}
在createGenerateResValuesTask任務創(chuàng)建后吩抓,接著會創(chuàng)建createMergeResourcesTask任何,這個任務就是用來合并資源的赴恨,我們一級一級的點進去疹娶,找到最終處理邏輯的地方:
TaskManager.basicCreateMergeResourcesTask() ->
MergeResources.CreationAction() ->
MergeResources.doFullTaskAction()
接著我們來看一下MergeResources的doFullTaskAction()方法:
protected void doFullTaskAction() throws IOException, JAXBException {
......
// create a new merger and populate it with the sets.
ResourceMerger merger = new ResourceMerger(minSdk.get());
......
Blocks.recordSpan(GradleBuildProfileSpan.ExecutionType.TASK_EXECUTION_PHASE_2,
() -> merger.mergeData(writer, false /*doCleanUp*/));
......
}
可以看到在執(zhí)行到第2階段的時候,傳進去的lambda會調(diào)用ResourceMerger的mergeData()方法,點進這個方法伦连,我們看看合并數(shù)據(jù)的邏輯是怎樣的:
public void mergeData(MergeConsumer<I> consumer, boolean doCleanUp) {
// get all the items keys.
Set<String> dataItemKeys = new HashSet<>();
//遍歷資源集蚓胸,并把全部資源名添加到dataItemKeys中
for (S dataSet : mDataSets) {
// quick check on duplicates in the resource set.
dataSet.checkItems();
ListMultimap<String, I> map = dataSet.getDataMap();
dataItemKeys.addAll(map.keySet());
}
//遍歷剛剛添加的全部資源名
for (String dataItemKey : dataItemKeys) {
I toWrite = null;
//倒序遍歷,查找存在相同名字的item
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);
//list沒內(nèi)容,開始下一輪查找
if (items.isEmpty()) {
continue;
}
//倒序遍歷
for (int ii = items.size() - 1; ii >= 0; ii--) {
I item = items.get(ii);
if (toWrite == null) {
toWrite = item;
}
if (toWrite != null) {
//這里跳出到爸爸層循環(huán)
//也就是上面“查找存在相同名字的item”的循環(huán)
break setLoop;
}
}
}
// now need to handle, the type of each (single res file, multi res file), whether
// they are the same object or not, whether the previously written object was
// deleted.
if (toWrite == null) {
// nothing to write? delete only then.
} else {
//看下面的原注釋: "替換成另一個資源扔枫。強行把新的值寫進去"汛聚,證明同名的資源值是在這里替換的
// replacement of a resource by another.
// force write the new value
toWrite.setTouched();
consumer.addItem(toWrite);
// and remove the old one 移除掉舊的
consumer.removeItem(previouslyWritten, toWrite);
}
}
}
可以看到,它首先會遍歷一個裝有全部資源名字的List短荐,并將符合條件資源的key添加到一個新的Set集合中倚舀,然后倒序遍歷這個Set集合,并在里面倒序遍歷一個裝有全部資源的List忍宋,然后逐個檢查有沒有和外面遍歷到的item同名的痕貌,如果有同名的,會用里面item的值替換外層那個同名item的值
那么這個裝有全部資源的List是怎么來的糠排?里面裝的都是什么舵稠?
可以先做個猜測:既然在mergeData方法中會倒序查找同名的資源,而在我們上面的測試中入宦,app的優(yōu)先級要比modul高哺徊,那么,這個list會不會就是【module2, module1, module0, app】這樣排序的呢乾闰?如果是的話落追,就剛好能對應剛剛的測試結(jié)果
回到MergeResources的doFullTaskAction方法中,會看到這一段代碼(merger就是剛剛調(diào)用mergeData
方法的ResourceMerger):
for (ResourceSet resourceSet : resourceSets) {
resourceSet.loadFromFiles(new LoggerWrapper(getLogger()));
merger.addDataSet(resourceSet);
}
可以看到涯肩,它遍歷了resourceSets轿钠,把全部的元素添加到了ResourceMerger(上面的mDataSets)中,找到resourceSets病苗,可以發(fā)現(xiàn)它是通過getResourceComputer的compute()方法獲取的:
fun compute(precompileRemoteResources: Boolean = false): List<ResourceSet> {
// app中的資源集
val sourceFolderSets = getResSet()
val resourceSetList = ArrayList<ResourceSet>(size)
// add at the beginning since the libraries are less important than the folder based
// resource sets.
// get the dependencies first
// libraries里面裝有各個依賴庫的相關數(shù)據(jù)
libraries?.let {
val libArtifacts = it.artifacts
for (artifact in libArtifacts) {
val resourceSet = ResourceSet()
resourceSet.isFromDependency = true
resourceSet.addSource(artifact.file)
// 每次添加元素在最前面
// add to 0 always, since we need to reverse the order.
resourceSetList.add(0, resourceSet)
}
}
// 最后疗垛,添加app里面的資源
// add the folder based next
resourceSetList.addAll(sourceFolderSets)
return resourceSetList
}
可以看到,在添加依賴庫的資源時铅乡,采用了頭插法继谚,如果原來依賴的順序是【module0,module1,module2】花履,那么當遍歷完成后resourceSetList里面的元素就是【module2芽世,module1,module0】诡壁,在最后還添加了app中的資源集济瓢,默認添加在了集合的末尾
這樣一來,也就對應了我們剛才的猜想:app的資源集在resourceSetList的最后面妹卿,那么在合并資源旺矾,倒序遍歷時也就會先找到app里面的資源,其次是modu0夺克,module1.....