Android AAPT(Android Asset Packaging Tool)详解
作者:徐正峰
AAPT解释,作用
AAPT - Android Asset Packaging Tool
看全称,就可知道AAPT是Android资源打包工具。讲这个之前,是有必要简单说下Android是如何构建一个APK的。
上图是Google官方发布的一张非常经典的Apk打包流程图。
流程概述
工程的资源文件(res文件夹下的文件),通过AAPT打包成R.java类(资源索引表),以及.arsc资源文件如果有aidl,通过aidl工具,打包成java接口类R.java和aidl.java通过java编译成想要的.class文件。源码class文件和第三方jar或者library通过dx工具打包成dex文件。dx工具的主要作用是将java字节码转换成Dalvik字节码,在此过程中会压缩常量池,消除一些冗余信息等。apkbuilder工具会将所有没有编译的资源,.arsc资源,.dex文件打包到一个完成apk文件中中。签名,5中完成apk通过配置的签名文件(debug和release都有),jarsigner工具会对齐签名。得到一个签名后的apk,signed.apkzipAlign工具对6中的signed.apk进行对齐处理,所谓对齐,主要过程是将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快。对齐的作用主要是为了减少运行时内存的使用。
总结
输入: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包的所有内容
-v 以table形式列出
-a 详细列出内容
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的相关信息
常用两个命令:
aapt dump badging f:\resign\ListenerApk.apk #获取标签(package&Acvitity)和图标相关信息
aapt dump permissions f:\resign\ListenerApk.apk #获取指定apk所具有的权限
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添加当前目录下的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 如果编译出来的文件已经存在,强制覆盖
-m 使生成的包的目录放在-J参数指定的目录
-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的地址配置好环境变量后,在终端中输入 aapt v 会得到aapt版本信息,如下:
再输入 aapt 命令会列出所有的aapt命令集合,下面我们一条条来使用并且分析其作用:
1.aapt l[ist] [-v] [-a] file.{zip,jar,apk}
作用:列出压缩文件(zip,jar,apk)中的目录内容。
例如:
aapt l /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
结果如下:
可以看出来不加任何参数,aapt只是简单的罗列压缩文件中每一项的内容。
aapt l -v /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
结果如下:
从图中可以看出,加上-v后,输出的内容很详细,并且以列表的形式标识出很多参数,其中表目有:
Length:原始文件的长度
Date:日期
Time:时间
Name:名称
Method:压缩方法,Deflate及Stored两种,即该Zip目录采用的算法是压缩模式还是存储模式;可以看出resources.arsc、*.png采用压缩模式,而其它采用压缩模式。
Ratio:压缩率
Size:这个是压缩省掉的大小,即如果压缩率是xx%。那Size是原始长度*(1-xx%)。
CRC-32:循环冗余校验。这个计算是有特定的算法的。
offset:zipfile中偏移量的意思
aapt l -a /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
结果如下:
-a表示会详细输出压缩文件中所有目录的内容,详细到什么程度的,可以看上图,上图截取的只是很小的一部分,这部分是manifest.xml文件的所有数据,可以看出来基本上所有的manifest信息都列了出来。
2.aapt d[ump] [--values] [--include-meta-data] WHAT file.{apk} [asset [asset ...]]
作用:通过参数配置可以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
结果:
不太了解这个结果是什么具体的意思,猜测应该是序列化的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
结果:
查看APK中的配置信息,包括包名,versionCode,versionName,platformBuildVersionName(编译的时候系统添加的字段,相当targetSdkVersionCode的描述)等。同事该命令还列出了manifest.xml的部分信息,包括启动界面,manifest里配置的label,icon等信息。还有分辨率,时区,uses-feature等信息。
permissions 官方解释:Print the permissions from the APK
较简单,输出APK中使用到的权限信息。
resources 官方解释:
Print the resource table from the APK
aapt dump resources /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
结果:
输出了apk中所有的资源信息,从这里也可以看出来aapt打包时也包含了android系统很多资源。并且这里也发现,系统通过标准的aapt构建出来的资源绝大部分的资源id都是0x7f开头的,这个也是和我们在R.java文件中看到的资源编号是对应起来的。
configurations 官方解释:Print the configurations in the APK
aapt dump configurations /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/build/outputs/apk/app-debug.apk
结果:
可以看出该命令输出了apk所有的资源目录,仅仅是目录,里面有语言,分辨率,夜间模式相关的数据。
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
结果:
该命令直接反编译除了apk中某一个xml布局文件的组织结构。命令需要两个参数 第一是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
结果:
从字面解释,输出xml文件中所有的string信息。看结果,实际上并没看出来什么特殊的,也并不是简单的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:包括一个或多个设备资源,由逗号分隔;-f:覆盖现有的文件命令,加上后编译生成直接覆盖目前已经存在的R.java;-m:使生成的包的目录放在-J参数指定的目录;-u:更新现有的包 u = update;-v:详细输出,加上此命令会在控制台输出每一个资源文件信息,R.java生成后还有注释。-x:创建扩展资源ID;-z:需要本地化的资源属性标记定位。-M:AndroidManifest.xml的路径-0:指定一个额外的扩展. apk文件将不会存储压缩-g:制定像素迫使图形的灰度-j:指定包含一个jar或zip文件包,这个命令很特别–debug-mode:指定的是调试模式下的编译资源;–min-sdk-versopm VAL:最小SDK版本 如是7以上 则默认编译资源的格式是 utf-8–target-sdk-version VAL:在androidMainfest中的目标编译SDK版本–app-version VAL:应用程序版本号–app-version-name TEXT:应该程序版本名字;–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最核心、最复杂的命令。这边我只尝试了一下简单的实践,讲工程的资源编译到一个包里。下面是命令
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打包的关键步骤之一,还有一个步骤就是把资源文件编译成R.java,供程序调用。命令如下:
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.
对多个或者单个资源文件夹进行处理,并且将结果保存在输出文件夹中
6.aapt s[ingleCrunch] [-v] -i input-file -o outputfile
官方解释:
Do PNG preprocessing on a single file
预处理一个文件
AAPT源码解析
首先下载Android源码
我这边下载的是Android 6.0的源码
AAPT代码地址:***/frameworks/tools/aapt/目录下。
我们这里以一个命令来跟踪源码的流程,即用aapt是如何构建一个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:由于某些函数较长,不会贴出所有的源码
入口 /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比较,去除命令中所有的参数,并且放进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; } //这个仅仅是把传进来的地址坐下系统路径分割线的转换 convertPath(argv[0]); bundle.setAndroidManifestFile(argv[0]); break; } *** } } *** result = handleCommand(&bundle); *** }
当通过所有的匹配规则后,该函数实际调用是 handleCommand(&bundle)。 至于执行什么命令说白了也是命令指定的,-p 设置的command参数是kCommandPackage。
分发指令 /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; } //资源本地化相关的配置,具体什么含义也没有理解清楚 if (configFilter->containsPseudo()) { bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_ACCENTED); } if (configFilter->containsPseudoBidi()) { bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_BIDI); } //校验命令中是否传入正确的参数 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(); // 如果输出文件存在,但是是不合格的,则直接报错结束,如果不存在,则新建空文件 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(); // 设置res和asset的成员,仅仅是外层new一个对象赋值给AaptAssets if (bundle->getGenDependencies()) { sp<FilePathStore> resPathStore = new FilePathStore; assets->setFullResPaths(resPathStore); sp<FilePathStore> assetPathStore = new FilePathStore; assets->setFullAssetPaths(assetPathStore); } //调用AaptAssets类的成员函数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应用程序资源的组织方式有18个维度,包括mcc(移动国家代码)、mnc(移动网络代码)、local(语言区域)等。改代码的主要实现是在 /framewors/base/tools/aapt/AaptConfig.cpp 里的parse方法。解析完成的数据,会丢给WeakResourceFilter类中的一个向量集合成员mConfigs。关于这块的详细解释,可以参考官网或者老罗的博客,地址如下:
https://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources
Android资源管理框架(Asset Manager)简要介绍和学习计划
编译res和xml资源 /frameworks/base/tools/aapt/Resource.cpp
ps:改函数较长,截取部分代码分步解析
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文件,调用的是parsePackage函数,解析之前,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对象。在调用公共类的parseXmlResource解析xml文件得到具体数据后,存放在对象ResXmlTree中。parseXMLResource函数在类 frameworks/base/tools/aapt/XMLNode.cpp 中。有兴趣的可以自己去读下,这里就不贴了。解析玩manifest.xml后,我们继续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; } ...省略的代码
这段代码的目的主要是收集当前编译的资源需要依赖的的资源并且存放在ResourceTable这个数据结构中。这边简单介绍一下ResourceTable这个数据结构,首先我们得知道R.java里面的资源标识id的构成,比方说 0x7f040002 其中0x7f表示是packageID,也就是上面的packageType,它是一个命名空间,限定资源的来源,7f表明是当前应用程序的资源,系统的资源是以0x01开头。04 表示TypeID。资源的类型animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。最后四位是EntryID,指的是每一个资源在起对应的TypID中出现的顺序。
而ResouceTable里面存储的最核心的元素就是这个id的区分。
收集完成当前应用依赖的资源以后,就要编译当前应用自己的资源。这里由于代码太过于复杂,本人也没有完全看懂,就不贴了,逻辑基本上就是通过命令的输出,一个个的编译资源文件和png。然后存储在一个xml文件中,为后面生成R.java文件中做准备。实际上前面也有提到,所有的资源都会存在ResouceTable这个数据结构中,做完编译工作以后,只需要去遍历这个向量表,然后对里面的packageID,typeID,EvtryID进行拼接,就可以得到我们所熟悉的 0x7f040002这种资源ID。ResouceTable的构造函数也可以看出来里面的过程:
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); }
完成上述的编译资源的工作以后,细心的读者就会发现,对于manifest.xml一直都是读取里面的配置信息,并没有编译,所以最后一步就是把manifest.xml编译成二进制文件。这个就不贴出源码了。
最后一步,将上述的编译结果输出到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++功底不佳,有的东西也只是靠猜测来完成,基本上能够理清楚大体的逻辑。如果想更详细的内容,可以自己参考源码。网上有篇博客,有点乱,但是很细,可以看下 http://www.cnblogs.com/dyllove98/p/3144950.html
AAPT命令修改,完成修改资源ID
在第三节我们讲AAPT是如何编译资源并且生成R.java文件的,也提到R.java中资源ID的含义,在组件化框架中,由于组件和宿主分开编译,为了防止组件的资源ID和宿主的资源ID冲突,所以就需要修改AAPT源码。基本思路就是每个组件分配一个不一样的ID,宿主的ID是以0x7f开头,组件的ID是0x**开头,这样就避免冲突。
可以看出如果不修改AAPT源码重新构建,就会导致组件之间或者组件与宿主之间的ID冲突。所以就会有如下模型:
既然分析AAPT的编译过程,那思路就很清晰了,在命令中添加一个自定义的ID,然后在代码中拿到这个ID,拼接的时候替换上即可。当然这只是最简单的,而实际情况呢,比方说宿主里有一部分资源是其他组件公用的,如何保证这部分资源和id和组件本身的id不会发生冲突呢?又如何在我们工程里自动化这套自定义的aapt从而代替系统标准的aapt呢?下篇博客,我会详细分析。