本文基于安卓11。
安卓应用的资源文件都在编译时通过aapt(frameworks/base/tools/aapt2)工具打包在APK中,安装后保存在userdata分区,当应用需要使用某个资源文件时,通常使用getResources().getString(R.string.name);
等方式,R.string.name
是一个32位的int类型,由aapt工具生成,保存的是每个资源文件对应的ID,下面就看看安卓是如何通过这个ID获取到对应的数据的。
应用从Java层的AssetManager通过JNI调用到native模块的AssetManager,这里从androidfw
模块的AssetManager2
开始。
AssetManager2::FindEntry
//AssetManager2.cpp
ApkAssetsCookie AssetManager2::FindEntry(uint32_t resid, uint16_t density_override,
bool /*stop_at_first_match*/,
bool ignore_configuration,
FindEntryResult* out_entry) const {
if (resource_resolution_logging_enabled_) {
// Clear the last logged resource resolution.
ResetResourceResolution();
last_resolution_.resid = resid;
}
// 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;
}
// Retrieve the package group from the package id of the resource id.
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);
uint8_t package_idx = package_ids_[package_id];
if (package_idx == 0xff) {
ANDROID_LOG(ERROR) << base::StringPrintf("No package ID %02x found for ID 0x%08x.",
package_id, resid);
return kInvalidCookie;
}
const PackageGroup& package_group = package_groups_[package_idx];
ApkAssetsCookie cookie = FindEntryInternal(package_group, type_idx, entry_idx, *desired_config,
false /* stop_at_first_match */,
ignore_configuration, out_entry);
if (UNLIKELY(cookie == kInvalidCookie)) {
return kInvalidCookie;
}
if (!apk_assets_[cookie]->IsLoader()) {
for (const auto& id_map : package_group.overlays_) {
auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid);
if (!overlay_entry) {
// No id map entry exists for this target resource.
continue;
}
if (overlay_entry.IsTableEntry()) {
// The target resource is overlaid by an inline value not represented by a resource.
out_entry->entry = overlay_entry.GetTableEntry();
out_entry->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
cookie = id_map.cookie;
continue;
}
FindEntryResult overlay_result;
ApkAssetsCookie overlay_cookie = FindEntry(overlay_entry.GetResourceId(), density_override,
false /* stop_at_first_match */,
ignore_configuration, &overlay_result);
if (UNLIKELY(overlay_cookie == kInvalidCookie)) {
continue;
}
if (!overlay_result.config.isBetterThan(out_entry->config, desired_config)
&& overlay_result.config.compare(out_entry->config) != 0) {
// The configuration of the entry for the overlay must be equal to or better than the target
// configuration to be chosen as the better value.
continue;
}
cookie = overlay_cookie;
out_entry->entry = std::move(overlay_result.entry);
out_entry->config = overlay_result.config;
out_entry->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
if (resource_resolution_logging_enabled_) {
last_resolution_.steps.push_back(
Resolution::Step{Resolution::Step::Type::OVERLAID, overlay_result.config.toString(),
overlay_result.package_name});
}
}
}
AssetManager2::FindEntry
方法接收5个参数,在资源文件中通过resid查找到对应的资源并返回:
- uint32_t resid:R.java中定义的resid,32位无符号整型,前2位标识package,后2位标识type,最后2位标识entry,例如:0x01040000,0x01表示package,0x04表示type,0x0000标识entry。
- uint16_t density_override:覆盖屏幕像素密度参数,通常为0u,也就是不覆盖。
- bool stop_at_first_match:通常为false。
- bool ignore_configuration:通常为false。
- FindEntryResult* out_entry:查找的结果。
AssetManager2::FindEntry
方法主要执行了3个步骤:
初始化
desired_config
变量,desired_config
是ResTable_config
类型,ResTable_config
从十几个维度定义了一个特定的资源配置,比如:MCC 和 MNC、语言、布局方向、最小宽度、可用宽度和高度、屏幕尺寸、屏幕宽高比等参数,具体可参考About app Resources,还定义了match()
、isBetterThan()
和compare()
等方法判断资源和配置是否匹配,desired_config
一开始被赋值为configuration_
,也就是当前AssetManager的资源配置值,如果需要覆盖屏幕像素密度参数,desired_config
的density
参数被重新赋值;拆解resid,分别得到package_id
,type_idx
,entry_idx
,一个应用可以获取多个package的资源,比如自己APK中的资源,还有系统的公共资源,其保存在framework-res.apk中,对于resid: 0x01040000,其中package_id: 0x1, type_idx: 0x3, entry_idx: 0,通过package_id找到PackageGroup
对象,继续在package范围内查找对应的资源。调用
AssetManager2::FindEntryInternal
,在package内部开始查找。如果这个Asset不是被动态加载的,继续查找overlay资源。
AssetManager2::FindEntryInternal
//AssetManager2.cpp
ApkAssetsCookie AssetManager2::FindEntryInternal(const PackageGroup& package_group,
uint8_t type_idx, uint16_t entry_idx,
const ResTable_config& desired_config,
bool /*stop_at_first_match*/,
bool ignore_configuration,
FindEntryResult* out_entry) const {
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;
Resolution::Step::Type resolution_type = Resolution::Step::Type::NO_ENTRY;
std::vector<Resolution::Step> resolution_steps;
// 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 = !ignore_configuration && &desired_config == &configuration_;
const size_t package_count = package_group.packages_.size();
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;
}
// If the package is an overlay or custom loader,
// then even configurations that are the same MUST be chosen.
const bool package_is_loader = loaded_package->IsCustomLoader();
type_flags |= type_spec->GetFlagsForEntryIndex(entry_idx);
if (use_fast_path) {
const FilteredConfigGroup& filtered_group = loaded_package_impl.filtered_configs_[type_idx];
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) {
resolution_type = Resolution::Step::Type::INITIAL;
} else if (this_config.isBetterThan(*best_config, &desired_config)) {
resolution_type = (package_is_loader) ? Resolution::Step::Type::BETTER_MATCH_LOADER
: Resolution::Step::Type::BETTER_MATCH;
} else if (package_is_loader && this_config.compare(*best_config) == 0) {
resolution_type = Resolution::Step::Type::OVERLAID_LOADER;
} else {
if (resource_resolution_logging_enabled_) {
resolution_type = (package_is_loader) ? Resolution::Step::Type::SKIPPED_LOADER
: Resolution::Step::Type::SKIPPED;
resolution_steps.push_back(Resolution::Step{resolution_type,
this_config.toString(),
&loaded_package->GetPackageName()});
}
continue;
}
// The configuration matches and is better than the previous selection.
// Find the entry value if it exists for this configuration.
const ResTable_type* type = filtered_group.types[i];
const uint32_t offset = LoadedPackage::GetEntryOffset(type, entry_idx);
if (offset == ResTable_type::NO_ENTRY) {
if (resource_resolution_logging_enabled_) {
if (package_is_loader) {
resolution_type = Resolution::Step::Type::NO_ENTRY_LOADER;
} else {
resolution_type = Resolution::Step::Type::NO_ENTRY;
}
resolution_steps.push_back(Resolution::Step{resolution_type,
this_config.toString(),
&loaded_package->GetPackageName()});
}
continue;
}
best_cookie = cookie;
best_package = loaded_package;
best_type = type;
best_config = &this_config;
best_offset = offset;
if (resource_resolution_logging_enabled_) {
last_resolution_.steps.push_back(Resolution::Step{resolution_type,
this_config.toString(),
&loaded_package->GetPackageName()});
}
}
} else {
// This is the slower path, which doesn't use the filtered list of configurations.
// Here we must read the ResTable_config from the mmapped APK, convert it to host endianness
// and fill in any new fields that did not exist when the APK was compiled.
// Furthermore when selecting configurations we can't just record the pointer to the
// ResTable_config, we must copy it.
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{};
if (!ignore_configuration) {
this_config.copyFromDtoH((*iter)->config);
if (!this_config.match(desired_config)) {
continue;
}
if (best_config == nullptr) {
resolution_type = Resolution::Step::Type::INITIAL;
} else if (this_config.isBetterThan(*best_config, &desired_config)) {
resolution_type = (package_is_loader) ? Resolution::Step::Type::BETTER_MATCH_LOADER
: Resolution::Step::Type::BETTER_MATCH;
} else if (package_is_loader && this_config.compare(*best_config) == 0) {
resolution_type = Resolution::Step::Type::OVERLAID_LOADER;
} else {
continue;
}
}
// The configuration matches and is better than the previous selection.
// Find the entry value if it exists for this configuration.
const uint32_t offset = LoadedPackage::GetEntryOffset(*iter, 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 (ignore_configuration) {
// Any configuration will suffice, so break.
break;
}
if (resource_resolution_logging_enabled_) {
last_resolution_.steps.push_back(Resolution::Step{resolution_type,
this_config.toString(),
&loaded_package->GetPackageName()});
}
}
}
}
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 = ResTable_entry_handle::unmanaged(best_entry);
out_entry->config = *best_config;
out_entry->type_flags = type_flags;
out_entry->package_name = &best_package->GetPackageName();
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.get();
return best_cookie;
}
PackageGroup
结构体变量std::vector<ConfiguredPackage> packages_;
是一个数组,保存了多个ConfiguredPackage
引用,ConfiguredPackage
定义了2个变量:const LoadedPackage* loaded_package_;
表示一个apk资源;ByteBucketArray<FilteredConfigGroup> filtered_configs_;
保存着匹配着当前AssetManager的配置ResTable_config
,以空间换时间,节省后续查找的时间。
Package
先遍历packages_
数组,得到每个ConfiguredPackage
的loaded_package_
,LoadedPackage
定义了一个apk的资源配置:
ResStringPool type_string_pool_
: 存储资源类型的名称。例如,资源的类型可能是string
、drawable
、layout
等。ResStringPool key_string_pool_
: 存储具体资源条目的名称(也就是键)。例如,strings.xml
中的每个<string>
元素的名称(如"app_name"
)。std::string package_name_
: 包名。int package_id_
: package_id对应resid的前2位,如0x01040000,package_id就是0x01。int type_id_offset_
: 内存偏移量,一般为0。ByteBucketArray<TypeSpecPtr> type_specs_
:TypeSpecPtr
是结构体TypeSpec
的指针(using TypeSpecPtr = util::unique_cptr<TypeSpec>;
),TypeSpec
定义了每一个Type(资源类型),一个Type的所有Entry都存储在同一个内存块中。ByteBucketArray<uint32_t> resource_ids_
:resource_ids_
描述了每个Type的Entry数量,比如string类型有100个,drawable类型有80个,代码形式表现为resource_ids_[typeIndex_] = entry_count
。
ByteBucketArray
是模板类,数据结构是数组,ByteBucketArray<TypeSpecPtr>
就是TypeSpecPtr
类型的数组。
回到AssetManager2::FindEntryInternal
方法,在遍历packages_
数组后,通过LoadedPackage
和type_idx
参数得到它指向的TypeSpec
对象,loaded_package->GetTypeSpecByTypeIndex(type_idx);
。
Type
对于每种Type(资源类型)都有多种配置,比如drawable资源有drawable
、drawable-hdpi
、drawable-ldrtl-hdpi
、drawable-ldrtl-mdpi
等,要找到匹配的资源类型,需要比较desired_config
和每个Type关联的配置是否匹配,可以通过上文提到的ResTable_config
提供的match()
、isBetterThan()
和compare()
等方法判断。
TypeSpec
定义了一个Type(资源类型)结构体。注意命名后缀Spec,Spec是Specification**(规范/规格)** 的缩写,它一般用于表示某种 定义、描述或元数据,而不是实际的数据本身,可以理解为在package和type中的一个抽象类型。ResTable_type
(在 types[0]
数组中)才是实际的资源类型数据,它存储了不同配置下的资源内容,type_count
表示一个package存在多个的Type(资源类型),比如有layout, drawable,那么type_count
的值就是2。
ResTable_type
才是实际的资源类型数据,定义了id
、entryCount
、config
、entriesStart
等关键数据。
header
: 头文件。id
: type_id对应resid的三四位,如0x01040000,id就是0x04。entryCount
: entry数量。entriesStart
: 从header数据开始,ResTable_entry
数据的偏移量。config
: 特定的资源配置。FLAG_SPARSE
: 是否分散保存。
回到AssetManager2::FindEntryInternal
方法,遍历TypeSpec
的每个ResTable_type
对象,比较其关联的config
,得到和desired_config
最匹配的ResTable_type
。
找到了匹配的Type后,下一步就是通过Type找到Entry,通过传递过来的entry_idx
参数:
const uint32_t offset = LoadedPackage::GetEntryOffset(*iter, entry_idx);
。
LoadedPackage::GetEntryOffset
方法的作用是获取指定资源条目(entry)在资源类型(type_chunk)中的偏移量,首先判断ResTable_type
是否有FLAG_SPARSE
标志表示分散保存,如果是分散保存,则需要通过二分搜索找到对应的entry,否则简单多了,找到头文件之后的内存指针,后面的内存保存着uint32_t类型的偏移量数据。
//LoadedArsc.cpp
uint32_t LoadedPackage::GetEntryOffset(const ResTable_type* type_chunk, uint16_t entry_index) {
const size_t offsets_offset = dtohs(type_chunk->header.headerSize);
const uint32_t* entry_offsets = reinterpret_cast<const uint32_t*>(
reinterpret_cast<const uint8_t*>(type_chunk) + offsets_offset);
return dtohl(entry_offsets[entry_index]);
}
reinterpret_cast<const uint8_t*>(type_chunk)
先将结构体指针重新解释为字节指针,type_chunk
是指向ResTable_type
结构的指针,而结构体指针一般会按结构体的大小和对齐方式对齐,这对于我们后续的内存访问和计算偏移并不方便,通过将type_chunk
转换为uint8_t*
,我们将其视为一个字节数组,这样就可以按 字节 访问内存。reinterpret_cast<const uint8_t*>(type_chunk) + offsets_offset
在type_chunk*
指针位置偏移offsets_offset
个字节。reinterpret_cast<const uint32_t*>(reinterpret_cast<const uint8_t*>(type_chunk) + offsets_offset)
将其重新解释为uint32_t*
类型的数据,在内存中,偏移量通常是以 4 字节为单位存储的。entry_offsets
存储了一组 偏移量,每个偏移量对应一个资源条目(例如,资源ID,图像,字符串等)。entry_offsets[entry_index]
便是 资源条目对应的偏移量。dtohl(entry_offsets[entry_index])
将网络字节序(大端字节序)转换为主机字节序(小端字节序)。
好了,现在确定了ResTable_type对象,和Entry的偏移量,接下来就是查找Entry对象了。
Entry
查找获取Entry对象:const ResTable_entry* best_entry = LoadedPackage::GetEntryFromOffset(best_type, best_offset);
,其中best_type
是最匹配的Type,ResTable_type
对象,best_offset
是Entry的偏移量。
//LoadedArsc.cpp
const ResTable_entry* LoadedPackage::GetEntryFromOffset(const ResTable_type* type_chunk,
uint32_t offset) {
return reinterpret_cast<const ResTable_entry*>(reinterpret_cast<const uint8_t*>(type_chunk) +
offset + dtohl(type_chunk->entriesStart));
}
在type_chunk*
指针位置偏移offset
个字节,然后加上type_chunk->entriesStart
偏移量,就是ResTable_entry
对象的内存数据了。
内存结构如下:
ResTable_type->header.headerSize
表示ResTable_type结构体的大小,ResTable_type->entriesStart
表示ResTable_type结构体和Entry offSets的大小。
回到LoadedPackage::GetEntryFromOffset
,将对应的内存数据重新解释为ResTable_entry
类型。
ResTable_entry
定义了每个资源条目的数据结构。
size
: 数据大小,字节数。flags
: FLAG_COMPLEX 或 FLAG_PUBLIC 或 FLAG_WEAK。key
:ResStringPool_ref
类型,已一个uint32_t
类型的index指向stringPool中的某个string,这里保存了每个entry对应的的值。
总结一下,ResTable_config
从十几个维度定义了一个特定的资源配置,先是通过package_idx
确定了PackageGroup
,遍历PackageGroup
中的所有LoadedPackage
,然后通过type_idx
找到LoadedPackage
中保存的TypeSpec
对象,比较desired_config
和TypeSpec
关联的ResTable_config
,找到最匹配的TypeSpec
,最后通过entry_idx
找到对应的内存地址,转换解释为ResTable_entry
对象。
返回结果
回到AssetManager2::FindEntryInternal
,找到了对应的Entry后通过FindEntryResult
对象返回。
//AssetManager2.cpp
const ResTable_entry* best_entry = LoadedPackage::GetEntryFromOffset(best_type, best_offset);
out_entry->entry = ResTable_entry_handle::unmanaged(best_entry);
out_entry->config = *best_config;
out_entry->type_flags = type_flags;
out_entry->package_name = &best_package->GetPackageName();
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.get();
FindEntryResult
的entry
是ResTable_entry_handle
类型,共享指针指向best_entry
。
FindEntryResult
指针保存结果返回给JNI调用者。
//AssetManager2.cpp
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 */,
false /* ignore_configuration */, &entry);
const ResTable_entry* table_entry = *entry.entry;
const Res_value* device_value = reinterpret_cast<const Res_value*>(
reinterpret_cast<const uint8_t*>(table_entry) + dtohs(table_entry->size));
out_value->copyFrom_dtoh(*device_value);
return cookie;
}
reinterpret_cast<const Res_value*>(reinterpret_cast<const uint8_t*>(table_entry) + dtohs(table_entry->size))
将ResTable_entry
内存地址后size
个字节的数据重新解释装换为Res_value
对象,size
的值是ResTable_entry
结构体的内存大小。
Res_value
类定义:
JNI的NativeGetResourceValue
方法将CPP中的Res_value
对象转换为Java中的TypedValue
对象:
//android_util_AssetManager.cpp
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);
uint32_t ref = static_cast<uint32_t>(resid);
return CopyValue(env, cookie, value, ref, flags, &selected_config, typed_value);
}
CopyValue将Res_value
对象数据复制给TypedValue
对象:
//android_util_AssetManager.cpp
static jint CopyValue(JNIEnv* env, ApkAssetsCookie cookie, const Res_value& value, uint32_t ref,
uint32_t type_spec_flags, ResTable_config* config, jobject out_typed_value) {
env->SetIntField(out_typed_value, gTypedValueOffsets.mType, value.dataType);
env->SetIntField(out_typed_value, gTypedValueOffsets.mAssetCookie,
ApkAssetsCookieToJavaCookie(cookie));
env->SetIntField(out_typed_value, gTypedValueOffsets.mData, value.data);
env->SetObjectField(out_typed_value, gTypedValueOffsets.mString, nullptr);
env->SetIntField(out_typed_value, gTypedValueOffsets.mResourceId, ref);
env->SetIntField(out_typed_value, gTypedValueOffsets.mChangingConfigurations, type_spec_flags);
if (config != nullptr) {
env->SetIntField(out_typed_value, gTypedValueOffsets.mDensity, config->density);
}
return static_cast<jint>(ApkAssetsCookieToJavaCookie(cookie));
}
gTypedValueOffsets.mType
: TypedValue.typegTypedValueOffsets.mData
: TypedValue.data
Java层的AssetManager
通过TypedValue
找到entry对应的值。
//AssetManger.java
boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
boolean resolveRefs) {
final int cookie = nativeGetResourceValue(
mObject, resId, (short) densityDpi, outValue, resolveRefs);
if (outValue.type == TypedValue.TYPE_STRING) {
outValue.string = getPooledStringForCookie(cookie, outValue.data);
}
}
判断type
是否为ypedValue.TYPE_STRING
,如果是的话getPooledStringForCookie(cookie, outValue.data);,cookie
是上文AssetManager2::FindEntryInternal
方法返回的best_cookie
,using ApkAssetsCookie = int32_t;
一个整型变量关联一个LoadedPackage
,相当于LoadedPackage
的索引。
//AssetManger.java
CharSequence getPooledStringForCookie(int cookie, int id) {
// Cookies map to ApkAssets starting at 1.
return getApkAssets()[cookie - 1].getStringFromPool(id);
}
通过cookie
索引到具体的ApkAssets
对象,然后通过outValue.data
在字符串池中获取对应的值。
getApkAssets()
返回一个ApkAssets[]
数组,一个应用可能有多个ApkAssets
,比如下面的应用com.example.myapplication
,3个systemApkAssets和1个mUserApkAssets,返回的ApkAssets[]
大小为4。
01-01 07:24:10.137 1143 1283 D AssetManager: AssetManager systemApkAssets: 0, /system/framework/framework-res.apk
01-01 07:24:10.137 1143 1283 D AssetManager: AssetManager systemApkAssets: 1, /vendor/overlay/framework-res__auto_generated_rro_vendor.apk
01-01 07:24:10.137 1143 1283 D AssetManager: AssetManager systemApkAssets: 2, /system/product/overlay/framework-res__auto_generated_rro_product.apk
01-01 07:24:10.137 1143 1283 D AssetManager: AssetManager mUserApkAssets: 0, /data/app/~~YZ0ivppt_GbGfdxqixf45Q==/com.example.myapplication-_rJfRy7PAecqI4zoHuVdbA==/base.apk