本文基于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里面会生成R.java和package.apk,整个过程总结大致分为如下步骤:
Created with Raphaël 2.1.0 doPackage 解析语言配置 收集所有文件 处理SplitApk 编译资源 写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(资源类型)三部分组成,下面用表格来表示一下对应的关系示意图:
sourceFilegroupEntryresTypeframeworks/base/tests/Split/AndroidManifest.xmlframeworks/base/tests/Split/assets/blah.txtframeworks/base/tests/Split/assets/statement.xmlframeworks/base/tests/Split/res/mipmap-xhdpi/ic_app.pngxhdpi-v4mipmapframeworks/base/tests/Split/res/values-sw600dp/values.xmlsw600dp-v13valuesframeworks/base/tests/Split/res/values-de/values.xmldevaluesframeworks/base/tests/Split/res/drawable-xxhdpi/image.pngxxhdpi-v4drawableframeworks/base/tests/Split/res/mipmap-mdpi/ic_app.pngmdpi-v4mipmapframeworks/base/tests/Split/res/drawable-xhdpi/image.pngxhdpi-v4drawableframeworks/base/tests/Split/res/values-v17/values.xmlv17valuesframeworks/base/tests/Split/res/mipmap-xxhdpi/ic_app.pngxxhdpi-v4mipmapframeworks/base/tests/Split/res/values-fr/values.xmlfrvaluesframeworks/base/tests/Split/res/values-xxhdpi/values.xmlxxhdpi-v4valuesframeworks/base/tests/Split/res/values-fr-rCA/strings.xmlfr-rCAvaluesframeworks/base/tests/Split/res/layout-fr-sw600dp/main.xmlfr-sw600dp-v13layoutframeworks/base/tests/Split/res/drawable-hdpi/image.pnghdpi-v4drawableframeworks/base/tests/Split/res/mipmap-xxxhdpi/ic_app.pngxxxhdpi-v4mipmapframeworks/base/tests/Split/res/drawable-mdpi/image.pngmdpi-v4drawableframeworks/base/tests/Split/res/values/values.xmlvaluesframeworks/base/tests/Split/res/values-b+fr+Latn+CA/strings.xmlb+fr+Latn+CAvaluesframeworks/base/tests/Split/res/layout/main.xmllayoutframeworks/base/tests/Split/res/mipmap-hdpi/ic_app.pnghdpi-v4mipmapframeworks/base/tests/Split/res/values-v10/values.xmlv10valuesaaptDir
表示资源文件的每一个目录,由leaf和path共同组成。
String8 mLeaf; 目录名。String8 mPath; 目录名对应的路径。DefaultKeyedVector<String8, sp<AaptGroup> > mFiles; key是文件名,value是文件名对应的aaptGroup对象。DefaultKeyedVector<String8, sp<AaptDir> > mDirs; key是文件名,value是文件名对应的aaptDir对象。下面的表格表示Split里面的资源目录用AaptDir表示之后,leaf和path的对应关系。 leafpathassetsassetsdrawabledrawablevaluesvaluesmipmapmipmaplayoutlayoutresresdrawable-mdpi-v4res/drawable-mdpi-v4drawable-hdpi-v4res/drawable-hdpi-v4drawable-xhdpi-v4res/drawable-xhdpi-v4drawable-xxhdpi-v4res/drawable-xxhdpi-v4mipmap-mdpi-v4res/mipmap-mdpi-v4mipmap-hdpi-v4res/mipmap-hdpi-v4mipmap-xhdpi-v4res/mipmap-xhdpi-v4mipmap-xxhdpi-v4res/mipmap-xxhdpi-v4mipmap-xxxhdpi-v4res/mipmap-xxxhdpi-v4layoutres/layoutlayout-fr-sw600dp-v13res/layout-fr-sw600dp-v13aaptGroup:
有三个很重要的成员变量:
String8 mLeaf; 资源文件的名字。String8 mPath; 资源文件路径。DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > mFiles; key是AaptGroupEntry,value是aaptFile。当aaptGroup初始化完成之后,leaf和path的对应关系如下: leafpathAndroidManifest.xmlblah.txtassets/blah.txtstatement.xmlassets/statement.xmlimage.pngdrawable/image.pngvalues.xmlvalues/values.xmlic_app.pngmipmap/ic_app.pngmain.xmllayout/main.xmlstrings.xmlvalues/strings.xmlimage.pngres/drawable-mdpi-v4/image.pngimage.pngres/drawable-hdpi-v4/image.pngimage.pngres/drawable-xhdpi-v4/image.pngimage.pngres/drawable-xxhdpi-v4/image.pngic_app.pngres/mipmap-mdpi-v4/ic_app.pngic_app.pngres/mipmap-hdpi-v4/ic_app.pngmain.xmlres/layout-fr-sw600dp-v13/main.xmlic_app.pngres/mipmap-xhdpi-v4/ic_app.pngic_app.pngres/mipmap-xxhdpi-v4/ic_app.pngic_app.pngres/mipmap-xxxhdpi-v4/ic_app.pngmain.xmlres/layout/main.xmlmain.xmlres/layout-fr-sw600dp-v13/main.xml同一个文件的所有配置构成一个aaptGroup对象,主要存储在mFiles里面,比如values.xml,总共有七种配置,main.xml总共有两种配置。用表格表示:
values.xml的aaptGroup对象:
toDirNamefile()frameworks/base/tests/Split/res/values/values.xml(v10)frameworks/base/tests/Split/res/values-v10/values.xml(v17)frameworks/base/tests/Split/res/values-v17/values.xml(xxhdpi-v4)frameworks/base/tests/Split/res/values-xxhdpi/values.xml(sw600dp-v13)frameworks/base/tests/Split/res/values-sw600dp/values.xml(de)frameworks/base/tests/Split/res/values-de/values.xml(fr)frameworks/base/tests/Split/res/values-fr/values.xmain.xml的aaptGroup对象。
toDirNamefile()frameworks/base/tests/Split/res/layout/main.xml(fr-sw600dp-v13)frameworks/base/tests/Split/res/layout-fr-sw600dp/main.xml通过aaptDir可以获取对应的aaptGroup,通过aaptGroup可以获取对应的aaptFile。
进入到正式收集资源文件的阶段,为了生成最后的apk文件,首先需要收集所有的资源,包括AndroidManifest.xml、assets、raw、res等。在assets->slurpFromArgs里面会根据传入的参数,开始收集所有的资源文件。
代码片段:
err = assets->slurpFromArgs(bundle); ssize_t AaptAssets::slurpFromArgs(Bundle* bundle) { int count; int totalCount = 0; FileType type; const Vector<const char *>& resDirs = bundle->getResourceSourceDirs(); const size_t dirCount =resDirs.size(); sp<AaptAssets> current = this; const int N = bundle->getFileSpecCount(); /* * If a package manifest was specified, include that first. */ if (bundle->getAndroidManifestFile() != NULL) { // place at root of zip. String8 srcFile(bundle->getAndroidManifestFile()); addFile(srcFile.getPathLeaf(), AaptGroupEntry(), srcFile.getPathDir(), NULL, String8()); totalCount++; } /* * If a directory of custom assets was supplied, slurp 'em up. */ const Vector<const char*>& assetDirs = bundle->getAssetSourceDirs(); const int AN = assetDirs.size(); for (int i = 0; i < AN; i++) { FileType type = getFileType(assetDirs[i]); if (type == kFileTypeNonexistent) { fprintf(stderr, "ERROR: asset directory '%s' does not exist\n", assetDirs[i]); return UNKNOWN_ERROR; } if (type != kFileTypeDirectory) { fprintf(stderr, "ERROR: '%s' is not a directory\n", assetDirs[i]); return UNKNOWN_ERROR; } String8 assetRoot(assetDirs[i]); sp<AaptDir> assetAaptDir = makeDir(String8(kAssetDir)); AaptGroupEntry group; count = assetAaptDir->slurpFullTree(bundle, assetRoot, group, String8(), mFullAssetPaths, true); if (count < 0) { totalCount = count; goto bail; } if (count > 0) { mGroupEntries.add(group); } totalCount += count; if (bundle->getVerbose()) { printf("Found %d custom asset file%s in %s\n", count, (count==1) ? "" : "s", assetDirs[i]); } } /* * If a directory of resource-specific assets was supplied, slurp 'em up. */ for (size_t i=0; i<dirCount; i++) { const char *res = resDirs[i]; if (res) { type = getFileType(res); if (type == kFileTypeNonexistent) { fprintf(stderr, "ERROR: resource directory '%s' does not exist\n", res); return UNKNOWN_ERROR; } if (type == kFileTypeDirectory) { if (i>0) { sp<AaptAssets> nextOverlay = new AaptAssets(); current->setOverlay(nextOverlay); current = nextOverlay; current->setFullResPaths(mFullResPaths); } count = current->slurpResourceTree(bundle, String8(res)); if (i > 0 && count > 0) { count = current->filter(bundle); } if (count < 0) { totalCount = count; goto bail; } totalCount += count; } else { fprintf(stderr, "ERROR: '%s' is not a directory\n", res); return UNKNOWN_ERROR; } } } /* * Now do any additional raw files. */ for (int arg=0; arg<N; arg++) { const char* assetDir = bundle->getFileSpecEntry(arg); FileType type = getFileType(assetDir); if (type == kFileTypeNonexistent) { fprintf(stderr, "ERROR: input directory '%s' does not exist\n", assetDir); return UNKNOWN_ERROR; } if (type != kFileTypeDirectory) { fprintf(stderr, "ERROR: '%s' is not a directory\n", assetDir); return UNKNOWN_ERROR; } String8 assetRoot(assetDir); if (bundle->getVerbose()) printf("Processing raw dir '%s'\n", (const char*) assetDir); /* * Do a recursive traversal of subdir tree. We don't make any * guarantees about ordering, so we're okay with an inorder search * using whatever order the OS happens to hand back to us. */ count = slurpFullTree(bundle, assetRoot, AaptGroupEntry(), String8(), mFullAssetPaths); if (count < 0) { /* failure; report error and remove archive */ totalCount = count; goto bail; } totalCount += count; if (bundle->getVerbose()) printf("Found %d asset file%s in %s\n", count, (count==1) ? "" : "s", assetDir); } count = validate(); if (count != NO_ERROR) { totalCount = count; goto bail; } count = filter(bundle); if (count != NO_ERROR) { totalCount = count; goto bail; } bail: return totalCount; }下面一步步来解析以上步骤:
a. 收集AndroidManifest.xml文件,直接把AndroidManifest.xml文件通过AaptAssets::addFile方法添加到AaptDir的mFiles容器里面。
b. assets文件解析。对应aapt的-A参数,实例中 -A frameworks/base/tests/Split/assets。首先打开assets目录,遍历获取到里面的所有目录和文件,如果是目录,重新进行slurpFullTree动作,收集里面的文件和目录。如果是文件直接根据sourceFile, groupEntry, resType创建一个aaptFile对象。对应于例子中assets下面的两个文件是statement.xml和blah.txt.
c. res目录文件收集。如果当前apk的资源路径有多个,-S后面传入多个路径。正常对应res目录,实例中-S frameworks/base/tests/Split/res。在slurpResourceTree遍历所有文件的过程中,会根据当前文件的路径赋予每一种资源的resType,比如目录名是drawable-mdpi,那么resType就是drawable。同理会对每个文件创建一个AaptFile对象,每个目录创建一个AaptDir对象,并且将每个AaptFile添加到AaptDir里面。
d. 其它的文件 其它原始目录以及文件收集。
经过以上步骤,当前系统所有的资源文件以及目录都转换成了AaptFile和AaptDir。
a. 解析–split参数。当把一个apk根据不同的配置分成几个apk的时候,会在Android.mk里面添加LOCAL_PACKAGE_SPLITS := mdpi-v4 hdpi-v4 xhdpi-v4 xxhdpi-v4,这样aapt 最后就包含了--split mdpi-v4 --split hdpi-v4 --split xhdpi-v4 --split xxhdpi-v4参数。在createSplitForConfigs里面将当前的配置添加进ApkBuilder。
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; } } }b. 根据--split的配置,创建InverseResourceFilter,添加进mDefaultFilter。后面会将这些配置的资源从我们的主apk里面删掉。创建新的ApkSplit,添加进mSplits里面,后面在生成独立的apk的时候,就是通过mSplits变量来判断。
status_t ApkBuilder::createSplitForConfigs(const std::set<ConfigDescription>& configs) { const size_t N = mSplits.size(); for (size_t i = 0; i < N; i++) { const std::set<ConfigDescription>& splitConfigs = mSplits[i]->getConfigs(); std::set<ConfigDescription>::const_iterator iter = configs.begin(); for (; iter != configs.end(); iter++) { if (splitConfigs.count(*iter) > 0) { // Can't have overlapping configurations. fprintf(stderr, "ERROR: Split configuration '%s' is already defined " "in another split.\n", iter->toString().string()); return ALREADY_EXISTS; } } } sp<StrongResourceFilter> splitFilter = new StrongResourceFilter(configs); // Add the inverse filter of this split filter to the base apk filter so it will // omit resources that belong in this split. mDefaultFilter->addFilter(new InverseResourceFilter(splitFilter)); // Now add the apk-wide config filter to our split filter. sp<AndResourceFilter> filter = new AndResourceFilter(); filter->addFilter(splitFilter); filter->addFilter(mConfigFilter); mSplits.add(new ApkSplit(configs, filter)); return NO_ERROR; }对于out目录下面生成的apk来说,里面的文件都是经过编译之后的特殊文件格式。和我们存放在工程里面的资源类型有较大的差别,编译的所有动作都在buildResources里面完成。
接下来我们就分析一下这些文件是如何被编译的。为了能够编译,首先必须获取所有的资源文件。
a. 从aaptAssets里面获取AndroidManifest.xml对应的AaptGroup对象,然后获取AndroidManifest.xml对应的AaptFile对象,通过parsePackage解析package、revisionCode、uses-sdk、minSdkVersion等信息。获取关键的package信息。
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; }b. 解析aapt的-private-symbols参数。本文实例中没有这个参数,在frameworks/base/core/res/Android.mk有LOCAL_AAPT_FLAGS += --private-symbols com.android.internal参数。具有这个参数的是编译系统私有资源。
c. 获取当前的PackageType。根据aapt传入的参数,解析出当前的资源类型。总共有四种:
APP: 默认都认为是APP类型。比如我们编译系统Settings.apk。SharedLibrary : LOCAL_AAPT_FLAGS := --shared-lib ,apk共享库,必须预先内置在system image里面供其他应用使用,正常情况不会用到。可以参考frameworks/base/tests/SharedLibrary/lib/Android.mk。System: LOCAL_AAPT_FLAGS := -x。AOSP中framework-res.apk里面的资源都是System类型。可以参考frameworks/base/core/res/Android.mkAppFeature: LOCAL_AAPT_FLAGS := --feature-of主要是为了支持Feature Split功能。可以参考frameworks/base/tests/FeatureSplit下面的几个Android.mk.代码片段:@Resource.cpp#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::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; }d. 创建ResourceTable,根据packageName和packageType构建新的ResourceTable,会把-I参数对应的值out/target/common/obj/APPS/framework-res_intermediates/package-export.apk加进AaptAssets对象里面。
ResourceTable table(bundle, String16(assets->getPackage()), packageType); err = table.addIncludedResources(bundle, assets); if (err != NO_ERROR) { return err; }e. 解析-utf16参数。如果应用程序配置了,表示采取utf16方式存储文件,否则默认utf8.
if (!bundle->getUTF16StringsOption()) { xmlFlags |= XML_COMPILE_UTF8; }f.收集所有的资源文件。根据前面收集到的aaptAssets对象assets,开始收集res目录下面的所有资源文件,并且存入resources里面。resources是KeyedVector,key表示resType,value是ResourceTypeSet。ResourceTypeSet继承于KeyedVector,它的key是leafName,value是AaptGroup。简单表示如下:
resTypeleafNamegroupdrawableimage.pngaaptgroup,有leaf和path组成,参考前面的介绍此时已经把res目录下面的所有资源文件都收集起来了,接着通过ASSIGN_IT的宏给所有的ResourceTypeSet对象赋值,然后给当前的assets设置Resources。
代码如下:
// resType -> leafName -> group KeyedVector<String8, sp<ResourceTypeSet> > *resources = new KeyedVector<String8, sp<ResourceTypeSet> >; collect_files(assets, resources); sp<ResourceTypeSet> drawables; sp<ResourceTypeSet> layouts; sp<ResourceTypeSet> anims; sp<ResourceTypeSet> animators; sp<ResourceTypeSet> interpolators; sp<ResourceTypeSet> transitions; sp<ResourceTypeSet> xmls; sp<ResourceTypeSet> raws; sp<ResourceTypeSet> colors; sp<ResourceTypeSet> menus; sp<ResourceTypeSet> mipmaps; ASSIGN_IT(drawable); ASSIGN_IT(layout); ASSIGN_IT(anim); ASSIGN_IT(animator); ASSIGN_IT(interpolator); ASSIGN_IT(transition); ASSIGN_IT(xml); ASSIGN_IT(raw); ASSIGN_IT(color); ASSIGN_IT(menu); ASSIGN_IT(mipmap); assets->setResources(resources);g. 收集Overlay目录下的资源文件。如果存在overly目录,那么收集所有Overlay下面的文件,然后添加到对应ResourceTypeSet里面。
// now go through any resource overlays and collect their files sp<AaptAssets> current = assets->getOverlay(); while(current.get()) { KeyedVector<String8, sp<ResourceTypeSet> > *resources = new KeyedVector<String8, sp<ResourceTypeSet> >; current->setResources(resources); collect_files(current, resources); current = current->getOverlay(); } // apply the overlay files to the base set if (!applyFileOverlay(bundle, assets, &drawables, "drawable") || !applyFileOverlay(bundle, assets, &layouts, "layout") || !applyFileOverlay(bundle, assets, &anims, "anim") || !applyFileOverlay(bundle, assets, &animators, "animator") || !applyFileOverlay(bundle, assets, &interpolators, "interpolator") || !applyFileOverlay(bundle, assets, &transitions, "transition") || !applyFileOverlay(bundle, assets, &xmls, "xml") || !applyFileOverlay(bundle, assets, &raws, "raw") || !applyFileOverlay(bundle, assets, &colors, "color") || !applyFileOverlay(bundle, assets, &menus, "menu") || !applyFileOverlay(bundle, assets, &mipmaps, "mipmap")) { return UNKNOWN_ERROR; }h. 添加文件到ResoucesTable。对于drawable、minimap、layout、anim、animator、transition、interpolator、xml、raw 共9种类型的资源文件。在makeFileResources函数里面,首先会判断当前文件的名字是否合法,然后通过table->addEntry给ResourceTable添加条目,主要是把当前的type和以及对应的文件名添加进去,比如drawable下面的image.png。对于drawable和minimap,会有一个预处理的动作,对图片进行Chunch操作。
代码片段:
static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets, ResourceTable* table, const sp<ResourceTypeSet>& set, const char* resType) { String8 type8(resType); String16 type16(resType); bool hasErrors = false; ResourceDirIterator it(set, String8(resType)); ssize_t res; while ((res=it.next()) == NO_ERROR) { if (bundle->getVerbose()) { printf(" (new resource id %s from %s)\n", it.getBaseName().string(), it.getFile()->getPrintableSource().string()); } String16 baseName(it.getBaseName()); const char16_t* str = baseName.string(); const char16_t* const end = str + baseName.size(); while (str < end) { if (!((*str >= 'a' && *str <= 'z') || (*str >= '0' && *str <= '9') || *str == '_' || *str == '.')) { fprintf(stderr, "%s: Invalid file name: must contain only [a-z0-9_.]\n", it.getPath().string()); hasErrors = true; } str++; } String8 resPath = it.getPath(); resPath.convertToResPath(); status_t result = table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()), type16, baseName, String16(resPath), NULL, &it.getParams()); if (result != NO_ERROR) { hasErrors = true; } else { assets->addResource(it.getLeafName(), resPath, it.getFile(), type8); } } return hasErrors ? STATUST(UNKNOWN_ERROR) : NO_ERROR; }在addEntry里面,主要做如下事情:
给新的Type创建一个type对象,通过new Type完成,然后添加进mOrderedTypes给entry创建一个ConfigList对象,通过new ConfigList完成,然后保存进mOrderedConfigs里面。给entry里面的item设置类型。e->setItem(sourcePos, value, style, format, overwrite)代码片段:
status_t ResourceTable::addEntry(const SourcePos& sourcePos, const String16& package, const String16& type, const String16& name, const String16& value, const Vector<StringPool::entry_style_span>* style, const ResTable_config* params, const bool doSetIndex, const int32_t format, const bool overwrite) { uint32_t rid = mAssets->getIncludedResources() .identifierForName(name.string(), name.size(), type.string(), type.size(), package.string(), package.size()); if (rid != 0) { sourcePos.error("Resource entry %s/%s is already defined in package %s.", String8(type).string(), String8(name).string(), String8(package).string()); return UNKNOWN_ERROR; } sp<Entry> e = getEntry(package, type, name, sourcePos, overwrite, params, doSetIndex); if (e == NULL) { return UNKNOWN_ERROR; } status_t err = e->setItem(sourcePos, value, style, format, overwrite); if (err == NO_ERROR) { mNumLocal++; } return err; }以上步骤主要是获取了mOrderedTypes和mOrderedConfigs。在生成R.java的时候,会通过这两个变量来获取所有Type(drawable、values、layout等)和每个Type里面具体的Item(image.png、strings.xml等)。
对于values文件夹下面的资源文件来说,系统会调用compileResourceFile对当前所有的资源进行解析。首先我们明确type和entry,以values-fr-rCA/strings.xml为例子:
<resources> <string name="test">Bonjour</string> </resources>type是string,entry是test。第一个string是元素名称,name是属性,test是属性值。Bonjour是text。前三个字段在compileResourceFile解析,Bonjour在parseStyledString里面解析。
把解析出来的字符串,通过addEntry添加到outTable里面。 代码片段:
err = outTable->addEntry(SourcePos(in->getPrintableSource(), block->getLineNumber()), myPackage, curType, ident, str, &spans, &config, false, curFormat, overwrite);对于values-b+fr+Latn+CA/strings.xml里面的test来说,以上信息如下分别如下:
in->getPrintableSource():frameworks/base/tests/Split/res/values-b+fr+Latn+CA/strings.xmlblock->getLineNumber() : 3myPackage:com.android.example.splitcurType:stringident:teststr: Bonsoir!curFormat: 3经过以上步骤,所有的文件的entry和所有value目录下面的资源的entry都添加到我们的ResourceTable里面去了。
a. 通过setIndex给每一种Type分配index,而且通过setEntryIndex给Type里面的每一个Item设置Index。
if (table.hasResources()) { err = table.assignResourceIds(); if (err < NO_ERROR) { return err; } } const size_t N = t->getOrderedConfigs().size(); t->setIndex(ti + 1 + typeIdOffset); for (size_t ei=0; ei<N; ei++) { sp<ConfigList> c = t->getOrderedConfigs().itemAt(ei); if (c == NULL) { continue; } printf(" entry: %s index: %zu \n" , String8(c->getName()).string(), ei); c->setEntryIndex(ei); }最终生成的R里面的ID是通过ResourceTable::getResId获取。先直接贴出代码:
inline uint32_t ResourceTable::getResId(const sp<Package>& p, const sp<Type>& t, uint32_t nameId) { return makeResId(p->getAssignedId(), t->getIndex(), nameId); }可以看到,最终ID是根据package + type index + nameId生成。packageId是在初始化ResourceTable的时候赋予。type IndexId和nameid是在以上a步骤中生成。
比如对于string.xml里面的app_title来说,最终在R.java里面的ID是0x7f 05 0001 ,packageId是0x7f,typeId是05,nameId是0001。
b.给每个bag里面的item分配ID。如下所示,toop就被认为是bagkey,里面的item是bagkeyid。
<style name="toop"> <item name="android:paddingStart">12dp</item> <item name="android:layout_width">23dp</item> </style>通过以上可知,type的index和entry item的index都是在assignResourceIds里面完成赋值。
经过以上两步,资源文件的id和文件里面的每个item的id都有了,此时我们就可以编译layout、anim、animator、interpolator、transition、xml、drawable、color、menu等需要引用values资源的文件了。
a、通过compileXmlFile来编译当前的资源文件。比如main.xml.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:colorAccent="#ffffffff" android:paddingStart="13dp" android:src="@drawable/image"/> </RelativeLayout>在XMLNode::assignResourceIds里面给每个属性赋值,比如android:layout_width里面的layout_width。赋值之前和赋值之后,文件结构对比如下:
赋值前:Input XML Resource: N: android=http://schemas.android.com/apk/res/android E: RelativeLayout / http://schemas.android.com/apk/res/android:layout_width=match_parent, http://schemas.android.com/apk/res/android:layout_height=match_parent C: "" E: ImageView / http://schemas.android.com/apk/res/android:layout_width=wrap_content, http://schemas.android.com/apk/res/android:layout_height=wrap_content, http://schemas.android.com/apk/res/android:layout_centerInParent=true, http://schemas.android.com/apk/res/android:colorAccent=#ffffffff, http://schemas.android.com/apk/res/android:paddingStart=13dp, http://schemas.android.com/apk/res/android:src=@drawable/image C: "" 赋值后:Input XML Resource: N: android=http://schemas.android.com/apk/res/android E: RelativeLayout / http://schemas.android.com/apk/res/android:layout_width(0x010100f4)=match_parent, http://schemas.android.com/apk/res/android:layout_height(0x010100f5)=match_parent E: ImageView / http://schemas.android.com/apk/res/android:layout_width(0x010100f4)=wrap_content, http://schemas.android.com/apk/res/android:layout_height(0x010100f5)=wrap_content, http://schemas.android.com/apk/res/android:src(0x01010119)=@drawable/image, http://schemas.android.com/apk/res/android:layout_centerInParent(0x0101018f)=true, http://schemas.android.com/apk/res/android:paddingStart(0x010103b3)=13dp, http://schemas.android.com/apk/res/android:colorAccent(0x01010435)=#ffffffff可以看到,每个属性后面都会有一个对应的ID。比如layout_width(0x010100f4)。
接下来会进行flatten的动作。
status_t err = root->flatten(target, (options&XML_COMPILE_STRIP_COMMENTS) != 0, (options&XML_COMPILE_STRIP_RAW_VALUES) != 0);经过以上的动作,xml文件会变成如下的结构:里面所有的引用都被替换成了具体的id值。比如我们的android:src="@drawable/image"变成了android:src(0x01010119)=@0x7f020000
Output XML Resource: N: android=http://schemas.android.com/apk/res/android E: RelativeLayout (line=17) A: android:layout_width(0x010100f4)=(type 0x10)0xffffffff A: android:layout_height(0x010100f5)=(type 0x10)0xffffffff E: ImageView (line=20) A: android:layout_width(0x010100f4)=(type 0x10)0xfffffffe A: android:layout_height(0x010100f5)=(type 0x10)0xfffffffe A: android:src(0x01010119)=@0x7f020000 A: android:layout_centerInParent(0x0101018f)=(type 0x12)0xffffffff A: android:paddingStart(0x010103b3)=(type 0x5)0xd01 A: android:colorAccent(0x01010435)=(type 0x1c)0xffffffff将前面获取到的相关数据通过ResourceTable::flatten存储到resources.arsc文件里面。如果配置了Split apk的功能,此时会给每个apk独自产生一个AndroidManifest.xml文件,里面包含独自的resources.arsc文件。
最后会完整检查所有的AndroidManifest.xml文件是不是合法。
至此buildResources就完成了。最终将所有的资源文件以及对应的ID都存储到了resources.arsc文件里面。
回到Command.cpp,等所有的资源文件编译以及收集完成之后,会生成R.java文件,简单来说,大致分一下几步:
1、创建R.java文件。 2、写header和package com.android.example.split; 3、写public final class R. 4、写type public static final class array 5、写Type里面的item,如ary、lotsofstrings、numList.
@Resources#writeResourceSymbols
err = writeResourceSymbols(bundle, assets, assets->getPackage(), true, bundle->getBuildSharedLibrary() || bundle->getBuildAppAsSharedLibrary()); String8 R("R"); // N = 1 const size_t N = assets->getSymbols().size(); for (size_t i=0; i<N; i++) { sp<AaptSymbols> symbols = assets->getSymbols().valueAt(i); String8 className(assets->getSymbols().keyAt(i)); String8 dest(bundle->getRClassDir()); if (bundle->getMakePackageDirs()) { String8 pkg(package); const char* last = pkg.string(); const char* s = last-1; if (bundle->getMakePackageDirs()) { String8 pkg(package); const char* last = pkg.string(); const char* s = last-1; do { s++; if (s > last && (*s == '.' || *s == 0)) { String8 part(last, s-last); dest.appendPath(part); #ifdef _WIN32 _mkdir(dest.string()); #else mkdir(dest.string(), S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IXGRP); #endif last = s+1; } } while (*s); } dest.appendPath(className); dest.append(".java"); className: Rdest: out/target/common/obj/APPS/Split_intermediates/srcpkg: com.android.example.splitmkdir: dest是一点点加上包名的一部分,所以不断的根据报名创建完整的目录,总共有四次创建动作。 out/target/common/obj/APPS/Split_intermediates/src/com out/target/common/obj/APPS/Split_intermediates/src/com/android out/target/common/obj/APPS/Split_intermediates/src/com/android/example out/target/common/obj/APPS/Split_intermediates/src/com/android/example/split给dest加上.java的后缀。最终生成了一个R.java的文件,但是里面是没有内容的。还是在writeResourceSymbols里面,首先会写上Header和当前的包名。
@Resource.cpp#writeResourceSymbols
fprintf(fp, "/* AUTO-GENERATED FILE. DO NOT MODIFY.\n" " *\n" " * This class was automatically generated by the\n" " * aapt tool from the resource data it found. It\n" " * should not be modified by hand.\n" " */\n" "\n" "package %s;\n\n", package.string());接下来就会调用writeSymbolClass写上类的声明。className是R。
@Resource.cpp#writeSymbolClass
static status_t writeSymbolClass( FILE* fp, const sp<AaptAssets>& assets, bool includePrivate, const sp<AaptSymbols>& symbols, const String8& className, int indent, bool nonConstantId, bool emitCallback) { fprintf(fp, "%spublic %sfinal class %s {\n", getIndentSpace(indent), indent != 0 ? "static " : "", className.string()); ... N = symbols->getNestedSymbols().size(); for (i=0; i<N; i++) { sp<AaptSymbols> nsymbols = symbols->getNestedSymbols().valueAt(i); String8 nclassName(symbols->getNestedSymbols().keyAt(i)); if (nclassName == "styleable") { styleableSymbols = nsymbols; } else { err = writeSymbolClass(fp, assets, includePrivate, nsymbols, nclassName, indent, nonConstantId, false); } if (err != NO_ERROR) { return err; } }这样就会向R.java文件里面写上public final class R {
我们接着往后看,在ResourceTable::addSymbols里面,有如下代码片段:
const size_t N = p->getOrderedTypes().size(); size_t ti; for (ti=0; ti<N; ti++) { sp<Type> t = p->getOrderedTypes().itemAt(ti); if (t == NULL) { continue; } const size_t N = t->getOrderedConfigs().size(); sp<AaptSymbols> typeSymbols; if (t->getName() == String16(kAttrPrivateType)) { typeSymbols = outSymbols->addNestedSymbol(String8("attr"), t->getPos()); } else { typeSymbols = outSymbols->addNestedSymbol(String8(t->getName()), t->getPos()); } if (typeSymbols == NULL) { return UNKNOWN_ERROR; } for (size_t ci=0; ci<N; ci++) { sp<ConfigList> c = t->getOrderedConfigs().itemAt(ci); if (c == NULL) { continue; } uint32_t rid = getResId(p, t, ci); if (rid == 0) { return UNKNOWN_ERROR; } if (Res_GETPACKAGE(rid) + 1 == p->getAssignedId()) { if (skipSymbolsWithoutDefaultLocalization && t->getName() == stringType) { // Don't generate symbols for strings without a default localization. if (mHasDefaultLocalization.find(c->getName()) == mHasDefaultLocalization.end()) { // printf("Skip symbol [x] %s\n", rid, // String8(c->getName()).string()); continue; } } typeSymbols->addSymbol(String8(c->getName()), rid, c->getPos());也就是说,我们在前面收集的mOrderedTypes和mOrderedConfigs会转换成Symbols。所以,symbols->getNestedSymbols()的值是Type的个数13个,symbols->getSymbols()`是entry的个数。接下来,会继续调用writeSymbolClass来写其他的符号了。
先直接上代码
static status_t writeSymbolClass( FILE* fp, const sp<AaptAssets>& assets, bool includePrivate, const sp<AaptSymbols>& symbols, const String8& className, int indent, bool nonConstantId, bool emitCallback) { fprintf(fp, "%spublic %sfinal class %s {\n", getIndentSpace(indent), indent != 0 ? "static " : "", className.string()); indent++; size_t i; status_t err = NO_ERROR; const char * id_format = nonConstantId ? "%spublic static int %s=0xx;\n" : "%spublic static final int %s=0xx;\n"; size_t N = symbols->getSymbols().size(); for (i=0; i<N; i++) { const AaptSymbolEntry& sym = symbols->getSymbols().valueAt(i); if (sym.typeCode != AaptSymbolEntry::TYPE_INT32) { continue; } if (!assets->isJavaSymbol(sym, includePrivate)) { continue; } String8 name8(sym.name); String16 comment(sym.comment); bool haveComment = false; AnnotationProcessor ann; if (comment.size() > 0) { haveComment = true; String8 cmt(comment); ann.preprocessComment(cmt); fprintf(fp, "%s/** %s\n", getIndentSpace(indent), cmt.string()); } String16 typeComment(sym.typeComment); if (typeComment.size() > 0) { String8 cmt(typeComment); ann.preprocessComment(cmt); if (!haveComment) { haveComment = true; fprintf(fp, "%s/** %s\n", getIndentSpace(indent), cmt.string()); } else { fprintf(fp, "%s %s\n", getIndentSpace(indent), cmt.string()); } } if (haveComment) { fprintf(fp,"%s */\n", getIndentSpace(indent)); } ann.printAnnotations(fp, getIndentSpace(indent)); fprintf(fp, id_format, getIndentSpace(indent), flattenSymbol(name8).string(), (int)sym.int32Val); }sp nsymbols = symbols->getNestedSymbols().valueAt(i); getNestedSymbols返回的值都是在compileResourceFile()里面写进去的。继续writeSymbolClass,此时className是array,就会写下: public static final class array {
接下来size_t N = symbols->getSymbols().size()的值是3,aaptSymbolEntry的内容。最终通过fprint将相关的name和id(sym.int32Val)写道R.java里面去。
public static final int ary=0x7f060002; public static final int lotsofstrings=0x7f060000; public static final int numList=0x7f060001;通过以上步骤,R.java文件就生成了。
通过Android Apk 编译原理解析 一文可以知道,在编译apk的时候,实际上是调用了两次aapt命令,第一次命令,仅仅是为了生成R.java文件。第二次调用aapt命令,才是真正的生成apk。 接下来我们分析第二次调用aapt命令生成package.apk的过程。
aapt的参数-F后面接着的是需要生成的apk文件。对于我们的例子来说,-F out/target/product/generic_x86_64/obj/APPS/Split_intermediates/package.apk,最终会生成package.apk文件。如果说有Split的功能,那么就会产生多个package.apk,package_hdpi-v4.apk、package_mdpi-v4.apk、package_xhdpi-v4.apk、package_xxhdpi-v4apk。 主要是通过writeApk把存储在ApkSplits里面的内容写到package.apk里面。
代码片段:
Command.cpp
// 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; } } }package.apk的目录结构如下,注意此时和我们最终生成的apk对比,缺少classes.dex文件。
