Android 资源管理-APK 内存形态(ApkAssets)

600 阅读17分钟

Android APK 文件结构中我们简单介绍了 Android APK 的构成,本文怎通过 ApkAssets 展开介绍 APK 在Android 运行时的形态,为后续详细介绍资源管理做铺垫。 ApkAssets在内存中用于表示已加载且不可变的 APK 文件。这个类的实现主要是 C++,而在 Java 层只暴露了非常少的 API。主要用于在 Android 应用中直接访问 APK 文件内部的资源文件,比如图片、布局、字符串等。ApkAssets 主要通过 AssetManager 来访问。 可以通过Android 资源管理-AssetManager获取关于AssetManager更多信息。

注意

Android 中一个 ApkAssets 对应一个 APK 资源包,并且它内部会有一个 LoadedArsc,一个LoadedArsc对应一个资源包中的 resources.arsc 文件,因此解读 ApkAssets 也可以理解解读 resources.arsc 文件。

1、图解 Native 层 ApkAssets

下述分析源代码均参考自frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h

apkassets_mem.jpg

在内存中,APK 其实就是一堆各种类型的数据集合。

ResTable_header(1)

位于 ARSC 文件的开头部分,用于描述整个 ARSC 文件的结构和内容,主要内容就是记录当前文件有几个LoadedPackage,一般都是一个。

反编译 ARSC 文件截图

ResStringPool(1)

字符串资源池用于存储和管理字符串资源。ResStringPool_header用与描述资源池的头部信息,它包含了关于资源池的基本信息,记录了资源池中 string 和 style 的数量 以及内存中的 index 偏移值。

ResStringPool_header中偏移的计算示例

stringsStart:8312+12(Ch)=8324(2084h)// 字符串数据段相对于这个resStringPool的header的起始地址的偏移量

stylesStart:此例中,APK文件不包含style // style数据段相对于这个resStringPool的header的起始地址的偏移量

ResStringPool_header之后则跟着mEntries、mEntryStyles、mStrings、mStyles

mEntries:string偏移数组,它的元素的个数为mHeader.stringCount

mEntryStyles:style偏移数组,它的元素的个数为mHeader.styleCount,本例则没有因为style为0

mStrings:存放所该字符串池中所有的字符串

mStyles:存放所该字符串池中所有的style

反编译 ARSC 文件截图

LoadedPackage(1)

包含了应用的所有资源,如图片、布局文件、字符串等。LoadedPackage是这些资源包在内存中的表示形式,用于在运行时动态加载和管理这些资源。

反编译 ARSC 文件截图

ResTable_package(1)

用于描述资源包的信息,在 ARSC 文件中,ResTable_package包含了资源包的数据块内容。有点类似各个chunk 的 header 作用。ResTable_package中定义的 name 便是我们 APK 的名字。

这也能看出来从 Android 8.0(Oreo)开始,APK 文件名的最大长度限制为最大 256(100h)个字符

type_string_pool_、key_string_pool_(1)

type_string_pool_key_string_pool_主要收集的信息都来自 R.java 文件。

反编译 ARSC 文件截图

TypeSpec(n)

用于描述某个特定资源,比如drawable,color。内部由ResTable_typeSpec、ResTable_typeResTable_entryRes_value

ResTable_typeSpec(1)

用于描述某个指定资源类型相关的元数据,更好的帮助描述该类型的资源如何被组织和访问。和type_string_pool_都是对应的。比如上例中展示的type_string_pool_字符串有 14 个,那么ResTable_typeSpec数量也会有 14 个。entryCount 则用于描述同类型资源的个数,比如上例中 anim 资源的个数实际为 28(反编译 R 文件获取),相应的我们反编译 ARSC 文件,获取的到数量也是一致的(下图中entryCount显示为28),

entryCount:该类型的资源的总数。

反编译 ARSC 文件截图

ResTable_type(n)

解析这块最为复杂,涉及到 APK 中各种资源的处理,还有相同资源针对不同config的处理。这里的 n 表示就是同一个资源涵盖的config数量。比如 demo 中 ic_launcher 和 ic_launcher_round 分别配置了 6 中不同的 config 资源文件,那么在 ARSC 文件中 ResTable_type数量也是 6 个。

ResTable_entry(1n)、Res_value(1n)

用于描述制定资源类型的真实数据,ResTable_type 类似 chunk header的作用,ResTable_entryRes_value则分别用于表示制定资源类型下的确定资源的key和value,比如 ResTable_type为 anim,ResTable_entry为abc_fade_in,Res_value为btn_radio_to_off_mtrl_dot_group_animation

反编译 ARSC 文件截图

DynamicPackageEntry

一般 APK 不涉及,暂不详细分析

OverlayableInfo

一般 APK 不涉及,暂不详细分析

2、核心源码释义参考

ApkAssets

ApkAssets 类是一个用于管理和访问 APK 文件资源的类,提供了多种方式加载资源(包括从文件路径、文件描述符、AssetsProvider 等),并提供了各种方法来获取和管理这些资源数据。

  • 通过 resources_asset_ 存储的资源文件
  • 通过 loaded_arsc_loaded_idmap_ 存储的解析后的资源数据占用内存,通常这些数据以特定的数据结构形式存在,供进一步访问和操作。
  • 通过assets_provider_ 负责管理和提供对资源的访问。

loaded_arsc_loaded_idmap_则是 ApkAssets 的重点

// ApkAssets 类用于处理 APK 资源,加载资源表、覆盖资源等。
class ApkAssets : public RefBase {
 public:
  // 从设备路径创建 ApkAssets。
  static ApkAssetsPtr Load(const std::string& path, package_property_t flags = 0U);

  // 从打开的文件描述符创建 ApkAssets。
  static ApkAssetsPtr LoadFromFd(base::unique_fd fd, const std::string& debug_name,
                                 package_property_t flags = 0U, off64_t offset = 0,
                                 off64_t len = AssetsProvider::kUnknownLength);

  // 从一个 AssetProvider 创建 ApkAssets。
  static ApkAssetsPtr Load(std::unique_ptr<AssetsProvider> assets, package_property_t flags = 0U);

  // 从资源文件(resources.arsc)创建 ApkAssets。
  static ApkAssetsPtr LoadTable(std::unique_ptr<Asset> resources_asset,
                                std::unique_ptr<AssetsProvider> assets,
                                package_property_t flags = 0U);

  // 从 IDMAP 文件创建 ApkAssets。
  static ApkAssetsPtr LoadOverlay(const std::string& idmap_path, package_property_t flags = 0U);

  // 获取资源提供者。
  const AssetsProvider* GetAssetsProvider() const { return assets_provider_.get(); }

  // 获取已加载的资源表(resources.arsc)。
  const LoadedArsc* GetLoadedArsc() const { return loaded_arsc_.get(); }

  // 获取已加载的 IDMAP 数据。
  const LoadedIdmap* GetLoadedIdmap() const { return loaded_idmap_.get(); }

 private:
  // 资源加载实现方法。
  static ApkAssetsPtr LoadImpl(std::unique_ptr<AssetsProvider> assets,
                               package_property_t property_flags,
                               std::unique_ptr<Asset> idmap_asset,
                               std::unique_ptr<LoadedIdmap> loaded_idmap);

  static ApkAssetsPtr LoadImpl(std::unique_ptr<Asset> resources_asset,
                               std::unique_ptr<AssetsProvider> assets,
                               package_property_t property_flags,
                               std::unique_ptr<Asset> idmap_asset,
                               std::unique_ptr<LoadedIdmap> loaded_idmap);

 public:
  ApkAssets(PrivateConstructorUtil, std::unique_ptr<Asset> resources_asset,
            std::unique_ptr<LoadedArsc> loaded_arsc, std::unique_ptr<AssetsProvider> assets,
            package_property_t property_flags, std::unique_ptr<Asset> idmap_asset,
            std::unique_ptr<LoadedIdmap> loaded_idmap);

 private:
  std::unique_ptr<Asset> resources_asset_;  // 表示 APK 中 resources.arsc
  std::unique_ptr<LoadedArsc> loaded_arsc_;  // 表示加载的资源表(resources.arsc)
  // 和resources_asset_的区别在于,resources_asset_是表示APK中的resources.arsc文件,
  // 而loaded_arsc_则是对这个文件加载解析后的一个数据结构
  std::unique_ptr<AssetsProvider> assets_provider_;  // 资源提供者
  package_property_t property_flags_ = 0U;  // 资源属性标志
  std::unique_ptr<Asset> idmap_asset_;  // 表示 IDMAP 文件(用于资源覆盖)
  std::unique_ptr<LoadedIdmap> loaded_idmap_;  // 已加载的 IDMAP 数据
  // 和resources_asset_和loaded_arsc_区别类似
};
}

LoadedArsc

LoadedArsc 类用于加载和解析 resources.arsc 文件,提供了对 APK 中所有资源(如字符串、图片、布局等)的访问。LoadedArsc非常简单,主要就是一个 Global String Pool 和 LoadedPackage,它的核心功能包括:

  • 加载和解析 resources.arsc 文件。
  • 提供访问资源包和字符串池的功能。
  • 支持资源覆盖(通过 IDMAP 文件)。
  • 提供管理和查找字符串资源、包资源的能力。

LoadedArsc 中 LoadedPackage 定义是一个 vector,从定义来看 Android 是支持 resources.arsc 中存在多个 package 的,但是从实际的运行来看,基本都是一对一的关系。

class LoadedArsc {
 public:
  // 从内存中加载资源表,`data` 是指向内存的指针,`length` 是数据的长度。
  // `data` 的生命周期必须长于返回的 LoadedArsc 对象。
  // `loaded_idmap` 用于资源覆盖,`property_flags` 是额外的加载属性标志。
  static std::unique_ptr<LoadedArsc> Load(incfs::map_ptr<void> data,
                                          size_t length,
                                          const LoadedIdmap* loaded_idmap = nullptr,
                                          package_property_t property_flags = 0U);

  // 通过 `loaded_idmap` 加载资源表。适用于没有传入具体的资源数据(例如资源表为空的情况)。
  static std::unique_ptr<LoadedArsc> Load(const LoadedIdmap* loaded_idmap = nullptr);

  // 创建一个空的 LoadedArsc 实例。用于没有 `resources.arsc` 文件的 APK。
  static std::unique_ptr<LoadedArsc> CreateEmpty();

  // 获取字符串池,所有字符串资源(如文本)都存储在这里。
  // 字符串池中的数据类型为 `Res_value::TYPE_STRING`。
  inline const ResStringPool* GetStringPool() const {
    return global_string_pool_.get();
  }

  // 根据指定的 package_id 获取对应的资源包(LoadedPackage)。
  // 如果不存在该包,返回 nullptr。
  const LoadedPackage* GetPackageById(uint8_t package_id) const;

  // 获取所有的资源包(LoadedPackage)的向量。
  // 返回当前 `LoadedArsc` 对象中所有加载的资源包。
  inline const std::vector<std::unique_ptr<const LoadedPackage>>& GetPackages() const {
    return packages_;
  }

 private:
  // 禁止拷贝构造和赋值操作,确保资源管理的唯一性。
  DISALLOW_COPY_AND_ASSIGN(LoadedArsc);

  // 默认构造函数
  LoadedArsc() = default;

  // 加载资源表,处理资源的块数据。会根据传入的 `Chunk` 进行解析。
  // 也会根据 `loaded_idmap` 进行资源覆盖,`property_flags` 设置其他加载特性。
  bool LoadTable(
      const Chunk& chunk, const LoadedIdmap* loaded_idmap, package_property_t property_flags);

  // 加载字符串池(即存储所有字符串资源的数据结构)。
  bool LoadStringPool(const LoadedIdmap* loaded_idmap);

  // 全局字符串池,存储所有字符串资源。
  // 使用 `ResStringPool` 来管理和查找字符串。
  std::unique_ptr<ResStringPool> global_string_pool_ = util::make_unique<ResStringPool>();

  // 所有资源包(LoadedPackage)列表。每个资源包包含了一组相关的资源。
  std::vector<std::unique_ptr<const LoadedPackage>> packages_;
};

LoadedPackage

LoadedPackage专注于从资源表中提取和管理资源信息。不仅支持标准的资源查找和访问,还提供了对动态包、Overlayable 资源和系统包等复杂功能的支持。通过 ResStringPool 管理字符串池、通过迭代器访问资源条目、通过各种方法查找资源,LoadedPackage 提供了一整套灵活且高效的接口,用于访问和操作 Android 应用的资源。

type_specs_ 是 LoadedPackage 中存储资源重要对象

class LoadedPackage {
 public:
  // 迭代器类,用于遍历 `LoadedPackage` 中的资源条目。
  class iterator {
...
  };

  // 静态方法:加载一个 `Chunk` 数据,并返回一个 `LoadedPackage` 实例。
  static std::unique_ptr<const LoadedPackage> Load(const Chunk& chunk,
                                                   package_property_t property_flags);

  // 根据类型名和条目名查找资源条目。返回一个部分资源 ID(包 ID 为 0x00)。调用者需自行修复包 ID。
  base::expected<uint32_t, NullOrIOError> FindEntryByName(const std::u16string& type_name,
                                                          const std::u16string& entry_name) const;

  // 静态方法:从指定的资源类型块中获取资源条目。
  static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError>
      GetEntry(incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index);

  // 静态方法:从资源类型块中获取资源条目的偏移量。
  static base::expected<uint32_t, NullOrIOError> GetEntryOffset(
      incfs::verified_map_ptr<ResTable_type> type_chunk, uint16_t entry_index);

  // 静态方法:根据资源类型块和偏移量获取资源条目。
  static base::expected<incfs::verified_map_ptr<ResTable_entry>, NullOrIOError>
      GetEntryFromOffset(incfs::verified_map_ptr<ResTable_type> type_chunk, uint32_t offset);

  // 返回类型字符串池,存储资源类型名的字符串。
  const ResStringPool* GetTypeStringPool() const {
    return &type_string_pool_;
  }

  // 返回键值字符串池,存储资源条目名称的字符串。
  const ResStringPool* GetKeyStringPool() const {
    return &key_string_pool_;
  }

  // 返回资源包的名称。
  const std::string& GetPackageName() const {
    return package_name_;
  }

  // 返回资源包的 ID。
  int GetPackageId() const {
    return package_id_;
  }

  ...
  
  // 返回动态包映射,即从包名到包 ID 的映射。这个映射可能与编译时的包 ID 不同。
  const std::vector<DynamicPackageEntry>& GetDynamicPackageMap() const {
    return dynamic_package_map_;
  }

  // 收集资源包中的所有配置(ResTable_config)。可以排除 mipmap 类型。
  base::expected<std::monostate, IOError> CollectConfigurations(
      bool exclude_mipmap, std::set<ResTable_config>* out_configs) const;

  // 根据类型索引获取类型规格(TypeSpec)。类型索引是 0xPPTTEEEE 格式中的 TT - 1。
  inline const TypeSpec* GetTypeSpecByTypeIndex(uint8_t type_index) const {
    const auto& type_spec = type_specs_.find(type_index + 1 - type_id_offset_);
    if (type_spec == type_specs_.end()) {
      return nullptr;
    }
    return &type_spec->second;
  }

  // 获取指定资源 ID 的 overlayable 信息。如果该资源不可覆盖,返回 nullptr。
  const OverlayableInfo* GetOverlayableInfo(uint32_t resid) const {
    for (const std::pair<OverlayableInfo, std::unordered_set<uint32_t>>& overlayable_info_ids
        : overlayable_infos_) {
      if (overlayable_info_ids.second.find(resid) != overlayable_info_ids.second.end()) {
        return &overlayable_info_ids.first;
      }
    }
    return nullptr;
  }

  // 判断该资源包是否定义了 overlayable 资源。
  bool DefinesOverlayable() const {
    return defines_overlayable_;
  }

  // 获取 overlayable 映射,即从 overlayable 名称到 actor 的映射。
  const std::unordered_map<std::string, std::string>& GetOverlayableMap() const {
    return overlayable_map_;
  }

  // 获取资源 ID 别名映射,即旧资源 ID 到新资源 ID 的映射。
  const std::vector<std::pair<uint32_t, uint32_t>>& GetAliasResourceIdMap() const {
    return alias_id_map_;
  }

 private:
  // 禁止拷贝和赋值操作,确保对象的唯一性。
  DISALLOW_COPY_AND_ASSIGN(LoadedPackage);

  // 类型字符串池和键值字符串池,分别用于存储类型名称和资源条目名称。
  ResStringPool type_string_pool_;
  ResStringPool key_string_pool_;

  // 资源包名称。
  std::string package_name_;

  // 标志:是否定义了 overlayable 资源。
  bool defines_overlayable_ = false;

  // 资源包的 ID 和类型 ID 偏移。
  int package_id_ = -1;
  int type_id_offset_ = 0;

  // 资源包的属性标志。
  package_property_t property_flags_ = 0U;

  // 类型规格、资源 ID 列表和动态包映射。
  std::unordered_map<uint8_t, TypeSpec> type_specs_;
  ByteBucketArray<uint32_t> resource_ids_;
  std::vector<DynamicPackageEntry> dynamic_package_map_;

  // Overlayable 信息和别名资源 ID 映射。
  std::vector<std::pair<OverlayableInfo, std::unordered_set<uint32_t>>> overlayable_infos_;
  std::vector<std::pair<uint32_t, uint32_t>> alias_id_map_;

  // overlayable 名称到 actor 的映射。
  std::unordered_map<std::string, std::string> overlayable_map_;
};

TypeSpec

struct TypeSpec {
  // TypeSpec 结构体的内部结构 TypeEntry。
  // 每个 TypeEntry 对应一个具体的资源类型条目,包含类型、配置等信息。
  struct TypeEntry {
    // type 是一个指向 mmap 映射区域的指针,用来访问具体的资源类型数据。
    // 它是一个经过验证的映射指针,指向一个 ResTable_type 类型的数据。
    incfs::verified_map_ptr<ResTable_type> type;

    // 该资源类型的配置(ResTable_config)信息。
    // 配置通常包含有关该资源类型的分辨率、语言、屏幕密度等信息,
    // 访问此缓存配置可以减少内存页面错误(page faults)。
    ResTable_config config;
  };

  // type_spec 是指向 mmapped 数据的指针,存储着标记(flags)数据。
  // 这些标记用于表示资源条目的访问权限(如 public)以及该资源条目的变化配置。
  incfs::verified_map_ptr<ResTable_typeSpec> type_spec;

  // type_entries 是一个包含多个 TypeEntry 的 vector,
  // 每个 TypeEntry 表示一个具体的资源类型条目,包括资源的配置和类型信息。
  std::vector<TypeEntry> type_entries;

  // GetFlagsForEntryIndex 方法用于获取指定索引的资源条目的标记信息。
  // entry_index 是条目索引,方法将返回该资源条目的标记信息(如是否为 public、配置等)。
  base::expected<uint32_t, NullOrIOError> GetFlagsForEntryIndex(uint16_t entry_index) const {
    // 如果 entry_index 超出了 entryCount 的范围,返回 0U
    if (entry_index >= dtohl(type_spec->entryCount)) {
      return 0U;
    }

    // 计算并访问 entry_flags_ptr 来获取资源条目的标记数据
    const auto entry_flags_ptr = ((type_spec + 1).convert<uint32_t>() + entry_index);

    // 如果指针为空,表示访问失败,返回 I/O 错误
    if (!entry_flags_ptr) {
      return base::unexpected(IOError::PAGES_MISSING);
    }

    // 返回对应资源条目的标记信息
    return entry_flags_ptr.value();
  }
};

3、ApkAssets 初始化

我们以loadFromPath 来构建一个ApkAssets为例介绍一下ApkAssets的初始化。

public static @NonNull ApkAssets loadFromPath(@NonNull String path, @PropertyFlags int flags)
        throws IOException {
    return new ApkAssets(FORMAT_APK, path, flags, null /* assets */);
}

private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags,
        @Nullable AssetsProvider assets) throws IOException {
    Objects.requireNonNull(path, "path");
    mFlags = flags;
    mNativePtr = nativeLoad(format, path, flags, assets);
    mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
    mAssets = assets;
}

核心代码

// NativeLoad 是一个加载不同格式(APK、IDMAP、ARSC 或目录)的资源并返回 jlong 值的函数。
// 该方法使用 JNIEnv 与 Java 对象交互,并根据提供的路径和属性加载资源。
static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t format,
                        jstring java_path, const jint property_flags, jobject assets_provider) {

  // ScopedUtfChars 将 Java 字符串(jstring)转换为 C++ 字符串(const char*)。
  // 这样可以确保 Java 字符串在函数结束后被自动释放。
  ScopedUtfChars path(env, java_path);

  // 如果字符串转换失败,则返回 0 作为错误信号。
  if (path.c_str() == nullptr) {
    return 0;
  }

  // 将资源路径记录到追踪日志中,方便调试。
  ATRACE_NAME(base::StringPrintf("LoadApkAssets(%s)", path.c_str()).c_str());

  // 使用提供的 assets_provider 创建资源加载器,这个加载器处理资源的加载过程。
  auto loader_assets = LoaderAssetsProvider::Create(env, assets_provider);

  // 声明一个指针,用于存储加载的 APK 资源。
  AssetManager2::ApkAssetsPtr apk_assets;

  // 根据 'format' 参数,函数将处理不同的资源加载方式:
  switch (format) {
    case FORMAT_APK: {
        // 对于 APK 格式:创建一个多资源提供者,它将 loader_assets 与一个 zip 提供者结合,
        // zip 提供者负责从 APK 文件中提取资源。
        // 然后使用提供的属性标志加载 APK 资源。
        auto assets = MultiAssetsProvider::Create(std::move(loader_assets),
                                                  ZipAssetsProvider::Create(path.c_str(),
                                                                            property_flags));
        apk_assets = ApkAssets::Load(std::move(assets), property_flags);
        break;
    }
    case FORMAT_IDMAP:
      // 对于 IDMAP 格式:将 APK 资源作为覆盖资源加载。
      apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags);
      break;
    case FORMAT_ARSC:
      // 对于 ARSC 格式:将 APK 资源作为表格加载,从资产文件加载数据。
      apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()),
                                        std::move(loader_assets),
                                        property_flags);
      break;
    case FORMAT_DIRECTORY: {
      // 对于目录格式:创建一个多资源提供者,将 loader_assets 与目录资源提供者结合。
      // 然后使用提供的属性标志加载 APK 资源。
      auto assets = MultiAssetsProvider::Create(std::move(loader_assets),
                                                DirectoryAssetsProvider::Create(path.c_str()));
      apk_assets = ApkAssets::Load(std::move(assets), property_flags);
      break;
    }
    default:
      // 如果传入的格式不支持,抛出一个异常,并返回错误信息。
      const std::string error_msg = base::StringPrintf("Unsupported format type %d", format);
      jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str());
      return 0;
  }

  // 如果 APK 资源加载失败,抛出一个 IOException 异常,并返回错误信息。
  if (apk_assets == nullptr) {
    const std::string error_msg = base::StringPrintf("Failed to load asset path %s", path.c_str());
    jniThrowException(env, "java/io/IOException", error_msg.c_str());
    return 0;
  }

  // 如果成功加载资源,返回结果(CreateGuardedApkAssets 将加载的 APK 资源包装成一个受保护的对象)。
  return CreateGuardedApkAssets(std::move(apk_assets));
}

根据不同的格式类型来选择相应的资源加载方式的分支结构。不同格式(APK、IDMAP、ARSC、目录)需要使用不同的资源提供者来加载资源:

  • FORMAT_APK:从 APK 文件中提取资源并加载。
  • FORMAT_IDMAP:加载 APK 资源作为覆盖资源。
  • FORMAT_ARSC:加载 APK 资源作为表格(可能是资源配置表)。
  • FORMAT_DIRECTORY:从指定的目录中加载资源。

针对 APK 核心逻辑,就是构建一个ZipAssetsProvider通过libziparchive 库,操作 APK 文件。执行读取、解压、打包等操作。

C++ 层 ApkAssets 构造

// ApkAssets::Load 是 ApkAssets 类的入口函数,用于加载 APK 资源。
ApkAssetsPtr ApkAssets::Load(std::unique_ptr<AssetsProvider> assets, package_property_t flags) {
  // 调用 LoadImpl 方法进行实际的资源加载
  return LoadImpl(std::move(assets), flags, nullptr /* idmap_asset */, nullptr /* loaded_idmap */);
}

// ApkAssets::LoadImpl 是一个实现细节函数,用于加载 APK 资源。
// 该函数处理打开资源表(如 ARSC 文件)并进行资源的加载。
// 它还处理 IDMAP 和已加载 IDMAP 的相关逻辑。
ApkAssetsPtr ApkAssets::LoadImpl(std::unique_ptr<AssetsProvider> assets,
                                 package_property_t property_flags,
                                 std::unique_ptr<Asset> idmap_asset,
                                 std::unique_ptr<LoadedIdmap> loaded_idmap) {
  // 如果 assets 为 nullptr,返回空指针,表示加载失败
  if (assets == nullptr) {
    return {};
  }

  // 定义资源表是否存在的标志
  bool resources_asset_exists = false;

  // 尝试打开资源文件(kResourcesArsc)并读取资源,如果文件不存在,resources_asset 为 nullptr
  auto resources_asset = assets->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER,
                                      &resources_asset_exists);
  // 如果打开资源文件失败且该文件存在,输出错误日志并返回空指针
  if (resources_asset == nullptr && resources_asset_exists) {
    LOG(ERROR) << "Failed to open '" << kResourcesArsc << "' in APK '" << assets->GetDebugName()
               << "'.";
    return {};
  }

  // 如果打开资源文件成功,继续调用 LoadImpl 处理后续逻辑,传递资源文件、资产提供者和其他参数
  return LoadImpl(std::move(resources_asset), std::move(assets), property_flags,
                  std::move(idmap_asset), std::move(loaded_idmap));
}

// ApkAssets::LoadImpl 的实际处理逻辑,负责加载 APK 资源表(ARSC 文件)和 IDMAP。
ApkAssetsPtr ApkAssets::LoadImpl(std::unique_ptr<Asset> resources_asset,
                                 std::unique_ptr<AssetsProvider> assets,
                                 package_property_t property_flags,
                                 std::unique_ptr<Asset> idmap_asset,
                                 std::unique_ptr<LoadedIdmap> loaded_idmap) {
  // 如果 assets 为 nullptr,表示资源加载失败,返回空指针
  if (assets == nullptr) {
    return {};
  }

  // 用于存储加载后的 ARSC 资源表
  std::unique_ptr<LoadedArsc> loaded_arsc;

  // 如果资源文件(resources_asset)不为空,表示需要加载 ARSC 资源表
  if (resources_asset != nullptr) {
    // 获取资源文件的数据缓冲区,并确保数据对齐
    const auto data = resources_asset->getIncFsBuffer(true /* aligned */);
    const size_t length = resources_asset->getLength();

    // 如果资源文件读取失败或数据长度为零,输出错误日志并返回空指针
    if (!data || length == 0) {
      LOG(ERROR) << "Failed to read resources table in APK '" << assets->GetDebugName() << "'.";
      return {};
    }

    // 调用 LoadedArsc::Load 方法加载资源表
    loaded_arsc = LoadedArsc::Load(data, length, loaded_idmap.get(), property_flags);
  } else if (loaded_idmap != nullptr && IsFabricatedOverlay(loaded_idmap->OverlayApkPath())) {
    // 如果没有资源文件但有已加载的 IDMAP,并且该 IDMAP 是一个“伪造的覆盖”,则加载 ARSC 数据
    loaded_arsc = LoadedArsc::Load(loaded_idmap.get());
  } else {
    // 如果没有资源文件且没有有效的 IDMAP,创建一个空的 ARSC 数据
    loaded_arsc = LoadedArsc::CreateEmpty();
  }

  // 如果加载 ARSC 资源表失败,输出错误日志并返回空指针
  if (loaded_arsc == nullptr) {
    LOG(ERROR) << "Failed to load resources table in APK '" << assets->GetDebugName() << "'.";
    return {};
  }

  // 最终构造并返回一个 ApkAssets 对象
  return ApkAssetsPtr::make(PrivateConstructorUtil{}, std::move(resources_asset),
                            std::move(loaded_arsc), std::move(assets), property_flags,
                            std::move(idmap_asset), std::move(loaded_idmap));
}

深刻理解完第一个章节,这部分代码就很简单了,只是通过代码将 ApkAssets 内存结构完成解析构造出来。

篇幅考虑并没有再额外罗列 LoadedArsc::Load 和 LoadedArsc::LoadTable 方法,请咨询感兴趣阅读

4、总结

ApkAssets 类是 Android 系统中用于表示 APK 文件资源的底层实现类。它提供了 APK 文件的不可变内存表示,并与 AssetManager 配合使用,允许高效地访问和共享 APK 内的资源。其实现主要是本地 C++ 代码,目的为了提高性能和减少资源消耗。