背景:
簡(jiǎn)單來(lái)說(shuō)我們的打包工作就是hack原始包,向其中注入代碼。ps:我們不是黑客!
原始打包(hack)方案:
- 1.反編譯原始apk,得到文件夾A。
- 2.將要加入的jar包變成dex,再變成smali,放入A。
- 3.將要加入的資源直接放入A。
- 4.重新編譯A生成新apk。
現(xiàn)在的方案來(lái)自Android逆向。
好了,看出問(wèn)題在哪嗎?
資源直接放入A,未經(jīng)編譯,所以沒(méi)有索引。
這種資源如何在運(yùn)行時(shí)加載?
采用Resources.getIdentifier()。
getResources().getIdentifier("icon", "drawable", "com.XX.XX.mainactivity");
官方不鼓勵(lì)用這種方式,采用索引比使用名稱更快。原話如下:
use of this function is discouraged. It is much more efficient to retrieve resources by identifier than by name.
這種打包方案是可行的,但是官方的提示,始終內(nèi)心不安,于是繼續(xù)深挖。
資源索引
反編譯得到文件夾,進(jìn)去是看不到索引的。所以一開(kāi)始我們也沒(méi)關(guān)注索引。
而直接解壓是能看到索引的。
解壓apk包得到內(nèi)容如下:
AndroidManifest.xml:直接解壓得到二進(jìn)制格式清單文件,打開(kāi)后一堆亂碼,需要將apk反編譯才能看到文本格式。
class.dex :代碼文件。
resources.arsc: 資源索引文件,這個(gè)是重點(diǎn),之前被忽略了。
res:資源,包括圖片,字符串、布局等。
assets: 也是資源文件夾。
assets和res的區(qū)別:
- res中的文件會(huì)被映射到R.java文件中,訪問(wèn)的時(shí)候直接使用資源ID即R.id.filename;用來(lái)存放android應(yīng)用程序的9中類型的資源,如圖標(biāo),顏色,動(dòng)畫(huà),布局,數(shù)值,配置等信息。
- assets:
這些文件最終會(huì)被原裝不動(dòng)地打包在apk文件中。assets文件夾下的文件不會(huì)被映射到R.java中,需要使用文件名來(lái)訪問(wèn)。
this is a good location for textures and game data.
存放游戲數(shù)據(jù)如聲明,協(xié)議等。
- assets:
待加入原始apk的渠道文件中包含的資源文件,大多數(shù)資源在res目錄下,是可以生成索引,這是我們優(yōu)化的點(diǎn)。
索引的好處:
為了使得應(yīng)用程序能夠在運(yùn)行時(shí)同時(shí)支持不同尺寸和密度的屏幕和不同的語(yǔ)言,Android應(yīng)用程序資源的組織方式有19個(gè)維度,組織方式如下圖所示。有不同的屏幕和中文語(yǔ)言。
resources.arsc是一種二進(jìn)制格式的文件。aapt在對(duì)資源文件進(jìn)行編譯時(shí),會(huì)為每一個(gè)資源分配唯一的id值,程序在執(zhí)行時(shí)會(huì)根據(jù)這些id值讀取特定的資源,而resources.arsc文件正是包含了所有id值的一個(gè)數(shù)據(jù)集合。在該文件中,如果某個(gè)id對(duì)應(yīng)的資源是String或者數(shù)值(包括int,long等),那么該文件會(huì)直接包含相應(yīng)的值,如果id對(duì)應(yīng)的資源是某個(gè)layout或者drawable資源,那么該文件會(huì)存入對(duì)應(yīng)資源的路徑地址。
可見(jiàn),這么多資源目錄,不通過(guò)索引而直接用資源名稱來(lái)查找資源是很費(fèi)時(shí)的。
萬(wàn)不得已要用資源名稱查找時(shí),可以采用下面的方式,據(jù)說(shuō)比前面提到的Resources.getIdentifier() 快4倍。
使用名稱查找資源的更優(yōu)方式:
/**
* @author Lonkly
* @param variableName - name of drawable, e.g R.drawable.<b>image</b>
* @param с - class of resource, e.g R.drawable.class or R.raw.class
* @return integer id of resource
*/
public static int getResId(String variableName, Class<?> с) {
Field field = null;
int resId = 0;
try {
field = с.getField(variableName);
try {
resId = field.getInt(null);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
return resId;
}
使用:
int id = ResourceMan.getResId("icon", R.drawable.class);
新加入的資源能生成索引嗎?
我們都知道編寫(xiě)代碼時(shí),res中的資源可以直接通過(guò)findViewById(R.XX.XX)的方式獲得。R.jav中存放了資源的id,這是IDE自動(dòng)生成的,其實(shí)IDE也是通過(guò)資源打包工具aapt(android Asset Package Tool)工具來(lái)編譯的,
Android資源打包工具aapt在編譯和打包資源的過(guò)程中,會(huì)執(zhí)行以下兩個(gè)額外的操作:
- 賦予每一個(gè)非assets資源一個(gè)ID值,這些ID值以常量的形式定義在一個(gè)R.Java文件中。
- 生成一個(gè)resources.arsc文件,用來(lái)描述那些具有ID值的資源的配置信息,它的內(nèi)容就相當(dāng)于是一個(gè)資源索引表。
有了資源ID以及資源索引表之后,Android資源管理框架就可以迅速將根據(jù)設(shè)備當(dāng)前配置信息來(lái)定位最匹配的資源了。
## Android簡(jiǎn)化版打包流程
打包流程圖
- 通過(guò)aapt打包res資源文件,生成R.java、resources.arsc和res文件(二進(jìn)制 & 非二進(jìn)制如res/raw和pic保持原樣)
- 處理.aidl文件,生成對(duì)應(yīng)的Java接口文件
- 通過(guò)Java Compiler編譯R.java、Java接口文件、Java源文件,生成.class文件
- 通過(guò)dex命令,將.class文件和第三方庫(kù)中的.class文件處理生成classes.dex
- 通過(guò)apkbuilder工具,將aapt生成的resources.arsc和res文件、未編譯的資源assets文件和classes.dex一起打包生成apk
- 通過(guò)Jarsigner工具,對(duì)上面的apk進(jìn)行debug或release簽名
- 通過(guò)zipalign工具,將簽名后的apk進(jìn)行對(duì)齊處理。
從完整的打包流程來(lái)看,使用aapt把待加入原始apk的資源文件編譯生成資源索引是關(guān)鍵。
生成索引
AAPT是Android Asset Packaging Tool的縮寫(xiě),它存放在SDK的tools/目錄下,AAPT的功能很強(qiáng)大,可以通過(guò)它查看查看、創(chuàng)建、更新壓縮文件(如 .zip文件,.jar文件, .apk文件), 它也可以把資源編譯為二進(jìn)制文件,并生成resources.arsc, AAPT這個(gè)工具在APK打包過(guò)程中起到了非常重要作用,在打包過(guò)程中使用AAPT對(duì)APK中用到的資源進(jìn)行打包,這里不對(duì)AAPT這個(gè)工具做過(guò)多的討論,只看一下AAPT這個(gè)工具在打包過(guò)程中起到的作用,下圖是AAPT打包的流程:
AAPT這個(gè)工具在打包過(guò)程中主要做了下列工作:
- 把"assets"和"res/raw"目錄下的所有資源進(jìn)行打包(會(huì)根據(jù)不同的文件后綴選擇壓縮或不壓縮),而"res/"目錄下的其他資源進(jìn)行編譯或者其他處理(具體處理方式視文件后綴不同而不同,例如:".xml"會(huì)編譯成二進(jìn)制文件,".png"文件會(huì)進(jìn)行優(yōu)化等等)后才進(jìn)行打包;
- 會(huì)對(duì)除了assets資源之外所有的資源賦予一個(gè)資源ID常量,并且會(huì)生成一個(gè)資源索引表resources.arsc;
- 編譯AndroidManifest.xml成二進(jìn)制的XML文件;
- 把上面3個(gè)步驟中生成結(jié)果保存在一個(gè).ap_文件,并把各個(gè)資源ID常量定義在一個(gè)R.java中,打入.dex;
**資源加密
修改aapt,編譯時(shí)對(duì)資源文件名稱壓縮,這樣通過(guò)使用修改過(guò)的AAPT編譯資源并進(jìn)行打包,
既可以減小包體,又可以對(duì)資源進(jìn)行一定程度的加密。**
aapt 命令參數(shù)
-f 如果編譯出來(lái)的文件已經(jīng)存在,強(qiáng)制覆蓋。
-m 使生成的包的目錄放在-J參數(shù)指定的目錄。
-J 指定生成的R.java的輸出目錄
-S res文件夾路徑
-A assert文件夾的路徑
-M AndroidManifest.xml的路徑
-I 某個(gè)版本平臺(tái)的android.jar的路徑
-F 具體指定apk文件的輸出
命令具體使用方法,這里不贅述,Android知識(shí)博大精深,隨便一個(gè)點(diǎn)可以拿出來(lái)說(shuō)幾篇。
Android資源加載
事實(shí)上,當(dāng)程序運(yùn)行時(shí),所需要的資源都要從原始文件中讀取(APK在安裝時(shí)都會(huì)被系統(tǒng)拷貝到/data/app目錄下)。加載資源時(shí),首先加載resources.arsc,然后根據(jù)id值找到指定的資源,沒(méi)有資源索引的就需要通過(guò)資源名稱來(lái)加載。
總結(jié):
打包流程:
- 新資源拷貝到原始文件夾相應(yīng)目錄,(這里可能會(huì)有命名沖突,如何解決?)。
- aapt編譯資源,重新生成R.java和resources.arsc并覆蓋舊版。
- 資源編譯時(shí)順便簡(jiǎn)化資源名稱,實(shí)現(xiàn)減小包體和資源加密的目的。
- 后續(xù)流程和之前的打包方式一樣。
資源加載:
有了索引之后就可以直接通過(guò)索引而不是資源名稱訪問(wèn)了,提高效率。
以上內(nèi)容是通過(guò)研究Android打包流程和資源加載時(shí)對(duì)現(xiàn)有打包方案不足之處的思考,僅理論,待實(shí)踐。
本人知識(shí)淺薄,有任何問(wèn)題請(qǐng)指正,不吝賜教!
參考文章:
Android官網(wǎng)
Android應(yīng)用程序資源的編譯和打包過(guò)程分析
Android資源管理框架(Asset Manager)
打包總覽
老羅的Android之旅
Android應(yīng)用程序資源的編譯和打包過(guò)程分析