R.java文件分析

变量修饰符

  1. 为什么App Module中的R.java文件的变量是final修饰而Lib Module中R.java文件却不是?

R文件是由编译器自动生成,每个模块中的R文件的id都是从0x7f+resId+0001开始分配的,所以说多个模块肯定会有资源冲突的(同名资源文件),其实lib module应该是没有R.java文件的,只是as的一个语法支持,在编译成apk时,会替换每个资源文件的id为具体的数值,而lib的资源文件的id是会变的,故不能用final进行修饰

  1. 为什么将App Module中的switch-case语句拷贝到Lib Module中需要转换成if-else?

由1可知,lib module中的资源文件id是可变的,而在Java语法中,switch的参数必须是常量或者值,否则会报语法错误,只需要修改成if-else即可解决

R.Java文件的生成规则

  1. 同一资源文件在不同的module下引用,大概率下资源id不同,那么最终打包是如何处理的?
    结论:最终打包出来的那些资源文件id,是会重新分配的(同一资源生成的id,就算不在同一R.class中,打包出来的id也是相同的)

下面做一个测试:

  1. app Module 依赖 test Module
  2. test模块中的string.xml有个test_value字符资源
  3. 在test模块引用这个资源文件,查看资源id为-1900022
  4. 在app模块引用这个资源文件,查看资源id为-1900023

这个时候可以看出,相同的资源文件确实有可能出现id不相同的情况,打包成apk文件后,把它拖进AndroidStudio中,打开classes.dex

  • 首先找到app包名下的R$stirng,右键查看字节码,发现test_value的id值由原来的-1900023变成了0x7f10005d
  • 接着找到test模块包名下的R$stirng,查看字节码发现id值也变成了0x7f10005d

由此看来,id值在打包成apk的时候,确实重新分配了,而且同一资源的id值也统一了

源码分析:如何重新分配这些资源的id值的
很多任务都是在ApplicationTaskManager里的createTasksForVariantScope方法中创建的,现在再来看一下这个方法:

@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);
        ......
}

可以看到在创建了BuildConfigTask任务后,接着调用了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的意思,再看一下接下来调用的mergeAndRenumberSymbols()方法:

fun mergeAndRenumberSymbols(): SymbolTable {
    ......
    // the ID value provider.
    val idProvider = IdProvider.sequential()
    ......
}

可以看到调用了IdProvider.sequential()方法,这个方法是用来提供id的,看下它里面是怎样实现的:

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()
            }
        }
}

可以发现,产生的id都会0x7f开头的,我们刚开始打包后资源文件的id值也是0x7f开头的,到这里基本可以确定,最终打包的资源id,就是通过这个IdProvider的匿名子类来重新创建的,而且同一个资源所对应的id也是一样的

项目中同名资源,会不会覆盖,规则是怎么样的?

我们来做一个测试:

  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,拖进AndroidStudio,点开resources.arsc文件,定位到test_value,会看到以下对应结果:

  1. test_value的值是app中的值
  2. test_value的值是app中的值
  3. test_value的值是module1中的值
  4. test_value的值是module2中的值

结论:多模块开发中,不同模块间如果有同名资源,那么最终采纳的优先级为:app的优先级要高于依赖的module,而module之间的优先级则由app/build.gradle文件中dependencies的implementation顺序决定的

那具体是怎么做到的呢?源码分析

打开ApplicationTaskManager,找到createTasksForVariantScope()方法,会发现:

public void createTasksForVariantScope() {
        ......
        createGenerateResValuesTask(variantScope);
        createMergeResourcesTask(variantScope);
        ......
}

在createGenerateResValuesTask任务创建后,接着会创建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*/));
        ......
}

可以看到在执行到第2阶段的时候,传进去的lambda会调用ResourceMerger的mergeData()方法,点进这个方法,我们看看合并数据的逻辑是怎样的:

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没内容,开始下一轮查找
                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) {
                        //这里跳出到爸爸层循环
                        //也就是上面“查找存在相同名字的item”的循环
                        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的优先级要比modul高,那么,这个list会不会就是【module2, module1, module0, app】这样排序的呢?如果是的话,就刚好能对应刚刚的测试结果

回到MergeResources的doFullTaskAction方法中,会看到这一段代码(merger就是刚刚调用mergeData方法的ResourceMerger):

for (ResourceSet resourceSet : resourceSets) {
    resourceSet.loadFromFiles(new LoggerWrapper(getLogger()));
    merger.addDataSet(resourceSet);
}

可以看到,它遍历了resourceSets,把全部的元素添加到了ResourceMerger(上面的mDataSets)中,找到resourceSets,可以发现它是通过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里面装有各个依赖库的相关数据
        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.....

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 模块通常是指编程语言所提供的代码组织机制,利用此机制可将程序拆解为独立且通用的代码单元。所谓模块化主要是解决代码分...
    MapleLeafFall阅读 1,165评论 0 0
  • 一、Python简介和环境搭建以及pip的安装 4课时实验课主要内容 【Python简介】: Python 是一个...
    _小老虎_阅读 5,720评论 0 10
  • pyspark.sql模块 模块上下文 Spark SQL和DataFrames的重要类: pyspark.sql...
    mpro阅读 9,446评论 0 13
  • 因为unittest支持的html报告在作为邮件附加时耗时较长,故将报告扩展支持为unishark框架。 基于un...
    五娃儿阅读 516评论 0 0
  • 前言 开发中,我习惯性会把一个模块的功能放在一个包下,便于查找,但烦于耦合性太高,后期维护太费劲,因此对项目进行组...
    吴小龙同學阅读 1,721评论 3 34