我们以一个包名为AaptDemo的Apk为例来分析。
首先我们用sdk中的工具来生成一个简单的应用工程。
D:\android_debug>..\sdk\tools\android.bat create project --activity MainActivity --package com.jackyperf.aaptdemo --path .\AaptDemo -t android-23
- 生成的应用工程目录
- 应用资源目录
接下来我们以该应用为例来分析resources.arsc的生成过程。
Android 应用资源表的生成
我们知道Android应用资源是通过sdk中的aapt工具编译和打包的,,可以利用下面的命令将我们之前创建的AaptDemo应用编译、打包成AaptDemo.apk。
D:\android_debug\AaptDemo>..\..\sdk\build-tools\23.0.2\aapt.exe package -f -M AndroidManifest.xml -S .\res -I ..\..\sdk\platforms\android-23\android.jar -F AaptDemo.apk
结合上述命令,我们从aapt的入口main函数开始分析。
源文件:android\frameworks\base\tools\aapt\main.cpp
int main(int argc, char* const argv[])
{
char *prog = argv[0];
Bundle bundle;
...
else if (argv[1][0] == 'p')
//aapt编译、打包资源命令
bundle.setCommand(kCommandPackage);
...
while (argc && argv[0][0] == '-') {
/* flag(s) found */
const char* cp = argv[0] +1;
while (*cp != '\0') {
switch (*cp) {
...
//强制重写最终的apk文件
case 'f':
bundle.setForce(true);
break;
...
//添加一个额外的package
case 'I':
argc--;
argv++;
if (!argc) {
fprintf(stderr, "ERROR: No argument supplied for '-I' option\n");
wantUsage = true;
goto bail;
}
convertPath(argv[0]);
bundle.addPackageInclude(argv[0]);
break;
//指定打包完成后的apk文件
case 'F':
argc--;
argv++;
if (!argc) {
fprintf(stderr, "ERROR: No argument supplied for '-F' option\n");
wantUsage = true;
goto bail;
}
convertPath(argv[0]);
bundle.setOutputAPKFile(argv[0]);
break;
...
//指定资源路径
case 'S':
argc--;
argv++;
if (!argc) {
fprintf(stderr, "ERROR: No argument supplied for '-S' option\n");
wantUsage = true;
goto bail;
}
convertPath(argv[0]);
bundle.addResourceSourceDir(argv[0]);
break;
...
result = handleCommand(&bundle);
...
}
解析我们编译、打包所使用的aapt命令,将解析得到的命令行参数保存到Bundle中,然后调用handleCommand进行处理,在handleCommand中直接,调用doPackage处理aapt package命令。
源文件:android\frameworks\base\tools\aapt\Command.cpp
int doPackage(Bundle* bundle)
{
...
//检查输出的apk文件是否合法
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();
...
//收集应用资源(AndroidManifest、asset目录、res目录等)保存在AaptAssets对象中
err = assets->slurpFromArgs(bundle);
...
// 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.
builder = new ApkBuilder(configFilter);
...
// If they asked for any fileAs that need to be compiled, do so.
if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
//编译应用资源,构建资源表
err = buildResources(bundle, assets, builder);
if (err != 0) {
goto bail;
}
}
...
//生成Proguard文件,用于字节码混淆、压缩字节码及资源文件
// Write out the ProGuard file
err = writeProguardFile(bundle, assets);
...
// 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);
//将编译后的资源文件写入apk包,包括resources.arsc
err = writeAPK(bundle, outputPath, split);
if (err != NO_ERROR) {
fprintf(stderr, "ERROR: packaging of '%s' failed\n", outputPath.string());
goto bail;
}
}
}
...
}
doPackage就是应用资源编译、打包的整个过程,我们重点分析以下三个阶段:
- 调用slurpFromArgs收集应用资源保存到AaptAssets
- 调用buildResources编译应用资源,构建资源表
- 资源表写入apk包的过程
调用slurpFromArgs收集应用资源保存到AaptAssets
在分析应用资源收集的过程之前,我们首先看下AaptAssets的类图,以便于了解它是如何保存应用资源的。
AaptAssets描述正在编译的资源
- mGroupEntries:描述包含的资源配置集合
- mRes:描述包含的资源类型集,每一种类型的资源用一个ResourceTypeSet表示
AaptDir描述单一目录(资源类型)下的资源,可以包含文件或者子目录
- mLeaf:目录的叶子路径的名称或者资源类型名
- mPath:目录的全路径
- mFiles:当前目录下的同名资源集合
- mDirs:当前目录下子目录资源
AaptGroup描述一组名字相同(配置不同)的资源文件
- mLeaf:资源文件名
- mPath:资源文件全路径
- mFiles:名字相同配置不同的一组资源文件
AaptFile描述单一资源文件
- mPath:资源文件路径
- mGroupEntry:对应的资源配置
- mResourceType:资源文件所属资源类型
AaptGroupEntry描述单一资源文件的配置
- mParams:资源文件的配置信息,包括移动网络、国家、地区、语言、屏幕密度等
下面分析收集资源保存到AaptAssets的过程
源文件:android\frameworks\base\tools\aapt\AaptAssets.cpp
ssize_t AaptAssets::slurpFromArgs(Bundle* bundle)
{
int count;
int totalCount = 0;
FileType type;
const Vector<const char *>& resDirs = bundle->getResourceSourceDirs();
//在本文的AaptDemo应用中,dirCount = 1
const size_t dirCount =resDirs.size();
...
/*
* If a package manifest was specified, include that first.
*/
//为AndroidManifest文件创建AaptGroup以及AaptGroupEntry,并添加到AaptAssets。
if (bundle->getAndroidManifestFile() != NULL) {
// place at root of zip.
String8 srcFile(bundle->getAndroidManifestFile());
addFile(srcFile.getPathLeaf(), AaptGroupEntry(), srcFile.getPathDir(),
NULL, String8());
totalCount++;
}
...
//收集应用内部的asset目录下的资源,在AaptDemo应用中,没有创建该路径
const Vector<const char*>& assetDirs = bundle->getAssetSourceDirs();
const int AN = assetDirs.size();
...
//收集应用内部的res目录下的资源
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) {
//收集当前package对应的overlay package的资源
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;
}
}
}
...
}
收集res目录下的资源是通过调用slurpResourceTree来实现的,继续分析slurpResourceTree。
ssize_t AaptAssets::slurpResourceTree(Bundle* bundle, const String8& srcDir)
{
ssize_t err = 0;
//打开res目录
DIR* dir = opendir(srcDir.string());
...
while (1) {
//收集res目录下子目录的资源
struct dirent* entry = readdir(dir);
...
String8 subdirName(srcDir);
subdirName.appendPath(entry->d_name);
AaptGroupEntry group;
String8 resType;
//解析entry目录的资源类型以及资源的配置信息
//资源类型保存在存数resType中,资源的配置信息保存在AaptGroupEntry中
bool b = group.initFromDirName(entry->d_name, &resType);
...
//返回当前文件的类型
FileType type = getFileType(subdirName.string());
if (type == kFileTypeDirectory) {
//为当前类型资源创建AaptDir对象
sp<AaptDir> dir = makeDir(resType);
//收集当前文件下的资源
ssize_t res = dir->slurpFullTree(bundle, subdirName, group,
resType, mFullResPaths);
if (res < 0) {
count = res;
goto bail;
}
if (res > 0) {
//AaptGroupEntry添加到AaptAssets
mGroupEntries.add(group);
count += res;
}
// Only add this directory if we don't already have a resource dir
// for the current type. This ensures that we only add the dir once
// for all configs.
//如果当前资源类型的AaptDir没有添加到AaptAssets,添加;
//一种资源类型对应一个AaptDir,即使有多种配置。
sp<AaptDir> rdir = resDir(resType);
if (rdir == NULL) {
mResDirs.add(dir);
}
} else {
if (bundle->getVerbose()) {
fprintf(stderr, " (ignoring file '%s')\n", subdirName.string());
}
}
}
...
}
slurpResourceTree通过遍历res目录的子目录收集各种类型的资源,同时创建AaptGroupEntry以及AaptDir,并添加到AaptAssets。下面分析slurpFullTree。
ssize_t AaptDir::slurpFullTree(Bundle* bundle, const String8& srcDir,
const AaptGroupEntry& kind, const String8& resType,
sp<FilePathStore>& fullResPaths, const bool overwrite)
{
Vector<String8> fileNames;
{
DIR* dir = NULL;
//收集当前文件中的子文件,并添加到fileNames
dir = opendir(srcDir.string());
...
while (1) {
struct dirent* entry;
entry = readdir(dir);
...
String8 name(entry->d_name);
fileNames.add(name);
...
}
}
...
const size_t N = fileNames.size();
size_t i;
for (i = 0; i < N; i++) {
String8 pathName(srcDir);
FileType type;
pathName.appendPath(fileNames[i].string());
type = getFileType(pathName.string());
if (type == kFileTypeDirectory) {
//如果当前文件是目录,与上文中处理类似
...
} else if (type == kFileTypeRegular) {
//如果是普通的资源文件,创建AaptFile对象
sp<AaptFile> file = new AaptFile(pathName, kind, resType);
//将AaptFile添加到文件名对应的AaptGroup对象中
status_t err = addLeafFile(fileNames[i], file, overwrite);
...
}
...
}
...
}
最终,res目录下所有的资源都被收集到AaptAssets中,我们以AaptDemo中的res目录为例,来看下生成的AaptAssets的数据结构。
调用buildResources编译应用资源,构建资源表
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.
...
// 解析AndroidManifest.xml获取包名、版本码等信息
status_t err = parsePackage(bundle, assets, androidManifestFile);
...
ResourceTable table(bundle, String16(assets->getPackage()), packageType);
err = table.addIncludedResources(bundle, assets);
...
// --------------------------------------------------------------
// First, gather all resource information.
// --------------------------------------------------------------
// resType -> leafName -> group
KeyedVector<String8, sp<ResourceTypeSet> > *resources =
new KeyedVector<String8, sp<ResourceTypeSet> >;
// 将AaptAssets中资源按照资源类型以AaptGroup为单位添加到resources中
collect_files(assets, resources);
...
// 将收集到的资源类型集Vector保存到AaptAssets的mRes中
assets->setResources(resources);
...
if (layouts != NULL) {
// 为资源集中的资源创建Type/ConfigList/Entry/Item结构存放到ResourceTable中
err = makeFileResources(bundle, assets, &table, layouts, "layout");
if (err != NO_ERROR) {
hasErrors = true;
}
}
...
while(current.get()) {
KeyedVector<String8, sp<ResourceTypeSet> > *resources =
current->getResources();
ssize_t index = resources->indexOfKey(String8("values"));
...
// 编译values类型资源,创建资源Type/ConfigList/Entry/Item结构存放到ResourceTable中
res = compileResourceFile(bundle, assets, file, it.getParams(),
(current!=assets), &table);
...
}
...
// --------------------------------------------------------------------
// Assignment of resource IDs and initial generation of resource table.
// --------------------------------------------------------------------
if (table.hasResources()) {
err = table.assignResourceIds();
if (err < NO_ERROR) {
return err;
}
}
...
// --------------------------------------------------------------
// Generate the final resource table.
// Re-flatten because we may have added new resource IDs
// --------------------------------------------------------------
ResTable finalResTable;
sp<AaptFile> resFile;
...
Vector<sp<ApkSplit> >& splits = builder->getSplits();
const size_t numSplits = splits.size();
for (size_t i = 0; i < numSplits; i++) {
sp<ApkSplit>& split = splits.editItemAt(i);
// 创建"resources.arsc"AaptFile用于保存资源表信息
sp<AaptFile> flattenedTable = new AaptFile(String8("resources.arsc"),
AaptGroupEntry(), String8());
// 将ResourceTable中收集的资源信息按照resources.arsc文件的格式flatten到flattenedTable中
err = table.flatten(bundle, split->getResourceFilter(),
flattenedTable, split->isBase());
if (err != NO_ERROR) {
fprintf(stderr, "Failed to generate resource table for split '%s'\n",
split->getPrintableName().string());
return err;
}
// 将flattenedTable添加到ApkSplit中,最终作为OutputEntry写入APK中
split->addEntry(String8("resources.arsc"), flattenedTable);
if (split->isBase()) {
resFile = flattenedTable;
err = finalResTable.add(flattenedTable->getData(), flattenedTable->getSize());
if (err != NO_ERROR) {
fprintf(stderr, "Generated resource table is corrupt.\n");
return err;
}
} else {
}
...
}
以AaptDemo为例,最终得到的资源项如下图所示(strings.xml中仅有app_name)
其中,Entry name最终作为keyString写入resources.arsc;Item value作为ValueString写入resources.arsc。
最终资源索引表resources.arsc文件的结构如下图所示
资源表写入apk包的过程
有了前面的知识,resources.arsc文件写入apk包的过程就比较简单了,这里不再分析。
网友评论