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

一、什么是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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理