关于android:Android构建工具AAPT2源码解析一

4次阅读

共计 24085 个字符,预计需要花费 61 分钟才能阅读完成。

一、什么是 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”,编译失常。

在 Android Studio 中,能够在 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 < 1) {return -1;}
    // 从下标 1 开始的输出,保留在 args 中
    std::vector<StringPiece> args;
    for (int i = 1; i < argc; i++) {args.push_back(argv[i]);
    }
    // 省略局部代码,这部分代码用于打印信息和错误处理
    // 创立一个 MainCommand
    aapt::MainCommand main_command(&printer, &diagnostics);
    // aapt2 的守护过程模式,main_command.AddOptionalSubcommand(aapt::util::make_unique<aapt::DaemonCommand>(&fout, &diagnostics));
    // 调用 Execute 办法执行命令
    return main_command.Execute(args, &std::cerr);
}

MainCommand 继承自 Command,在 MainCommand 初始化办法中会增加多个二级命令,通过类名,能够容易的揣测出,这些 Command 和终端通过命令查看的二级命令一一对应。

explicit MainCommand(text::Printer* printer, IDiagnostics* diagnostics)
 : Command("aapt2"), diagnostics_(diagnostics) {
    // 对应 Compile 命令
    AddOptionalSubcommand(util::make_unique<CompileCommand>(diagnostics));
    // 对应 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 中,内容比较简单,把传入的 subCommand 保留在数组中。

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));
    }
}

接下来,再来剖析 main_command.Execute 的内容,从办法名能够揣测这个办法外面有指令执行的相干代码。在 MainCommand 中并没有 Execute 办法的实现,那应该是在父类中实现了,再到 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];
        // 参数不是 '-'
        if (*(arg.data()) != '-') {
        // 是第一个参数
        if (i == 0) {for (auto& subcommand : subcommands_) {
                // 判断是否是子命令
                if (arg == subcommand->name_ || (!subcommand->short_name_.empty() && arg == subcommand->short_name_)) {
                // 执行子命令的 Execute 办法,传入参数向后挪动一位
                return subcommand->Execute(std::vector<StringPiece>(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<std::string>& args) {
    // 省略局部代码....
    std::unique_ptr<io::IFileCollection> 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<IArchiveWriter> 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 error ? 1 : 0;
}

不同的资源类型会有四种编译函数:

  • CompileFile
  • CompileTable
  • CompileXml
  • CompilePng

raw 目录下的 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<uint32_t>(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::ostringstream 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<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;}
 
        // ... 省略局部图片校验代码, 这部分代码会比拟优化后的图片和原图片的大小,如果优化后比原图片大,则应用原图片。(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<xml::XmlResource> 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<std::unique_ptr<xml::XmlResource>>& inline_documents =
      inline_xml_format_parser.GetExtractedInlineXmlDocuments();
 
  {
    // 和 CompileFile 相似,创立可解决 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;}
    // 解决内嵌的元素(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;}
    }
  }
  // 开释内存
  if (!writer->FinishEntry()) {context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to finish writing data");
    return false;
  }
  // 编译选项局部,省略
  return true;
}

在编译 XML 办法中,并没有像后面两个办法那样创立 ResourceFile,而是创立了 XmlResource,用于保留 XML 资源的相干信息,其构造蕴含如下内容:

在执行 Inflate 办法后,XmlResource 中会蕴含资源信息和 XML 的 dom 树信息。InlineXmlFormatParser 是用于解析出内联属性 aapt:attr。

应用 AAPT 的内嵌资源格局,能够在同一 XML 文件中定义所有多种资源,如果不须要资源复用的话,这种形式更加紧凑。XML 标记通知 AAPT,该标记的子标记应被视为资源并提取到其本人的资源文件中。属性名称中的值用于指定在父标记内应用内嵌资源的地位。AAPT 会为所有内嵌资源生成资源文件和名称。应用此内嵌格局构建的利用可与所有版本的 Android 兼容。——官网文档

解析后的 FlattenXmlToOutStream 中首先会调用 SerializeCompiledFileToPb 办法,把资源文件的相干信息转化成 protobuf 格局,而后在调用 SerializeXmlToPb 把之前解析的 Element 节点信息转换成 XmlNode(protobuf 构造,同样定义在 Resources),而后再把生成 XmlNode 转换成字符串。最初,再通过上文的 AddResFileEntry 办法增加到 FLAT 文件的资源项中。这里能够看出,通过 XML 生成的 FLAT 文件文件,存在一个 FLAT 文件中可蕴含多个资源项。

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);
  // 保留到资源项中
  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 中,其通过遍历和递归的形式实现节点构造的复制,有趣味的读者的能够查看源码。

CompileTable

CompileTable 函数用于解决 values 下的资源,从上文中可知,values 下的资源在编译时会被批改扩大为 arsc。最终输入的文件名为 *.arsc.flat,成果如下图:

在函数开始,会读取资源文件,实现 xml 解析并保留为 ResourceTable 构造,而后在通过 SerializeTableToPb 将其转换成 protobuf 格局的 pb::ResourceTable,而后调用 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;
    }
 
    // 创立 XmlPullParser,设置很多 handler,用于 xml 解析
    xml::XmlPullParser xml_parser(fin.get());
     
    // 设置解析选项
    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;
     
    // 创立 ResourceParser,并把后果保留到 ResourceTable 中
    ResourceParser res_parser(context->GetDiagnostics(), &table, path_data.source, path_data.config,
        parser_options);
    // 执行解析
    if (!res_parser.Parse(&xml_parser)) {return false;}
  }
  // 省略局部校验代码
 
  // 关上输入文件
  if (!writer->StartEntry(output_path, 0)) {context->GetDiagnostics()->Error(DiagMessage(output_path) << "failed to open");
    return false;
  }
 
  {
    // 和后面一样,创立 ContainerWriter 用于写文件
    CopyingOutputStreamAdaptor copying_adaptor(writer);
    ContainerWriter container_writer(©ing_adaptor, 1u);
 
    pb::ResourceTable pb_table;
    // 把 ResourceTable 序列化为 pb::ResourceTable
    SerializeTableToPb(table, &pb_table, context->GetDiagnostics());
    // 写入数据项 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;
}

三、问题和总结

通过上文的学习,咱们晓得 AAPT2 是 Android 资源打包的构建工具,它把资源编译分为编译和链接两个局部。其中,编译是把不同的资源文件,对立编译生成针对 Android 平台进行过优化的二进制格局(flat)。FLAT 文件除了蕴含原始资源文件的内容,还有该资源起源,类型等信息,这样一个文件中蕴含资源所需的所有信息,于其它依赖接耦。

在本文的结尾,咱们有如下的问题:

Java 文件须要编译能力生.class 文件,这个我能明确,但资源文件编译到底是干什么的?为什么要对资源做编译?

那么,本文的答案是:AAPT2 的编译时把资源文件编译为 FLAT 文件,而且从资源项的文件构造能够晓得,FLAT 文件中局部数据是原始的资源内容,一部分是文件的相干信息。通过编译,生成的两头文件蕴含的信息比拟全面,可用于增量编译。另外,网上的一些材料还示意,二进制的资源体积更小,且加载更快。

AAPT2 通过编译,实现把资源文件编译成 FLAT 文件,接下来则通过链接,来生成 R 文件和资源表。因为篇幅问题,链接的过程将在下篇文章中剖析。

四、参考文档

1.https://juejin.cn

2.https://github.com

3.https://booster.johnsonlee.io

作者:vivo 互联网前端团队 -Shi Xiang

正文完
 0