Android AAPT(Android Asset Packaging Tool)詳解
AAPT解釋,作用
AAPT - Android Asset Packaging Tool
看全稱,就可知道AAPT是Android資源打包工具。講這個之前,是有必要簡單說下Android是如何構(gòu)建一個APK的。
上圖是Google官方發(fā)布的一張非常經(jīng)典的Apk打包流程圖。
流程概述
工程的資源文件(res文件夾下的文件),通過AAPT打包成R.java類(資源索引表),以及.arsc資源文件如果有aidl,通過aidl工具,打包成java接口類R.java和aidl.java通過java編譯成想要的.class文件。源碼class文件和第三方j(luò)ar或者library通過dx工具打包成dex文件。dx工具的主要作用是將java字節(jié)碼轉(zhuǎn)換成Dalvik字節(jié)碼,在此過程中會壓縮常量池,消除一些冗余信息等。apkbuilder工具會將所有沒有編譯的資源,.arsc資源,.dex文件打包到一個完成apk文件中中。簽名,5中完成apk通過配置的簽名文件(debug和release都有),jarsigner工具會對齊簽名。得到一個簽名后的apk,signed.apkzipAlign工具對6中的signed.apk進行對齊處理,所謂對齊,主要過程是將APK包中所有的資源文件距離文件起始偏移為4字節(jié)整數(shù)倍,這樣通過內(nèi)存映射訪問apk文件時的速度會更快。對齊的作用主要是為了減少運行時內(nèi)存的使用。
總結(jié)
輸入:res文件夾所有的資源(layout\drawable\string\array等),asset下的資源,AndroidManifest.xml,Android.jar文件工具: aapt 地址(/your sdk path/build-tools/your build tools version/aapt)輸出:res下的資源都會被編譯成一個資源索引文件resource.arsc以及一個R.java類。asset下的資源不會編譯,直接壓縮進apk。
AAPT命令詳解
1、aapt l[ist] [-v] [-a] file.{zip,jar,apk} #列出指定apk包的所有內(nèi)容
-v 以table形式列出
-a 詳細列出內(nèi)容
D:\>aapt l f:\resign\ListenerApk.apk
res/anim/alpha_anim_in.xml
res/anim/alpha_anim_out.xml
res/anim/in_from_left.xml
res/anim/in_from_right.xml
......
res/drawable-xxhdpi/ic_launcher.png
classes.dex
org/apache/http/entity/mime/version.properties
org/achartengine/image/zoom-1.png
org/achartengine/image/zoom_in.png
org/achartengine/image/zoom_out.png
META-INF/MANIFEST.MF
META-INF/CERT.SF
META-INF/CERT.RSA
2、aapt d[ump] [--values] WHAT file.{apk} [asset [asset ...]] #查看指定apk的相關(guān)信息
常用兩個命令:
aapt dump badging f:\resign\ListenerApk.apk #獲取標(biāo)簽(package&Acvitity)和圖標(biāo)相關(guān)信息
aapt dump permissions f:\resign\ListenerApk.apk #獲取指定apk所具有的權(quán)限
3、aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...] #刪除指定apk的file1文件
aapt r f:\resign\ListenerApk.apk META-INF/MANIFEST.MF #刪除f:\resign\ListenerApk.apk里的META-INF/MANIFEST.MF文件,可以通過aapt -l來驗證是否刪除成功
4、aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...] #往指定apk中添加File1文件
aapt a f:\resign\ListenerApk.apk test.txt #往f:\resign\ListenerApk.apk添加當(dāng)前目錄下的test.txt文件
5、編譯Android資源
aapt p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml] \
[-0 extension [-0 extension ...]] [-g tolerance] [-j jarfile] \
[--debug-mode] [--min-sdk-version VAL] [--target-sdk-version VAL] \
[--app-version VAL] [--app-version-name TEXT] [--custom-package VAL] \
[--rename-manifest-package PACKAGE] \
[--rename-instrumentation-target-package PACKAGE] \
[--utf16] [--auto-add-overlay] \
[--max-res-version VAL] \
[-I base-package [-I base-package ...]] \
[-A asset-source-dir] [-G class-list-file] [-P public-definitions-file] \
[-S resource-sources [-S resource-sources ...]] \
[-F apk-file] [-J R-file-dir] \
[--product product1,product2,...] \
[-c CONFIGS] [--preferred-configurations CONFIGS] \
[raw-files-dir [raw-files-dir] ...] \
[--output-text-symbols DIR]
-f 如果編譯出來的文件已經(jīng)存在,強制覆蓋
-m 使生成的包的目錄放在-J參數(shù)指定的目錄
-J 指定生成的R.java的輸出目錄
-S res文件夾路徑
-A assert文件夾的路徑
-M AndroidManifest.xml的路徑
-I 某個版本平臺的android.jar的路徑
-F 具體指定apk文件的輸出
如:
A、將工程的資源編譯R.java文件
aapt package -m -J <R.java目錄> -S <res目錄> -I <android.jar目錄> -M <AndroidManifest.xml目錄>
B、將工程的資源編譯編譯到一個包里
aapt package -f -S <res目錄> -I <android.jar目錄> -A<assert目錄> -M <AndroidManifest.xml目錄> -F <輸出的包目錄>
6、顯示aapt版本號
aapt v
按照上面aapt的地址配置好環(huán)境變量后,在終端中輸入 aapt v 會得到aapt版本信息,如下:
再輸入 aapt 命令會列出所有的aapt命令集合,下面我們一條條來使用并且分析其作用:
1.aapt l[ist] [-v] [-a] file.{zip,jar,apk}
作用:列出壓縮文件(zip,jar,apk)中的目錄內(nèi)容。
例如:
aapt l /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
結(jié)果如下:
可以看出來不加任何參數(shù),aapt只是簡單的羅列壓縮文件中每一項的內(nèi)容。
aapt l -v /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
結(jié)果如下:
從圖中可以看出,加上-v后,輸出的內(nèi)容很詳細,并且以列表的形式標(biāo)識出很多參數(shù),其中表目有:
Length:原始文件的長度
Date:日期
Time:時間
Name:名稱
Method:壓縮方法,Deflate及Stored兩種,即該Zip目錄采用的算法是壓縮模式還是存儲模式;可以看出resources.arsc、*.png采用壓縮模式,而其它采用壓縮模式。
Ratio:壓縮率
Size:這個是壓縮省掉的大小,即如果壓縮率是xx%。那Size是原始長度*(1-xx%)。
CRC-32:循環(huán)冗余校驗。這個計算是有特定的算法的。
offset:zipfile中偏移量的意思
aapt l -a /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
結(jié)果如下:
-a表示會詳細輸出壓縮文件中所有目錄的內(nèi)容,詳細到什么程度的,可以看上圖,上圖截取的只是很小的一部分,這部分是manifest.xml文件的所有數(shù)據(jù),可以看出來基本上所有的manifest信息都列了出來。
2.aapt d[ump] [--values] [--include-meta-data] WHAT file.{apk} [asset [asset ...]]
作用:通過參數(shù)配置可以dump apk中各種詳細信息。
strings 官方解釋:
Print the contents of the resource table string pool in the APK
即打印apk中所有string資源表
aapt dump strings /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
結(jié)果:
不太了解這個結(jié)果是什么具體的意思,猜測應(yīng)該是序列化的string字段。
bading 官方解釋:
Print the label and icon for the app declared in APK.
aapt dump badging /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
結(jié)果:
查看APK中的配置信息,包括包名,versionCode,versionName,platformBuildVersionName(編譯的時候系統(tǒng)添加的字段,相當(dāng)targetSdkVersionCode的描述)等。同事該命令還列出了manifest.xml的部分信息,包括啟動界面,manifest里配置的label,icon等信息。還有分辨率,時區(qū),uses-feature等信息。
permissions 官方解釋:Print the permissions from the APK
較簡單,輸出APK中使用到的權(quán)限信息。
resources 官方解釋:
Print the resource table from the APK
aapt dump resources /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
結(jié)果:
輸出了apk中所有的資源信息,從這里也可以看出來aapt打包時也包含了android系統(tǒng)很多資源。并且這里也發(fā)現(xiàn),系統(tǒng)通過標(biāo)準(zhǔn)的aapt構(gòu)建出來的資源絕大部分的資源id都是0x7f開頭的,這個也是和我們在R.java文件中看到的資源編號是對應(yīng)起來的。
configurations 官方解釋:Print the configurations in the APK
aapt dump configurations /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
結(jié)果:
可以看出該命令輸出了apk所有的資源目錄,僅僅是目錄,里面有語言,分辨率,夜間模式相關(guān)的數(shù)據(jù)。
xmltree 官方解釋:
Print the compiled xmls in the given assets
aapt d xmltree /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk res/layout/activity_main.xml
結(jié)果:
該命令直接反編譯除了apk中某一個xml布局文件的組織結(jié)構(gòu)。命令需要兩個參數(shù) 第一是apk的地址 第二后面是apk中某個編譯好的xml的相對路徑地址
xmlstrings 官方解釋:
Print the strings of the given compiled xml assets
aapt d xmlstrings /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk res/layout/activity_main.xml
結(jié)果:
從字面解釋,輸出xml文件中所有的string信息??唇Y(jié)果,實際上并沒看出來什么特殊的,也并不是簡單的string信息,猜測可能是索引吧。
3.aapt p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml]
aapt p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml] \ [-0 extension [-0 extension ...]] [-g tolerance] [-j jarfile] \ [--debug-mode] [--min-sdk-version VAL] [--target-sdk-version VAL] \ [--app-version VAL] [--app-version-name TEXT] [--custom-package VAL] \ [--rename-manifest-package PACKAGE] \ [--rename-instrumentation-target-package PACKAGE] \ [--utf16] [--auto-add-overlay] \ [--max-res-version VAL] \ [-I base-package [-I base-package ...]] \ [-A asset-source-dir] [-G class-list-file] [-P public-definitions-file] \ [-D main-dex-class-list-file] \ [-S resource-sources [-S resource-sources ...]] \ [-F apk-file] [-J R-file-dir] \ [--product product1,product2,...] \ [-c CONFIGS] [--preferred-density DENSITY] \ [--split CONFIGS [--split CONFIGS]] \ [--feature-of package [--feature-after package]] \ [raw-files-dir [raw-files-dir] ...] \ [--output-text-symbols DIR]
官方解釋:
Package the android resources. It will read assets and resources that are supplied with the -M -A -S or raw-files-dir arguments. The -J -P -F and -R options control which files are output.
android 編譯資源打包資源文件的命令
-d:包括一個或多個設(shè)備資源,由逗號分隔;-f:覆蓋現(xiàn)有的文件命令,加上后編譯生成直接覆蓋目前已經(jīng)存在的R.java;-m:使生成的包的目錄放在-J參數(shù)指定的目錄;-u:更新現(xiàn)有的包 u = update;-v:詳細輸出,加上此命令會在控制臺輸出每一個資源文件信息,R.java生成后還有注釋。-x:創(chuàng)建擴展資源ID;-z:需要本地化的資源屬性標(biāo)記定位。-M:AndroidManifest.xml的路徑-0:指定一個額外的擴展. apk文件將不會存儲壓縮-g:制定像素迫使圖形的灰度-j:指定包含一個jar或zip文件包,這個命令很特別–debug-mode:指定的是調(diào)試模式下的編譯資源;–min-sdk-versopm VAL:最小SDK版本 如是7以上 則默認編譯資源的格式是 utf-8–target-sdk-version VAL:在androidMainfest中的目標(biāo)編譯SDK版本–app-version VAL:應(yīng)用程序版本號–app-version-name TEXT:應(yīng)該程序版本名字;–custom-package VAL:生成R.java到一個不同的包–rename-mainifest-package PACKAGE:修改APK包名的選項;–rename-instrumentation-target-package PACKAGE:重寫指定包名的選項;–utf16:資源編碼修改為更改默認utf – 16編碼;–auto-add-overlay:自動添加資源覆蓋–max-res-version:最大資源版本-I:指定的SDK版本中android.jar的路徑-A:assert文件夾的路徑-G:一個文件輸出混淆器選項,后面加文件逗號隔開.-P:指定的輸出公共資源,可以指定一個文件 讓資源ID輸出到那上面;-S:指定資源目錄 一般是 res-F:指定把資源輸出到 apk文件中-J:指定R.java輸出的路徑raw-file-dir:附加打包進APK的文件
該命令也是aapt最核心、最復(fù)雜的命令。這邊我只嘗試了一下簡單的實踐,講工程的資源編譯到一個包里。下面是命令
aapt package -f -S /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/src/main/res -I /Users/zfxu/Library/Android/sdk/platforms/android-25/android.jar -A /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/src/main/assets -M /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/src/main/AndroidManifest.xml -F /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/out.apk
輸出了一個apk文件,解壓以后文件格式如下:
這個apk文件除了沒有代碼dex,資源都在的。這個是aapt打包的關(guān)鍵步驟之一,還有一個步驟就是把資源文件編譯成R.java,供程序調(diào)用。命令如下:
aapt package -m -J <R.java目錄> -S <res目錄> -I <android.jar目錄> -M <AndroidManifest.xml目錄>
4.aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 ...]
官方解釋:
Delete specified files from Zip-compatible archive
就是從一個zip archive文件中刪除一個文件。較簡單,不做實例了。
5.aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 ...]
官方解釋:
Add specified files to Zip-compatible archive.
即在一個zip包中添加一個一個指定的文件。
6.aapt c[runch] [-v] -S resource-sources ... -C output-folder ...
官方解釋:
Do PNG preprocessing on one or several resource folders and store the results in the output folder.
對多個或者單個資源文件夾進行處理,并且將結(jié)果保存在輸出文件夾中
6.aapt s[ingleCrunch] [-v] -i input-file -o outputfile
官方解釋:
Do PNG preprocessing on a single file
預(yù)處理一個文件
AAPT源碼解析
首先下載Android源碼
我這邊下載的是Android 6.0的源碼
AAPT代碼地址:***/frameworks/tools/aapt/目錄下。
我們這里以一個命令來跟蹤源碼的流程,即用aapt是如何構(gòu)建一個R.java的,命令格式如下:
aapt package –m –J <R.java目錄> -S <res目錄> -I <android.jar目錄> -M <AndroidManifest.xml目錄>
實踐:
aapt package -m -J /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/ -S /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/src/main/res/ -I /Users/zfxu/Library/Android/sdk/platforms/android-25/android.jar -M /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/src/main/AndroidManifest.xml
運行該命令后,在配置的R.java目錄下 會生成一個自己app包名的目錄,里面會生成R.java文件,如下:
R.java里會生成這樣的索引ID類,都是以0x7f開頭
public final class R { public static final class attr { } public static final class color { public static final int colorAccent=0x7f040002; public static final int colorPrimary=0x7f040000; public static final int colorPrimaryDark=0x7f040001; } public static final class layout { public static final int activity_main=0x7f030000; } public static final class mipmap { public static final int ic_launcher=0x7f020000; public static final int ic_launcher_round=0x7f020001; } public static final class string { public static final int app_name=0x7f050000; } }
好,既然我們知道了輸入以及輸出,那讓我們來分析這塊的代碼。
PS:由于某些函數(shù)較長,不會貼出所有的源碼
入口 /frameworks/base/tools/aapt/Main.cpp
int main(int argc, char* const argv[]) { *** else if (argv[1][0] == 'p') bundle.setCommand(kCommandPackage); *** while (argc && argv[0][0] == '-') { //通過case比較,去除命令中所有的參數(shù),并且放進bundle中 /* flag(s) found */ const char* cp = argv[0] +1; while (*cp != '\0') { *** switch (*cp) { case 'M': argc--; argv++; if (!argc) { fprintf(stderr, "ERROR: No argument supplied for '-M' option\n"); wantUsage = true; goto bail; } //這個僅僅是把傳進來的地址坐下系統(tǒng)路徑分割線的轉(zhuǎn)換 convertPath(argv[0]); bundle.setAndroidManifestFile(argv[0]); break; } *** } } *** result = handleCommand(&bundle); *** }
當(dāng)通過所有的匹配規(guī)則后,該函數(shù)實際調(diào)用是 handleCommand(&bundle)。 至于執(zhí)行什么命令說白了也是命令指定的,-p 設(shè)置的command參數(shù)是kCommandPackage。
分發(fā)指令 /frameworks/base/tools/aapt/Main.cpp
int handleCommand(Bundle* bundle){ switch (bundle->getCommand()) { case kCommandVersion: return doVersion(bundle); case kCommandList: return doList(bundle); case kCommandDump: return doDump(bundle); case kCommandAdd: return doAdd(bundle); case kCommandRemove: return doRemove(bundle); case kCommandPackage: return doPackage(bundle); case kCommandCrunch: return doCrunch(bundle); case kCommandSingleCrunch: return doSingleCrunch(bundle); case kCommandDaemon: return runInDaemonMode(bundle); default: fprintf(stderr, "%s: requested command not yet supported\n", gProgName); return 1; } }
處理package指令 /frameworks/base/tools/aapt/Command.cpp
int doPackage(Bundle* bundle) { const char* outputAPKFile; int retVal = 1; status_t err; sp<AaptAssets> assets; int N; FILE* fp; String8 dependencyFile; sp<ApkBuilder> builder; sp<WeakResourceFilter> configFilter = new WeakResourceFilter(); //見注釋3-1 err = configFilter->parse(bundle->getConfigurations()); if (err != NO_ERROR) { goto bail; } //資源本地化相關(guān)的配置,具體什么含義也沒有理解清楚 if (configFilter->containsPseudo()) { bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_ACCENTED); } if (configFilter->containsPseudoBidi()) { bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_BIDI); } //校驗命令中是否傳入正確的參數(shù) N = bundle->getFileSpecCount(); if (N < 1 && bundle->getResourceSourceDirs().size() == 0 && bundle->getJarFiles().size() == 0 && bundle->getAndroidManifestFile() == NULL && bundle->getAssetSourceDirs().size() == 0) { fprintf(stderr, "ERROR: no input files\n"); goto bail; } outputAPKFile = bundle->getOutputAPKFile(); // 如果輸出文件存在,但是是不合格的,則直接報錯結(jié)束,如果不存在,則新建空文件 if (outputAPKFile) { FileType type; type = getFileType(outputAPKFile); if (type != kFileTypeNonexistent && type != kFileTypeRegular) { fprintf(stderr, "ERROR: output file '%s' exists but is not regular file\n", outputAPKFile); goto bail; } } // Load the assets. assets = new AaptAssets(); // 設(shè)置res和asset的成員,僅僅是外層new一個對象賦值給AaptAssets if (bundle->getGenDependencies()) { sp<FilePathStore> resPathStore = new FilePathStore; assets->setFullResPaths(resPathStore); sp<FilePathStore> assetPathStore = new FilePathStore; assets->setFullAssetPaths(assetPathStore); } //調(diào)用AaptAssets類的成員函數(shù)slurpFromArgs將AndroidManifest.xml文件,目錄assets和res下的資源目錄和資源文件收錄起來保存到AaptAssets中的 //成員變量中 err = assets->slurpFromArgs(bundle); if (err < 0) { goto bail; } //如果命令中指定需要詳細日志輸出,這里會打印所有的資源信息 if (bundle->getVerbose()) { assets->print(String8()); } // Create the ApkBuilder, which will collect the compiled files // to write to the final APK (or sets of APKs if we are building // a Split APK. //new一個ApkBuilder對象,如果需要生成多個apk,則需要將上層的配置寫入改對象中 builder = new ApkBuilder(configFilter); // If we are generating a Split APK, find out which configurations to split on. if (bundle->getSplitConfigurations().size() > 0) { const Vector<String8>& splitStrs = bundle->getSplitConfigurations(); const size_t numSplits = splitStrs.size(); for (size_t i = 0; i < numSplits; i++) { std::set<ConfigDescription> configs; if (!AaptConfig::parseCommaSeparatedList(splitStrs[i], &configs)) { fprintf(stderr, "ERROR: failed to parse split configuration '%s'\n", splitStrs[i].string()); goto bail; } err = builder->createSplitForConfigs(configs); if (err != NO_ERROR) { goto bail; } } } // If they asked for any fileAs that need to be compiled, do so. //這是最核心的一步,編譯資源(res和asset)。這個成功后,下面的步驟就僅僅是寫輸出文件了 if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) { err = buildResources(bundle, assets, builder); if (err != 0) { goto bail; } } // At this point we've read everything and processed everything. From here // on out it's just writing output files. if (SourcePos::hasErrors()) { goto bail; } // Update symbols with information about which ones are needed as Java symbols. assets->applyJavaSymbols(); if (SourcePos::hasErrors()) { goto bail; } // If we've been asked to generate a dependency file, do that here if (bundle->getGenDependencies()) { // If this is the packaging step, generate the dependency file next to // the output apk (e.g. bin/resources.ap_.d) if (outputAPKFile) { dependencyFile = String8(outputAPKFile); // Add the .d extension to the dependency file. dependencyFile.append(".d"); } else { // Else if this is the R.java dependency generation step, // generate the dependency file in the R.java package subdirectory // e.g. gen/com/foo/app/R.java.d dependencyFile = String8(bundle->getRClassDir()); dependencyFile.appendPath("R.java.d"); } // Make sure we have a clean dependency file to start with fp = fopen(dependencyFile, "w"); fclose(fp); } // Write out R.java constants if (!assets->havePrivateSymbols()) { if (bundle->getCustomPackage() == NULL) { // Write the R.java file into the appropriate class directory // e.g. gen/com/foo/app/R.java err = writeResourceSymbols(bundle, assets, assets->getPackage(), true, bundle->getBuildSharedLibrary()); } else { const String8 customPkg(bundle->getCustomPackage()); err = writeResourceSymbols(bundle, assets, customPkg, true, bundle->getBuildSharedLibrary()); } if (err < 0) { goto bail; } // If we have library files, we're going to write our R.java file into // the appropriate class directory for those libraries as well. // e.g. gen/com/foo/app/lib/R.java if (bundle->getExtraPackages() != NULL) { // Split on colon String8 libs(bundle->getExtraPackages()); char* packageString = strtok(libs.lockBuffer(libs.length()), ":"); while (packageString != NULL) { // Write the R.java file out with the correct package name err = writeResourceSymbols(bundle, assets, String8(packageString), true, bundle->getBuildSharedLibrary()); if (err < 0) { goto bail; } packageString = strtok(NULL, ":"); } libs.unlockBuffer(); } } else { err = writeResourceSymbols(bundle, assets, assets->getPackage(), false, false); if (err < 0) { goto bail; } err = writeResourceSymbols(bundle, assets, assets->getSymbolsPrivatePackage(), true, false); if (err < 0) { goto bail; } } // Write out the ProGuard file err = writeProguardFile(bundle, assets); if (err < 0) { goto bail; } // Write the apk if (outputAPKFile) { // Gather all resources and add them to the APK Builder. The builder will then // figure out which Split they belong in. err = addResourcesToBuilder(assets, builder); if (err != NO_ERROR) { goto bail; } const Vector<sp<ApkSplit> >& splits = builder->getSplits(); const size_t numSplits = splits.size(); for (size_t i = 0; i < numSplits; i++) { const sp<ApkSplit>& split = splits[i]; String8 outputPath = buildApkName(String8(outputAPKFile), split); err = writeAPK(bundle, outputPath, split); if (err != NO_ERROR) { fprintf(stderr, "ERROR: packaging of '%s' failed\n", outputPath.string()); goto bail; } } } // If we've been asked to generate a dependency file, we need to finish up here. // the writeResourceSymbols and writeAPK functions have already written the target // half of the dependency file, now we need to write the prerequisites. (files that // the R.java file or .ap_ file depend on) if (bundle->getGenDependencies()) { // Now that writeResourceSymbols or writeAPK has taken care of writing // the targets to our dependency file, we'll write the prereqs fp = fopen(dependencyFile, "a+"); fprintf(fp, " : "); bool includeRaw = (outputAPKFile != NULL); err = writeDependencyPreReqs(bundle, assets, fp, includeRaw); // Also manually add the AndroidManifeset since it's not under res/ or assets/ // and therefore was not added to our pathstores during slurping fprintf(fp, "%s \\\n", bundle->getAndroidManifestFile()); fclose(fp); } retVal = 0; bail: if (SourcePos::hasErrors()) { SourcePos::printErrors(stderr); } return retVal; }
注釋:
3-1:這個過程主要是對資源配置信息進行校驗,Android應(yīng)用程序資源的組織方式有18個維度,包括mcc(移動國家代碼)、mnc(移動網(wǎng)絡(luò)代碼)、local(語言區(qū)域)等。改代碼的主要實現(xiàn)是在 /framewors/base/tools/aapt/AaptConfig.cpp 里的parse方法。解析完成的數(shù)據(jù),會丟給WeakResourceFilter類中的一個向量集合成員mConfigs。關(guān)于這塊的詳細解釋,可以參考官網(wǎng)或者老羅的博客,地址如下:
https://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources
Android資源管理框架(Asset Manager)簡要介紹和學(xué)習(xí)計劃
編譯res和xml資源 /frameworks/base/tools/aapt/Resource.cpp
ps:改函數(shù)較長,截取部分代碼分步解析
status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder) { // First, look for a package file to parse. This is required to // be able to generate the resource information. sp<AaptGroup> androidManifestFile = assets->getFiles().valueFor(String8("AndroidManifest.xml")); if (androidManifestFile == NULL) { fprintf(stderr, "ERROR: No AndroidManifest.xml file found.\n"); return UNKNOWN_ERROR; } status_t err = parsePackage(bundle, assets, androidManifestFile); if (err != NO_ERROR) { return err; } ...... }
首先解析manifest文件,調(diào)用的是parsePackage函數(shù),解析之前,manifest被封裝成一個AaptGroup對象。
static status_t parsePackage(Bundle* bundle, const sp<AaptAssets>& assets, const sp<AaptGroup>& grp) { if (grp->getFiles().size() != 1) { fprintf(stderr, "warning: Multiple AndroidManifest.xml files found, using %s\n", grp->getFiles().valueAt(0)->getPrintableSource().string()); } sp<AaptFile> file = grp->getFiles().valueAt(0); ResXMLTree block; status_t err = parseXMLResource(file, &block); if (err != NO_ERROR) { return err; } ......省略代碼 return NO_ERROR; }
沒有具體細看里面的代碼,說下具體思路,通過傳進來的形參AaptGroup拿到具體的AaptFile對象。在調(diào)用公共類的parseXmlResource解析xml文件得到具體數(shù)據(jù)后,存放在對象ResXmlTree中。parseXMLResource函數(shù)在類 frameworks/base/tools/aapt/XMLNode.cpp 中。有興趣的可以自己去讀下,這里就不貼了。解析玩manifest.xml后,我們繼續(xù)buildResources的分析。
ResourceTable::PackageType packageType = ResourceTable::App; ......省略的代碼 if (bundle->getBuildSharedLibrary()) { packageType = ResourceTable::SharedLibrary; } else if (bundle->getExtending()) { packageType = ResourceTable::System; } else if (!bundle->getFeatureOfPackage().isEmpty()) { packageType = ResourceTable::AppFeature; } ResourceTable table(bundle, String16(assets->getPackage()), packageType); err = table.addIncludedResources(bundle, assets); if (err != NO_ERROR) { return err; } ...省略的代碼
這段代碼的目的主要是收集當(dāng)前編譯的資源需要依賴的的資源并且存放在ResourceTable這個數(shù)據(jù)結(jié)構(gòu)中。這邊簡單介紹一下ResourceTable這個數(shù)據(jù)結(jié)構(gòu),首先我們得知道R.java里面的資源標(biāo)識id的構(gòu)成,比方說 0x7f040002 其中0x7f表示是packageID,也就是上面的packageType,它是一個命名空間,限定資源的來源,7f表明是當(dāng)前應(yīng)用程序的資源,系統(tǒng)的資源是以0x01開頭。04 表示TypeID。資源的類型animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種,每一種都會被賦予一個ID。最后四位是EntryID,指的是每一個資源在起對應(yīng)的TypID中出現(xiàn)的順序。
而ResouceTable里面存儲的最核心的元素就是這個id的區(qū)分。
收集完成當(dāng)前應(yīng)用依賴的資源以后,就要編譯當(dāng)前應(yīng)用自己的資源。這里由于代碼太過于復(fù)雜,本人也沒有完全看懂,就不貼了,邏輯基本上就是通過命令的輸出,一個個的編譯資源文件和png。然后存儲在一個xml文件中,為后面生成R.java文件中做準(zhǔn)備。實際上前面也有提到,所有的資源都會存在ResouceTable這個數(shù)據(jù)結(jié)構(gòu)中,做完編譯工作以后,只需要去遍歷這個向量表,然后對里面的packageID,typeID,EvtryID進行拼接,就可以得到我們所熟悉的 0x7f040002這種資源ID。ResouceTable的構(gòu)造函數(shù)也可以看出來里面的過程:
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type) : mAssetsPackage(assetsPackage) , mPackageType(type) , mTypeIdOffset(0) , mNumLocal(0) , mBundle(bundle) { ssize_t packageId = -1; switch (mPackageType) { case App: case AppFeature: packageId = 0x7f; break; case System: packageId = 0x01; break; case SharedLibrary: packageId = 0x00; break; default: assert(0); break; } sp<Package> package = new Package(mAssetsPackage, packageId); mPackages.add(assetsPackage, package); mOrderedPackages.add(package); // Every resource table always has one first entry, the bag attributes. const SourcePos unknown(String8("????"), 0); getType(mAssetsPackage, String16("attr"), unknown); }
完成上述的編譯資源的工作以后,細心的讀者就會發(fā)現(xiàn),對于manifest.xml一直都是讀取里面的配置信息,并沒有編譯,所以最后一步就是把manifest.xml編譯成二進制文件。這個就不貼出源碼了。
最后一步,將上述的編譯結(jié)果輸出到R.java和Apk中。其中還會輸出混淆文件,java符號表等。
...省略代碼 // Write out R.java constants if (!assets->havePrivateSymbols()) { if (bundle->getCustomPackage() == NULL) { // Write the R.java file into the appropriate class directory // e.g. gen/com/foo/app/R.java err = writeResourceSymbols(bundle, assets, assets->getPackage(), true, bundle->getBuildSharedLibrary()); } else { const String8 customPkg(bundle->getCustomPackage()); err = writeResourceSymbols(bundle, assets, customPkg, true, bundle->getBuildSharedLibrary()); } if (err < 0) { goto bail; } // If we have library files, we're going to write our R.java file into // the appropriate class directory for those libraries as well. // e.g. gen/com/foo/app/lib/R.java if (bundle->getExtraPackages() != NULL) { // Split on colon String8 libs(bundle->getExtraPackages()); char* packageString = strtok(libs.lockBuffer(libs.length()), ":"); while (packageString != NULL) { // Write the R.java file out with the correct package name err = writeResourceSymbols(bundle, assets, String8(packageString), true, bundle->getBuildSharedLibrary()); if (err < 0) { goto bail; } packageString = strtok(NULL, ":"); } libs.unlockBuffer(); } } else { err = writeResourceSymbols(bundle, assets, assets->getPackage(), false, false); if (err < 0) { goto bail; } err = writeResourceSymbols(bundle, assets, assets->getSymbolsPrivatePackage(), true, false); if (err < 0) { goto bail; } } ...省略代碼
綜上所述,分析完成了了apk資源編譯的過程,由于本人c++功底不佳,有的東西也只是靠猜測來完成,基本上能夠理清楚大體的邏輯。如果想更詳細的內(nèi)容,可以自己參考源碼。網(wǎng)上有篇博客,有點亂,但是很細,可以看下 http://www.cnblogs.com/dyllove98/p/3144950.html
AAPT命令修改,完成修改資源ID
在第三節(jié)我們講AAPT是如何編譯資源并且生成R.java文件的,也提到R.java中資源ID的含義,在組件化框架中,由于組件和宿主分開編譯,為了防止組件的資源ID和宿主的資源ID沖突,所以就需要修改AAPT源碼。基本思路就是每個組件分配一個不一樣的ID,宿主的ID是以0x7f開頭,組件的ID是0x**開頭,這樣就避免沖突。
可以看出如果不修改AAPT源碼重新構(gòu)建,就會導(dǎo)致組件之間或者組件與宿主之間的ID沖突。所以就會有如下模型:
既然分析AAPT的編譯過程,那思路就很清晰了,在命令中添加一個自定義的ID,然后在代碼中拿到這個ID,拼接的時候替換上即可。當(dāng)然這只是最簡單的,而實際情況呢,比方說宿主里有一部分資源是其他組件公用的,如何保證這部分資源和id和組件本身的id不會發(fā)生沖突呢?又如何在我們工程里自動化這套自定義的aapt從而代替系統(tǒng)標(biāo)準(zhǔn)的aapt呢?下篇博客,我會詳細分析。
相關(guān)文章
Android原生ViewPager控件實現(xiàn)卡片翻動效果
這篇文章主要為大家詳細介紹了Android原生ViewPager控件實現(xiàn)卡片翻動效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07Android 全屏無標(biāo)題欄的三種實現(xiàn)方法
這篇文章主要介紹了Android的三種實現(xiàn)方法的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-07-07Android利用MediaRecorder實現(xiàn)錄音功能
這篇文章主要為大家詳細介紹了Android利用MediaRecorder實現(xiàn)錄音功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-03-03Android中TextView文本高亮和點擊行為的封裝方法
這篇文章主要介紹了Android中TextView文本高亮和點擊行為的封裝方法,文中介紹的非常詳細,相信對大家具有一定的參考價值,需要的朋友們下面來一起看看吧。2017-03-03詳解Android權(quán)限管理之Android 6.0運行時權(quán)限及解決辦法
本篇文章主要介紹Android權(quán)限管理之Android 6.0運行時權(quán)限及解決辦法,具有一定的參考價值,有興趣的可以了解一下。2016-11-11