Android资源学习(三)资源查找

Android资源学习系列

Posted by Cc1over on December 29, 2019

Android资源学习(三)资源查找

本文源码基于Android9.0

# Resources-> getLayout

 public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
        return loadXmlResourceParser(id, "layout");
 }

# Resources-> loadXmlResourceParser

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
            throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValue(id, value, true);
            if (value.type == TypedValue.TYPE_STRING) {
                return impl.loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }
            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                    + " type #0x" + Integer.toHexString(value.type) + " is not valid");
        } finally {
            releaseTempTypedValue(value);
        }
    }

实际的资源查找工作会委托给Resources中的ResourcesImpl执行,主要步骤分为两步:

  • 调用getValue函数通过资源id,把资源查找的结果设置到TypeValue
  • 调用loadXmlResourceParservalue中的字符串转换为XmlResourceParser对象返回给上层

# ResourcesImpl-> getValue

void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
        boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
        if (found) {
            return;
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}

# AssetManager-> getResourceValue

boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        synchronized (this) {
            ensureValidLocked();
            final int cookie = nativeGetResourceValue(
                    mObject, resId, (short) densityDpi, outValue, resolveRefs);

            outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
                    outValue.changingConfigurations);

            if (outValue.type == TypedValue.TYPE_STRING) {
                outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
            }
            return true;
        }
    }

调用nativeGetResourceValue从native层获取TypeValue,并且会拿到一个cookie的值

cookie的获取是为了定位ApkAssets,而native层获取到的TypeValue是不含有实际的字符串,不过可以根据它的data从GlobalStringPool中获取字符串,这就和Res_value类似啦

# android_util_AssetManager.cpp-> NativeGetResourceValue

static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
                                   jshort density, jobject typed_value,
                                   jboolean resolve_references) {
  ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
  Res_value value;
  ResTable_config selected_config;
  uint32_t flags;
  ApkAssetsCookie cookie =
      assetmanager->GetResource(static_cast<uint32_t>(resid), false /*may_be_bag*/,
                                static_cast<uint16_t>(density), &value, &selected_config, &flags);
  if (cookie == kInvalidCookie) {
    return ApkAssetsCookieToJavaCookie(kInvalidCookie);
  }

  uint32_t ref = static_cast<uint32_t>(resid);
  if (resolve_references) {
    cookie = assetmanager->ResolveReference(cookie, &value, &selected_config, &flags, &ref);
    if (cookie == kInvalidCookie) {
      return ApkAssetsCookieToJavaCookie(kInvalidCookie);
    }
  }
  return CopyValue(env, cookie, value, ref, flags, &selected_config, typed_value);
}

这个函数会根据AssetManager中的地址值获取到对应的AssetManager2

然后通过AssetManager2的GetResource函数去获取value以及cookie

然后其实这个ApkAssetsCookie就暴露,因为它其实就是一个32位int,在AssetsManager2添加ApkAsset的时候会把数据转移到PackageGroup中,在这个时机,其实就会创建ApkAssetsCookie,其实也就是索引,有了它就可以快速拿到PackageGroup里面的Package

最后通过CopyValue把Res_value中的内容拷贝到TypeValue中就可以了

# AssetManager2-> GetValue

ApkAssetsCookie AssetManager2::GetResource(uint32_t resid, bool may_be_bag,
                                           uint16_t density_override, Res_value* out_value,
                                           ResTable_config* out_selected_config,
                                           uint32_t* out_flags) const {
  FindEntryResult entry;
  ApkAssetsCookie cookie =
      FindEntry(resid, density_override, false /* stop_at_first_match */, &entry);
  if (cookie == kInvalidCookie) {
    return kInvalidCookie;
  }

  if (dtohs(entry.entry->flags) & ResTable_entry::FLAG_COMPLEX) {
    if (!may_be_bag) {
      // ......
      return kInvalidCookie;
    }

    // Create a reference since we can't represent this complex type as a Res_value.
    out_value->dataType = Res_value::TYPE_REFERENCE;
    out_value->data = resid;
    *out_selected_config = entry.config;
    *out_flags = entry.type_flags;
    return cookie;
  }

  const Res_value* device_value = reinterpret_cast<const Res_value*>(
      reinterpret_cast<const uint8_t*>(entry.entry) + dtohs(entry.entry->size));
  out_value->copyFrom_dtoh(*device_value);

  // Convert the package ID to the runtime assigned package ID.
  entry.dynamic_ref_table->lookupResourceValue(out_value);

  *out_selected_config = entry.config;
  *out_flags = entry.type_flags;
  return cookie;
}

通过FindEntry函数找到resid对应的entry,并且从这里返回相应的ApkAssetsCookie

然后获取到entry之后就会对entry的flags进行判断,有两种情况:

  • 那就是这个flags为0x01的时候,也就是bag资源,这种情况下:
    • 如果may_be_bag为false,就直接invalid了,这也说明查找bag类型的资源,是要提前定义标记位的
    • 如果may_be_bag为true,就给Res_value赋值,其中data就是resid,type就是reference
  • 如果这个flags不为0x01的时候,也就非bag资源,这种情况下就直接把entry中的value给out_value就可以了,

然后如果是shared library的话就用dynamic_ref_table去吧编译时packageID转换成运行时的packageID

最后设置返回值,把cookie给出去就完事了

# AssetManager2-> FindEntry

ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_override,
                                         bool /*stop_at_first_match*/,
                                         FindEntryResult* out_entry) const {
  // Might use this if density_override != 0.
  ResTable_config density_override_config;

  // Select our configuration or generate a density override configuration.
  const ResTable_config* desired_config = &configuration_;
  if (density_override != 0 && density_override != configuration_.density) {
    density_override_config = configuration_;
    density_override_config.density = density_override;
    desired_config = &density_override_config;
  }

  if (!is_valid_resid(resid)) {
    LOG(ERROR) << base::StringPrintf("Invalid ID 0x%08x.", resid);
    return kInvalidCookie;
  }

  const uint32_t package_id = get_package_id(resid);
  const uint8_t type_idx = get_type_id(resid) - 1;
  const uint16_t entry_idx = get_entry_id(resid);

  const uint8_t package_idx = package_ids_[package_id];
  if (package_idx == 0xff) {
  ...
    return kInvalidCookie;
  }

  const PackageGroup& package_group = package_groups_[package_idx];
  const size_t package_count = package_group.packages_.size();

  ApkAssetsCookie best_cookie = kInvalidCookie;
  const LoadedPackage* best_package = nullptr;
  const ResTable_type* best_type = nullptr;
  const ResTable_config* best_config = nullptr;
  ResTable_config best_config_copy;
  uint32_t best_offset = 0u;
  uint32_t type_flags = 0u;

  // If desired_config is the same as the set configuration, then we can use our filtered list
  // and we don't need to match the configurations, since they already matched.
  const bool use_fast_path = desired_config == &configuration_;

  for (size_t pi = 0; pi < package_count; pi++) {
    const ConfiguredPackage& loaded_package_impl = package_group.packages_[pi];
    const LoadedPackage* loaded_package = loaded_package_impl.loaded_package_;
    ApkAssetsCookie cookie = package_group.cookies_[pi];

    // If the type IDs are offset in this package, we need to take that into account when searching
    // for a type.
    const TypeSpec* type_spec = loaded_package->GetTypeSpecByTypeIndex(type_idx);
    if (UNLIKELY(type_spec == nullptr)) {
      continue;
    }

    uint16_t local_entry_idx = entry_idx;

    // If there is an IDMAP supplied with this package, translate the entry ID.
    if (type_spec->idmap_entries != nullptr) {
      if (!LoadedIdmap::Lookup(type_spec->idmap_entries, local_entry_idx, &local_entry_idx)) {
        // There is no mapping, so the resource is not meant to be in this overlay package.
        continue;
      }
    }

    type_flags |= type_spec->GetFlagsForEntryIndex(local_entry_idx);

    // If the package is an overlay, then even configurations that are the same MUST be chosen.
    const bool package_is_overlay = loaded_package->IsOverlay();

    const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx];
    if (use_fast_path) {
      const std::vector<ResTable_config>& candidate_configs = filtered_group.configurations;
      const size_t type_count = candidate_configs.size();
      for (uint32_t i = 0; i < type_count; i++) {
        const ResTable_config& this_config = candidate_configs[i];

        // We can skip calling ResTable_config::match() because we know that all candidate
        // configurations that do NOT match have been filtered-out.
        if ((best_config == nullptr || this_config.isBetterThan(*best_config, desired_config)) ||
            (package_is_overlay && this_config.compare(*best_config) == 0)) {
          const ResTable_type* type_chunk = filtered_group.types[i];
          const uint32_t offset = LoadedPackage::GetEntryOffset(type_chunk, local_entry_idx);
          if (offset == ResTable_type::NO_ENTRY) {
            continue;
          }

          best_cookie = cookie;
          best_package = loaded_package;
          best_type = type_chunk;
          best_config = &this_config;
          best_offset = offset;
        }
      }
    } else {

      const auto iter_end = type_spec->types + type_spec->type_count;
      for (auto iter = type_spec->types; iter != iter_end; ++iter) {
        ResTable_config this_config;
        this_config.copyFromDtoH((*iter)->config);

        if (this_config.match(*desired_config)) {
          if ((best_config == nullptr || this_config.isBetterThan(*best_config, desired_config)) ||
              (package_is_overlay && this_config.compare(*best_config) == 0)) {
            const uint32_t offset = LoadedPackage::GetEntryOffset(*iter, local_entry_idx);
            if (offset == ResTable_type::NO_ENTRY) {
              continue;
            }

            best_cookie = cookie;
            best_package = loaded_package;
            best_type = *iter;
            best_config_copy = this_config;
            best_config = &best_config_copy;
            best_offset = offset;
          }
        }
      }
    }
  }

  if (UNLIKELY(best_cookie == kInvalidCookie)) {
    return kInvalidCookie;
  }

  const ResTable_entry* best_entry = LoadedPackage::GetEntryFromOffset(best_type, best_offset);
  if (UNLIKELY(best_entry == nullptr)) {
    return kInvalidCookie;
  }

  out_entry->entry = best_entry;
  out_entry->config = *best_config;
  out_entry->type_flags = type_flags;
  out_entry->type_string_ref = StringPoolRef(best_package->GetTypeStringPool(), best_type->id - 1);
  out_entry->entry_string_ref =
      StringPoolRef(best_package->GetKeyStringPool(), best_entry->key.index);
  out_entry->dynamic_ref_table = &package_group.dynamic_ref_table;
  return best_cookie;
}

这个方法有点长,先梳理方法流程

方法流程:

  • 在AssetManager2中其实存了一个configuration_的配置,而一开始会根据标记位密度信息判断要不要对目标匹配配置进行屏幕密度这一项的覆盖
  • 判断输入的resid是否合法,不合法直接validate,合法则按照PTEE拆开成package_id,type_idx,entry_idx
  • 根据package_id,从package_ids中获取package_idx,package_ids的初始化也是在AssetManager2添加ApkAsset的时候,它的值就是package_group中package的数量,然后根据package_idx拿到package_group,然后就可以遍历里面的ConfiguredPackage
  • 然后有了package就可以拿到cookie了,因为在AssetManager2构建动态表的时候cookie就是package_group中package的索引
  • 然后通过type_idx拿到对应的typeSpce,如果发现typeSpec中有idmap说明有id需要被覆盖,就尝试通过Loadedmap::Lookup,转化一下entryID(从IdEntryMap的数组获取对应entry数组中的id)
  • 然后就是从ConfiguredPackage中获得package数据,这里会有两种情况:

    • 和原来的配置config一致:这种情况下会在ConfiguredPackage中存放着在根据config已经过滤好的资源列表,直接从里面循环直接拿到对应资源Type,并调用方法GetEntryOffset根据资源entryId获取对应的资源entry对象
    • 和原来的config不一致:循环typeSpec中映射好的资源关系,先寻找合适的config接着在尝试寻找有没有对应的entryID
  • 最后确定已经存在了资源的存在,则会通过当前的资源类型以及资源类型中的偏移数组通过方法GetEntryFromOffset获取对应的entry

注意点 config的匹配规则为:

  • 淘汰与设备配置冲突的资源文件
  • 选择 Config 中下一个优先级最高的限定符,从 MCC 开始,向下移动
  • 是否有资源目录包含此限定符?
    • 若无,返回第 2 步
    • 若有,继续执行
  • 淘汰不含此限定符的资源目录。在该示例中,系统会淘汰所有不含语言限定符的目录
  • 返回并重复第 2 步、第 3 步和第 4 步,直到仅剩一个目录为止

# Resources-> loadXmlResourceParser

XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
            throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValue(id, value, true);
            if (value.type == TypedValue.TYPE_STRING) {
                return impl.loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }
            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                    + " type #0x" + Integer.toHexString(value.type) + " is not valid");
        } finally {
            releaseTempTypedValue(value);
        }
    }

经历了第一个流程回到了这里,这个时候TypeValue已经具备了从native中获得的Res_value中的全局常量池的idx,然后就可以执行第二个步骤去加载真正的xmlResourceParser

# ResourcesImpl-> loadXmlResourceParser

 XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
            @NonNull String type)
            throws NotFoundException {
        if (id != 0) {
            try {
                synchronized (mCachedXmlBlocks) {
                    final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
                    final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
                    final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
                    // First see if this block is in our cache.
                    final int num = cachedXmlBlockFiles.length;
                    for (int i = 0; i < num; i++) {
                        if (cachedXmlBlockCookies[i] == assetCookie 
                            && cachedXmlBlockFiles[i] != null
                                && cachedXmlBlockFiles[i].equals(file)) {
                            return cachedXmlBlocks[i].newParser();
                        }
                    }

                    final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
                    if (block != null) {
                        final int pos = (mLastCachedXmlBlockIndex + 1) % num;
                        mLastCachedXmlBlockIndex = pos;
                        final XmlBlock oldBlock = cachedXmlBlocks[pos];
                        if (oldBlock != null) {
                            oldBlock.close();
                        }
                        cachedXmlBlockCookies[pos] = assetCookie;
                        cachedXmlBlockFiles[pos] = file;
                        cachedXmlBlocks[pos] = block;
                        return block.newParser();
                    }
                }
            } catch (Exception e) {
               ...
            }
        }

        ...
    }

在真正去获取xmlResourceParser之前会有这么一层缓存,缓存分为三个数组:

  • cachedXmlBlockCookies用来存储package的索引cookie,用于匹配
  • cachedXmlBlockFiles用来存储xml文件名,用于匹配
  • cachedXmlBlocks用来存储实际的XmlBlock对象,用于返回结果

如果缓存命中就直接返回,如果缓存不命中则走创建流程,然后添加缓存,创建工作会转交给mAssets的openXmlBlockAsset函数去构建一个XmlBlock

# AssetManager-> openXmlBlockAsset

    @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException {
        Preconditions.checkNotNull(fileName, "fileName");
        synchronized (this) {
            ensureOpenLocked();
            final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName);
            if (xmlBlock == 0) {
                throw new FileNotFoundException("Asset XML file: " + fileName);
            }
            final XmlBlock block = new XmlBlock(this, xmlBlock);
            incRefsLocked(block.hashCode());
            return block;
        }
    }

XmlBlock其实和ApkAssets类似,它里面包含了一个native的地址,以及一个常量池,然后在只需要通过nativeOpenXmlAsset把实际数据的创建工作交给native层,然后在java层存一个地址值就可以了

# android_util_AssetManager.cpp-> NativeOpenXmlAsset

static jlong NativeOpenXmlAsset(JNIEnv* env, jobject /*clazz*/, jlong ptr, jint jcookie,
                                jstring asset_path) {
  ApkAssetsCookie cookie = JavaCookieToApkAssetsCookie(jcookie);
  ScopedUtfChars asset_path_utf8(env, asset_path);
  // ......
  ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
  std::unique_ptr<Asset> asset;
  if (cookie != kInvalidCookie) {
    asset = assetmanager->OpenNonAsset(asset_path_utf8.c_str(), cookie, Asset::ACCESS_RANDOM);
  } else {
    asset = assetmanager->OpenNonAsset(asset_path_utf8.c_str(), Asset::ACCESS_RANDOM, &cookie);
  }
  // ......
  const DynamicRefTable* dynamic_ref_table = assetmanager->GetDynamicRefTableForCookie(cookie);

  std::unique_ptr<ResXMLTree> xml_tree = util::make_unique<ResXMLTree>(dynamic_ref_table);
  status_t err = xml_tree->setTo(asset->getBuffer(true), asset->getLength(), true);
  asset.reset();
  // ......
  return reinterpret_cast<jlong>(xml_tree.release());
}

通过java层AssetManager中存的地址值得到native层AssetManagr2

通过OpenNonAsset读取资源名称对应的Asset

通过cookie获取AssetManager2的动态映射表

把动态映射表转化为ResXMLTree,读取asset中对应的数据,设置到ResXMLTree中,等待解析

## # AssetManagr2-> OpenNonAsset

std::unique_ptr<Asset> AssetManager2::OpenNonAsset(const std::string& filename,
                                                   Asset::AccessMode mode,
                                                   ApkAssetsCookie* out_cookie) const {
  for (int32_t i = apk_assets_.size() - 1; i >= 0; i--) {
    // Prevent RRO from modifying assets and other entries accessed by file
    // path. Explicitly asking for a path in a given package (denoted by a
    // cookie) is still OK.
    if (apk_assets_[i]->IsOverlay()) {
      continue;
    }

    std::unique_ptr<Asset> asset = apk_assets_[i]->Open(filename, mode);
    if (asset) {
      if (out_cookie != nullptr) {
        *out_cookie = i;
      }
          return asset;
    }
  }

遍历AssetManager2中管理的所有ApkAsset,然后调用Open函数根据filename和mode返回一个asset

asset在这里代表了对应的xml文件

# ApkAssets-> Open

std::unique_ptr<Asset> ApkAssets::Open(const std::string& path, Asset::AccessMode mode) const {
  CHECK(zip_handle_ != nullptr);

  ::ZipString name(path.c_str());
  ::ZipEntry entry;
  int32_t result = ::FindEntry(zip_handle_.get(), name, &entry);
  // ......
  if (entry.method == kCompressDeflated) {
    std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
    // ......
    std::unique_ptr<Asset> asset =
    Asset::createFromCompressedMap(std::move(map), entry.uncompressed_length, mode);
    // ......
    return asset;
  } else {
    std::unique_ptr<FileMap> map = util::make_unique<FileMap>();
    // ......
    std::unique_ptr<Asset> asset = Asset::createFromUncompressedMap(std::move(map), mode);
    // ......
    return asset;
  }
}

首先根据文件名在Assets,也就是资源目录中找到对应xml的ZipEntry

然后和resources.arsc加载解析类似,都是区分为两种情况:

  • 如果经过压缩:
    • 先通过FileMap把ZipEntry通过mmap映射到虚拟内存中
    • 再通过Asset::createFromCompressedMap通过CompressedAsset::openChunk拿到StreamingZipInflater,返回CompressedAsset对象
    • CompressedAsset是Asset的子类,在openChunk创建了StreamingZipInflater之后用成员变量保存起来,但是不会马上进行解压操作,而是等到第一次读操作执行
  • 没有经过压缩:
    • 通过FileMap把ZipEntry通过mmap映射到虚拟内存中
    • 最后Asset::createFromUncompressedMap,返回FileAsset对象

拿到这个asset之后就在NativeOpenXmlAsset解析成ResXmlTree

# 小结

到这里,其实非asset的资源查找就结束了,主要的流程为:

  • 获得TypeValue也就是Res_value
    • 把resId拆开,拆成packageId,typeId,entryId
    • 根据packageId从AssetManager2中获取相应的package_group,然后遍历其中的每一个ConfiguredPackage
    • 然后如果当前期望的配置信息和初始化时设置的配置信息一致的话,就可以采取快速通道,从FilteredConfigGroup中获取资源
    • 否则就直接采取typeSpec,type,entry这样去遍历及查找
    • 最后把Res_value拷贝到TypeValue中返回
  • 根据Res_value中的全局常量池的index拿到文件名
  • 然后走一波native层从压缩包里拿到对应的xml文件,如果没压缩的话直接用mmap

# AssetManager-> open

    public @NonNull InputStream open(@NonNull String fileName, int accessMode) throws IOException {
        Preconditions.checkNotNull(fileName, "fileName");
        synchronized (this) {
            ensureOpenLocked();
            final long asset = nativeOpenAsset(mObject, fileName, accessMode);
            if (asset == 0) {
                throw new FileNotFoundException("Asset file: " + fileName);
            }
            final AssetInputStream assetInputStream = new AssetInputStream(asset);
            incRefsLocked(assetInputStream.hashCode());
            return assetInputStream;
        }
    }

主要流程nativeOpenAsset拿到个Asset,而在Java层依然是通过地址值去访问

然后构建一个AssetInputStream返回

## # AssetManager2-> Open

std::unique_ptr<Asset> AssetManager2::Open(const std::string& filename, ApkAssetsCookie cookie,
                                           Asset::AccessMode mode) const {
  const std::string new_path = "assets/" + filename;
  return OpenNonAsset(new_path, cookie, mode);
}

# AssetManager2-> OpenNonAsset

static jint NativeAssetReadChar(JNIEnv* /*env*/, jclass /*clazz*/, jlong asset_ptr) {
  Asset* asset = reinterpret_cast<Asset*>(asset_ptr);
  uint8_t b;
  ssize_t res = asset->read(&b, sizeof(b));
  return res == sizeof(b) ? static_cast<jint>(b) : -1;
}

系统这种方式,设置了相对路径。一样还是从OpenNonAsset中查找数据

最后会把Asset返回回去。此时AssetInputStream就会尝试着操作这个Asset对象,会持有着对应zipEntry,生成的FileMap

# 小结

到这里,其实asset的资源查找就结束了,主要的流程为:

  • 为路径新增一个asset的路径前缀
  • 把zip数据流交给AssetInputStream 这个Stream对象处理

参考资料:

Android重学系列 资源查找