admin 管理员组

文章数量: 887019

上一篇:Android打包流程之资源管理
Android应用最终是以apk的形式放在手机上安装并运行的,而负责将资源文件和代码进行打包的工具就叫appt,全称Android Asset Packaging Tool,翻译过来就是Android资源打包工具,是Android打包流程中不可或缺的一环。虽然build-tools中都会有一个aapt.exe负责打包apk,但底层还是通过执行aapt命令的方式来进行操作,所以这里需要了解一下aapt的相关命令,有助于更好的理解打包的流程。

Android打包流程简述

先来看张官方的打包流程图:


  1. 首先将资源文件(res目录下)通过aapt工具打包成R.java类(资源索引)和.arsc资源文件。
  2. 倘若工程中又aidl,则通过aidl工具将aidl打包成java接口类。
  3. R.java、aidl生成的java文件和项目中的源代码混合编译成.class文件
  4. class文件和第三方jar或者library通过dx工具打包成dex文件。dx工具的主要作用是将java字节码转换成Dalvik字节码,在此过程中会压缩常量池,消除一些冗余信息等。
  5. 通过apkbuilder工具将所有没有编译的资源,.arsc资源,.dex文件打包到一个完成apk文件中。
  6. 生成的apk通过Jarsinger工具,利用relese或debug下配置的keystore文件进行签名,得到签名后的apk文件。
  7. 通过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的权限、字符、资源等等。
主要有以下几种参数:

  1. strings
    列出apk中的所有字符资源(包括不同语言限定符):示例:
aapt dump strings app-debug.apk>a.txt

结果:


这里有乱码,有其它国家语言符号。大家可以自行试一试。

  1. badging
    Print the label and icon for the app declared in APK。实际内容不止label和icon。还有包名、versionCode、versionName、compileSdkVersion、targetSdkVersion等等信息,这里截取一小部分:


  2. permissions
    列出apk所用到的所有权限。示例:

aapt dump permissions app-debug.apk

结果会列出apk所需要的所有权限,这里不贴图了。

  1. resources。
    输出apk中所有的资源信息,包括用户添加的资源、不同限定符下的系统资源等等,示例:
aapt dump resources app-debug.apk>a.txt

结果:


  1. 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命令,那又是另外一回事了。

好了,关于aapt命令就写到这里了,知道怎么用,底层大概是怎么实现的就可以了,有深厚c++功底的同学可以自己尝试改一改。文章中若是有不足或错误,看到的小伙伴可以在底下留言告诉我,小菜鸟不胜感激~

本文标签: 流程 文件 资源 Android aapt