外掛化-解決外掛資源ID與宿主資源ID衝突的問題
前面分析了VirtualApk
Android/blob/master/%E6%8F%92%E4%BB%B6%E5%8C%96/README.md" target="_blank" rel="nofollow,noindex">支援外掛中的4大元件執行的原理
。本文就來討論一下如何解決外掛資源id和宿主資源id衝突的問題。
本文會涉及到Andoird資源的編譯和打包
原理。因此對這方面的知識最好有一定的瞭解。可以參考老羅
的Andoird資源的編譯和打包
一文。
為什麼會衝突?為什麼要解決資源id衝突?
首先宿主apk和外掛apk是兩個不同的apk,他們在編譯時都會產生自己的resources.arsc
。即他們是兩個獨立的編譯過程。那麼它們的resources.arsc
中的資源id必定是有相同的情況。這就會出現問題了:
我們前面已經瞭解過,宿主在載入外掛的資源的時候其實是新new了一個Resources
,這個新的Resources
是包含宿主和外掛的資源的。所以一個Resources
中就出現了資源id重複的情況,這樣在執行的時候使用資源id來獲取資源就會報錯。
怎麼解決呢?
目前一共有兩種思路:
- 修改aapt原始碼,定製aapt工具,編譯期間修改PP段。(PP欄位是資源id的第一個位元組,表示包空間)
DynamicAPK
的做法就是如此,定製aapt,替換google的原始aapt,在編譯的時候可以傳入引數修改PP段:例如傳入0x05編譯得到的資源的PP段就是0x05。對於具體實現可以參考這篇部落格Android中如何修改編譯的資源ID值
- 修改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
。
VirtualApk
hook了ProcessAndroidResources
task。這個task是用來編譯Android資源的。VirtualApk
拿到這個task的輸出結果,做了以下處理:
-
根據編譯產生的
R.txt
檔案收集外掛中所有的資源 -
根據編譯產生的
R.txt
檔案收集宿主apk中的所有資源 -
過濾外掛資源:過濾掉在宿主中已經存在的資源
-
重新設定外掛資源的資源ID
-
刪除掉外掛資源目錄下前面已經被過濾掉的資源
-
重新編排外掛
resources.arsc
檔案中外掛資源ID為新設定的資源ID -
重新產生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
、資源型別
等。然後儲存在集合中:allResources
和styleableList
根據編譯產生的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進階計劃 ,看更多幹貨。