本文基于AOSP-7.1.1-R9源码分析,源码可以参见frameworks/base/+/android-7.1.1_r9。
Android Apk 编译原理解析的分析过程中,可以看到,为了生成最终的apk
,在资源文件的编译生成过程中,会两次使用到aapt命令。
- 生成R.java,编译系统通过
acp
命令将这个文件复制一份变成R.stamp。 - 生成中间文件package.apk。编译系统为了生成Split.apk,aapt会把相关资源文件打包生成一个中间文件package.apk,位于通用的路径out/target/product/generic_x86_64/obj/APPS/Split_intermediates/,然后经过签名优化之后,直接拷贝生成我们最终的apk,out/target/product/generic_x86_64/data/app/Split/Split.apk。如果定义了分包功能,那么会同时生成对应的小包,比如中间文件package_mdpi-v4.apk和最终目标Split_mdpi-v4.apk。
实例选择frameworks/base/tests/Split/,正是因为Android.mk里面定义了
LOCAL_PACKAGE_SPLITS := mdpi-v4 hdpi-v4 xhdpi-v4 xxhdpi-v4
,方便讲解Split功能,分包之后总共会生成5个apk。
接下来我们将分析以上两个文件生成的原理。
编译参数
对于以上两个文件生成的编译依赖,请参考Android Apk 编译原理解析。本文不在讲解。
当我们执行mmm frameworks/base/tests/Split/
编译Split的时候。对于aapt来说有如下的参数传递。
- 生成R.java时,编译系统解析传给aapt的参数如下:
out/host/linux-x86/bin/aapt package --split mdpi-v4 --split hdpi-v4 --split xhdpi-v4 --split xxhdpi-v4 --pseudo-localize -m -J out/target/common/obj/APPS/Split_intermediates/src -M frameworks/base/tests/Split/AndroidManifest.xml -P out/target/common/obj/APPS/Split_intermediates/public_resources.xml -S frameworks/base/tests/Split/res -A frameworks/base/tests/Split/assets -I out/target/common/obj/APPS/framework-res_intermediates/package-export.apk -G out/target/common/obj/APPS/Split_intermediates/proguard_options --min-sdk-version 25 --target-sdk-version 25 --version-code 25 --version-name 7.1.1 --skip-symbols-without-default-localization
- 生成Package.apk时,编译系统传递给aapt的参数如下:
out/host/linux-x86/bin/aapt package -u --split mdpi-v4 --split hdpi-v4 --split xhdpi-v4 --split xxhdpi-v4 --pseudo-localize -c en_US,en_US,cs_CZ,da_DK,de_AT,de_CH,de_DE,de_LI,el_GR,en_AU,en_CA,en_GB,en_NZ,en_SG,eo_EU,es_ES,fr_CA,fr_CH,fr_BE,fr_FR,it_CH,it_IT,ja_JP,ko_KR,nb_NO,nl_BE,nl_NL,pl_PL,pt_PT,ru_RU,sv_SE,tr_TR,zh_CN,zh_HK,zh_TW,am_ET,hi_IN,en_US,en_AU,en_IN,fr_FR,it_IT,es_ES,et_EE,de_DE,nl_NL,cs_CZ,pl_PL,ja_JP,zh_TW,zh_CN,zh_HK,ru_RU,ko_KR,nb_NO,es_US,da_DK,el_GR,tr_TR,pt_PT,pt_BR,sv_SE,bg_BG,ca_ES,en_GB,fi_FI,hi_IN,hr_HR,hu_HU,in_ID,iw_IL,lt_LT,lv_LV,ro_RO,sk_SK,sl_SI,sr_RS,uk_UA,vi_VN,tl_PH,ar_EG,fa_IR,th_TH,sw_TZ,ms_MY,af_ZA,zu_ZA,am_ET,en_XA,ar_XB,fr_CA,km_KH,lo_LA,ne_NP,si_LK,mn_MN,hy_AM,az_AZ,ka_GE,my_MM,mr_IN,ml_IN,is_IS,mk_MK,ky_KG,eu_ES,gl_ES,bn_BD,ta_IN,kn_IN,te_IN,uz_UZ,ur_PK,kk_KZ,sq_AL,gu_IN,pa_IN,be_BY,bs_BA -M frameworks/base/tests/Split/AndroidManifest.xml -S frameworks/base/tests/Split/res -A frameworks/base/tests/Split/assets -I out/target/common/obj/APPS/framework-res_intermediates/package-export.apk --min-sdk-version 25 --target-sdk-version 25 --product emulator --version-code 25 --version-name 7.1.1 --skip-symbols-without-default-localization -F out/target/product/generic_x86_64/obj/APPS/Split_intermediates/package.apk
为了快速了解aapt的相关功能,可以查看aapt的帮助文档,在终端下执行aapt –help。aapt的源码位于frameworks/base/tools/aapt,里面有一个aapt2的文件夹,aapt2是新版的aapt,Google在持续优化之中,aapt2可以编译aar资源文件。当所有的参数传递给aapt之后,直接执行aapt源码Main.cpp的main函数。
解析所有的参数
如前面所述,aapt的入口类Main.cpp里面会解析当前传入的所有参数,在aapt的源码中,专门有一个Bundle类用来设置解析之后的所有参数。实例中我们传递的参数是argv[1][0] == 'p'
,表示我们进行的是打包的动作,会设置bundle.setCommand(kCommandPackage)
,等参数解析完成之后在handleCommand
函数里面会进行打包的动作。
doPackage
在doPackage里面会生成R.java和package.apk,整个过程总结大致分为如下步骤:
接下来我们一一进行展开分析:
解析语言配置
a. 解析当前编译的apk需要的所有语言,对应-c
参数,如en_US,en_US,cs_CZ,da_DK,de_AT。这一步里面除了正常的语言参数之外,还会判断是否传入了en_XA和ar_XB,分别表示伪本地化语言,en_XA表示从左向右的伪语言,ar_XB表示从右向左的语言伪语言。这两种语言主要作用:方便测试翻译成从左向右和从右向左的语言之后,可能发生的UI以及Layout问题。详情可以参考: Test Your App with Pseudolocales
b. 判断当前aapt传入的参数个数必须大于0,否则无效。
c. 判断-F 后面的参数对应的文件格式必须正确。-F out/target/product/generic_x86_64/obj/APPS/Split_intermediates/package.apk
必须是常规的文件类型。
代码片段:
@Command.cpp
// -c en_XA or/and ar_XB means do pseudolocalization
sp<WeakResourceFilter> configFilter = new WeakResourceFilter();
err = configFilter->parse(bundle->getConfigurations()); //[a]
if (err != NO_ERROR) {
goto bail;
}
if (configFilter->containsPseudo()) {
bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_ACCENTED);
}
if (configFilter->containsPseudoBidi()) {
bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_BIDI);
}
//[b]
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();
// Make sure the filenames provided exist and are of the appropriate type.
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();
收集所有文件
在aapt的源码里面定义了三个类来表示资源文件、目录、和配置。
aaptFile
对于每个资源文件来说,aapt会用aaptFile来表示。aaptFile由sourceFile(文件名)、groupEntry(当前的config配置信息,如xhdpi-v4)、resType(资源类型)三部分组成,下面用表格来表示一下对应的关系示意图:
sourceFile | groupEntry | resType |
---|---|---|
frameworks/base/tests/Split/AndroidManifest.xml | ||
frameworks/base/tests/Split/assets/blah.txt | ||
frameworks/base/tests/Split/assets/statement.xml | ||
frameworks/base/tests/Split/res/mipmap-xhdpi/ic_app.png |