登录

去注册

登录

注册

去登录

注册

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

xiaoyang   2019-09-09   收藏

上次我们问了每日一问 关于 R.java 的生成规则,你知道多少? ,但是一个有用的知识点还是没弄清楚。

对于不同的 module 下,同名但值不同的资源,最终:

  1. 会被覆盖成其中一个资源吗?
  2. 如果会,那么覆盖的规则是,随机吗?
11

测试了以下几种情况:

  1. app中test_value资源,依赖了同样有test_value资源的module1;

  2. app中test_value资源,依赖了同样有test_value资源的module1和module2;

  3. app中test_value资源,先后依赖了有test_value资源的module1和module2;

  4. app中test_value资源,先后依赖了有test_value资源的module2和module1;


打包后把apk拖进AS,点开resources.arsc文件,定位到test_value,会看到(下面4个结果分别对应上面的4种不同的做法):

  1. test_value的值是app中的值;

  2. test_value的值是app中的值;

  3. test_value的值是module1中的值;

  4. test_value的值是module2中的值;


这也就对应了 @526227576@cscxzxzc 同学所说规则:
如有同名资源,那么最终采纳的,app的优先级要高于module,而module之间的优先级则由app/build.gradle里面dependencies中的implementation顺序决定的。


那它具体是怎么做到的呢?

源码分析:

按之前的方法打开ApplicationTaskManagercreateTasksForVariantScope方法:

    public void createTasksForVariantScope() {

        ......

        createGenerateResValuesTask(variantScope);

        createMergeResourcesTask(variantScope);

        ......
    }

可以看到,在生成资源值任务创建后,接着会创建一个叫合并资源的任务,一级级点进去:

TaskManager.basicCreateMergeResourcesTask() ->

MergeResources.CreationAction() ->

MergeResources.doFullTaskAction()

来看看这个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会调用ResourceMergermergeData方法,点进去(逻辑有点复杂,注意看注释):
两个斜杠的英文注释是源码本身的注释:

    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】,并且在里面倒序遍历一个【装有全部资源的List】,然后逐个检查有没有跟外面遍历到的item同名,如果有的话,会用【它】所对应的值,替换掉【外面遍历到的item】所对应的值。
那这个【装有全部资源的List】是怎么来的?里面装的都是什么?
可以先做个猜测:既然在mergeData方法中会倒序查找同名的资源,而在我们上面的测试中,app的优先级比module高,
那么,这个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,可以发现它是通过getResourcesComputer的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里面的资源(如果有重名的话),其次是module0、 module1。。。。

回复
陈小缘 : @warden 

会优先采用前面的(如果有重名的话)

2019-09-06 回复
warden : @陈小缘 

而module之间的优先级则由app/build.gradle里面dependencies中的implementation顺序决定的。优先使用前面的还是后面的哇?  ...查看更多

2019-09-06 回复
陈小缘 : @warden 

那也要学

2019-09-05 回复
warden : @陈小缘 

现在好多插件Kotlin还用不了...

2019-09-05 回复
陈小缘 : @陈小缘 

现在Gradle插件很多地方都改成了用Kotlin实现了,还没学习Kotlin的同学要赶紧学啦~

2019-09-05 回复
5

对于不同的 module 下,同名但值不同的资源,最终:
1.会覆盖成其中一个资源。
2.不是随机的,他是根据相应的优先级进行资源合并的。

https://developer.android.com/studio/write/add-resources.html

如果存在两个或两个以上的匹配版本的相同的资源,那么只有一个版本包含在最终的APK。构建工具选择哪个版本保持基于以下优先顺序(左边最高优先级):
build variant > build type > product flavor > main source set > library dependencies

源集编译这也说明了优先级 https://developer.android.com/studio/build/build-variants.html#sourceset-build


回复
cscxzxzc : @luzeping 

如果优先级相同的话,好像会出现冲突错误的。(自己还没试过) 你可以看看这个(在最后一行):https://developer.android.com/studio/write/add-resource  ...查看更多

2019-09-05 回复
luzeping : @cscxzxzc 

我想请问一下,如果我有子module A和B 两个module下都定义了<string name="test">,那这个时候怎么确定合并后最终取的是哪个子module下的值?  ...查看更多

2019-09-05 回复
4

1.不同模块下同名的资源文件一定会覆盖;
2.不是随机的。
本人实际开发中真的遇到过,只要文件名相同,不管文件是SVG或PNG等等都会覆盖,至于覆盖规则应该是根据编译时module先后次序决定的。拿MVVM举例,Databinding在编译时不关在module中的/xxx/build/generated/source/kapt生成对应的java文件,App壳在/xxx/build/generated/source/kapt也会生成module中的java文件。
如果使用ARouter,同样命名和文件,编译运行可能没问题,但是操作时会因找到文件与实践文件不一致而报错。
cscxzxzc 童鞋提到的阿里Android开发手册中很有用处。

回复
1

补充下:一个App那么多模块,不可能你一个人开发,没有约束的话很容易重名(现实生活中就有那么多重名的,但xx省xx市xx大学xx班xx学号的小葱就一个)。所以尽量规范化命名。阿里的Android开发手册中就【推荐】模块化的开发要带模块前缀。

可以养成个习惯:
在 library 的 build.gradle 中添加 resourcePrefix , 则所有的资源须以 csc_ 开头。

android {
    ...
    buildTypes {
    ...
    }
    resourcePrefix 'csc_'
}

回复
于慢慢家的吴蜀黍 : @cscxzxzc 

<在 library 的 build.gradle 中添加 resourcePrefix , 则所有的资源须以 csc_ 开头。> 并不能对xml文件做限制,如果能在git提交的时候做c  ...查看更多

2019-09-05 回复
0

我说一种非常恶心的情况,你在app下写了一个资源test_string放在values下,用这个资源好好的,;然后你一来了一个模块也有资源test_string放在values下,然后values-zh也有,然后恭喜你,生气的事情发生了,现在你app下的test_string在中文下用的是模块在values-zh下写的了

回复

删除留言

确认删除留言,会导致相关评论丢失?

取消 确定