插件化-解决插件资源ID与宿主资源ID冲突的问题

前面分析了VirtualApk支持插件中的4大组件运行的原理。本文就来讨论一下如何解决插件资源id和宿主资源id冲突的问题。
本文会涉及到Andoird资源的编译和打包原理。因此对这方面的知识最好有一定的了解。可以参考老罗Andoird资源的编译和打包一文。

为什么会冲突?为什么要解决资源id冲突?

首先宿主apk和插件apk是两个不同的apk,他们在编译时都会产生自己的resources.arsc。即他们是两个独立的编译过程。那么它们的resources.arsc中的资源id必定是有相同的情况。这就会出现问题了:

我们前面已经了解过,宿主在加载插件的资源的时候其实是新new了一个Resources,这个新的Resources是包含宿主和插件的资源的。所以一个Resources中就出现了资源id重复的情况,这样在运行的时候使用资源id来获取资源就会报错。

怎么解决呢?

目前一共有两种思路:

  1. 修改aapt源码,定制aapt工具,编译期间修改PP段。(PP字段是资源id的第一个字节,表示包空间)

DynamicAPK的做法就是如此,定制aapt,替换google的原始aapt,在编译的时候可以传入参数修改PP段:例如传入0x05编译得到的资源的PP段就是0x05。对于具体实现可以参考这篇博客Android中如何修改编译的资源ID值

  1. 修改aapt的产物,即,编译后期重新整理插件Apk的资源,编排ID。

VirtualApk采用的就是这个方案。本文就大致看一下这个方案的实现。

VirtualApk的解决方案

大体实现思路:自定义gradle transform 插件,在apk资源编译任务完成后,重新设置插件的resources.arsc文件中的资源id,并更新R.java文件

比如,你在编译插件apk时设置了:

apply plugin: 'com.didi.virtualapk.plugin'

virtualApk {
    packageId = 0x6f //插件资源ID的PP字段
    targetHost = '../VirtualApk/app' // 宿主的目录
    applyHostMapping = true 
}

在运行编译插件apk的任务后,产生的插件的资源id的PP字段都是0x6f

VirtualApkhook了ProcessAndroidResourcestask。这个task是用来编译Android资源的。VirtualApk拿到这个task的输出结果,做了以下处理:

  1. 根据编译产生的R.txt文件收集插件中所有的资源

  2. 根据编译产生的R.txt文件收集宿主apk中的所有资源

  3. 过滤插件资源:过滤掉在宿主中已经存在的资源

  4. 重新设置插件资源的资源ID

  5. 删除掉插件资源目录下前面已经被过滤掉的资源

  6. 重新编排插件resources.arsc文件中插件资源ID为新设置的资源ID

  7. 重新产生R.java文件

下面呢我们就来看下具体代码。这块水很深。所以下面的代码就当伪代码看一下就好,我们的主要目的是理解大致的实现思路。

粗略浏览具体实现代码

根据R.txt文件收集插件中所有的资源

R.txt文件是在编译资源过程中产生的资源ID记录文件,在build/intermediates/symbols/xx/xx/R.txt可以找到这个问题,它的格式如下:

int anim abc_fade_in 0x7f010000  
int anim abc_fade_out 0x7f010001
.....

看一下具体代码:

  private void parseResEntries(File RSymbolFile, ListMultimap allResources, List styleableList) {
        RSymbolFile.eachLine { line ->
            /**
             *  Line Content:
             *  Common Res:  int string abc_action_bar_home_description 0x7f090000
             *  Styleable:   int[] styleable TagLayout { 0x010100af, 0x7f0102b5, 0x7f0102b6 }
             *               or int styleable TagLayout_android_gravity 0
             */
            if (!line.empty) {
                def tokenizer = new StringTokenizer(line)
                def valueType = tokenizer.nextToken()     // value type (int or int[])
                def resType = tokenizer.nextToken()      // resource type (attr/string/color etc.)
                def resName = tokenizer.nextToken()
                def resId = tokenizer.nextToken('\r\n').trim()

                if (resType == 'styleable') {
                    styleableList.add(new StyleableEntry(resName, resId, valueType))
                } else {
                    allResources.put(resType, new ResourceEntry(resType, resName, Integer.decode(resId)))
                }
            }
        }
    }

即收集所有资源:资源名称资源ID资源类型等。然后保存在集合中:allResourcesstyleableList

根据编译产生的R.txt文件收集宿主apk中的所有资源

和第一步相同

过滤插件资源:过滤掉在宿主中已经存在的资源

    private void filterPluginResources() {
        allResources.values().each {  // allResources 就是前面解析出来的插件的所有资源
            def index = hostResources.get(it.resourceType).indexOf(it)   
            if(index >= 0){  //插件的资源在宿主中存在
                it.newResourceId = hostResources.get(it.resourceType).get(index).resourceId //把这个一样的插件资源的id设置成宿主的id
                hostResources.get(it.resourceType).set(index, it) //在宿主中更新这个资源
            } else { //插件的资源在宿主中不存在
                pluginResources.put(it.resourceType, it)  
            }
        }

        allStyleables.each {
            def index = hostStyleables.indexOf(it)
            if(index >= 0) {
                it.value = hostStyleables.get(index).value
                hostStyleables.set(index, it)
            } else {
                pluginStyleables.add(it)
            }
        }
    }

即经过上面的操作,pluginResources只含有插件的资源。这份资源和宿主的资源集合没有交集,即没有相同的资源。

重新设置插件的资源ID

这一步就是核心了,逻辑很简单,即基于自定义的PP字段的值,修改上面已经收集好的pluginResources中资源的资源ID:

 private void reassignPluginResourceId() {

        //先根据 typeId 把前面收集到的资源排序
        def resourceIdList = []
        pluginResources.keySet().each { String resType ->
            List<ResourceEntry> entryList = pluginResources.get(resType)
            resourceIdList.add([resType: resType, typeId: entryList.empty ? -100 : parseTypeIdFromResId(entryList.first().resourceId)])
        }

        resourceIdList.sort { t1, t2 ->
            t1.typeId - t2.typeId
        }

        //重新设置插件的资源id
        int lastType = 1
        resourceIdList.each {
            if (it.typeId < 0) {
                return
            }
            def typeId = 0
            def entryId = 0
            typeId = lastType++
            pluginResources.get(it.resType).each {
                it.setNewResourceId(virtualApk.packageId, typeId, entryId++)  // virtualApk.packageId 即我们在gradle中自定义的 packageId
            }
        }

        List<ResourceEntry> attrEntries = allResources.get('attr')

        pluginStyleables.findAll { it.valueType == 'int[]'}.each { StyleableEntry styleableEntry->
            List<String> values = styleableEntry.valueAsList
            values.eachWithIndex { hexResId, idx ->
                ResourceEntry resEntry = attrEntries.find { it.hexResourceId == hexResId }
                if (resEntry != null) {
                    values[idx] = resEntry.hexNewResourceId
                }
            }
            styleableEntry.value = values
        }
    }

ok,经过上面的处理,pluginResources中的资源的资源id都是重新设置的新的资源Id。

删除掉插件资源目录下前面已经被过滤掉的资源

我们前面经过和宿主的资源对比后,可能已经删除了插件中的一些资源id,但是对应的文件还没有删除,因此需要把文件也删除掉:

 void filterResources(final List<?> retainedTypes, final Set<String> outFilteredResources) {
        def resDir = new File(assetDir, 'res')   //遍历插件的资源目录
        resDir.listFiles().each { typeDir ->
            def type = retainedTypes.find { typeDir.name.startsWith(it.name) }
            if (type == null) {   //插件过滤后的资源已经不含有这个目录了,直接删除掉
                typeDir.listFiles().each {
                    outFilteredResources.add("res/$typeDir.name/$it.name")
                }
                typeDir.deleteDir()
                return  //这个return 是跳过这次循环
            }

            def entryFiles = typeDir.listFiles()
            def retainedEntryCount = entryFiles.size()

            entryFiles.each { entryFile ->
                def entry = type.entries.find { entryFile.name.startsWith("${it.name}.") }
                if (entry == null) {   //逻辑同上
                    outFilteredResources.add("res/$typeDir.name/$entryFile.name")
                    entryFile.delete()
                    retainedEntryCount--
                }
            }

            if (retainedEntryCount == 0) {
                typeDir.deleteDir()
            }
        }
    }

重新编排插件resources.arsc文件中插件资源ID为新设置的资源ID

这个代码就不列了,感兴趣可以查看VirtualApk源代码 : ArscEditor.slice()

重新产生R.java文件

 public static void generateRJava(File dest, String pkg, ListMultimap<String, ResourceEntry> resources, List<StyleableEntry> styleables) {
        if (!dest.parentFile.exists()) {
            dest.parentFile.mkdirs()
        }

        if (!dest.exists()) {
            dest.createNewFile()
        }

        dest.withPrintWriter { pw ->
            pw.println "package ${pkg};"
            pw.println "public final class R {"

            resources.keySet().each { type ->
                pw.println "    public static final class ${type} {"
                resources.get(type).each { entry ->
                    pw.println "        public static final int ${entry.resourceName} = ${entry.hexNewResourceId};"
                }
                pw.println "    }"
            }

            pw.println "    public static final class styleable {"
            styleables.each { styleable ->
                    pw.println "        public static final ${styleable.valueType} ${styleable.name} = ${styleable.value};"
            }
            pw.println "    }"
            pw.println "}"
        }
    }

欢迎Star我的Android进阶计划,看更多干货。

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

推荐阅读更多精彩内容