R.java文件分析

變量修飾符

  1. 為什么App Module中的R.java文件的變量是final修飾而Lib Module中R.java文件卻不是?

R文件是由編譯器自動生成,每個模塊中的R文件的id都是從0x7f+resId+0001開始分配的,所以說多個模塊肯定會有資源沖突的(同名資源文件),其實lib module應(yīng)該是沒有R.java文件的,只是as的一個語法支持,在編譯成apk時,會替換每個資源文件的id為具體的數(shù)值,而lib的資源文件的id是會變的,故不能用final進(jìn)行修飾

  1. 為什么將App Module中的switch-case語句拷貝到Lib Module中需要轉(zhuǎn)換成if-else?

由1可知,lib module中的資源文件id是可變的,而在Java語法中,switch的參數(shù)必須是常量或者值,否則會報語法錯誤,只需要修改成if-else即可解決

R.Java文件的生成規(guī)則

  1. 同一資源文件在不同的module下引用,大概率下資源id不同,那么最終打包是如何處理的?
    結(jié)論:最終打包出來的那些資源文件id,是會重新分配的(同一資源生成的id,就算不在同一R.class中,打包出來的id也是相同的)

下面做一個測試:

  1. app Module 依賴 test Module
  2. test模塊中的string.xml有個test_value字符資源
  3. 在test模塊引用這個資源文件,查看資源id為-1900022
  4. 在app模塊引用這個資源文件,查看資源id為-1900023

這個時候可以看出,相同的資源文件確實有可能出現(xiàn)id不相同的情況,打包成apk文件后,把它拖進(jìn)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值的
很多任務(wù)都是在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任務(wù)后,接著調(diào)用了createApkProcessResTask()方法,查看注釋,可以知道這個任務(wù)是用來處理資源和生成源文件的,一步步點進(jìn)去看看最終處理邏輯:

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)建的,而且同一個資源所對應(yīng)的id也是一樣的

項目中同名資源,會不會覆蓋,規(guī)則是怎么樣的?

我們來做一個測試:

  1. app Module中test_value 資源,依賴了同樣有test_value資源的module1
  2. app Module中test_value 資源,依賴了同樣有test_value資源的module1,module2
  3. app Module中test_value 資源,先后依賴了有test_value資源的module1,module2
  4. app Module中test_value 資源,先后依賴了有test_value資源的module2,module1

然后打包apk,拖進(jìn)AndroidStudio,點開resources.arsc文件,定位到test_value,會看到以下對應(yīng)結(jié)果:

  1. test_value的值是app中的值
  2. test_value的值是app中的值
  3. test_value的值是module1中的值
  4. 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任務(wù)創(chuàng)建后,接著會創(chuàng)建createMergeResourcesTask任何,這個任務(wù)就是用來合并資源的,我們一級一級的點進(jìn)去,找到最終處理邏輯的地方:

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階段的時候,傳進(jìn)去的lambda會調(diào)用ResourceMerger的mergeData()方法,點進(jìn)這個方法,我們看看合并數(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 {
                //看下面的原注釋: "替換成另一個資源。強(qiáng)行把新的值寫進(jìn)去",證明同名的資源值是在這里替換的
                // 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】這樣排序的呢?如果是的話,就剛好能對應(yīng)剛剛的測試結(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里面裝有各個依賴庫的相關(guān)數(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】,那么當(dāng)遍歷完成后resourceSetList里面的元素就是【module2,module1,module0】,在最后還添加了app中的資源集,默認(rèn)添加在了集合的末尾

這樣一來,也就對應(yīng)了我們剛才的猜想:app的資源集在resourceSetList的最后面,那么在合并資源,倒序遍歷時也就會先找到app里面的資源,其次是modu0,module1.....

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 模塊通常是指編程語言所提供的代碼組織機(jī)制,利用此機(jī)制可將程序拆解為獨立且通用的代碼單元。所謂模塊化主要是解決代碼分...
    MapleLeafFall閱讀 1,256評論 0 0
  • 一、Python簡介和環(huán)境搭建以及pip的安裝 4課時實驗課主要內(nèi)容 【Python簡介】: Python 是一個...
    _小老虎_閱讀 6,341評論 0 10
  • pyspark.sql模塊 模塊上下文 Spark SQL和DataFrames的重要類: pyspark.sql...
    mpro閱讀 9,922評論 0 13
  • 因為unittest支持的html報告在作為郵件附加時耗時較長,故將報告擴(kuò)展支持為unishark框架。 基于un...
    五娃兒閱讀 603評論 0 0
  • 前言 開發(fā)中,我習(xí)慣性會把一個模塊的功能放在一個包下,便于查找,但煩于耦合性太高,后期維護(hù)太費勁,因此對項目進(jìn)行組...
    吳小龍同學(xué)閱讀 1,986評論 3 34

友情鏈接更多精彩內(nèi)容