一、什么是AAPT2
在Android開(kāi)發(fā)過(guò)程中,我們通過(guò)Gradle命令,啟動(dòng)一個(gè)構(gòu)建任務(wù),最終會(huì)生成構(gòu)建產(chǎn)物“APK”文件。常規(guī)APK的構(gòu)建流程如下:
[圖片上傳失敗...(image-ec914a-1635216078176)]
(引用自Google官方文檔)
編譯所有的資源文件,生成資源表和R文件;
編譯Java文件并把class文件打包為dex文件;
打包資源和dex文件,生成未簽名的APK文件;
簽名APK生成正式包。
老版本的Android默認(rèn)使用AAPT編譯器進(jìn)行資源編譯,從Android Studio 3.0開(kāi)始,AS默認(rèn)開(kāi)啟了 AAPT2 作為資源編譯的編譯器,目前看來(lái),AAPT2也是Android發(fā)展的主流趨勢(shì),學(xué)習(xí)AAPT2的工作原理可以幫助Android開(kāi)發(fā)更好的掌握APK構(gòu)建流程,從而幫助解決實(shí)際開(kāi)發(fā)中遇到的問(wèn)題。
AAPT2 的可執(zhí)行文件隨 Android SDK 的 Build Tools 一起發(fā)布,在Android Studio的build-tools文件夾中就包含AAPT2工具,目錄為(SDK目錄/build-tools/version/aapt2)。
[圖片上傳失敗...(image-a2fd2c-1635216078176)]
二、AAPT2如何工作
在看Android編譯流程的時(shí)候,我忍不住會(huì)想一個(gè)問(wèn)題:
Java文件需要編譯才能生class文件,這個(gè)我能明白,但資源文件編譯到底是干什么的?為什么要對(duì)資源做編譯?
帶著這個(gè)問(wèn)題,讓我們深入的學(xué)習(xí)一下AAPT2。和AAPT不同,AAPT2把資源編譯打包過(guò)程拆分為兩部分,即編譯和鏈接:
編譯:將資源文件編譯為二進(jìn)制文件(flat)。
鏈接:將編譯后的文件合并,打包成單獨(dú)文件。
通過(guò)把資源編譯拆分為兩個(gè)部分,AAPT2能夠很好的提升資源編譯的性能。例如,之前一個(gè)資源文件發(fā)生變動(dòng),AAPT需要做一全量編譯,AAPT2只需要重新編譯改變的文件,然后和其他未發(fā)生改變的文件進(jìn)行鏈接即可。
2.1 Compile命令
如上文描述,Complie指令用于編譯資源,AAPT2提供多個(gè)選項(xiàng)與Compile命令搭配使用。
[圖片上傳失敗...(image-71586-1635216078176)]
Complie的一般用法如下:
aapt2 compile path-to-input-files [options] -o output-directory/
執(zhí)行命令后,AAPT2會(huì)把資源文件編譯為.flat格式的文件,文件對(duì)比如下。
[圖片上傳失敗...(image-6b6ec1-1635216078176)]
Compile命令會(huì)對(duì)資源文件的路徑做校驗(yàn),輸入文件的路徑必須符合以下結(jié)構(gòu):path/resource-type[-config]/file。
例如,把資源文件保存在“aapt2”文件夾下,使用Compile命令編譯,則會(huì)報(bào)錯(cuò)“error: invalid file path '.../aapt2/ic_launcher.png'”。把a(bǔ)apt改成“drawable-hdpi”,編譯正常。
在Android Studio中,可以在app/build/intermediates/res/merged/ 目錄下找到編譯生成的.flat文件。當(dāng)然Compile也支持編譯多個(gè)文件;
aapt2 compile path-to-input-files1 path-to-input-files2 [options] -o output-directory/
編譯整個(gè)目錄,需要制定數(shù)據(jù)文件,編譯產(chǎn)物是一個(gè)壓縮文件,包含目錄下所有的資源,通過(guò)文件名把資源目錄結(jié)構(gòu)扁平化。
aapt2 compile --dir .../res [options] -o output-directory/resource.ap_
[圖片上傳失敗...(image-a097dd-1635216078176)]
可以看到經(jīng)過(guò)編譯后,資源文件(png,xml ... )會(huì)被編譯成一個(gè)FLAT格式的文件,直接把FLAT文件拖拽到as中打開(kāi),是亂碼的。那么這個(gè)FLAT文件到底是什么?
2.2 FLAT文件
FLAT文件是AAPT2編譯的產(chǎn)物文件,也叫做AAPT2容器,文件由文件頭和資源項(xiàng)兩大部分組成:
文件頭
[圖片上傳失敗...(image-1a2f63-1635216078176)]
資源項(xiàng)
[圖片上傳失敗...(image-4077e1-1635216078176)]
資源項(xiàng)中,按照 entry_type 值分為兩種類型:
當(dāng)entry_type 的值等于 0x00000000時(shí),為RES_TABLE類型。
當(dāng)entry_type的值等于 0x00000001時(shí),為RES_FILE類型。
RES_TABLE包含的是protobuf格式的 ResourceTable 結(jié)構(gòu)。數(shù)據(jù)結(jié)構(gòu)如下:
// Top level message representing a resource table.
message ResourceTable {
// 字符串池
StringPool source_pool = 1;
// 用于生成資源id
repeated Package package = 2;
// 資源疊加層相關(guān)
repeated Overlayable overlayable = 3;
// 工具版本
repeated ToolFingerprint tool_fingerprint = 4;
}
資源表(ResourceTable)中包含:
StringPool:字符串池,字符串常量池是為了把資源文件中的string復(fù)用起來(lái),從而減少體積,資源文件中對(duì)應(yīng)的字符串會(huì)被替換為字符串池中的索引。
message StringPool {
bytes data = 1;
}
Package:包含資源id的相關(guān)信息
// 資源id的包id部分,在 [0x00, 0xff] 范圍內(nèi)
message PackageId {
uint32 id = 1;
}
// 資源id的命名規(guī)則
message Package {
// [0x02, 0x7f) 簡(jiǎn)單的說(shuō),由系統(tǒng)使用
// 0x7f 應(yīng)用使用
// (0x7f, 0xff] 預(yù)留Id
PackageId package_id = 1;
// 包名
string package_name = 2;
// 資源類型,對(duì)應(yīng)string, layout, xml, dimen, attr等,其對(duì)應(yīng)的資源id區(qū)間為[0x01, 0xff]
repeated Type type = 3;
}
資源id的命令方式遵循0xPPTTEEEE的規(guī)則,其中PP對(duì)應(yīng)PackageId,一般應(yīng)用使用的資源為7f,TT對(duì)應(yīng)的是資源文件夾的名成,最后4位為資源的id,從0開(kāi)始。
RES_FILE類型格式如下:
[圖片上傳失敗...(image-cc4a51-1635216078176)]
RES_FILE類型的FLAT文件結(jié)構(gòu)可以參考下圖;
[圖片上傳失敗...(image-7323d-1635216078176)]
從上圖展示的文件格式中可以看出,一個(gè)FLAT中可以包含多個(gè)資源項(xiàng),在資源項(xiàng)中,Header字段中保存的是protobuf格式序列化的 CompiledFile 內(nèi)容。在這個(gè)結(jié)構(gòu)中,保存了文件名、文件路徑、文件配置和文件類型等信息。data字段中保存資源文件的內(nèi)容。通過(guò)這種方式,一個(gè)文件中既保存了文件的外部相關(guān)信息,又包含文件的原始內(nèi)容。
2.3 編譯的源碼
上文,我們學(xué)習(xí)了編譯命令Compile的用法和編譯產(chǎn)物FLAT文件的文件格式,接下來(lái),我們通過(guò)查看代碼,從源碼層面來(lái)學(xué)習(xí)AAPT2的編譯流程,本文源碼地址。
2.3.1 命令執(zhí)行流程
根據(jù)常識(shí),一般函數(shù)的入口都是和main有關(guān),打開(kāi)Main.cpp,可以找到main函數(shù)入口;
int main(int argc, char** argv) {
#ifdef _WIN32
......
//參數(shù)格式轉(zhuǎn)換
argv = utf8_argv.get();
#endif
//具體的實(shí)現(xiàn)MainImpl中
return MainImpl(argc, argv);
}
在MainImpl中,首先從輸入中獲取參數(shù)部分,然后創(chuàng)建一個(gè)MainCommand來(lái)執(zhí)行命令。
int MainImpl(int argc, char** argv) {
if (argc < 1) {
return -1;
}
// 從下標(biāo)1開(kāi)始的輸入,保存在args中
std::vector<StringPiece> args;
for (int i = 1; i < argc; i++) {
args.push_back(argv[i]);
}
//省略部分代碼,這部分代碼用于打印信息和錯(cuò)誤處理
//創(chuàng)建一個(gè)MainCommand
aapt::MainCommand main_command(&printer, &diagnostics);
// aapt2的守護(hù)進(jìn)程模式,
main_command.AddOptionalSubcommand( aapt::util::make_unique<aapt::DaemonCommand>(&fout, &diagnostics));
// 調(diào)用Execute方法執(zhí)行命令
return main_command.Execute(args, &std::cerr);
}
MainCommand繼承自Command,在MainCommand初始化方法中會(huì)添加多個(gè)二級(jí)命令,通過(guò)類名,可以容易的推測(cè)出,這些Command和終端通過(guò)命令查看的二級(jí)命令一一對(duì)應(yīng)。
explicit MainCommand(text::Printer* printer, IDiagnostics* diagnostics)
: Command("aapt2"), diagnostics_(diagnostics) {
//對(duì)應(yīng)Compile 命令
AddOptionalSubcommand(util::make_unique<CompileCommand>(diagnostics));
//對(duì)應(yīng)link 命令
AddOptionalSubcommand(util::make_unique<LinkCommand>(diagnostics));
AddOptionalSubcommand(util::make_unique<DumpCommand>(printer, diagnostics));
AddOptionalSubcommand(util::make_unique<DiffCommand>());
AddOptionalSubcommand(util::make_unique<OptimizeCommand>());
AddOptionalSubcommand(util::make_unique<ConvertCommand>());
AddOptionalSubcommand(util::make_unique<VersionCommand>());
}
AddOptionalSubcommand方法定義在基類Command中,內(nèi)容比較簡(jiǎn)單,把傳入的subCommand保存在數(shù)組中。
void Command::AddOptionalSubcommand(std::unique_ptr<Command>&& subcommand, bool experimental) {
subcommand->full_subcommand_name_ = StringPrintf("%s %s", name_.data(), subcommand->name_.data());
if (experimental) {
experimental_subcommands_.push_back(std::move(subcommand));
} else {
subcommands_.push_back(std::move(subcommand));
}
}
接下來(lái),再來(lái)分析main_command.Execute的內(nèi)容,從方法名可以推測(cè)這個(gè)方法里面有指令執(zhí)行的相關(guān)代碼。在MainCommand中并沒(méi)有Execute方法的實(shí)現(xiàn),那應(yīng)該是在父類中實(shí)現(xiàn)了,再到Command類中搜索,果然在這里。
int Command::Execute(const std::vector<StringPiece>& args, std::ostream* out_error) {
TRACE_NAME_ARGS("Command::Execute", args);
std::vector<std::string> file_args;
for (size_t i = 0; i < args.size(); i++) {
const StringPiece& arg = args[i];
// 參數(shù)不是 '-'
if (*(arg.data()) != '-') {
//是第一個(gè)參數(shù)
if (i == 0) {
for (auto& subcommand : subcommands_) {
//判斷是否是子命令
if (arg == subcommand->name_ || (!subcommand->short_name_.empty() && arg == subcommand->short_name_)) {
//執(zhí)行子命令的Execute 方法,傳入?yún)?shù)向后移動(dòng)一位
return subcommand->Execute( std::vector<StringPiece>(args.begin() + 1, args.end()), out_error);
}
}
//省略部分代碼
//調(diào)用Action方法,在執(zhí)行二級(jí)命令時(shí),file_args保存的是位移后的參數(shù)
return Action(file_args);
}
在Execute方法中,會(huì)先對(duì)參數(shù)作判斷,如果參數(shù)第一位命中二級(jí)命令(Compile,link,.....),則調(diào)用二級(jí)命令的Execute方法。參考上文編譯命令的示例可知,一般情況下,在這里就會(huì)命中二級(jí)命令的判斷,從而調(diào)用二級(jí)命令的Execute方法。
在Command.cpp的同級(jí)目錄下,可以找到Compile.cpp,其Execute繼承自父類。但是由于參數(shù)已經(jīng)經(jīng)過(guò)移位,所以最終會(huì)執(zhí)行Action方法。在Compile.cpp中可以找到Action方法,同樣在其他二級(jí)命令的實(shí)現(xiàn)類中(Link.cpp,Dump.cpp...),其核心處理的處理也都有Action方法中。整體調(diào)用的示意圖如下:
[圖片上傳失敗...(image-f5ac1a-1635216078176)]
在開(kāi)始看Action代碼之前,我們先看一下Compile.cpp的頭文件Compile.h的內(nèi)容,在CompileCommand初始化時(shí),會(huì)把必須參數(shù)和可選參數(shù)都初始化定義好。
SetDescription("Compiles resources to be linked into an apk.");
AddRequiredFlag("-o", "Output path", &options_.output_path, Command::kPath);
AddOptionalFlag("--dir", "Directory to scan for resources", &options_.res_dir, Command::kPath);
AddOptionalFlag("--zip", "Zip file containing the res directory to scan for resources", &options_.res_zip, Command::kPath);
AddOptionalFlag("--output-text-symbols", "Generates a text file containing the resource symbols in the\n" "specified file", &options_.generate_text_symbols_path, Command::kPath);
AddOptionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales " "(en-XA and ar-XB)", &options_.pseudolocalize);
AddOptionalSwitch("--no-crunch", "Disables PNG processing", &options_.no_png_crunch);
AddOptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", &options_.legacy_mode);
AddOptionalSwitch("--preserve-visibility-of-styleables", "If specified, apply the same visibility rules for\n" "styleables as are used for all other resources.\n" "Otherwise, all stylesables will be made public.", &options_.preserve_visibility_of_styleables);
AddOptionalFlag("--visibility", "Sets the visibility of the compiled resources to the specified\n" "level. Accepted levels: public, private, default", &visibility_);
AddOptionalSwitch("-v", "Enables verbose logging", &options_.verbose);
AddOptionalFlag("--trace-folder", "Generate systrace json trace fragment to specified folder.", &trace_folder_);
官網(wǎng)中列出的編譯選項(xiàng)并不全,使用compile -h打印信息后就會(huì)發(fā)現(xiàn)打印的信息和代碼中的設(shè)置是一致的。
在Action方法的執(zhí)行流程可以總結(jié)為:
1)會(huì)根據(jù)傳入?yún)?shù)判斷資源類型,并創(chuàng)建對(duì)應(yīng)的文件加載器(file_collection)。
2)根據(jù)傳入的輸出路徑判斷輸出文件的類型,并創(chuàng)建對(duì)應(yīng)的歸檔器(archive_writer),archive_writer在后續(xù)的調(diào)用鏈中一直向下傳遞,最終通過(guò)archive_writer把編譯后的文件寫(xiě)到輸出目錄下。
3)調(diào)用Compile方法執(zhí)行編譯。
過(guò)程1,2中涉及的文件讀寫(xiě)對(duì)象如下表。
[圖片上傳失敗...(image-6bc183-1635216078176)]
簡(jiǎn)化的主流程代碼如下:
int CompileCommand::Action(const std::vector<std::string>& args) {
//省略部分代碼....
std::unique_ptr<io::IFileCollection> file_collection;
//加載輸入資源,簡(jiǎn)化邏輯,下面會(huì)省略掉校驗(yàn)的代碼
if (options_.res_dir && options_.res_zip) {
context.GetDiagnostics()->Error(DiagMessage() << "only one of --dir and --zip can be specified");
return 1;
} else if (options_.res_dir) {
//加載目錄下的資源文件...
file_collection = io::FileCollection::Create(options_.res_dir.value(), &err);
//...
}else if (options_.res_zip) {
//加載壓縮包格式的資源文件...
file_collection = io::ZipFileCollection::Create(options_.res_zip.value(), &err);
//...
} else {
//也是FileCollection,先定義collection,通過(guò)循環(huán)依次添加輸入文件,再拷貝到file_collection
file_collection = std::move(collection);
}
std::unique_ptr<IArchiveWriter> archive_writer;
//產(chǎn)物輸出文件類型
file::FileType output_file_type = file::GetFileType(options_.output_path);
if (output_file_type == file::FileType::kDirectory) {
//輸出到文件目錄
archive_writer = CreateDirectoryArchiveWriter(context.GetDiagnostics(), options_.output_path);
} else {
//輸出到壓縮包
archive_writer = CreateZipFileArchiveWriter(context.GetDiagnostics(), options_.output_path);
}
if (!archive_writer) {
return 1;
}
return Compile(&context, file_collection.get(), archive_writer.get(), options_);
}
Compile方法中會(huì)編譯輸入的資源文件名,每個(gè)資源文件的處理方式如下:
解析輸入的資源路徑獲取資源名,擴(kuò)展名等信息;
根據(jù)path判斷文件類型,然后給compile_func設(shè)置不同的編譯函數(shù);
生成輸出的文件名。輸出的就是FLAT文件名,會(huì)對(duì)全路徑拼接,最終生成上文案例中類似的文件名—“drawable-hdpi_ic_launcher.png.flat”;
傳入各項(xiàng)參數(shù),調(diào)用compile_func方法執(zhí)行編譯。
ResourcePathData中包含了資源路徑,資源名,資源擴(kuò)展名等信息,AAPT2會(huì)從中獲取資源的類型。
int Compile(IAaptContext* context, io::IFileCollection* inputs, IArchiveWriter* output_writer, CompileOptions& options) {
TRACE_CALL();
bool error = false;
// 編譯輸入的資源文件
auto file_iterator = inputs->Iterator();
while (file_iterator->HasNext()) {
// 省略部分代碼(文件校驗(yàn)相關(guān)...)
std::string err_str;
ResourcePathData path_data;
// 獲取path全名,用于后續(xù)文件類型判斷
if (auto maybe_path_data = ExtractResourcePathData(path, inputs->GetDirSeparator(), &err_str)) {
path_data = maybe_path_data.value();
} else {
context->GetDiagnostics()->Error(DiagMessage(file->GetSource()) << err_str);
error = true;
continue;
}
// 根據(jù)文件類型,選擇編譯方法,這里的 CompileFile 是函數(shù)指針,指向一個(gè)編譯方法。
// 使用使用設(shè)置為CompileFile方法
auto compile_func = &CompileFile;
// 如果是values目錄下的xml資源,使用 CompileTable 方法編譯,并修改擴(kuò)展名為arsc
if (path_data.resource_dir == "values" && path_data.extension == "xml") {
compile_func = &CompileTable;
// We use a different extension (not necessary anymore, but avoids altering the existing // build system logic).
path_data.extension = "arsc";
} else if (const ResourceType* type = ParseResourceType(path_data.resource_dir)) {
// 解析資源類型,如果kRaw類型,執(zhí)行默認(rèn)的編譯方法,否則執(zhí)行如下代碼。
if (*type != ResourceType::kRaw) {
//xml路徑或者文件擴(kuò)展為.xml
if (*type == ResourceType::kXml || path_data.extension == "xml") {
// xml類,使用CompileXml方法編譯
compile_func = &CompileXml;
} else if ((!options.no_png_crunch && path_data.extension == "png") || path_data.extension == "9.png") { //如果后綴名是.png并且開(kāi)啟png優(yōu)化或者是點(diǎn)9圖類型
// png類,使用CompilePng方法編譯
compile_func = &CompilePng;
}
}
} else {
// 不合法的類型,輸出錯(cuò)誤信息,繼續(xù)循環(huán)
context->GetDiagnostics()->Error(DiagMessage() << "invalid file path '" << path_data.source << "'");
error = true;
continue;
}
// 校驗(yàn)文件名中是否有.
if (compile_func != &CompileFile && !options.legacy_mode && std::count(path_data.name.begin(), path_data.name.end(), '.') != 0) {
error = true;
context->GetDiagnostics()->Error(DiagMessage(file->GetSource()) << "file name cannot contain '.' other than for" << " specifying the extension");
continue;
}
// 生成產(chǎn)物文件名,這個(gè)方法會(huì)生成完成的flat文件名,例如上文demo中的 drawable-hdpi_ic_launcher.png.flat
const std::string out_path = BuildIntermediateContainerFilename(path_data);
// 執(zhí)行編譯方法
if (!compile_func(context, options, path_data, file, output_writer, out_path)) {
context->GetDiagnostics()->Error(DiagMessage(file->GetSource()) << "file failed to compile"); error = true;
}
}
return error ? 1 : 0;
}
不同的資源類型會(huì)有四種編譯函數(shù):
CompileFile
CompileTable
CompileXml
CompilePng
raw目錄下的XML文件不會(huì)執(zhí)行CompileXml,猜測(cè)是因?yàn)閞aw下的資源是直接復(fù)制到APK中,不會(huì)做XML優(yōu)化編譯。values目錄下資源除了執(zhí)行CompileTable編譯之外,還會(huì)修改資源文件的擴(kuò)展名,可以認(rèn)為除了CompileFile,其他編譯方法多多少少會(huì)對(duì)原始資源做處理后,在寫(xiě)編譯生成的FLAT文件中。這部分的流程如下圖所示:
[圖片上傳失敗...(image-95d2f0-1635216078176)]
編譯命令執(zhí)行的主流程到這里就結(jié)束了,通過(guò)源碼分析,我們可以知道AAPT2把輸入文件編譯為FLAT文件。下面,我們?cè)谶M(jìn)一步分析4個(gè)編譯方法。
2.3.2 四種編譯函數(shù)
CompileFile
函數(shù)中先構(gòu)造ResourceFile對(duì)象和原始文件數(shù)據(jù),然后調(diào)用 WriteHeaderAndDataToWriter 把數(shù)據(jù)寫(xiě)到輸出文件(flat)中。
static bool CompileFile(IAaptContext* context, const CompileOptions& options, const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) {
TRACE_CALL();
if (context->IsVerbose()) {
context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "compiling file");
}
// 定義ResourceFile 對(duì)象,保存config,source等信息
ResourceFile res_file;
res_file.name = ResourceName({}, *ParseResourceType(path_data.resource_dir), path_data.name);
res_file.config = path_data.config;
res_file.source = path_data.source;
res_file.type = ResourceFile::Type::kUnknown; //這類型下可能有xml,png或者其他的什么,統(tǒng)一設(shè)置類型為unknow。
// 原始文件數(shù)據(jù)
auto data = file->OpenAsData();
if (!data) {
context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to open file ");
return false;
}
return WriteHeaderAndDataToWriter(output_path, res_file, data.get(), writer, context->GetDiagnostics());
}
ResourceFile的內(nèi)容相對(duì)簡(jiǎn)單,完成文件相關(guān)信息的賦值后就會(huì)調(diào)用通過(guò)WriteHeaderAndDataToWriter方法。
在WriteHeaderAndDataToWriter這個(gè)方法中,對(duì)之前創(chuàng)建的archive_writer(可在本文搜索,這個(gè)歸檔寫(xiě)創(chuàng)建完成后,會(huì)一直傳下來(lái))做一次包裝,經(jīng)過(guò)包裝的ContainerWriter則具備普通文件寫(xiě)和protobuf格式序列化寫(xiě)的能力。
pb提供了ZeroCopyStream 接口用戶數(shù)據(jù)讀寫(xiě)和序列化/反序列化操作。
WriteHeaderAndDataToWriter的流程可以簡(jiǎn)單歸納為:
IArchiveWriter.StartEntry,打開(kāi)文件,做好寫(xiě)入準(zhǔn)備;
ContainerWriter.AddResFileEntry,寫(xiě)入數(shù)據(jù);
IArchiveWriter.FinishEntry,關(guān)閉文件,釋放內(nèi)存。
static bool WriteHeaderAndDataToWriter(const StringPiece& output_path, const ResourceFile& file, io::KnownSizeInputStream* in, IArchiveWriter* writer, IDiagnostics* diag) {
// 打開(kāi)文件
if (!writer->StartEntry(output_path, 0)) {
diag->Error(DiagMessage(output_path) << "failed to open file");
return false;
}
// Make sure CopyingOutputStreamAdaptor is deleted before we call writer->FinishEntry().
{
// 對(duì)write做一層包裝,用來(lái)寫(xiě)protobuf數(shù)據(jù)
CopyingOutputStreamAdaptor copying_adaptor(writer);
ContainerWriter container_writer(?ing_adaptor, 1u);
//把file按照protobuf格式序列化,序列化后的文件是 pb_compiled_file,這里的file文件是ResourceFile文件,包含了原始文件的路徑,配置等信息
pb::internal::CompiledFile pb_compiled_file;
SerializeCompiledFileToPb(file, &pb_compiled_file);
// 再把pb_compiled_file 和 in(原始文件) 寫(xiě)入到產(chǎn)物文件中
if (!container_writer.AddResFileEntry(pb_compiled_file, in)) {
diag->Error(DiagMessage(output_path) << "failed to write entry data");
return false;
}
}
// 退出寫(xiě)狀態(tài)
if (!writer->FinishEntry()) {
diag->Error(DiagMessage(output_path) << "failed to finish writing data");
return false;
}
return true;
}
我們?cè)俜謩e來(lái)看這三個(gè)方法,首先是StartEntry和FinishEntry,這個(gè)方法在Archive.cpp中,ZipFileWriter和DirectoryWriter實(shí)現(xiàn)有些區(qū)別,但邏輯上是一致的,這里只分析DirectoryWriter的實(shí)現(xiàn)。
StartEntry,調(diào)用fopen打開(kāi)文件。
bool StartEntry(const StringPiece& path, uint32_t flags) override {
if (file_) {
return false;
}
std::string full_path = dir_;
file::AppendPath(&full_path, path);
file::mkdirs(file::GetStem(full_path).to_string());
//打開(kāi)文件
file_ = {::android::base::utf8::fopen(full_path.c_str(), "wb"), fclose};
if (!file_) {
error_ = SystemErrorCodeToString(errno);
return false;
}
return true;
}
FinishEntry,調(diào)用reset釋放內(nèi)存。
bool FinishEntry() override {
if (!file_) {
return false;
}
file_.reset(nullptr);
return true;
}
ContainerWriter類定義在Container.cpp這個(gè)類文件中。在ContainerWriter類的構(gòu)造方法中,可以找到文件頭的寫(xiě)入代碼,其格式和上文“FLAT格式”一節(jié)中介紹的一致。
// 在類的構(gòu)造方法中,寫(xiě)入文件頭的信息
ContainerWriter::ContainerWriter(ZeroCopyOutputStream* out, size_t entry_count)
: out_(out), total_entry_count_(entry_count), current_entry_count_(0u) {
CodedOutputStream coded_out(out_);
// 魔法數(shù)據(jù),kContainerFormatMagic = 0x54504141u
coded_out.WriteLittleEndian32(kContainerFormatMagic);
// 版本號(hào),kContainerFormatVersion = 1u
coded_out.WriteLittleEndian32(kContainerFormatVersion);
// 容器中包含的條目數(shù) total_entry_count_是在ContainerReader構(gòu)造時(shí)賦值,值由外部傳入
coded_out.WriteLittleEndian32(static_cast<uint32_t>(total_entry_count_));
if (coded_out.HadError()) {
error_ = "failed writing container format header";
}
}
調(diào)用ContainerWriter的AddResFileEntry方法,寫(xiě)入資源項(xiàng)內(nèi)容。
// file:protobuf格式的信息文件,in:原始文件
bool ContainerWriter::AddResFileEntry(const pb::internal::CompiledFile& file, io::KnownSizeInputStream* in) {
// 判斷條目數(shù)量,大于設(shè)定數(shù)量就直接報(bào)錯(cuò)
if (current_entry_count_ >= total_entry_count_) {
error_ = "too many entries being serialized";
return false;
}
// 條目++
current_entry_count_++;
constexpr const static int kResFileEntryHeaderSize = 12; 、
//輸出流
CodedOutputStream coded_out(out_);
//寫(xiě)入資源類型
coded_out.WriteLittleEndian32(kResFile);
const ::google::protobuf::uint32
// ResourceFile 文件長(zhǎng)度 ,該部分包含了當(dāng)前文件的路徑,類型,配置等信息
header_size = file.ByteSize();
const int header_padding = CalculatePaddingForAlignment(header_size);
// 原始文件長(zhǎng)度
const ::google::protobuf::uint64 data_size = in->TotalSize();
const int data_padding = CalculatePaddingForAlignment(data_size);
// 寫(xiě)入數(shù)據(jù)長(zhǎng)度,計(jì)算公式:kResFileEntryHeaderSize(固定12) + ResourceFile文件長(zhǎng)度 + header_padding + 原始文件長(zhǎng)度 + data_padding
coded_out.WriteLittleEndian64(kResFileEntryHeaderSize + header_size + header_padding + data_size + data_padding);
// 寫(xiě)入文件頭長(zhǎng)度
coded_out.WriteLittleEndian32(header_size);
// 寫(xiě)入數(shù)據(jù)長(zhǎng)度
coded_out.WriteLittleEndian64(data_size);
// 寫(xiě)入“頭信息”
file.SerializeToCodedStream(&coded_out);
// 對(duì)齊
WritePadding(header_padding, &coded_out);
// 使用Copy之前需要調(diào)用Trim(至于為什么,其實(shí)也不太清楚,好在我們學(xué)習(xí)AAPT2,了解底層API的功能即可。如果有讀者知道,希望賜教)
coded_out.Trim();
// 異常判斷
if (coded_out.HadError()) {
error_ = "failed writing to output"; return false;
} if (!io::Copy(out_, in)) { //資源數(shù)據(jù)(源碼中也叫payload,可能是png,xml,或者XmlNode)
if (in->HadError()) {
std::ostringstream error;
error << "failed reading from input: " << in->GetError();
error_ = error.str();
} else {
error_ = "failed writing to output";
}
return false;
}
// 對(duì)其
WritePadding(data_padding, &coded_out);
if (coded_out.HadError()) {
error_ = "failed writing to output";
return false;
}
return true;
}
這樣,F(xiàn)LAT文件就完成寫(xiě)入了,并且產(chǎn)物文件除了包含資源內(nèi)容,還包含了文件名,路徑,配置等信息。
CompilePng
該方法和CompileFile流程上是類似的,區(qū)別在于會(huì)先對(duì)PNG圖片做處理(png優(yōu)化和9圖處理),處理完成后在寫(xiě)入FLAT文件。
static bool CompilePng(IAaptContext* context, const CompileOptions& options, const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) {
//..省略部分校驗(yàn)代碼
BigBuffer buffer(4096);
// 基本一樣的代碼,區(qū)別是type不一樣
ResourceFile res_file;
res_file.name = ResourceName({}, *ParseResourceType(path_data.resource_dir), path_data.name);
res_file.config = path_data.config;
res_file.source = path_data.source;
res_file.type = ResourceFile::Type::kPng;
{
// 讀取資源內(nèi)容到data中
auto data = file->OpenAsData();
// 讀取結(jié)果校驗(yàn)
if (!data) {
context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to open file ");
return false;
}
// 用來(lái)保存輸出流
BigBuffer crunched_png_buffer(4096);
io::BigBufferOutputStream crunched_png_buffer_out(&crunched_png_buffer);
// 對(duì)PNG圖片做優(yōu)化
const StringPiece content(reinterpret_cast<const char*>(data->data()), data->size());
PngChunkFilter png_chunk_filter(content);
std::unique_ptr<Image> image = ReadPng(context, path_data.source, &png_chunk_filter);
if (!image) {
return false;
}
// 處理.9圖
std::unique_ptr<NinePatch> nine_patch;
if (path_data.extension == "9.png") {
std::string err;
nine_patch = NinePatch::Create(image->rows.get(), image->width, image->height, &err);
if (!nine_patch) {
context->GetDiagnostics()->Error(DiagMessage() << err); return false;
}
// 移除1像素的邊框
image->width -= 2;
image->height -= 2;
memmove(image->rows.get(), image->rows.get() + 1, image->height * sizeof(uint8_t**));
for (int32_t h = 0; h < image->height; h++) {
memmove(image->rows[h], image->rows[h] + 4, image->width * 4);
} if (context->IsVerbose()) {
context->GetDiagnostics()->Note(DiagMessage(path_data.source) << "9-patch: " << *nine_patch);
}
}
// 保存處理后的png到 &crunched_png_buffer_out
if (!WritePng(context, image.get(), nine_patch.get(), &crunched_png_buffer_out, {})) {
return false;
}
// ...省略部分圖片校驗(yàn)代碼,這部分代碼會(huì)比較優(yōu)化后的圖片和原圖片的大小,如果優(yōu)化后比原圖片大,則使用原圖片。(PNG優(yōu)化后是有可能比原圖片還大的)
}
io::BigBufferInputStream buffer_in(&buffer);
// 和 CompileFile 調(diào)用相同的方法,寫(xiě)入flat文件,資源文件內(nèi)容是
return WriteHeaderAndDataToWriter(output_path, res_file, &buffer_in, writer, context->GetDiagnostics());
}
AAPT2 對(duì)于 PNG 圖片的壓縮可以分為三個(gè)方面:
RGB 是否可以轉(zhuǎn)化成灰度;
透明通道是否可以刪除;
是不是最多只有 256 色(Indexed_color 優(yōu)化)。
PNG優(yōu)化,有興趣的同學(xué)可以看看
在完成PNG處理后,同樣會(huì)調(diào)用WriteHeaderAndDataToWriter來(lái)寫(xiě)數(shù)據(jù),這部分內(nèi)容可閱讀上文分析,不再贅述。
CompileXml
該方法先會(huì)解析XML,然后創(chuàng)建XmlResource,其中包含了資源名,配置,類型等信息。通過(guò)FlattenXmlToOutStream函數(shù)寫(xiě)入輸出文件。
static bool CompileXml(IAaptContext* context, const CompileOptions& options,
const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
const std::string& output_path) {
// ...省略校驗(yàn)代碼
std::unique_ptr<xml::XmlResource> xmlres;
{
// 打開(kāi)xml文件
auto fin = file->OpenInputStream();
// ...省略校驗(yàn)代碼
// 解析XML
xmlres = xml::Inflate(fin.get(), context->GetDiagnostics(), path_data.source);
if (!xmlres) {
return false;
}
}
//
xmlres->file.name = ResourceName({}, *ParseResourceType(path_data.resource_dir), path_data.name);
xmlres->file.config = path_data.config;
xmlres->file.source = path_data.source;
xmlres->file.type = ResourceFile::Type::kProtoXml;
// 判斷id類型的資源是否有id合法(是否有id異常,如果有提示“has an invalid entry name”)
XmlIdCollector collector;
if (!collector.Consume(context, xmlres.get())) {
return false;
}
// 處理aapt:attr內(nèi)嵌資源
InlineXmlFormatParser inline_xml_format_parser;
if (!inline_xml_format_parser.Consume(context, xmlres.get())) {
return false;
}
// 打開(kāi)輸出文件
if (!writer->StartEntry(output_path, 0)) {
context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to open file");
return false;
}
std::vector<std::unique_ptr<xml::XmlResource>>& inline_documents =
inline_xml_format_parser.GetExtractedInlineXmlDocuments();
{
// 和CompileFile 類似,創(chuàng)建可處理protobuf格式的writer,用于protobuf格式序列化
CopyingOutputStreamAdaptor copying_adaptor(writer);
ContainerWriter container_writer(?ing_adaptor, 1u + inline_documents.size());
if (!FlattenXmlToOutStream(output_path, *xmlres, &container_writer,
context->GetDiagnostics())) {
return false;
}
// 處理內(nèi)嵌的元素(aapt:attr)
for (const std::unique_ptr<xml::XmlResource>& inline_xml_doc : inline_documents) {
if (!FlattenXmlToOutStream(output_path, *inline_xml_doc, &container_writer,
context->GetDiagnostics())) {
return false;
}
}
}
// 釋放內(nèi)存
if (!writer->FinishEntry()) {
context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to finish writing data");
return false;
}
// 編譯選項(xiàng)部分,省略
return true;
}
在編譯XML方法中,并沒(méi)有像前面兩個(gè)方法那樣創(chuàng)建ResourceFile,而是創(chuàng)建了XmlResource,用于保存XML資源的相關(guān)信息,其結(jié)構(gòu)包含如下內(nèi)容:
[圖片上傳失敗...(image-720beb-1635216078176)]
在執(zhí)行Inflate方法后,XmlResource 中會(huì)包含資源信息和XML的dom樹(shù)信息。InlineXmlFormatParser是用于解析出內(nèi)聯(lián)屬性aapt:attr。
使用 AAPT 的內(nèi)嵌資源格式,可以在同一 XML 文件中定義所有多種資源,如果不需要資源復(fù)用的話,這種方式更加緊湊。XML 標(biāo)記告訴 AAPT,該標(biāo)記的子標(biāo)記應(yīng)被視為資源并提取到其自己的資源文件中。屬性名稱中的值用于指定在父標(biāo)記內(nèi)使用內(nèi)嵌資源的位置。AAPT 會(huì)為所有內(nèi)嵌資源生成資源文件和名稱。使用此內(nèi)嵌格式構(gòu)建的應(yīng)用可與所有版本的 Android 兼容?!俜轿臋n
解析后的FlattenXmlToOutStream 中首先會(huì)調(diào)用SerializeCompiledFileToPb方法,把資源文件的相關(guān)信息轉(zhuǎn)化成protobuf格式,然后在調(diào)用SerializeXmlToPb把之前解析的Element 節(jié)點(diǎn)信息轉(zhuǎn)換成XmlNode(protobuf結(jié)構(gòu),同樣定義在 Resources),然后再把生成XmlNode轉(zhuǎn)換成字符串。最后,再通過(guò)上文的AddResFileEntry方法添加到FLAT文件的資源項(xiàng)中。這里可以看出,通過(guò)XML生成的FLAT文件文件,存在一個(gè)FLAT文件中可包含多個(gè)資源項(xiàng)。
static bool FlattenXmlToOutStream(const StringPiece& output_path, const xml::XmlResource& xmlres,
ContainerWriter* container_writer, IDiagnostics* diag) {
// 序列化CompiledFile部分
pb::internal::CompiledFile pb_compiled_file;
SerializeCompiledFileToPb(xmlres.file, &pb_compiled_file);
// 序列化XmlNode部分
pb::XmlNode pb_xml_node;
SerializeXmlToPb(*xmlres.root, &pb_xml_node);
// 專程string格式的流,這里可以再找源碼看看
std::string serialized_xml = pb_xml_node.SerializeAsString();
io::StringInputStream serialized_in(serialized_xml);
// 保存到資源項(xiàng)中
if (!container_writer->AddResFileEntry(pb_compiled_file, &serialized_in)) {
diag->Error(DiagMessage(output_path) << "failed to write entry data");
return false;
}
return true;
}
protobuf格式處理的方法(SerializeXmlToPb)在ProtoSerialize.cpp中,其通過(guò)遍歷和遞歸的方式實(shí)現(xiàn)節(jié)點(diǎn)結(jié)構(gòu)的復(fù)制,有興趣的讀者的可以查看源碼。
CompileTable
CompileTable函數(shù)用于處理values下的資源,從上文中可知,values下的資源在編譯時(shí)會(huì)被修改擴(kuò)展為arsc。最終輸出的文件名為*.arsc.flat,效果如下圖:
[圖片上傳失敗...(image-747e2b-1635216078176)]
在函數(shù)開(kāi)始,會(huì)讀取資源文件,完成xml解析并保存為ResourceTable結(jié)構(gòu),然后在通過(guò)SerializeTableToPb將其轉(zhuǎn)換成protobuf格式的pb::ResourceTable,然后調(diào)用SerializeWithCachedSizes把protobuf格式的table序列化到輸出文件。
static bool CompileTable(IAaptContext* context, const CompileOptions& options,
const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer,
const std::string& output_path) {
// Filenames starting with "donottranslate" are not localizable
bool translatable_file = path_data.name.find("donottranslate") != 0;
ResourceTable table;
{
// 讀取文件
auto fin = file->OpenInputStream();
if (fin->HadError()) {
context->GetDiagnostics()->Error(DiagMessage(path_data.source)
<< "failed to open file: " << fin->GetError());
return false;
}
// 創(chuàng)建XmlPullParser,設(shè)置很多handler,用于xml解析
xml::XmlPullParser xml_parser(fin.get());
// 設(shè)置解析選項(xiàng)
ResourceParserOptions parser_options;
parser_options.error_on_positional_arguments = !options.legacy_mode;
parser_options.preserve_visibility_of_styleables = options.preserve_visibility_of_styleables;
parser_options.translatable = translatable_file;
parser_options.visibility = options.visibility;
// 創(chuàng)建ResourceParser,并把結(jié)果保存到ResourceTable中
ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config,
parser_options);
// 執(zhí)行解析
if (!res_parser.Parse(&xml_parser)) {
return false;
}
}
// 省略部分校驗(yàn)代碼
// 打開(kāi)輸出文件
if (!writer->StartEntry(output_path, 0)) {
context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to open");
return false;
}
{
// 和前面一樣,創(chuàng)建ContainerWriter 用于寫(xiě)文件
CopyingOutputStreamAdaptor copying_adaptor(writer);
ContainerWriter container_writer(?ing_adaptor, 1u);
pb::ResourceTable pb_table;
// 把ResourceTable序列化為pb::ResourceTable
SerializeTableToPb(table, &pb_table, context->GetDiagnostics());
// 寫(xiě)入數(shù)據(jù)項(xiàng)pb::ResourceTable
if (!container_writer.AddResTableEntry(pb_table)) {
context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to write");
return false;
}
}
if (!writer->FinishEntry()) {
context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to finish entry");
return false;
}
// ...省略部分代碼...
}
return true;
}
三、問(wèn)題和總結(jié)
通過(guò)上文的學(xué)習(xí),我們知道AAPT2是Android資源打包的構(gòu)建工具,它把資源編譯分為編譯和鏈接兩個(gè)部分。其中,編譯是把不同的資源文件,統(tǒng)一編譯生成針對(duì) Android 平臺(tái)進(jìn)行過(guò)優(yōu)化的二進(jìn)制格式(flat)。FLAT文件除了包含原始資源文件的內(nèi)容,還有該資源來(lái)源,類型等信息,這樣一個(gè)文件中包含資源所需的所有信息,于其它依賴接耦。
在本文的開(kāi)頭,我們有如下的問(wèn)題:
Java文件需要編譯才能生.class文件,這個(gè)我能明白,但資源文件編譯到底是干什么的?為什么要對(duì)資源做編譯?
那么,本文的答案是:AAPT2的編譯時(shí)把資源文件編譯為FLAT文件,而且從資源項(xiàng)的文件結(jié)構(gòu)可以知道,F(xiàn)LAT文件中部分?jǐn)?shù)據(jù)是原始的資源內(nèi)容,一部分是文件的相關(guān)信息。通過(guò)編譯,生成的中間文件包含的信息比較全面,可用于增量編譯。另外,網(wǎng)上的一些資料還表示,二進(jìn)制的資源體積更小,且加載更快。
AAPT2通過(guò)編譯,實(shí)現(xiàn)把資源文件編譯成FLAT文件,接下來(lái)則通過(guò)鏈接,來(lái)生成R文件和資源表。由于篇幅問(wèn)題,鏈接的過(guò)程將在下篇文章中分析。
四、參考文檔
3.https://booster.johnsonlee.io
作者:vivo互聯(lián)網(wǎng)前端團(tuán)隊(duì)-Shi Xiang