找回密码
 会员注册
查看: 27|回复: 0

Android构建工具--AAPT2源码解析(一)

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
63697
发表于 2024-10-4 16:43:53 | 显示全部楼层 |阅读模式
互联网前端团队-Shi Xiang一、什么是AAPT2在Android开发过程中,我们通过Gradle命令,启动一个构建任务,最终会生成构建产物“APK”文件。常规APK的构建流程如下:(引用自Google官方文档)编译所有的资源文件,生成资源表和R文件;编译Java文件并把class文件打包为dex文件;打包资源和dex文件,生成未签名的APK文件;签名APK生成正式包。老版本的Android默认使用AAPT编译器进行资源编译,从Android Studio 3.0开始,AS默认开启了 AAPT2 作为资源编译的编译器,目前看来,AAPT2也是Android发展的主流趋势,学习AAPT2的工作原理可以帮助Android开发更好的掌握APK构建流程,从而帮助解决实际开发中遇到的问题。AAPT2 的可执行文件随 Android SDK 的 Build Tools 一起发布,在Android Studio的build-tools文件夹中就包含AAPT2工具,目录为(SDK目录/build-tools/version/aapt2)。二、AAPT2如何工作在看Android编译流程的时候,我忍不住会想一个问题:Java文件需要编译才能生class文件,这个我能明白,但资源文件编译到底是干什么的?为什么要对资源做编译?带着这个问题,让我们深入的学习一下AAPT2。和AAPT不同,AAPT2把资源编译打包过程拆分为两部分,即编译和链接:编译:将资源文件编译为二进制文件(flat)。链接:将编译后的文件合并,打包成单独文件。通过把资源编译拆分为两个部分,AAPT2能够很好的提升资源编译的性能。例如,之前一个资源文件发生变动,AAPT需要做一全量编译,AAPT2只需要重新编译改变的文件,然后和其他未发生改变的文件进行链接即可。2.1 Compile命令如上文描述,Complie指令用于编译资源,AAPT2提供多个选项与Compile命令搭配使用。Complie的一般用法如下:aapt2 compile path-to-input-files [options] -o output-directory/执行命令后,AAPT2会把资源文件编译为.flat格式的文件,文件对比如下。Compile命令会对资源文件的路径做校验,输入文件的路径必须符合以下结构:path/resource-type[-config]/file。例如,把资源文件保存在“aapt2”文件夹下,使用Compile命令编译,则会报错“error: invalid file path '.../aapt2/ic_launcher.png'”。把aapt改成“drawable-hdpi”,编译正常。在AndroidStudio中,可以在app/build/intermediates/res/merged/ 目录下找到编译生成的.flat文件。当然Compile也支持编译多个文件;aapt2 compile path-to-input-files1 path-to-input-files2 [options] -o output-directory/编译整个目录,需要制定数据文件,编译产物是一个压缩文件,包含目录下所有的资源,通过文件名把资源目录结构扁平化。aapt2 compile --dir .../res [options] -o output-directory/resource.ap_可以看到经过编译后,资源文件(png,xml ... )会被编译成一个FLAT格式的文件,直接把FLAT文件拖拽到as中打开,是乱码的。那么这个FLAT文件到底是什么?2.2 FLAT文件FLAT文件是AAPT2编译的产物文件,也叫做AAPT2容器,文件由文件头和资源项两大部分组成:文件头资源项资源项中,按照 entry_type 值分为两种类型:当entry_type 的值等于 0x00000000时,为RES_TABLE类型。当entry_type的值等于 0x00000001时,为RES_FILE类型。RES_TABLE包含的是protobuf格式的 ResourceTable 结构。数据结构如下:// Top level message representing a resource table.message ResourceTable { // 字符串池 StringPool source_pool = 1; // 用于生成资源id repeated Package package = 2; // 资源叠加层相关 repeated Overlayable overlayable = 3; // 工具版本 repeated ToolFingerprint tool_fingerprint = 4;}资源表(ResourceTable)中包含:StringPool:字符串池,字符串常量池是为了把资源文件中的string复用起来,从而减少体积,资源文件中对应的字符串会被替换为字符串池中的索引。message StringPool { bytes data = 1;}Package:包含资源id的相关信息// 资源id的包id部分,在 [0x00, 0xff] 范围内message PackageId { uint32 id = 1;}// 资源id的命名规则message Package { // [0x02, 0x7f) 简单的说,由系统使用 // 0x7f 应用使用 // (0x7f, 0xff] 预留Id PackageId package_id = 1; // 包名 string package_name = 2; // 资源类型,对应string, layout, xml, dimen, attr等,其对应的资源id区间为[0x01, 0xff] repeated Type type = 3;}资源id的命令方式遵循0xPPTTEEEE的规则,其中PP对应PackageId,一般应用使用的资源为7f,TT对应的是资源文件夹的名成,最后4位为资源的id,从0开始。RES_FILE类型格式如下:RES_FILE类型的FLAT文件结构可以参考下图;从上图展示的文件格式中可以看出,一个FLAT中可以包含多个资源项,在资源项中,Header字段中保存的是protobuf格式序列化的 CompiledFile 内容。在这个结构中,保存了文件名、文件路径、文件配置和文件类型等信息。data字段中保存资源文件的内容。通过这种方式,一个文件中既保存了文件的外部相关信息,又包含文件的原始内容。2.3 编译的源码上文,我们学习了编译命令Compile的用法和编译产物FLAT文件的文件格式,接下来,我们通过查看代码,从源码层面来学习AAPT2的编译流程,本文源码地址。2.3.1 命令执行流程根据常识,一般函数的入口都是和main有关,打开Main.cpp,可以找到main函数入口;int main(int argc, char** argv) {#ifdef _WIN32 ...... //参数格式转换 argv = utf8_argv.get();#endif //具体的实现MainImpl中 return MainImpl(argc, argv);}在MainImpl中,首先从输入中获取参数部分,然后创建一个MainCommand来执行命令。int MainImpl(int argc, char** argv) { if (argc args; for (int i = 1; i (&fout, &diagnostics)); // 调用Execute方法执行命令 return main_command.Execute(args, &std::cerr);}MainCommand继承自Command,在MainCommand初始化方法中会添加多个二级命令,通过类名,可以容易的推测出,这些Command和终端通过命令查看的二级命令一一对应。explicit MainCommand(text:rinter* printer, IDiagnostics* diagnostics) : Command("aapt2"), diagnostics_(diagnostics) { //对应Compile 命令 AddOptionalSubcommand(util::make_unique(diagnostics)); //对应link 命令 AddOptionalSubcommand(util::make_unique(diagnostics)); AddOptionalSubcommand(util::make_unique(printer, diagnostics)); AddOptionalSubcommand(util::make_unique()); AddOptionalSubcommand(util::make_unique()); AddOptionalSubcommand(util::make_unique()); AddOptionalSubcommand(util::make_unique());}AddOptionalSubcommand方法定义在基类Command中,内容比较简单,把传入的subCommand保存在数组中。void Command::AddOptionalSubcommand(std::unique_ptr& 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)); }}接下来,再来分析main_command.Execute的内容,从方法名可以推测这个方法里面有指令执行的相关代码。在MainCommand中并没有Execute方法的实现,那应该是在父类中实现了,再到Command类中搜索,果然在这里。int Command::Execute(const std::vector& args, std:stream* out_error) { TRACE_NAME_ARGS("Command::Execute", args); std::vector file_args; for (size_t i = 0; i name_ || (!subcommand->short_name_.empty() & arg == subcommand->short_name_)) { //执行子命令的Execute 方法,传入参数向后移动一位 return subcommand->Execute( std::vector(args.begin() + 1, args.end()), out_error); } } //省略部分代码 //调用Action方法,在执行二级命令时,file_args保存的是位移后的参数 return Action(file_args);}在Execute方法中,会先对参数作判断,如果参数第一位命中二级命令(Compile,link,.....),则调用二级命令的Execute方法。参考上文编译命令的示例可知,一般情况下,在这里就会命中二级命令的判断,从而调用二级命令的Execute方法。在Command.cpp的同级目录下,可以找到Compile.cpp,其Execute继承自父类。但是由于参数已经经过移位,所以最终会执行Action方法。在Compile.cpp中可以找到Action方法,同样在其他二级命令的实现类中(Link.cpp,Dump.cpp...),其核心处理的处理也都有Action方法中。整体调用的示意图如下:在开始看Action代码之前,我们先看一下Compile.cpp的头文件Compile.h的内容,在CompileCommand初始化时,会把必须参数和可选参数都初始化定义好。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_);官网中列出的编译选项并不全,使用compile -h打印信息后就会发现打印的信息和代码中的设置是一致的。在Action方法的执行流程可以总结为:1)会根据传入参数判断资源类型,并创建对应的文件加载器(file_collection)。2)根据传入的输出路径判断输出文件的类型,并创建对应的归档器(archive_writer),archive_writer在后续的调用链中一直向下传递,最终通过archive_writer把编译后的文件写到输出目录下。3)调用Compile方法执行编译。过程1,2中涉及的文件读写对象如下表。简化的主流程代码如下:int CompileCommand::Action(const std::vector& args) { //省略部分代码.... std::unique_ptr file_collection; //加载输入资源,简化逻辑,下面会省略掉校验的代码 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,通过循环依次添加输入文件,再拷贝到file_collection file_collection = std::move(collection); } std::unique_ptr archive_writer; //产物输出文件类型 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方法中会编译输入的资源文件名,每个资源文件的处理方式如下:解析输入的资源路径获取资源名,扩展名等信息;根据path判断文件类型,然后给compile_func设置不同的编译函数;生成输出的文件名。输出的就是FLAT文件名,会对全路径拼接,最终生成上文案例中类似的文件名—“drawable-hdpi_ic_launcher.png.flat”;传入各项参数,调用compile_func方法执行编译。ResourcePathData中包含了资源路径,资源名,资源扩展名等信息,AAPT2会从中获取资源的类型。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()) { // 省略部分代码(文件校验相关...) std::string err_str; ResourcePathData path_data; // 获取path全名,用于后续文件类型判断 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; } // 根据文件类型,选择编译方法,这里的 CompileFile 是函数指针,指向一个编译方法。 // 使用使用设置为CompileFile方法 auto compile_func = &CompileFile; // 如果是values目录下的xml资源,使用 CompileTable 方法编译,并修改扩展名为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类型,执行默认的编译方法,否则执行如下代码。 if (*type != ResourceType::kRaw) { //xml路径或者文件扩展为.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并且开启png优化或者是点9图类型 // png类,使用CompilePng方法编译 compile_func = &CompilePng; } } } else { // 不合法的类型,输出错误信息,继续循环 context->GetDiagnostics()->Error(DiagMessage() << "invalid file path '" << path_data.source << "'"); error = true; continue; } // 校验文件名中是否有. 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; } // 生成产物文件名,这个方法会生成完成的flat文件名,例如上文demo中的 drawable-hdpi_ic_launcher.png.flat const std::string out_path = BuildIntermediateContainerFilename(path_data); // 执行编译方法 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 error1 : 0;}不同的资源类型会有四种编译函数:CompileFileCompileTableCompileXmlCompilePngraw目录下的XML文件不会执行CompileXml,猜测是因为raw下的资源是直接复制到APK中,不会做XML优化编译。values目录下资源除了执行CompileTable编译之外,还会修改资源文件的扩展名,可以认为除了CompileFile,其他编译方法多多少少会对原始资源做处理后,在写编译生成的FLAT文件中。这部分的流程如下图所示:编译命令执行的主流程到这里就结束了,通过源码分析,我们可以知道AAPT2把输入文件编译为FLAT文件。下面,我们在进一步分析4个编译方法。2.3.2 四种编译函数CompileFile函数中先构造ResourceFile对象和原始文件数据,然后调用 WriteHeaderAndDataToWriter 把数据写到输出文件(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 对象,保存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或者其他的什么,统一设置类型为unknow。 // 原始文件数据 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的内容相对简单,完成文件相关信息的赋值后就会调用通过WriteHeaderAndDataToWriter方法。在WriteHeaderAndDataToWriter这个方法中,对之前创建的archive_writer(可在本文搜索,这个归档写创建完成后,会一直传下来)做一次包装,经过包装的ContainerWriter则具备普通文件写和protobuf格式序列化写的能力。pb提供了ZeroCopyStream 接口用户数据读写和序列化/反序列化操作。WriteHeaderAndDataToWriter的流程可以简单归纳为:IArchiveWriter.StartEntry,打开文件,做好写入准备;ContainerWriter.AddResFileEntry,写入数据;IArchiveWriter.FinishEntry,关闭文件,释放内存。static bool WriteHeaderAndDataToWriter(const StringPiece& output_path, const ResourceFile& file, io::KnownSizeInputStream* in, IArchiveWriter* writer, IDiagnostics* diag) { // 打开文件 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(). { // 对write做一层包装,用来写protobuf数据 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(原始文件) 写入到产物文件中 if (!container_writer.AddResFileEntry(pb_compiled_file, in)) { diag->Error(DiagMessage(output_path) << "failed to write entry data"); return false; } } // 退出写状态 if (!writer->FinishEntry()) { diag->Error(DiagMessage(output_path) << "failed to finish writing data"); return false; } return true;}我们再分别来看这三个方法,首先是StartEntry和FinishEntry,这个方法在Archive.cpp中,ZipFileWriter和DirectoryWriter实现有些区别,但逻辑上是一致的,这里只分析DirectoryWriter的实现。StartEntry,调用fopen打开文件。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()); //打开文件 file_ = {::android::base::utf8::fopen(full_path.c_str(), "wb"), fclose}; if (!file_) { error_ = SystemErrorCodeToString(errno); return false; } return true;}FinishEntry,调用reset释放内存。bool FinishEntry() override { if (!file_) { return false; } file_.reset(nullptr); return true;}ContainerWriter类定义在Container.cpp这个类文件中。在ContainerWriter类的构造方法中,可以找到文件头的写入代码,其格式和上文“FLAT格式”一节中介绍的一致。// 在类的构造方法中,写入文件头的信息ContainerWriter::ContainerWriter(ZeroCopyOutputStream* out, size_t entry_count) : out_(out), total_entry_count_(entry_count), current_entry_count_(0u) { CodedOutputStream coded_out(out_); // 魔法数据,kContainerFormatMagic = 0x54504141u coded_out.WriteLittleEndian32(kContainerFormatMagic); // 版本号,kContainerFormatVersion = 1u coded_out.WriteLittleEndian32(kContainerFormatVersion); // 容器中包含的条目数 total_entry_count_是在ContainerReader构造时赋值,值由外部传入 coded_out.WriteLittleEndian32(static_cast(total_entry_count_)); if (coded_out.HadError()) { error_ = "failed writing container format header"; }}调用ContainerWriter的AddResFileEntry方法,写入资源项内容。// file:protobuf格式的信息文件,in:原始文件bool ContainerWriter::AddResFileEntry(const pb::internal::CompiledFile& file, io::KnownSizeInputStream* in) { // 判断条目数量,大于设定数量就直接报错 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_); //写入资源类型 coded_out.WriteLittleEndian32(kResFile); const ::google::protobuf::uint32 // ResourceFile 文件长度 ,该部分包含了当前文件的路径,类型,配置等信息 header_size = file.ByteSize(); const int header_padding = CalculatePaddingForAlignment(header_size); // 原始文件长度 const ::google::protobuf::uint64 data_size = in->TotalSize(); const int data_padding = CalculatePaddingForAlignment(data_size); // 写入数据长度,计算公式:kResFileEntryHeaderSize(固定12) + ResourceFile文件长度 + header_padding + 原始文件长度 + data_padding coded_out.WriteLittleEndian64(kResFileEntryHeaderSize + header_size + header_padding + data_size + data_padding); // 写入文件头长度 coded_out.WriteLittleEndian32(header_size); // 写入数据长度 coded_out.WriteLittleEndian64(data_size); // 写入“头信息” file.SerializeToCodedStream(&coded_out); // 对齐 WritePadding(header_padding, &coded_out); // 使用Copy之前需要调用Trim(至于为什么,其实也不太清楚,好在我们学习AAPT2,了解底层API的功能即可。如果有读者知道,希望赐教) coded_out.Trim(); // 异常判断 if (coded_out.HadError()) { error_ = "failed writing to output"; return false; } if (!io::Copy(out_, in)) { //资源数据(源码中也叫payload,可能是png,xml,或者XmlNode) if (in->HadError()) { std:stringstream error; error << "failed reading from input: " << in->GetError(); error_ = error.str(); } else { error_ = "failed writing to output"; } return false; } // 对其 WritePadding(data_padding, &coded_out); if (coded_out.HadError()) { error_ = "failed writing to output"; return false; } return true;}这样,FLAT文件就完成写入了,并且产物文件除了包含资源内容,还包含了文件名,路径,配置等信息。CompilePng该方法和CompileFile流程上是类似的,区别在于会先对PNG图片做处理(png优化和9图处理),处理完成后在写入FLAT文件。static bool CompilePng(IAaptContext* context, const CompileOptions& options, const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) { //..省略部分校验代码 BigBuffer buffer(4096); // 基本一样的代码,区别是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; { // 读取资源内容到data中 auto data = file->OpenAsData(); // 读取结果校验 if (!data) { context->GetDiagnostics()->Error(DiagMessage(path_data.source) << "failed to open file "); return false; } // 用来保存输出流 BigBuffer crunched_png_buffer(4096); io::BigBufferOutputStream crunched_png_buffer_out(&crunched_png_buffer); // 对PNG图片做优化 const StringPiece content(reinterpret_cast(data->data()), data->size()); PngChunkFilter png_chunk_filter(content); std::unique_ptr image = ReadPng(context, path_data.source, &png_chunk_filter); if (!image) { return false; } // 处理.9图 std::unique_ptr 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 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; } // ...省略部分图片校验代码,这部分代码会比较优化后的图片和原图片的大小,如果优化后比原图片大,则使用原图片。(PNG优化后是有可能比原图片还大的) } io::BigBufferInputStream buffer_in(&buffer); // 和 CompileFile 调用相同的方法,写入flat文件,资源文件内容是 return WriteHeaderAndDataToWriter(output_path, res_file, &buffer_in, writer, context->GetDiagnostics());}AAPT2 对于 PNG 图片的压缩可以分为三个方面:RGB 是否可以转化成灰度;透明通道是否可以删除;是不是最多只有 256 色(Indexed_color 优化)。PNG优化,有兴趣的同学可以看看在完成PNG处理后,同样会调用WriteHeaderAndDataToWriter来写数据,这部分内容可阅读上文分析,不再赘述。CompileXml该方法先会解析XML,然后创建XmlResource,其中包含了资源名,配置,类型等信息。通过FlattenXmlToOutStream函数写入输出文件。static bool CompileXml(IAaptContext* context, const CompileOptions& options, const ResourcePathData& path_data, io::IFile* file, IArchiveWriter* writer, const std::string& output_path) { // ...省略校验代码 std::unique_ptr xmlres; { // 打开xml文件 auto fin = file->OpenInputStream(); // ...省略校验代码 // 解析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内嵌资源 InlineXmlFormatParser inline_xml_format_parser; if (!inline_xml_format_parser.Consume(context, xmlres.get())) { return false; } // 打开输出文件 if (!writer->StartEntry(output_path, 0)) { context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to open file"); return false; } std::vector
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-25 13:43 , Processed in 0.519350 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表