aapt2 适配之资源 id 固定
前言
資源id的固定在熱修復(fù)和插件化中極其重要。在熱修復(fù)中,構(gòu)建patch時(shí),需要保持patch包的資源id和基線包的資源id一致;在插件化中,如果插件需要引用宿主的資源,則需要將宿主的資源id進(jìn)行固定,因此,資源id的固定在這兩種場(chǎng)景下是尤為重要的。而在Android Gradle Plugin 3.0.0中,默認(rèn)開啟了aapt2,原先aapt的資源固定方式public.xml也將失效,必須尋找一種新的資源固定的方式,而不是簡(jiǎn)單的禁用掉aapt2,因此本文來探討一下開啟aapt2的情況下如何進(jìn)行資源id的固定。
aapt的資源固定方式
在探索aapt2資源固定方式前,先來溫習(xí)一下aapt原先的資源固定方式。
- 編譯基線包時(shí)添加aapt參數(shù)-P導(dǎo)出public.xml文件
編譯插件或者patch包時(shí),將public.xml文件拷貝至資源merge完成的目錄,并根據(jù)values.xml中的定義和public.xml中的定義,選擇性的生成ids.xml文件
對(duì)應(yīng)的代碼如下
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990 | apply plugin: PublicPluginclass PublicPlugin implements Plugin<Project> { void apply(Project project) { project.afterEvaluate { if (project.plugins.hasPlugin("com.android.application")) { def android = project.extensions.getByName("android")android.applicationVariants.all {def variant -> File publicXmlFile = project.rootProject.file('public.xml') //public文件存在則應(yīng)用,不存在則生成 if (publicXmlFile.exists()) { //aapt的應(yīng)用需要將文件拷貝到對(duì)應(yīng)的目錄 //aapt public.xml文件的應(yīng)用并不是只是拷貝public.xml文件那么簡(jiǎn)單,還要根據(jù)生成的public.xml生成ids.xml文件,并將ids.xml中與values.xml中重復(fù)定義的id去除String mergeResourcesTaskName = variant.variantData.getScope().getMergeResourcesTask().name def mergeResourcesTask = project.tasks.getByName(mergeResourcesTaskName) //資源merge的task存在則在其merge完資源后拷貝public.xml并生成ids.xml if (mergeResourcesTask) {mergeResourcesTask.doLast { //拷貝public.xml文件 File toDir = new File(mergeResourcesTask.outputDir, "values") project.copy { project.logger.error "${variant.name}:copy from ${publicXmlFile.getAbsolutePath()} to ${toDir}/public.xml" from(publicXmlFile.getParentFile()) { include "public.xml"rename "public.xml", "public.xml"} into(toDir)} //生成ids.xml文件 File valuesFile = new File(toDir, "values.xml") File idsFile = new File(toDir, "ids.xml") if (valuesFile.exists() && publicXmlFile.exists()) { //記錄在values.xml中存在的id定義 def valuesNodes = new XmlParser().parse(valuesFile)Set<String> existIdItems = new HashSet<String>()valuesNodes.each { if ("id".equalsIgnoreCase("${it.@type}")) {existIdItems.add("${it.@name}")}}GFileUtils.deleteQuietly(idsFile)GFileUtils.touch(idsFile)idsFile.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>")idsFile.append("\n")idsFile.append("<resources>")idsFile.append("\n") def publicXMLNodes = new XmlParser().parse(publicXmlFile)Pattern drawableGeneratePattern = Pattern.compile('^(.*?_)([0-9]{0,})$')publicXMLNodes.each { //獲取public.xml中定義的id類型item if ("id".equalsIgnoreCase("${it.@type}")) { //如果在values.xml中沒有定義,則添加到ids.xml中 //如果已經(jīng)在values.xml中定義,則忽略它 if (!existIdItems.contains("${it.@name}")) {idsFile.append("\t<item type=\"id\" name=\"${it.@name}\" />\n")} else { project.logger.error "already exist id item ${it.@name}, ignore it"}} else if ("drawable".equalsIgnoreCase("${it.@type}")) { //以'_數(shù)字'結(jié)尾的drawable資源,此類資源是aapt編譯時(shí)生成的nested資源,如avd_hide_password_1, avd_hide_password_2 //但是可能會(huì)有其他資源摻雜,如abc_btn_check_to_on_mtrl_000, abc_btn_check_to_on_mtrl_015 //為了將此類資源過濾掉,將正則匹配到的數(shù)字轉(zhuǎn)成int,對(duì)比原始數(shù)字部分匹配字符串,如果一致,則是aapt生成 //重要:為了避免此類nested資源生成順序發(fā)生改變,應(yīng)該禁止修改此類資源Matcher matcher = drawableGeneratePattern.matcher(it.@name) if (matcher.matches() && matcher.groupCount() == 2) {String number = matcher.group(2) if (number.equalsIgnoreCase(Integer.parseInt(number).toString())) { project.logger.info "[${PREFIX}] declared drawable resource ${it.@name} which is generated by aapt. like use '<aapt:attr name=\"android:drawable\">'"idsFile.append("\t<item type=\"drawable\" name=\"${it.@name}\" />\n")}}}}idsFile.append("</resources>")}}}} else { //不存在則生成 project.logger.error "${publicXmlFile} not exists, generate it" //aapt 添加-P參數(shù)生成aaptOptions.additionalParameters("-P", "${publicXmlFile}")}}}}}} |
很簡(jiǎn)單,生成public.xml時(shí)使用aapt的參數(shù)-P,指定生成的文件路徑即可;應(yīng)用public.xml則將其拷貝到values目錄下,唯一需要注意的是這個(gè)導(dǎo)出的public.xml文件會(huì)存在資源id未定義的情況,因此需要生成ids.xml文件,對(duì)未定義的id類型資源進(jìn)行定義。而這個(gè)生成的ids.xml文件,可能與values/values.xml文件中的id存在重復(fù)定義的現(xiàn)象,因此生成的時(shí)候,則需要判斷對(duì)應(yīng)的id名在values.xml文件中是否存在,如果存在則直接忽略,因?yàn)樗呀?jīng)定義了;如果不存在,則添加到ids.xml中進(jìn)行定義。
這種方式不支持刪除現(xiàn)有的資源,如果刪除了現(xiàn)有的資源,public.xml中的定義也得刪除,否則會(huì)報(bào)資源未定義的錯(cuò)誤。
aapt2的資源固定方式
那么在aapt2中上面這種方式還生效嗎,答案是否定的,至于為什么,可以參考之前的一篇文章aapt2 資源 compile 過程,因?yàn)樗衜erge的資源都已經(jīng)經(jīng)過了預(yù)編譯,產(chǎn)生了flat文件,這時(shí)候?qū)ublic.xml文件拷貝至該目錄就會(huì)產(chǎn)生編譯錯(cuò)誤。那么如何解決了。通過查看Android Gradle Plugin 3.0.0的代碼發(fā)現(xiàn)了一些貓膩,關(guān)鍵代碼如下
| 12345678910111213 | public static ImmutableList<String> makeLink(@NonNull AaptPackageConfig config, @NonNull File intermediateDir) throws AaptException {ImmutableList.Builder<String> builder = ImmutableList.builder(); if (config.isVerbose()) {builder.add("-v");} File stableResourceIdsFile = new File(intermediateDir, "stable-resource-ids.txt"); // TODO: For now, we ignore this file, but as soon as aapt2 supports it, we'll use it. //此處省略n行代碼} |
大概可以猜測(cè)可以通過指定穩(wěn)定的資源id映射文件達(dá)到固定資源id的作用,但是代碼中這個(gè)文件并沒有共同參與資源編譯的過程,因此這部分代碼暫時(shí)無效,接下來去aapt2命令中尋找一下。通過aapt2 link –help可以發(fā)現(xiàn)有兩個(gè)參數(shù)。如下
| 12 | --stable-ids arg File containing a list of name to ID mapping.--emit-ids arg Emit a file at the given path with a list of name to ID mappings, suitable for use with --stable-ids. |
大概意思就是說可以通過–emit-ids參數(shù)指定一個(gè)文件,該文件會(huì)輸出資源名字到資源id的一個(gè)映射,這個(gè)文件可以被–stable-ids參數(shù)使用。
通過簡(jiǎn)單的測(cè)試,發(fā)現(xiàn)這兩個(gè)參數(shù)可以完全滿足我們的需求。而且這種方式支持刪除現(xiàn)有的資源。編寫代碼驗(yàn)證之。
| 12345678910111213141516171819202122232425262728 | apply plugin: PublicPluginclass PublicPlugin implements Plugin<Project> { void apply(Project project) { project.afterEvaluate { if (project.plugins.hasPlugin("com.android.application")) { def android = project.extensions.getByName("android")android.applicationVariants.all {def variant -> def processResourcesTask = project.tasks.getByName("process${variant.name.capitalize()}Resources") if (processResourcesTask) { def aaptOptions = processResourcesTask.aaptOptions File publicTxtFile = project.rootProject.file('public.txt') //public文件存在,則應(yīng)用,不存在則生成 if (publicTxtFile.exists()) { project.logger.error "${publicTxtFile} exists, apply it." //aapt2添加--stable-ids參數(shù)應(yīng)用aaptOptions.additionalParameters("--stable-ids", "${publicTxtFile}")} else { project.logger.error "${publicTxtFile} not exists, generate it." //aapt2添加--emit-ids參數(shù)生成aaptOptions.additionalParameters("--emit-ids", "${publicTxtFile}")}}}}}}} |
代碼很簡(jiǎn)單,就是當(dāng)public.txt文件不存在時(shí),添加–emit-ids參數(shù)進(jìn)行生產(chǎn),如果存在時(shí),則添加–stable-ids進(jìn)行應(yīng)用。
簡(jiǎn)單驗(yàn)證一下,定義3個(gè)顏色資源
| 123456 | xml version="1.0" encoding="utf-8"<resources> <color name="colorAccent">#FF4081</color> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#303F9F</color></resources> |
編譯,可以看到項(xiàng)目根目錄下產(chǎn)生了public.txt文件,其中這三個(gè)資源對(duì)應(yīng)的內(nèi)容為
| 123 | io.github.lizhangqu.aapt2:color/colorAccent = 0x7f040026io.github.lizhangqu.aapt2:color/colorPrimary = 0x7f040027io.github.lizhangqu.aapt2:color/colorPrimaryDark = 0x7f040028 |
在這三個(gè)資源中插入一個(gè)資源,打亂資源順序,如下
| 1234567 | xml version="1.0" encoding="utf-8"<resources> <color name="colorAccent">#FF4081</color> <color name="colorAccentBBBBB">#FF4081</color> <color name="colorPrimary">#3F51B5</color> <color name="colorPrimaryDark">#303F9F</color></resources> |
將剛才生成的public.txt文件備份至其他目錄,刪除根目錄下的public.txt文件,保證重新生成該文件,重新編譯資源,此時(shí)生成的public.txt文件中的這四個(gè)資源對(duì)應(yīng)的內(nèi)容為
| 1234 | io.github.lizhangqu.aapt2:color/colorAccent = 0x7f040026io.github.lizhangqu.aapt2:color/colorAccentBBBBB = 0x7f040027io.github.lizhangqu.aapt2:color/colorPrimary = 0x7f040028io.github.lizhangqu.aapt2:color/colorPrimaryDark = 0x7f040029 |
app/build/intermediates/res/symbol-table-with-package/debug/package-aware-r.txt文件中對(duì)應(yīng)的資源id為
| 1234 | int color colorAccent 0x7f040026int color colorAccentBBBBB 0x7f040027int color colorPrimary 0x7f040028int color colorPrimaryDark 0x7f040029 |
兩個(gè)文件中的內(nèi)容完全可以對(duì)的上,只要看其中一個(gè)就可以了;可以看到由于colorAccentBBBBB的插入,colorPrimary和colorPrimaryDark都向后順延了一位,也就是說,在沒有資源固定的情況下,如果增刪改等操作發(fā)生,是有可能導(dǎo)致現(xiàn)有資源id發(fā)生變化的。
因此我們開始驗(yàn)證–stable-ids參數(shù)的有效性,將根目錄下的public.txt文件刪除,將之前備份好的public.txt文件拷貝到根目錄,重新編譯資源。這時(shí)候編譯產(chǎn)生的資源id映射為
| 1234 | int color colorAccent 0x7f040026int color colorAccentBBBBB 0x7f040057int color colorPrimary 0x7f040027int color colorPrimaryDark 0x7f040028 |
可以看到colorAccent,colorPrimary和colorPrimaryDark的資源id并沒有因?yàn)閏olorAccentBBBBB的插入而發(fā)生變化,而是保持了原有的資源id,而新增的資源colorAccentBBBBB則是重新分配了一個(gè)新的資源id。
至此,基本可以確定該方案是可行的(但不保證有沒有坑)
適配aapt和aapt2
因此,需要進(jìn)行aapt的版本判斷,適配不同的情況,這里我已經(jīng)把代碼寫好了,基本上就是對(duì)上面兩段代碼的組合,其中aapt版本的獲取需要捕獲一下異常,該函數(shù)在低版本中不存在,具體實(shí)現(xiàn)看代碼,這里不再過多解釋了
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 | apply plugin: PublicPluginclass PublicPlugin implements Plugin<Project> { void apply(Project project) { project.afterEvaluate { if (project.plugins.hasPlugin("com.android.application")) { def android = project.extensions.getByName("android")android.applicationVariants.all {def variant -> boolean aapt2Enable = false def processResourcesTask = project.tasks.getByName("process${variant.name.capitalize()}Resources") if (processResourcesTask) { try { //判斷aapt2是否開啟,低版本不存在這個(gè)方法,因此需要捕獲異常aapt2Enable = processResourcesTask.isAapt2Enabled()} catch (Exception e) { project.logger.error "${e.getMessage()}"} def aaptOptions = processResourcesTask.aaptOptions //aapt2開啟走此流程 if (aapt2Enable) { project.logger.error "aapt2 is enabled" File publicTxtFile = project.rootProject.file('public.txt') //public文件存在,則應(yīng)用,不存在則生成 if (publicTxtFile.exists()) { project.logger.error "${publicTxtFile} exists, apply it." //aapt2添加--stable-ids參數(shù)應(yīng)用aaptOptions.additionalParameters("--stable-ids", "${publicTxtFile}")} else { project.logger.error "${publicTxtFile} not exists, generate it." //aapt2添加--emit-ids參數(shù)生成aaptOptions.additionalParameters("--emit-ids", "${publicTxtFile}")}} else { //aapt2禁用走此流程 project.logger.error "aapt2 is disabled" File publicXmlFile = project.rootProject.file('public.xml') //public文件存在則應(yīng)用,不存在則生成 if (publicXmlFile.exists()) { //aapt的應(yīng)用需要將文件拷貝到對(duì)應(yīng)的目錄 //aapt public.xml文件的應(yīng)用并不是只是拷貝public.xml文件那么簡(jiǎn)單,還要根據(jù)生成的public.xml生成ids.xml文件,并將ids.xml中與values.xml中重復(fù)定義的id去除String mergeResourcesTaskName = variant.variantData.getScope().getMergeResourcesTask().name def mergeResourcesTask = project.tasks.getByName(mergeResourcesTaskName) if (mergeResourcesTask) {mergeResourcesTask.doLast { //拷貝public.xml文件 File toDir = new File(mergeResourcesTask.outputDir, "values") project.copy { project.logger.error "${variant.name}:copy from ${publicXmlFile.getAbsolutePath()} to ${toDir}/public.xml" from(publicXmlFile.getParentFile()) { include "public.xml"rename "public.xml", "public.xml"} into(toDir)} //生成ids.xml文件 File valuesFile = new File(toDir, "values.xml") File idsFile = new File(toDir, "ids.xml") if (valuesFile.exists() && publicXmlFile.exists()) { //記錄在values.xml中存在的id定義 def valuesNodes = new XmlParser().parse(valuesFile)Set<String> existIdItems = new HashSet<String>()valuesNodes.each { if ("id".equalsIgnoreCase("${it.@type}")) {existIdItems.add("${it.@name}")}}GFileUtils.deleteQuietly(idsFile)GFileUtils.touch(idsFile)idsFile.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>")idsFile.append("\n")idsFile.append("<resources>")idsFile.append("\n") def publicXMLNodes = new XmlParser().parse(publicXmlFile)Pattern drawableGeneratePattern = Pattern.compile('^(.*?_)([0-9]{0,})$')publicXMLNodes.each { //獲取public.xml中定義的id類型item if ("id".equalsIgnoreCase("${it.@type}")) { //如果在values.xml中沒有定義,則添加到ids.xml中 //如果已經(jīng)在values.xml中定義,則忽略它 if (!existIdItems.contains("${it.@name}")) {idsFile.append("\t<item type=\"id\" name=\"${it.@name}\" />\n")} else { project.logger.error "already exist id item ${it.@name}, ignore it"}} else if ("drawable".equalsIgnoreCase("${it.@type}")) { //以'_數(shù)字'結(jié)尾的drawable資源,此類資源是aapt編譯時(shí)生成的nested資源,如avd_hide_password_1, avd_hide_password_2 //但是可能會(huì)有其他資源摻雜,如abc_btn_check_to_on_mtrl_000, abc_btn_check_to_on_mtrl_015 //為了將此類資源過濾掉,將正則匹配到的數(shù)字轉(zhuǎn)成int,對(duì)比原始數(shù)字部分匹配字符串,如果一致,則是aapt生成 //重要:為了避免此類nested資源生成順序發(fā)生改變,應(yīng)該禁止修改此類資源Matcher matcher = drawableGeneratePattern.matcher(it.@name) if (matcher.matches() && matcher.groupCount() == 2) {String number = matcher.group(2) if (number.equalsIgnoreCase(Integer.parseInt(number).toString())) { project.logger.info "[${PREFIX}] declared drawable resource ${it.@name} which is generated by aapt. like use '<aapt:attr name=\"android:drawable\">'"idsFile.append("\t<item type=\"drawable\" name=\"${it.@name}\" />\n")}}}}idsFile.append("</resources>")}}}} else { //不存在則生成 project.logger.error "${publicXmlFile} not exists, generate it" //aapt 添加-P參數(shù)生成aaptOptions.additionalParameters("-P", "${publicXmlFile}")}}}}}}}} |
另一種解決方式
如果項(xiàng)目確定使用的是aapt2,并且不想通過編寫插件解決,這里提供一種更加簡(jiǎn)單的方式,就是直接利用aaptOptions參數(shù)進(jìn)行指定,但是這種方式不好做aapt和aapt2之間的無縫適配,只適合aapt2,參考代碼如下
| 12345678910 | android { aaptOptions { File publicTxtFile = project.rootProject.file('public.txt')if (publicTxtFile.exists()) { additionalParameters "--stable-ids", "${project.rootProject.file('public.txt').absolutePath}"} else { additionalParameters "--emit-ids", "${project.rootProject.file('public.txt').absolutePath}"}}} |
public.xml到public.txt的轉(zhuǎn)換
如果之前是用aapt備份下來的public.xml,如果現(xiàn)在使用了aapt2,則需要將文件進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換方式也很簡(jiǎn)單,如下
| 1234567891011121314151617181920212223242526272829303132333435 | task convertPublicXmlToPublicTxt() {doLast { //源public.xmlFile publicXmlFile = project.rootProject.file('backup/public.xml') //目標(biāo)public.txtFile publicTxtFile = project.rootProject.file('backup/generate_public.txt') //包名 String applicationId = "io.github.lizhangqu.aapt2"GFileUtils.deleteQuietly(publicTxtFile)GFileUtils.touch(publicTxtFile)def nodes = new XmlParser().parse(publicXmlFile) Pattern drawableGeneratePattern = Pattern.compile('^(.*?_)([0-9]{0,})$')nodes.each {project.logger.error "${it}" if ("drawable".equalsIgnoreCase("${it.@type}")) { //以'_數(shù)字'結(jié)尾的drawable資源,此類資源是aapt編譯時(shí)生成的nested資源,如avd_hide_password_1, avd_hide_password_2 //但是可能會(huì)有其他資源摻雜,如abc_btn_check_to_on_mtrl_000, abc_btn_check_to_on_mtrl_015 //為了將此類資源過濾掉,將正則匹配到的數(shù)字轉(zhuǎn)成int,對(duì)比原始數(shù)字部分匹配字符串,如果一致,則是aapt生成 //重要:為了避免此類nested資源生成順序發(fā)生改變,應(yīng)該禁止修改此類資源 //aapt生成的是以下表1開始,aapt2是以下標(biāo)0開始,因此轉(zhuǎn)換的過程需要-1Matcher matcher = drawableGeneratePattern.matcher(it.) if (matcher.matches() && matcher.groupCount() == 2) { String number = matcher.group(2) if (number.equalsIgnoreCase(Integer.parseInt(number).toString())) { String prefixName = matcher.group(1)publicTxtFile.append("${applicationId}:${it.@type}/\$${prefixName}_${Integer.parseInt(number) - 1} = ${it.@id}\n") return} } } publicTxtFile.append("${applicationId}:${it.@type}/${it.@name} = ${it.@id}\n")}}} |
執(zhí)行 gradle convertPublicXmlToPublicTxt即可完成轉(zhuǎn)換。
值得注意的是,這種轉(zhuǎn)換方式,由于原先aapt導(dǎo)出的public.xml中沒有styleable的定義,所以轉(zhuǎn)換后的public.txt中也沒有styleable,即轉(zhuǎn)換后的數(shù)據(jù)是aapt2導(dǎo)出的數(shù)據(jù)的子集,而aapt2生成的public.txt是具有styleable類型的id的,但是實(shí)際應(yīng)用過程中并沒有發(fā)現(xiàn)什么大的問題,因此幾乎可以忽略不計(jì)。如果你發(fā)現(xiàn)有問題,可以及時(shí)聯(lián)系我。
意外的收獲aapt2資源分區(qū)
在尋找解決aapt2資源id固定的過程中,意外發(fā)現(xiàn)aapt2自帶了資源PP段分區(qū)功能。就是通過–package-id參數(shù),指定PP段分區(qū),但是值得注意的是這個(gè)值必須大于0x7f,經(jīng)過測(cè)試,大于0x7f的PP段分區(qū)在Android7.0以下是無法識(shí)別的,可以安裝但是啟動(dòng)會(huì)崩潰。因此通過這種方式分區(qū)生成的apk文件,只能安裝在Android7.0以上的系統(tǒng),比較雞肋。示例代碼如下
| 12345 | android { aaptOptions { additionalParameters "--package-id", "0x80"}} |
此時(shí)生成的apk的資源PP段都是0x80,可以通過app/build/intermediates/res/symbol-table-with-package/debug/package-aware-r.txt文件進(jìn)行驗(yàn)證。
總結(jié)
當(dāng)上帝為你關(guān)上一扇門的時(shí)候,還會(huì)用門夾你的腦袋(開個(gè)玩笑),雖然public.xml在aapt2中無法用了,但是google在aapt2中提供給我們的–stable-ids和–emit-ids兩個(gè)參數(shù)不見得那么不好用,甚至比aapt的public.xml還要好用,只需要生成和應(yīng)用就好了,不需要進(jìn)行中間的處理過程。簡(jiǎn)直完美!但是該方案還沒有用足夠多的case進(jìn)行驗(yàn)證,不代表沒有坑,出了坑,不負(fù)責(zé)!
http://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/總結(jié)
以上是生活随笔為你收集整理的aapt2 适配之资源 id 固定的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Tensorflow Lite 编译
- 下一篇: aapt2 资源 compile 过程