Android应用最终是以apk的形式放在手机上安装并运行的,而负责将资源文件和代码进行打包的工具就叫appt,全称Android Asset Packaging Tool,翻译过来就是Android资源打包工具,是Android打包流程中不可或缺的一环。虽然build-tools中都会有一个aapt.exe负责打包apk,但底层还是通过执行aapt命令的方式来进行操作,所以这里需要了解一下aapt的相关命令,有助于更好的理解打包的流程。
Android打包流程简述
先来看张官方的打包流程图:
- 首先将资源文件(res目录下)通过aapt工具打包成R.java类(资源索引)和.arsc资源文件。
- 倘若工程中又aidl,则通过aidl工具将aidl打包成java接口类。
- R.java、aidl生成的java文件和项目中的源代码混合编译成.class文件
- class文件和第三方jar或者library通过dx工具打包成dex文件。dx工具的主要作用是将java字节码转换成Dalvik字节码,在此过程中会压缩常量池,消除一些冗余信息等。
- 通过apkbuilder工具将所有没有编译的资源,.arsc资源,.dex文件打包到一个完成apk文件中。
- 生成的apk通过Jarsinger工具,利用relese或debug下配置的keystore文件进行签名,得到签名后的apk文件。
- 通过zipAlign工具对签名后的apk进行对齐处理。即将APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快。减少运行时内存的使用。
这就是Android打包成apk的流程,而资源的打包则涉及到许多aapt命令。
AAPT命令详解
1、aapt l[ist] [-v] [-a] file.{zip,jar,apk}
列出(zip、jar、apk)等类型压缩包中的所有文件列表,示例:
aapt list app-debug.apk
#list可以简写为l,如下:
aapt l app-debug.apk
列出app-debug.apk中所有的文件列表:
打印出来的内容很长,在命令行中不方便观察,我们可以将输出的内容存放到.txt文件中便于查看。示例:
aapt list app-debug.apk>a.txt
把输出的内容存入a.txt文件中。
aapt list后面还可以加-v和-a。例如加-v参数:
aapt list -v app-debug.apk>a.txt
也会列出apk中所有的文件列表,只不过更加详细:
其中各字段代表的含义如下:
Length:文件的长度。
Method:数据压缩算法,有Deflate和Stored两种。知道是用来压缩文件的就行。
Ratio:压缩率。
Size:文件压缩后节省的大小。跟压缩率有关。Size=(1-压缩率)*Length。
Date:日期。
Time:时间。
CRC-32:循环冗余校验,是一种加密算法。
Name:文件名称。
aapt list后面加-a参数:
aapt list -a app-debug.apk>a.txt
列出apk中所有的文件列表、资源id、各限定符下资源文件对应的id、AndroidManifest文件内容。
看下右边的滚动条,发现这个输出的内容不要太多,不要太详细。最底下列出了Android Manifest文件的内容。
2、aapt d[ump] [–values] [–include-meta-data] WHAT file.{apk} [asset [asset …]]
通过参数配置列出apk中各种详细信息,例如apk的权限、字符、资源等等。
主要有以下几种参数:
- strings
列出apk中的所有字符资源(包括不同语言限定符):示例:
aapt dump strings app-debug.apk>a.txt
结果:
这里有乱码,有其它国家语言符号。大家可以自行试一试。
- badging
Print the label and icon for the app declared in APK。实际内容不止label和icon。还有包名、versionCode、versionName、compileSdkVersion、targetSdkVersion等等信息,这里截取一小部分: - permissions
列出apk所用到的所有权限。示例:
aapt dump permissions app-debug.apk
结果会列出apk所需要的所有权限,这里不贴图了。
- resources。
输出apk中所有的资源信息,包括用户添加的资源、不同限定符下的系统资源等等,示例:
aapt dump resources app-debug.apk>a.txt
结果:
- configurations
列出apk中所有的资源目录,注意,只是目录,不包含任何文件内容,资源目录对应不同的限定符。
aapt dump configurations app-debug.apk
关于限定符这篇文章有写到:限定符,结果如下:
5.xmltree
打印出指定xml文件的文档树结构。例如打印出项目中res/layout/activity_main.xml的树形结构,则可以这样写:
aapt d xmltree app-debug.apk res/layout/activity_main.xml
打印结果如下:
从上到下列出了activity_main.xml的文档树结构。
6.xmlstrings
列出给出的xml文件中所包含的控件名、控件的属性名和属性值等等。示例:
aapt d xmlstrings app-debug.apk res/layout/activity_main.xml
结果:
3、aapt p[ackage]
负责打包资源文件的命令,这个命令是aapt中最核心、最复杂的命令:
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]
好吧,来看下每个参数啥意义:
-d:包含一个或多个设备资源,由逗号分割。
-f:强制覆盖现有文件。
-m:生成包的目录在-j参数指定的目录。
-u:更新现有的包。
-v:详细输出,加上此命令会在控制台输出每一个资源文件信息,R.java生成后还有注释。
-x:创建拓展资源id。
-z:需要本地化的资源属性标记定位。
-M:指定AndroidManifest文件的路径。
-0:指定一个附加扩展名,对于该扩展名,此类文件将不会压缩存储在.apk中。空字符串表示不压缩所有文件。
-g:强制图片灰度,灰度值默认为0。
-jar:指定要包含的类的jar或zip文件。
--debug-mode:设置调试模式,即在AndroidManifest中加入 android:debuggable="true"
--min-sdk-version VAL:指定最小SDK版本 如是7以上 则默认编译资源的格式是 utf-8
--target-sdk-version VAL:在AndroidManifest中指定目标编译版本sdk。
--app-version VAL:指定app的版本号。
--app-version-name TEXT:指定app的版本名。
--custom-package VAL:生成R.java到不同的包。
--rename-manifest-package PACKAGE:修改manifest中的应用包名。
--rename-instrumentation-target-package PACKAGE:重写指定包名的选项
--utf16:将资源的默认编码更改为UTF-16。在api在7或者更高时有用,资源的默认编码是utf-8。
--auto-add-overlay:自动添加资源覆盖。
--max-res-version VAL:最大资源版本,忽略高于给定值的版本化资源目录。
-I base-package:指定的SDK版本中android.jar的路径。
-A asset-source-dir:指定Assets文件夹路径。
-G class-list-file:指定用于输出混淆选项的文件。
-P public-definitions-file:指定的输出公共资源,可以指定一个文件 让资源ID输出到上面。
-D main-dex-class-list-file:指定主DEX的混淆选项输出的文件。
-S resource-sources:指定资源目录,一般是res。
-F apk-file:指定把资源输出到 apk文件中。
-J R-file-dir指定R.java的输出路径。
-c CONFIGS:指定资源有哪些限定符,中间以逗号分割,如en、port、land、en_US。
--preferred-density DENSITY:指定设备的屏幕密度,不符合此密度的资源将会被删除。
--split CONFIGS:分包构建apk,可以和主apk一起加载。
--output-text-symbols DIR:在指定的文件夹下生成一个包含R.java中的资源标识符的文本文件。
这命令复杂的分分钟让人抓狂-。-
但其实只要知道这个命令是做什么的就好,它就是用来将项目中的所有资源文件打包,经过我的实践,其最小执行单元如下:
aapt package -S [res文件夹路径] -M [AndroidManifest.xml文件路径] -I [sdk中android.jar的路径] -F [xxx.apk]
意思就是最起码要指定这四个值,才能执行aapt package命令。于是我这里执行了一个命令。
aapt package -S res -M E:\AndroidProjects\Dagger2\app\src\main\AndroidManifest.xml -I D:\Android\android-sdk-windows\platforms\android-28\android.jar -F out.apk
结果:
然后就报了这个错误,说是资源找不到,可用as打开工程,这些资源明明都能找到。一度陷入绝望,后来实在没办法,把这些资源删了,AndroidManifest.xml文件中删除这一行代码:android:theme="@style/TheTheme",于是才可以执行成功。至于加上这三个资源、指定主题为什么会报错,我在网上找了许多资料都没有找到这个问题的症结所在,若是有哪位大神知道这个问题的解决办法,还请在底下留言告诉我,不胜感激~我真的很想知道他喵的到底是为什么-。-
好了,至于打包后就是一个out.apk压缩文件,我们解压后,目录如下:
res文件夹里面是我们的资源目录:
至于resources.arsc就是资源打成的包了,将资源文件打包成.arsc的文件,就是我们上面流程图的第一步。以后打包apk的时候会将这个文件也一同打包进去。还有一个AndroidManifest.xml,打开后一堆看不懂的:
这里的文件内容做了处理,防止别人窥探到其中的代码。至于其它可以接的参数,这里只演示一个:
--rename-manifest-package PACKAGE:修改manifest中的应用包名。
好吧,然后我们在原来的命令上接入该参数:
aapt package -S res -M E:\AndroidProjects\Dagger2\app\src\main\AndroidManifest.xml -I D:\Android\android-sdk-windows\platforms\android-28\android.jar -F out.apk --rename-manifest-package com.aapt.demo
在刚才命令的最后面加入–rename-manifest-package com.aapt.demo,把AndroidManifest.xml中的包名改成com.aapt.demo,打包完成后解压并查看AndroidManifest文件:
虽然大部分是乱码,可是我们还是能看到包名已经改成了com.aapt.demo。至于接其它的参数,笔者这里没有一个个试,其实常用的就那些,其他的都是辅助参数。
4、aapt r[emove] [-v] file.{zip,jar,apk} file1 [file2 …]
用于移除打包好的apk中的文件。例如移除打包好的apk中的AndroidManifest.xml文件:
aapt r out.apk AndroidManifest.xml
这里移除了out.apk中的AndroidManifest.xml文件,然后执行如下命令:
aapt d xmmtree out.apk AndroidManifest.xml
然后会出现如下错误:
提示该文件找不到,可见确实文件是被删除了。可是这里奇怪的是,解压apk,发现AndroidManifest.xml文件依旧在,但执行命令的时候就提示找不到。什么原因,暂时还不知道,有知道的可以告诉我。
5、 aapt a[dd] [-v] file.{zip,jar,apk} file1 [file2 …]
添加文件到打包好的apk中。示例:将a.txt、b.txt添加到打包好的apk中:
aapt a out.apk a.txt b.txt
多个文件中间以空格分割就好,然后看下解压apk,看其中的目录:
将a.txt和b.txt成功加入到apk中了。
6、 aapt c[runch] [-v] -S resource-sources … -C output-folder …
做PNG文件的预处理,并将结果存储到一个文件夹中。示例,把res目录下的图片预处理,并存储到任意路径pictures中:
aapt c -S res -C E:\AndroidProjects\Dagger2\app\src\main\pictures
执行后结果如下:
表示执行成功,然后我们看下pictures路径中的文件:
好吧,没什么可说的。
7、aapt s[ingleCrunch] [-v] -i input-file -o outputfile
对单个PNG文件进行预处理,并输出到指定文件:
aapt s -v -i[需要处理的图片文件路径] -o[处理完成后存储的图片文件路径]
示例:
aapt s -v -i E:\AndroidProjects\Dagger2\app\src\main\res\mipmap-hdpi\ic_launcher.png -o E:\AndroidProjects\Dagger2\app\src\main\pictures\a.png
这里预先在pictures文件夹中新建a.txt文件,然后把后缀名改成.png,这时候a.png是无法打开的,执行此命令后,a.png就可以打开了吗,显示的图像就是res中的hdpi目录下的ic_launcher.png。
8、 aapt v[ersion]
没什么好说的,显示aapt的版本
aapt的命令差不多就这么几种。在命令行中执行aapt命令,就可以查看aapt详细的用法。有兴趣的小伙伴可以自行试一试哈~
AAPT源码分析
关于源码,这里只是看一个脉络。需要下载Android系统源码并解压,下载地址:各版本系统源码下载。我这里下载的是6.0的源码。关于aapt部分的源码在frameworks\base\tools\aapt文件夹下。其入口在Main.cpp中main方法中。main方法共有近500行,这里只贴一部分来分析:
int main(int argc, char* const argv[])
{
char *prog = argv[0];
//Bundle对象用来存储输入的操作类型和相关的参数。
Bundle bundle;
bool wantUsage = false;
int result = 1; // pessimistically assume an error.
int tolerance = 0;
/* default to compression */
bundle.setCompressionMethod(ZipEntry::kCompressDeflated);
if (argc < 2) {
wantUsage = true;
goto bail;
}
....
//argv[] 一行aapt命令被分割成字符串数组。
//这边判断该数组的第二个元素的第1个字符,如aapt package,argv[1][0]获取到的就是p。
else if (argv[1][0] == 'p')
//设置执行类型为打包。
bundle.setCommand(kCommandPackage);
....
argc -= 2;
argv += 2;
/*
* Pull out flags. We support "-fv" and "-f -v".
*/
while (argc && argv[0][0] == '-') {
/* flag(s) found */
const char* cp = argv[0] +1;
while (*cp != '\0') {
switch (*cp) {
......
//收集appt命令输入的参数,这些参数以"-"开头。
case '-':
if (strcmp(cp, "-debug-mode") == 0) {
//例如这里就获取到了-debug-mode,然后设置Bundle的debugMode为true。
bundle.setDebugMode(true);
}
.....
cp += strlen(cp) - 1;
break;
default:
fprintf(stderr, "ERROR: Unknown flag '-%c'\n", *cp);
wantUsage = true;
goto bail;
}
cp++;
}
argc--;
argv++;
}
/*
* We're past the flags. The rest all goes straight in.
*/
bundle.setFileSpec(argv, argc);
//执行最终的命令,并得到结果
result = handleCommand(&bundle);
bail:
if (wantUsage) {
usage();
result = 2;
}
//printf("--> returning %d\n", result);
return result;
}
源码中比较详细,这里举个例子。整行aapt命令会被分割成字符串数组。然后去字符串数组的第二个的第一个字符,如aapt package…,得到p,代表这是aapt打包命令。Bundle对象用以存储输入的操作类型和相关的参数,供后面执行命令详细操作时使用。当然,命令解析错误时,会通过goto跳转到bail代码块,比如:
default:
fprintf(stderr, "ERROR: Unknown flag '-%c'\n", *cp);
wantUsage = true;
goto bail;
bail代码块:
bail:
if (wantUsage) {
usage();
result = 2;
}
//printf("--> returning %d\n", result);
return result;
}
好吧,会调用usage()函数,这个函数会打印出aapt的用法文档。
void usage(void)
{
fprintf(stderr, "Android Asset Packaging Tool\n\n");
fprintf(stderr, "Usage:\n");
fprintf(stderr,
" %s l[ist] [-v] [-a] file.{zip,jar,apk}\n"
" List contents of Zip-compatible archive.\n\n", gProgName);
fprintf(stderr,
" %s d[ump] [--values] [--include-meta-data] WHAT file.{apk} [asset [asset ...]]\n"
" strings Print the contents of the resource table string pool in the APK.\n"
" badging Print the label and icon for the app declared in APK.\n"
" permissions Print the permissions from the APK.\n"
" resources Print the resource table from the APK.\n"
" configurations Print the configurations in the APK.\n"
" xmltree Print the compiled xmls in the given assets.\n"
" xmlstrings Print the strings of the given compiled xml assets.\n\n", gProgName);
fprintf(stderr,
" %s p[ackage] [-d][-f][-m][-u][-v][-x][-z][-M AndroidManifest.xml] \\\n"
" [-0 extension [-0 extension ...]] [-g tolerance] [-j jarfile] \\\n"
" [--debug-mode] [--min-sdk-version VAL] [--target-sdk-version VAL] \\\n"
" [--app-version VAL] [--app-version-name TEXT] [--custom-package VAL] \\\n"
" [--rename-manifest-package PACKAGE] \\\n"
" [--rename-instrumentation-target-package PACKAGE] \\\n"
" [--utf16] [--auto-add-overlay] \\\n"
" [--max-res-version VAL] \\\n"
" [-I base-package [-I base-package ...]] \\\n"
" [-A asset-source-dir] [-G class-list-file] [-P public-definitions-file] \\\n"
" [-S resource-sources [-S resource-sources ...]] \\\n"
" [-F apk-file] [-J R-file-dir] \\\n"
" [--product product1,product2,...] \\\n"
" [-c CONFIGS] [--preferred-density DENSITY] \\\n"
" [--split CONFIGS [--split CONFIGS]] \\\n"
" [--feature-of package [--feature-after package]] \\\n"
" [raw-files-dir [raw-files-dir] ...] \\\n"
" [--output-text-symbols DIR]\n"
这是代码,看下我们执行aapt时的输出:
是不是就是这个?好吧,当命令校验成功,会执行handleCommand方法,并传入一个Bundle对象。看下handleCommand方法的代码:
/*
* Dispatch the command.
*/
int handleCommand(Bundle* bundle)
{
//printf("--- command %d (verbose=%d force=%d):\n",
// bundle->getCommand(), bundle->getVerbose(), bundle->getForce());
//for (int i = 0; i < bundle->getFileSpecCount(); i++)
// printf(" %d: '%s'\n", i, bundle->getFileSpecEntry(i));
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、dump、add、remove,然后再去做具体的操作,如doPackage、doDump、doRemove等等。不过这些方法是通过外部引用的,其真正的代码在同目录下的Command.cpp文件中,这个c文件实现了所有的aapt命令的具体代码。这里举一个稍微简单的例子doRemove看一下:
/*
* Delete files from an existing archive.
*/
int doRemove(Bundle* bundle)
{
//命令行举例:aapt r out.apk AndroidManifest.xml
ZipFile* zip = NULL;
status_t result = UNKNOWN_ERROR;
const char* zipFileName;
if (bundle->getFileSpecCount() < 1) {
//如果没有指定压缩包名称,提示必须指定压缩包名称
fprintf(stderr, "ERROR: must specify zip file name\n");
goto bail;
}
//获取到压缩文件名称。[out.apk,AndroidManifest.xml]数组中的第一个。
zipFileName = bundle->getFileSpecEntry(0);
//[out.apk,AndroidManifest.xml]数组大小小于2,说明没有要移除的文件。
if (bundle->getFileSpecCount() < 2) {
fprintf(stderr, "NOTE: nothing to do\n");
goto bail;
}
//类似于java中的打开输入输出流。
zip = openReadWrite(zipFileName, false);
if (zip == NULL) {
//打开文件失败。
fprintf(stderr, "ERROR: failed opening Zip archive '%s'\n",
zipFileName);
goto bail;
}
//索引从1开始,可以有多个要删除的文件。
for (int i = 1; i < bundle->getFileSpecCount(); i++) {
//获取要删除的文件名。
const char* fileName = bundle->getFileSpecEntry(i);
ZipEntry* entry;
//获取要删除的文件。
entry = zip->getEntryByName(fileName);
if (entry == NULL) {
//文件找不到
printf(" '%s' NOT FOUND\n", fileName);
continue;
}
//找到该文件则删除该文件。
result = zip->remove(entry);
if (result != NO_ERROR) {
fprintf(stderr, "Unable to delete '%s' from '%s'\n",
bundle->getFileSpecEntry(i), zipFileName);
goto bail;
}
}
//相当于java中输入输出流的刷新。
zip->flush();
bail:
delete zip;
return (result != NO_ERROR);
}
首先会判断命令中有没有指定压缩包,如果没有,会报错。有指定压缩包才会去获取命令中压缩包的名称。如果没有指定要删除的文件,会提示没有指定要删除的文件。否则打开文件,在打开文件成功的情况下,循环遍历要删除的文件,依次把该文件从压缩包中删除,全部操作完成后刷新压缩包中的内容。
以上就是执行aapt remove命令底层所执行的详细代码了,说到底aapt所有的命令最底层都是在操作文件。只是google把这些复杂的文件操作封装成命令工具供我们使用。这里的源码分析也只是看一个大体的结构,更具体的实现,各位小伙伴有兴趣可以去翻一翻源码啊,不一定要精通c++,有一点基础或者其它语言基础也能看得懂大概的流程,除非涉及到修改然后重新编译成定制化的aapt命令,那又是另外一回事了。