一文帶你搞清楚Android游戲發(fā)行切包資源ID那點(diǎn)事
概述
大家在做游戲切包時(shí),可能都會(huì)遇到上圖這種資源找不到導(dǎo)致崩潰的問(wèn)題,本文將全面而詳細(xì)地分析在處理游戲切包時(shí),關(guān)于資源合并的問(wèn)題。
問(wèn)題分析
原理
在切包時(shí),我們一般是將游戲母包和sdk包兩個(gè)apk合并,在本文中我們稱游戲母包為基礎(chǔ)包,稱sdk包為擴(kuò)展包
我們?cè)谇邪鼤r(shí),基本流程如上,目的是為了將擴(kuò)展包的內(nèi)容合并到基礎(chǔ)包中,達(dá)到更新代碼的目的,因此主要流程就是
- 反編譯得到包體內(nèi)容
- 合并擴(kuò)展包內(nèi)容到基礎(chǔ)包中,本文主要探討res目錄
- 回編譯得到新的apk
分析
那么為什么會(huì)出現(xiàn)Resources$NotFoundException
異常呢?
該異常表明指定id對(duì)應(yīng)的資源不存在,可是資源明明都已經(jīng)合并,并且回編譯進(jìn)新apk了,為什么會(huì)說(shuō)資源不存在?
那是因?yàn)樵贏ndroid中,我們是根據(jù)資源ID,在resources.arsc
中查找對(duì)應(yīng)資源的,資源都已經(jīng)回編譯進(jìn)新apk,那么在resources.arsc
中應(yīng)該是確實(shí)存在這個(gè)資源信息的(我們可以通過(guò)Androd Studio查看驗(yàn)證),那么問(wèn)題就是出在資源ID上了。
那為什么資源在基礎(chǔ)包合擴(kuò)展包中分別訪問(wèn)都正常,合并后會(huì)不正常?
這是因?yàn)樵诰幾g基礎(chǔ)包和擴(kuò)展包時(shí),它們的資源是獨(dú)立編譯的,在產(chǎn)生R.java
時(shí),即使是雙方共有的資源,資源ID也可能不一樣。如下圖:
假設(shè)基礎(chǔ)包和擴(kuò)展包中都包含hello_world
這個(gè)字符串資源
在基礎(chǔ)包中,這個(gè)資源對(duì)應(yīng)的資源ID是0x7f050001
,而在擴(kuò)展包中,對(duì)應(yīng)的資源ID是0x7f0f0013
在合并回編譯后,這個(gè)資源ID確實(shí)還存在于apk中,但是資源ID變成了0x7f050033
如果我們不做任何處理,那么在新包里面,來(lái)自擴(kuò)展包的代碼在獲取hello_world
時(shí)還是會(huì)使用0x7f0f0013
去獲取這個(gè)資源,那么此時(shí)要么會(huì)獲取到錯(cuò)誤的資源,要么就會(huì)報(bào)Resources$NotFoundException
異常了。
解決思路
根據(jù)上面分析,我們已經(jīng)知道關(guān)鍵問(wèn)題在于資源ID映射錯(cuò)誤上,那么我們只要確?;鼐幾g后,資源ID映射關(guān)系是正確的,就可以解決這個(gè)問(wèn)題了。如下圖:
如果我們可以
- 保持新包的資源ID和基礎(chǔ)包一樣,那么基礎(chǔ)包的代碼就不會(huì)出問(wèn)題
- 將擴(kuò)展包的資源ID修改成新包的資源ID,那么擴(kuò)展包的代碼也不會(huì)出問(wèn)題了
接下來(lái),我們就向這兩點(diǎn)努力~
行動(dòng)
0x01:保留舊ID
要做到保留舊ID,我們要解決2個(gè)問(wèn)題
- 怎樣獲取所有舊ID
- 怎樣在回編譯時(shí)復(fù)用舊ID
獲取舊ID:public.xml
獲取舊ID方法很簡(jiǎn)單,apk通過(guò)apktool
反編譯后,我們可以找到res/values/public.xml
,這個(gè)文件包含了apk所有的資源ID,內(nèi)容一般如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <public type="anim" name="abc_fade_in" id="0x7f010000"/> <public type="animator" name="design_appbar_state_list_animator" id="0x7f020000"/> <public type="array" name="TDDisPresetProperties" id="0x7f030000"/> <public type="attr" name="SharedValue" id="0x7f040000"/> <public type="bool" name="abc_action_bar_embed_tabs" id="0x7f050000"/> <public type="color" name="abc_background_cache_hint_selector_material_dark" id="0x7f060000"/> <public type="dimen" name="abc_action_bar_content_inset_material" id="0x7f070000"/> <public type="drawable" name="$avd_hide_password__0" id="0x7f080000"/> <public type="id" name="ALT" id="0x7f090000"/> <public type="integer" name="abc_config_activityDefaultDur" id="0x7f0a0000"/> <public type="interpolator" name="btn_checkbox_checked_mtrl_animation_interpolator_0" id="0x7f0b0000"/> <public type="layout" name="abc_action_bar_title_item" id="0x7f0c0000"/> <public type="menu" name="menu_openchat_info" id="0x7f0d0000"/> <public type="mipmap" name="app_icon" id="0x7f0e0000"/> <public type="raw" name="firebase_common_keep" id="0x7f0f0000"/> <public type="string" name="FreeformWindowOrientation_landscape" id="0x7f100000"/> <public type="style" name="AlertDialog.AppCompat" id="0x7f110000"/> <public type="xml" name="tab_menu" id="0x7f130005"/> </resources>
可以看到,其中包含了public
標(biāo)簽,我們只需要解析xml,就可以獲取到所有資源的類型,名稱和資源ID的值。
復(fù)用舊ID
我們?cè)诤喜U(kuò)展包的資源到基礎(chǔ)包后,新增的資源并沒(méi)有分配資源ID,因?yàn)榛A(chǔ)包的public.xml
中不存在擴(kuò)展包的資源,所以我們要對(duì)合并后,新增的資源分配資源ID。
大部分博客提到的方式是自己根據(jù)資源,手動(dòng)在public.xml
中添加資源,并且遞增資源ID實(shí)現(xiàn)。
本文使用aapt2
處理。
Aapt2命令
關(guān)鍵為在link
資源時(shí)使用--stable-ids
來(lái)指定資源ID。
該命令接受一個(gè)特定格式的文件,如下:
com.demo.res:anim/abc_fade_in = 0x7f010000 com.demo.res:animator/design_appbar_state_list_animator = 0x7f020000 com.demo.res:array/TDDisPresetProperties = 0x7f030000 com.demo.res:attr/SharedValue = 0x7f040000 com.demo.res:bool/abc_action_bar_embed_tabs = 0x7f050000 com.demo.res:color/abc_background_cache_hint_selector_material_dark = 0x7f060000 com.demo.res:dimen/abc_action_bar_content_inset_material = 0x7f070000 com.demo.res:drawable/$avd_hide_password__0 = 0x7f080000 com.demo.res:id/ALT = 0x7f090000 com.demo.res:integer/abc_config_activityDefaultDur = 0x7f0a0000 com.demo.res:layout/abc_action_bar_title_item = 0x7f0c0000 com.demo.res:menu/menu_openchat_info = 0x7f0d0000 com.demo.res:mipmap/app_icon = 0x7f0e0000 com.demo.res:raw/firebase_common_keep = 0x7f0f0000 com.demo.res:string/FreeformWindowOrientation_landscape = 0x7f100000 com.demo.res:style/AlertDialog.AppCompat = 0x7f110000 com.demo.res:xml/appsflyer_backup_rules = 0x7f130000
很容易看到,內(nèi)容格式為package:type/name = value
,我們只需要把上面獲取到的public.xml
中的所有資源轉(zhuǎn)換成這個(gè)格式,就可以用來(lái)輸入到aapt2 link
命令的--stable-ids
參數(shù)中,那么新編譯的資源,只要public.xml
中存在,那么資源ID的值就不會(huì)變化,另外aapt2
也會(huì)自動(dòng)給新增的資源分配一個(gè)合適的資源ID。
保留新資源ID
那么我們?cè)鯓拥玫叫碌?code>public.xml呢,這里要在使用link
命令時(shí)使用--emit-ids
來(lái)指定資源ID的輸出目錄,那么我們就可以得到一個(gè)格式同上的文件,里面包含的所有參與編譯的資源的資源ID。
然后我們?cè)賹⑦@個(gè)文件的內(nèi)容,轉(zhuǎn)換為public.xml
,再放進(jìn)新包的res/values
下。
至此,我們已經(jīng)成功做到
- 保留了舊的資源ID
- 對(duì)新增的資源分配了資源ID
- 獲取到了合并后的所有資源的資源ID
0x02:修改資源ID
因?yàn)槲覀儽A袅嘶A(chǔ)包的資源ID,那么對(duì)于基礎(chǔ)包的代碼來(lái)說(shuō),資源沒(méi)有任何變化,那么就不需要修改基礎(chǔ)包的資源ID了,那么我們接下來(lái)要處理擴(kuò)展包的資源ID,把用到的資源ID改為新的值。
tips:擴(kuò)展包一般是sdk包,我們可以使用
Resources#getIdentifier
來(lái)通過(guò)資源名稱來(lái)動(dòng)態(tài)獲取資源ID來(lái)規(guī)避擴(kuò)展包中資源ID不正確的問(wèn)題。在實(shí)踐中是可以了,不過(guò)會(huì)給代碼維護(hù)帶來(lái)一些問(wèn)題。
需要修改的位置
資源ID本質(zhì)上是R.java
中的靜態(tài)變量,在實(shí)際代碼中,我們也是通過(guò)引用的方式使用資源ID的,而R.java
在反編譯后,會(huì)變成smali文件,因此我們需要處理的就是擴(kuò)展包中所有R.smali
的資源ID。
特別地,由于app
模塊在編譯的時(shí)候有可能會(huì)對(duì)靜態(tài)變量進(jìn)行編譯優(yōu)化,所以實(shí)際上,其他smali文件,例如Activity
的smali文件中,會(huì)直接使用資源ID常量值的情況。
但是在游戲切包場(chǎng)景下,sdk一般都是以庫(kù)的形式存在,對(duì)于library
模塊,因?yàn)橘Y源ID在編譯時(shí)不是常量,所以并不會(huì)出現(xiàn)編譯優(yōu)化的情況。而對(duì)于游戲母包,因?yàn)槲覀儚?fù)用了游戲母包(基礎(chǔ)包)的資源ID,游戲母包的資源ID沒(méi)有發(fā)生變化,那么即使它使用了常量值也不會(huì)有問(wèn)題。
更新R Smali
那么接下來(lái)我們就對(duì)R Smali文件進(jìn)行修改,按照上述分析,我們只需要對(duì)來(lái)自擴(kuò)展包的R文件進(jìn)行更新即可。
普通ID
對(duì)于一般的R文件,結(jié)構(gòu)如下:
.class public final Landroidx/activity/R$id; .super Ljava/lang/Object; # annotations // .... # static fields .field public static final view_tree_on_back_pressed_dispatcher_owner:I = 0x7f0902df # direct methods .method private constructor <init>()V // ... .end method
可以看到,R文件內(nèi)部就是一系列的static fields
,我們通過(guò)文件名稱確定資源類型,如R$id.smali
保存的是id
資源,通過(guò).field public static final name:I = value
來(lái)獲取資源的名稱(name
)和當(dāng)前值(value
)。
在上面我們已經(jīng)獲取到新的public.xml
,可以獲取到所有資源的資源ID,那么我們只需要匹配資源的類型和名稱,然后用新的值替換當(dāng)前值就可以了。
R$styleable.smali
對(duì)于R$styleable.smali
,情況有點(diǎn)不同。
R$styleable.smali
的內(nèi)容是通過(guò)<declare-styleable>
生成的,每個(gè)<declare-styleable>
對(duì)應(yīng)一個(gè)attr
數(shù)組和數(shù)個(gè)下標(biāo)值。例如:
<declare-styleable name="ColorStateListItem"> <attr name="android:color"/> <attr format="float" name="alpha"/> <attr name="android:alpha"/> <attr format="float" name="lStar"/> <attr name="android:lStar"/> </declare-styleable>
對(duì)應(yīng)
.field public static final ColorStateListItem:[I .field public static final ColorStateListItem_alpha:I = 0x3 .field public static final ColorStateListItem_android_alpha:I = 0x1 .field public static final ColorStateListItem_android_color:I = 0x0 .field public static final ColorStateListItem_android_lStar:I = 0x2 .field public static final ColorStateListItem_lStar:I = 0x4
對(duì)于下標(biāo),我們可以忽略,因?yàn)橹灰獢?shù)組大小和順序不變,下標(biāo)也不需要變。
那么我們需要修改數(shù)組內(nèi)的值,但是我們可以發(fā)現(xiàn),數(shù)組的內(nèi)容并沒(méi)有和定義放在一起,那么我們要怎樣處理?
實(shí)際上,我們可以忽略數(shù)組定義,因?yàn)?code><declare-styleable>并沒(méi)有新增資源ID,數(shù)組內(nèi)的資源ID都是attr
資源ID,所以我們要做的就是:
- 找到當(dāng)前文件的舊的資源ID,找到它對(duì)應(yīng)的
attr
資源名稱,我們通過(guò)解析擴(kuò)展包的public.xml
就可以獲取到舊的資源ID對(duì)應(yīng)的名稱。 - 再通過(guò)資源名稱找到它在新包中的資源ID值,通過(guò)上面保存的新的資源ID很容易可以做到。
- 替換,這一步需要注意不能全局替換,逐行遍歷替換就可以了。
其他細(xì)節(jié)
系統(tǒng)資源
在資源合并的時(shí)候我們有時(shí)候會(huì)看到一些特別的資源ID,例如0x101011c
,這類資源ID為系統(tǒng)資源,實(shí)際上我們可以忽略這部分資源ID。也可以在Android源碼中找到這些資源ID
aapt2輸出R.java
除了使用--emit-ids
可以獲取到所有資源ID之外,也可以選擇使用--java
來(lái)輸出R.java
,同樣可以獲取到所有資源ID
aapt2編譯時(shí)報(bào)系統(tǒng)資源找不到
需要使用比apk編譯版本高的android.jar
總結(jié)
希望對(duì)大家有幫助,歡迎在評(píng)論區(qū)一起交流~
以上就是一文帶你搞清楚Android游戲發(fā)行切包資源ID那點(diǎn)事的詳細(xì)內(nèi)容,更多關(guān)于Android 游戲發(fā)行切包資源ID的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android實(shí)現(xiàn)手機(jī)監(jiān)控?cái)z像頭
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)手機(jī)監(jiān)控?cái)z像頭,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03Android 支付寶支付、微信支付、銀聯(lián)支付 整合第三方支付接入方法(后臺(tái)訂單支付API設(shè)計(jì))
這篇文章主要介紹了Android 支付寶支付、微信支付、銀聯(lián)支付 整合第三方支付接入方法(后臺(tái)訂單支付API設(shè)計(jì))的相關(guān)資料,需要的朋友可以參考下2016-11-11Android Data Binding數(shù)據(jù)綁定詳解
本文主要介紹Android Data Binding數(shù)據(jù)綁定的知識(shí),這里整理了詳細(xì)的資料及簡(jiǎn)單示例代碼幫助大家學(xué)習(xí)理解此部分知識(shí),有需要的小伙伴可以參考下2016-09-09Android Studio實(shí)現(xiàn)發(fā)短信功能
這篇文章主要介紹了Android Studio實(shí)現(xiàn)發(fā)短信功能,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-06-06Android中AutoCompleteTextView自動(dòng)提示
這篇文章主要為大家詳細(xì)介紹了Android中AutoCompleteTextView自動(dòng)提示的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12學(xué)習(xí)Android開發(fā)之RecyclerView使用初探
Android開發(fā)學(xué)習(xí)之路的第一課RecyclerView使用初探,感興趣的小伙伴們可以參考一下2016-07-07