原文 http://blog.votzone.com/2018/05/12/apk-merge.html 之前接手過一個sdk的開發工作,在開發過程中有一個很重要的點就是儘量使用代碼來創建控制項,資源文件最好放到assets目錄下,如果必須使用res資源,需要通過 getResources().get ...
原文 http://blog.votzone.com/2018/05/12/apk-merge.html
之前接手過一個sdk的開發工作,在開發過程中有一個很重要的點就是儘量使用代碼來創建控制項,資源文件最好放到assets目錄下,如果必須使用res資源,需要通過 getResources().getIdentifier("activity_splash","layout", getPackageName())
這種方式來獲取資源id,而不能直接通過R文件獲取。
今天就來研究一下這個問題。
一、lib項目中r文件中資源唯一標誌為static變數
一般的app項目中自動生成的R文件為常量,而在library項目中為變數。根據Android官方文檔,在android 14 之後添加的這一特性,之前編譯後的lib項目中是常量,之後的為static 變數。 目的是為了在資源衝突時能夠修改資源唯一值。 如圖在library項目中,自動生成的R文件如下
有一個需要關註的點是R文件是java 代碼,在build時會生成.class文件並添加到dex中。 因為R文件中的常量值僅僅受編譯器控制,在lib發佈之後添加到jar包中的.class並不會受到當前編譯器的影響。而通常lib發佈之後要給第三方使用。
看一下反編譯後的apk
反編譯後查看mylib 下對應的R文件smali代碼,可見其值又被設置為final(常量)了。 因此我們可以知道編譯器在生成apk時雖然沒有lib的java代碼可以重置和修改,但是在將jar轉化為dex時可以轉換為常量。 轉化為常量後並不影響其使用, 如圖在lib中使用layout資源的反編譯代碼
我們可以看出使用靜態常量可以在生成apk時動態修改其指定的值。
如果是常量,編譯後v0的值將直接使用常量值,這樣修改R.class文件中的值將沒有意義。例如在app中反編譯後代碼如下:
二、從反編譯後代結構中查看資源對應問題
可以看到,反編譯後的代碼與我們寫的java代碼基本是一一對應的,那麼問題來了,Android是怎麼通過一個id值來找到需要的資源呢?通過分析Android源碼我們當然可以找出過程,但是分析apk反編譯後的結構可以給我們更直接的思路。
在res/values/文件夾下有個public.xml文件,其中每行有三對值,分別為type
、name
、id
通過分析我們可以直接得出結論: public.xml中的對應關係直接關係到哪個id找那個資源。
三、添加資源
Unity3d和cocos2d引擎有自己一套方式來添加資源id,假如我們自己搞一個游戲,又想為其添加一些java代碼和資源該怎麼操作?
根據之前的分析我們可以設計如下測試方案
- 假定我們要添加代碼的apk為targetapp,我們編寫一個叫mergelib的apk,然後將mergelib中資源和代碼添加到targetapp中。
- 在編寫mergelib時,使用
getResources().getIdentifier("activity_splash","layout", getPackageName())
的方式獲取資源id; - 通過apktool反編譯代碼,將資源文件複製到要加入的目標apk中;
- 將mergelib的public.xml文件中需要的資源項添加到targetapp中;
- 編譯並簽名測試。
根據如上實驗我們可以確定這樣的操作是可行的。測試流程如下:
需要的資源:
- targetapp- 目標app, 在這裡代表游戲
- mergelib- 要將其中包含資源的代碼合併進去
mergelib 中對資源通過getIdentifier()的方式使用: 例如設置啟動頁Activity中的ContentView
的設置
int id = getResources().getIdentifier("activity_splash","layout", getPackageName());
setContentView(id);
我們的目標:為targetapp添加一個啟動頁,啟動頁代碼在mergelib中編寫。
執行流程
-
修改targetapp中AndroidManifest.xml文件
1) 將SplashActivity 的聲明添加進去
2) 修改啟動Activity -
複製需要的代碼進入targetapp:
複製mergelib中SplashActivity的代碼並修改啟動MainActivity的啟動代碼 如圖 -
複製資源
1) 將res/anim 下alpha.xml複製到target
2) 將res/layout 下 spalsh.xml複製到target 下 -
修改ids
將 res/values/ids.xml 中多出來的行複製到 對應ids.xml文件中如圖, 本例中僅有一個id 即ImageView的id, 因此將該行複製到targetapp 中對應的ids.xml文件中即可 位置不重要
-
修改public.xml文件 mergelib Splash中用到了
anim
,layout
,id
, 並且間接用到了mipmap
,因此這些對應的值都需要添加到targetapp。觀察public.xml的文件結構,可以發現如下特點:
1) 所有同類型(type相同)的id連續
2) 同類型的id 前4位元組相同, 如下圖 anim 的前四位元組0x7f01 與 attr 不同3) 所有id唯一
根據以上三個特點,我們將多出來的id添加到target為了保證唯一且方便修改,我們做瞭如下替換(右側為target) -
一切就緒,編譯並安裝
四、工具化處理public.xml的替換過程
上述手動測試僅僅只有一個資源id的情況,假如我們加入了一個support包或者其他一些包含資源的包,那麼資源數量將會增加到幾百個,這樣的話手動添加肯定是不行的,我們需要一個腳本工具來實現。
腳本接收兩個public.xml格式的文本,並輸出一個合併版本。 其中targetapp中的id值是不可變得,在遇到id衝突時,我們改變mergelib的public.xml。
1) 複製mergelib的public文件為mergeid.xml
2) 複製target app 的public 並命名為oriid.xml
3) 將mergeid.xml 和oriid.xml 放到RIDreset.py同目錄下, 運行py腳本
4) 出現 oriid.xml_ 文件, 即生成的合併文件
案例及腳本
https://github.com/votzone/DroidCode/tree/master/VotAndroid/Mergeapp