In my last article, I talked about how the LoadedPackage::Load method in the resource management module that parses Package data starts parsing Package data blocks. This article will take a closer look at how Package data blocks are parsed and how AssetManager manages them. Resource. Arsc is resolved as follows:
Preparation before parsing Package
const static int kAppPackageId = 0x7f; std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, const LoadedIdmap* loaded_idmap, bool system, bool load_as_shared_library) { std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage()); Constexpr size_t kMinPackageSize = sizeof(ResTable_package) - sizeof(ResTable_package::typeIdOffset); constexpr size_t kMinPackageSize = sizeof(ResTable_package) - sizeof(ResTable_package::typeIdOffset); const ResTable_package* header = chunk.header<ResTable_package, kMinPackageSize>(); loaded_package->system_ = system; loaded_package->package_id_ = dtohl(header->id); if (loaded_package->package_id_ == 0 || (loaded_package->package_id_ == kAppPackageId && load_as_shared_library)) { // Package ID of 0 means this is a shared library. loaded_package->dynamic_ = true; } if (loaded_idmap ! = nullptr) { // This is an overlay and so it needs to pretend to be the target package. loaded_package->package_id_ = loaded_idmap->TargetPackageId(); loaded_package->overlay_ = true; } if (header->header.headerSize >= sizeof(ResTable_package)) { uint32_t type_id_offset = dtohl(header->typeIdOffset); . loaded_package->type_id_offset_ = static_cast<int>(type_id_offset); } util::ReadUtf16StringFromDevice(header->name, arraysize(header->name), &loaded_package->package_name_); . }Copy the code
This section is the preparation for parsing the Package data block, first parsing the package data block header information. In fact, parsing is the following module:
First fetch the current header ID and get the swap low and high as the packageId(0x7F000000 swap high and low becomes 0x7F). If the current ID is 0x7f and the identifier bit for load_AS_shared_library is enabled, Or if the ID is 0x00, it is loaded as a dynamic resource. If it is a third-party resource library, its ID is 0.
If loaded_idmap is passed down, this part of the resource needs to be overwritten. Finally, set loaded_idmap.
At this point, it can be known that the packageID of the APK package in the general App application is 0x7f
Parsing Package data
std::unordered_map<int, std::unique_ptr<TypeSpecPtrBuilder>> type_builder_map; ChunkIterator iter(chunk.data_ptr(), chunk.data_size()); while (iter.HasNext()) { const Chunk child_chunk = iter.Next(); switch (child_chunk.type()) { case RES_STRING_POOL_TYPE: { const uintptr_t pool_address = reinterpret_cast<uintptr_t>(child_chunk.header<ResChunk_header>()); const uintptr_t header_address = reinterpret_cast<uintptr_t>(header); if (pool_address == header_address + dtohl(header->typeStrings)) { // This string pool is the type string pool. status_t err = loaded_package->type_string_pool_.setTo( child_chunk.header<ResStringPool_header>(), child_chunk.size()); . } else if (pool_address == header_address + dtohl(header->keyStrings)) { // This string pool is the key string pool. status_t err = loaded_package->key_string_pool_.setTo( child_chunk.header<ResStringPool_header>(), child_chunk.size()); . } else { ... } } break; case RES_TABLE_TYPE_SPEC_TYPE: { const ResTable_typeSpec* type_spec = child_chunk.header<ResTable_typeSpec>(); . // The data portion of this chunk contains entry_count 32bit entries, // each one representing a set of flags. // Here we only validate that the chunk is well formed. const size_t entry_count = dtohl(type_spec->entryCount); // There can only be 2^16 entries in a type, because that is the ID // space for entries (EEEE) in the resource ID 0xPPTTEEEE. .... // If this is an overlay, associate the mapping of this type to the target type // from the IDMAP. const IdmapEntry_header* idmap_entry_header = nullptr; if (loaded_idmap ! = nullptr) { idmap_entry_header = loaded_idmap->GetEntryMapForType(type_spec->id); } std::unique_ptr<TypeSpecPtrBuilder>& builder_ptr = type_builder_map[type_spec->id - 1]; if (builder_ptr == nullptr) { builder_ptr = util::make_unique<TypeSpecPtrBuilder>(type_spec, idmap_entry_header); } else { ... } } break; case RES_TABLE_TYPE_TYPE: { const ResTable_type* type = child_chunk.header<ResTable_type, kResTableTypeMinSize>(); . // Type chunks must be preceded by their TypeSpec chunks. std::unique_ptr<TypeSpecPtrBuilder>& builder_ptr = type_builder_map[type->id - 1]; if (builder_ptr ! = nullptr) { builder_ptr->AddType(type); } else { ... } } break; case RES_TABLE_LIBRARY_TYPE: { const ResTable_lib_header* lib = child_chunk.header<ResTable_lib_header>(); . loaded_package->dynamic_package_map_.reserve(dtohl(lib->count)); const ResTable_lib_entry* const entry_begin = reinterpret_cast<const ResTable_lib_entry*>(child_chunk.data_ptr()); const ResTable_lib_entry* const entry_end = entry_begin + dtohl(lib->count); for (auto entry_iter = entry_begin; entry_iter ! = entry_end; ++entry_iter) { std::string package_name; util::ReadUtf16StringFromDevice(entry_iter->packageName, arraysize(entry_iter->packageName), &package_name); . loaded_package->dynamic_package_map_.emplace_back(std::move(package_name), dtohl(entry_iter->packageId)); } } break; default: ... break; }}... // Flatten and construct the TypeSpecs. for (auto& entry : type_builder_map) { uint8_t type_idx = static_cast<uint8_t>(entry.first); TypeSpecPtr type_spec_ptr = entry.second->Build(); . // We only add the type to the package if there is no IDMAP, or if the type is // overlaying something. if (loaded_idmap == nullptr || type_spec_ptr->idmap_entries ! = nullptr) { // If this is an overlay, insert it at the target type ID. if (type_spec_ptr->idmap_entries ! = nullptr) { type_idx = dtohs(type_spec_ptr->idmap_entries->target_type_id) - 1; } loaded_package->type_specs_.editItemAt(type_idx) = std::move(type_spec_ptr); } } return std::move(loaded_package);Copy the code
Parse the pool of strings in the Package Package
case RES_STRING_POOL_TYPE: { const uintptr_t pool_address = reinterpret_cast<uintptr_t>(child_chunk.header<ResChunk_header>()); const uintptr_t header_address = reinterpret_cast<uintptr_t>(header); if (pool_address == header_address + dtohl(header->typeStrings)) { // This string pool is the type string pool. status_t err = loaded_package->type_string_pool_.setTo( child_chunk.header<ResStringPool_header>(), child_chunk.size()); . } else if (pool_address == header_address + dtohl(header->keyStrings)) { // This string pool is the key string pool. status_t err = loaded_package->key_string_pool_.setTo( child_chunk.header<ResStringPool_header>(), child_chunk.size()); . } else { ... } } break;Copy the code
In this case, what you’re actually doing is very simple, parsing the following part
Unlike this figure, there is actually a header in the string resource pool, which is missing. The algorithm is very simple, as follows:
Resource typeString pool address = restable_package. header + typeString (offset)
Resource item Name String pool address = restable_package.header + keyStrings(offset)
Finally, the address is compared with the sub-chunk of the current chunk to see which one is the same as the address. If the address is the same, the value is assigned to the corresponding resource pool.
Global_string_pool_ Global content resource pool loaded_package->type_string_pool_ Resource string resource pool Loaded_package ->key_string_pool_ Resource item name Specifies the resource pool.
Parse resource type data
case RES_TABLE_TYPE_SPEC_TYPE: { const ResTable_typeSpec* type_spec = child_chunk.header<ResTable_typeSpec>(); . // The data portion of this chunk contains entry_count 32bit entries, // each one representing a set of flags. // Here we only validate that the chunk is well formed. const size_t entry_count = dtohl(type_spec->entryCount); // There can only be 2^16 entries in a type, because that is the ID // space for entries (EEEE) in the resource ID 0xPPTTEEEE. .... // If this is an overlay, associate the mapping of this type to the target type // from the IDMAP. const IdmapEntry_header* idmap_entry_header = nullptr; if (loaded_idmap ! = nullptr) { idmap_entry_header = loaded_idmap->GetEntryMapForType(type_spec->id); } std::unique_ptr<TypeSpecPtrBuilder>& builder_ptr = type_builder_map[type_spec->id - 1]; if (builder_ptr == nullptr) { builder_ptr = util::make_unique<TypeSpecPtrBuilder>(type_spec, idmap_entry_header); } else { ... } } break;Copy the code
The following data block is parsed:
Let’s first look at the structure of the resource type:
struct ResTable_typeSpec
{
struct ResChunk_header header;
// The type identifier this chunk is holding. Type IDs start
// at 1 (corresponding to the value of the type bits in a
// resource identifier). 0 is invalid.
uint8_t id;
// Must be 0.
uint8_t res0;
// Must be 0.
uint16_t res1;
// Number of uint32_t entry configuration masks that follow.
uint32_t entryCount;
enum : uint32_t {
// Additional flag indicating an entry is public.
SPEC_PUBLIC = 0x40000000u,
// Additional flag indicating an entry is overlayable at runtime.
// Added in Android-P.
SPEC_OVERLAYABLE = 0x80000000u,
};
};
Copy the code
This structure defines the ID of each resource type and an entryCount that can be configured. The ID is incremented one by one, and entryCount means that each resource item in the resource type can be followed by several configurations. For example, layout can be followed by several layout-v21, V22, etc., which contain the corresponding specific layout file value resource value.
If loaded_idmap is not empty, then the package needs to override the resource type of a package. Try to find IdmapEntry_header by id. Prepare for coverage.
At this point, the following is done:
Type_builder_map [typeSpec ID-1] = Create a TypeSpecPtrBuilder (type_spec, IdmapEntry_header).
The mapping between each resource type and ID is preliminarily constructed.
Parsing resource items
case RES_TABLE_TYPE_TYPE: { const ResTable_type* type = child_chunk.header<ResTable_type, kResTableTypeMinSize>(); . // Type chunks must be preceded by their TypeSpec chunks. std::unique_ptr<TypeSpecPtrBuilder>& builder_ptr = type_builder_map[type->id - 1]; if (builder_ptr ! = nullptr) { builder_ptr->AddType(type); } else { ... } } break;Copy the code
The following data block is parsed:
The TypeSpecPtrBuilder object was built when each resource type was parsed in the previous section. When no new resource item is encountered, the TypeSpecPtrBuilder is fetched and added to the object by AddType. This completes the mapping of ID to resource type to resource item. Take a look at the data structure of the resource item:
struct ResTable_type { struct ResChunk_header header; enum { NO_ENTRY = 0xFFFFFFFF }; // The type identifier this chunk is holding. Type IDs start // at 1 (corresponding to the value of the type bits in a // resource identifier). 0 is invalid. uint8_t id; enum { // If set, the entry is sparse, and encodes both the entry ID and offset into each entry, // and a binary search is used to find the key. Only available on platforms >= O. // Mark any types that use this with a v26 qualifier to prevent runtime issues on older // platforms. FLAG_SPARSE = 0x01, }; uint8_t flags; // Must be 0. uint16_t reserved; // Number of uint32_t entry indices that follow. uint32_t entryCount; // Offset from header where ResTable_entry data starts. uint32_t entriesStart; // Configuration this collection of entries is designed for. This must always be last. ResTable_config config; };Copy the code
Each resource entry contains a header, a configuration (locale), and an entry offset. What is an entry?
This entry is the data that we read programmatically.
Finally let’s look at TypeSpecPtrBuilder
class TypeSpecPtrBuilder {
public:
explicit TypeSpecPtrBuilder(const ResTable_typeSpec* header,
const IdmapEntry_header* idmap_header)
: header_(header), idmap_header_(idmap_header) {
}
void AddType(const ResTable_type* type) {
types_.push_back(type);
}
TypeSpecPtr Build() {
// Check for overflow.
using ElementType = const ResTable_type*;
if ((std::numeric_limits<size_t>::max() - sizeof(TypeSpec)) / sizeof(ElementType) <
types_.size()) {
return {};
}
TypeSpec* type_spec =
(TypeSpec*)::malloc(sizeof(TypeSpec) + (types_.size() * sizeof(ElementType)));
type_spec->type_spec = header_;
type_spec->idmap_entries = idmap_header_;
type_spec->type_count = types_.size();
memcpy(type_spec + 1, types_.data(), types_.size() * sizeof(ElementType));
return TypeSpecPtr(type_spec);
}
private:
DISALLOW_COPY_AND_ASSIGN(TypeSpecPtrBuilder);
const ResTable_typeSpec* header_;
const IdmapEntry_header* idmap_header_;
std::vector<const ResTable_type*> types_;
};
Copy the code
It’s easy to see this data type. It holds the headers for ResTable_typeSpec, idMAP_header_ to override, and data items to add.
Parsing third-party repository resources (especially resource sharing libraries, those with only resources and no code)
case RES_TABLE_LIBRARY_TYPE: { const ResTable_lib_header* lib = child_chunk.header<ResTable_lib_header>(); . loaded_package->dynamic_package_map_.reserve(dtohl(lib->count)); const ResTable_lib_entry* const entry_begin = reinterpret_cast<const ResTable_lib_entry*>(child_chunk.data_ptr()); const ResTable_lib_entry* const entry_end = entry_begin + dtohl(lib->count); for (auto entry_iter = entry_begin; entry_iter ! = entry_end; ++entry_iter) { std::string package_name; util::ReadUtf16StringFromDevice(entry_iter->packageName, arraysize(entry_iter->packageName), &package_name); . loaded_package->dynamic_package_map_.emplace_back(std::move(package_name), dtohl(entry_iter->packageId)); } } break;Copy the code
Here, too, it is simple to add each ResTable_lib_entry to the dynamic_package_map_ management of loaded_package. The ResTable_lib_entry data is also simple and simply records the ID and name of the package to which the resource belongs
{
// The package-id this shared library was assigned at build time.
// We use a uint32 to keep the structure aligned on a uint32 boundary.
uint32_t packageId;
// The package name of the shared library. \0 terminated.
uint16_t packageName[128];
};
Copy the code
Build the mapping in LoadPackage
// Flatten and construct the TypeSpecs.
for (auto& entry : type_builder_map) {
uint8_t type_idx = static_cast<uint8_t>(entry.first);
TypeSpecPtr type_spec_ptr = entry.second->Build();
...
// We only add the type to the package if there is no IDMAP, or if the type is
// overlaying something.
if (loaded_idmap == nullptr || type_spec_ptr->idmap_entries != nullptr) {
// If this is an overlay, insert it at the target type ID.
if (type_spec_ptr->idmap_entries != nullptr) {
type_idx = dtohs(type_spec_ptr->idmap_entries->target_type_id) - 1;
}
loaded_package->type_specs_.editItemAt(type_idx) = std::move(type_spec_ptr);
}
}
return std::move(loaded_package);
Copy the code
As you can see, each item cached in type_Builder_map is constructed into TypeSpecPtr and stored according to the current ID -1.
So there is a mapping of all resources in the LoadPackage. By the way, why is the first resource directory anim TypeID 1? This is so that the underlying calculation can start at subscript 0.
summary
Throughout the AssetManager initialization architecture, all string resources are stored in three string resource pools:
- Global_string_pool_ Global content resource pool
- Loaded_package ->type_string_pool_ Resource type Specifies the resource pool
- 3. Loaded_package ->key_string_pool_ Resource item name Specifies the resource pool.
The data for the Package data block is then saved. All package data blocks are stored in the loadedPackage object, which holds all TypeSpec objects, which are resource types, and a lot of ResTable_type, This object just uses the current specific resource entryID and has no specific data. It also maintains a dynamic mapping table for third-party repositories
Back to ApkAsset
Going a bit too far, recall that all of the above was done in NativeLoad, and that this procedure is just filling the Loaded_arsc_ object in ApkAssets.
Once you’ve done that, you go back to the method and pass the native object address back to the Java layer.
private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
throws IOException {
Preconditions.checkNotNull(path, "path");
mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
}
Copy the code
Then I’ll go back to the Native layer again with nativeGetStringBlock to get the Native StringBlock object.
static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) {
const ApkAssets* apk_assets = reinterpret_cast<const ApkAssets*>(ptr);
return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool());
}
Copy the code
inline const ResStringPool* GetStringPool() const {
return &global_string_pool_;
}
Copy the code
You can see that StringBlock gets the global string resource pool from Resource. Arsc. Contains all resource specific values.
At this point, ApkAssets holds two Native objects, one of which is the ApkAssets corresponding to the Native layer and the string resource pool.
AssetManager creation
AssetManager is created by adding ApkAssets to the Builder object and calling build.
public static class Builder { private ArrayList<ApkAssets> mUserApkAssets = new ArrayList<>(); public Builder addApkAssets(ApkAssets apkAssets) { mUserApkAssets.add(apkAssets); return this; } public AssetManager build() { // Retrieving the system ApkAssets forces their creation as well. final ApkAssets[] systemApkAssets = getSystem().getApkAssets(); final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size(); final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount]; System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length); final int userApkAssetCount = mUserApkAssets.size(); for (int i = 0; i < userApkAssetCount; i++) { apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i); } // Calling this constructor prevents creation of system ApkAssets, which we took care // of in this Builder. final AssetManager assetManager = new AssetManager(false /*sentinel*/); assetManager.mApkAssets = apkAssets; AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets, false /*invalidateCaches*/); return assetManager; }}Copy the code
}
Copy the code
In the build method, you can see that the entire ApkAssets are divided into two categories: System and App. Both types of ApkAssets are held by the AssetManager and are set to the native layer via Native SetApkAssets.
Obtain the System resource package
private static final String FRAMEWORK_APK_PATH = "/system/framework/framework-res.apk";
static AssetManager sSystem = null;
public static AssetManager getSystem() {
synchronized (sSync) {
createSystemAssetsInZygoteLocked();
return sSystem;
}
}
private static void createSystemAssetsInZygoteLocked() {
if (sSystem != null) {
return;
}
// Make sure that all IDMAPs are up to date.
nativeVerifySystemIdmaps();
try {
final ArrayList<ApkAssets> apkAssets = new ArrayList<>();
apkAssets.add(ApkAssets.loadFromPath(FRAMEWORK_APK_PATH, true /*system*/));
loadStaticRuntimeOverlays(apkAssets);
sSystemApkAssetsSet = new ArraySet<>(apkAssets);
sSystemApkAssets = apkAssets.toArray(new ApkAssets[apkAssets.size()]);
sSystem = new AssetManager(true /*sentinel*/);
sSystem.setApkAssets(sSystemApkAssets, false /*invalidateCaches*/);
} catch (IOException e) {
throw new IllegalStateException("Failed to create system AssetManager", e);
}
}
Copy the code
Can see right now will be to build a static AssetManager, the AssetManager management only one resource package: / system/framework/framework – res. Apk. It was also good to overlay the resources that needed to be overlapped on the apK according to the /data/resource-cache/overlays. List duplicate resource file.
Open the resource. Arsc file and find that the packageID is 0x01, which is different from the application 0x7f
The construction of AssetManager
private AssetManager(boolean sentinel) { mObject = nativeCreate(); . }Copy the code
The only thing you do in the constructor is create a GuardedAssetManager object under Native via nativeCreate.
struct GuardedAssetManager : public ::AAssetManager {
Guarded<AssetManager2> guarded_assetmanager;
};
static jlong NativeCreate(JNIEnv* /*env*/, jclass /*clazz*/) {
// AssetManager2 needs to be protected by a lock. To avoid cache misses, we allocate the lock and
// AssetManager2 in a contiguous block (GuardedAssetManager).
return reinterpret_cast<jlong>(new GuardedAssetManager());
}
Copy the code
This is essentially an AAssetManager object wrapped around AssetManager2. “Guarded” is a little like “smart Pointers”, but it lets objects keep their own mutex and its operations atomic.
NativeSetApkAssets Sets all ApkAssets to the AssetManager2 object
Guarded<AssetManager2>* AssetManagerForNdkAssetManager(::AAssetManager* assetmanager) {
if (assetmanager == nullptr) {
return nullptr;
}
return &reinterpret_cast<GuardedAssetManager*>(assetmanager)->guarded_assetmanager;
}
static Guarded<AssetManager2>& AssetManagerFromLong(jlong ptr) {
return *AssetManagerForNdkAssetManager(reinterpret_cast<AAssetManager*>(ptr));
}
static void NativeSetApkAssets(JNIEnv* env, jclass /*clazz*/, jlong ptr,
jobjectArray apk_assets_array, jboolean invalidate_caches) {
const jsize apk_assets_len = env->GetArrayLength(apk_assets_array);
std::vector<const ApkAssets*> apk_assets;
apk_assets.reserve(apk_assets_len);
for (jsize i = 0; i < apk_assets_len; i++) {
jobject obj = env->GetObjectArrayElement(apk_assets_array, i);
if (obj == nullptr) {
std::string msg = StringPrintf("ApkAssets at index %d is null", i);
jniThrowNullPointerException(env, msg.c_str());
return;
}
jlong apk_assets_native_ptr = env->GetLongField(obj, gApkAssetsFields.native_ptr);
if (env->ExceptionCheck()) {
return;
}
apk_assets.push_back(reinterpret_cast<const ApkAssets*>(apk_assets_native_ptr));
}
ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
assetmanager->SetApkAssets(apk_assets, invalidate_caches);
}
Copy the code
In fact, the logic is quite simple, which is to convert a Java array into a vector set to AssetManager2.
AssetManager2 builds the in-memory resource table
bool AssetManager2::SetApkAssets(const std::vector<const ApkAssets*>& apk_assets,
bool invalidate_caches) {
apk_assets_ = apk_assets;
BuildDynamicRefTable();
RebuildFilterList();
if (invalidate_caches) {
InvalidateCaches(static_cast<uint32_t>(-1));
}
return true;
}
Copy the code
AssetManager2 builds a dynamic resource table in memory:
- 1.BuildDynamicRefTable build dynamic resource reference table
- 2.RebuildFilterList Builds the filtered configuration list
- 3.InvalidateCaches refreshes the cache
Build dynamic resource reference tables
void AssetManager2::BuildDynamicRefTable() {
package_groups_.clear();
package_ids_.fill(0xff);
// 0x01 is reserved for the android package.
int next_package_id = 0x02;
const size_t apk_assets_count = apk_assets_.size();
for (size_t i = 0; i < apk_assets_count; i++) {
const LoadedArsc* loaded_arsc = apk_assets_[i]->GetLoadedArsc();
for (const std::unique_ptr<const LoadedPackage>& package : loaded_arsc->GetPackages()) {
// Get the package ID or assign one if a shared library.
int package_id;
if (package->IsDynamic()) {
package_id = next_package_id++;
} else {
package_id = package->GetPackageId();
}
// Add the mapping for package ID to index if not present.
uint8_t idx = package_ids_[package_id];
if (idx == 0xff) {
package_ids_[package_id] = idx = static_cast<uint8_t>(package_groups_.size());
package_groups_.push_back({});
DynamicRefTable& ref_table = package_groups_.back().dynamic_ref_table;
ref_table.mAssignedPackageId = package_id;
ref_table.mAppAsLib = package->IsDynamic() && package->GetPackageId() == 0x7f;
}
PackageGroup* package_group = &package_groups_[idx];
// Add the package and to the set of packages with the same ID.
package_group->packages_.push_back(ConfiguredPackage{package.get(), {}});
package_group->cookies_.push_back(static_cast<ApkAssetsCookie>(i));
// Add the package name -> build time ID mappings.
for (const DynamicPackageEntry& entry : package->GetDynamicPackageMap()) {
String16 package_name(entry.package_name.c_str(), entry.package_name.size());
package_group->dynamic_ref_table.mEntries.replaceValueFor(
package_name, static_cast<uint8_t>(entry.package_id));
}
}
}
// Now assign the runtime IDs so that we have a build-time to runtime ID map.
const auto package_groups_end = package_groups_.end();
for (auto iter = package_groups_.begin(); iter != package_groups_end; ++iter) {
const std::string& package_name = iter->packages_[0].loaded_package_->GetPackageName();
for (auto iter2 = package_groups_.begin(); iter2 != package_groups_end; ++iter2) {
iter2->dynamic_ref_table.addMapping(String16(package_name.c_str(), package_name.size()),
iter->dynamic_ref_table.mAssignedPackageId);
}
}
}
Copy the code
Why build a dynamic resource mapping table? Almost all the relationships between resources have been built in the original LoadArsc object.
The problem is that when the packageID of this third-party resource is compiled to this location, the packageID is actually loaded sequentially and incrementally set according to the compile order. From the first double loop, we also have a packageID running, based on the order in which it was loaded into memory.
So this is a big question, right? If there is a third-party repository with the packageId of 0x03, the loading order is the first, and the corresponding packageId in memory is 0x02(0x01 is always given to the system), so the wrong object will be found.
So the second loop is designed to solve this problem.
- 1. What the double loop does is actually collect all the packages stored in the LoadArsc object into the package_group and set a cookie for each index. This cookie is essentially an int that increases as the package increases. Next, load your own packageId for each dynamic repository. And set it to the mEntries of DynamicPackageEntry.
- 2. Get all the data in package_group, loop through all the package_group, and call addMapping:
status_t DynamicRefTable::addMapping(const String16& packageName, uint8_t packageId)
{
ssize_t index = mEntries.indexOfKey(packageName);
if (index < 0) {
return UNKNOWN_ERROR;
}
mLookupTable[mEntries.valueAt(index)] = packageId;
return NO_ERROR;
}
Copy the code
As you can see from the above, mEntries stores the compile-time packageID and mLookupTable stores the runtime ID. This will correctly find the compile-time ID by the runtime ID.
RebuildFilterList Builds the filtered entry list
void AssetManager2::RebuildFilterList() { for (PackageGroup& group : package_groups_) { for (ConfiguredPackage& impl : group.packages_) { // Destroy it. impl.filtered_configs_.~ByteBucketArray(); // Re-create it. new (&impl.filtered_configs_) ByteBucketArray<FilteredConfigGroup>(); // Create the filters here. impl.loaded_package_->ForEachTypeSpec([&](const TypeSpec* spec, uint8_t type_index) { FilteredConfigGroup& group = impl.filtered_configs_.editItemAt(type_index); const auto iter_end = spec->types + spec->type_count; for (auto iter = spec->types; iter ! = iter_end; ++iter) { ResTable_config this_config; this_config.copyFromDtoH((*iter)->config); if (this_config.match(configuration_)) { group.configurations.push_back(this_config); group.types.push_back(*iter); }}}); }}}Copy the code
TypeSpec is the object that holds all resource mappings when ApkAssets are generated. The method is to select the ResTable_type (resource item) by iterating through the current config(e.g. locale, SIM card environment) consistent config.
This allows us to map all relationships to the Package_groups_ object.
Clears all resource ids in the cache
void AssetManager2::InvalidateCaches(uint32_t diff) { if (diff == 0xffffffffu) { // Everything must go. cached_bags_.clear(); return; } for (auto iter = cached_bags_.cbegin(); iter ! = cached_bags_.cend();) { if (diff & iter->second->type_spec_flags) { iter = cached_bags_.erase(iter); } else { ++iter; }}}Copy the code
Cached_bags_ actually caches resource ids that have been generated in the past and is cleared if necessary. This is usually done when the AssetManager configuration changes to avoid interfering with cached_bags_.
After three steps, the Native layer AssetManager holds the resource mapping in APK via package_group in a disguised way.
conclusion
Due to the limitation of space, the next article will analyze how Android initializes the resource system to search for resources. You’ll see how ResTable_entry, which you haven’t used in this article, and Res_Value, which holds real data, work in resource look-up.
First of all, the sequence diagram of Java layer and Native layer is included:
The process is long and not exhaustive, just taking care of the backbone. It can be known that in the whole process, frequent communication with native layer is kept. Time series diagrams alone may not sum up well enough.
Here we can see that the AssetManager controls ApkAsset in the Java layer. The opposite AssetManager will correspond to the Native layer’s AssetManager2, which controls the Native layer’s ApkAsset object.
In other words, ApkAsset is the resource folder unit, and AssetManager only controls this granularity. There are also four important data structures in ApkAsset:
-
- Resources_asset_ represents an Asset that is essentially the resource. Arsc zip resource FileMap
-
- Loaded_arsc_ is actually a LoadedArsc, which is a mapping object generated after resource. Arsc parses resources.
LoadedArsc also has two important data structures:
-
- Global_string_pool_ Global string resource pool
-
- LoadedPackage Package data object
LoadedPackage contains a large amount of information about resource objects, as well as real data, which also contains several important data structures:
-
- Type_string_pool_ Resource type A character string, such as layout, menu, and ANim folder names
-
- Key_string_pool_ Specifies the name of the resource
-
- Type_specs_ stores the mapping between all resource types and resource items
-
- Dynamic_package_map_ is a 2-time map built to handle conflicts between packgeids and runtime ids compiled by third-party repositories, but resolving conflicts is not resolved here
With this information, ApkAsset can construct a complete relationship between resources based on resource. Arsc.
Of course, this is not enough. When ApkAsset is set to AssetManager2, AssetManager2 makes the following efforts to load memory more quickly and accurately:
- 1. Save multiple PackageGroup objects (including ConfiguredPackage), which contain all package data blocks.
- 2. Build a dynamic resource table and place it in package_group to solve the packageID runtime/compile-time conflict problem
- 3. Select resource configurations that match the current environment to FilteredConfigGroup in advance for quick access.
- 4. Cache the BagID that has been accessed, that is, the complete resource ID.
That’s why it’s called Asset Manager. At the same time, we can see that throughout the process, the resource parsing process will be led by resource. Arsc, parsing the entire Apk resource. But essentially, zip decompresses the corresponding data blocks, and only accessing these zipentries can actually access the data. Of course, the relevant strings are centrally controlled in the three string cache pools. If you want to retrieve them, you can retrieve them from the index of each buffer pool.
So, let’s pick up where we left off last time, and see what Android is doing to make it more efficient. Here’s a summary of the overall cache situation:
- ActivityResources an ArrayList for weak references to Resources
- 2. Use ResourcesKey as the key, and weak references of ResourcesImpl as the Map cache of value.
- 3.ApkAssets also have a cache in the memory. The cache is divided into two parts: Active ApkAssets loaded by mLoadedApkAssets and inactive ApkAssets loaded by mCacheApkAssets
- 4. ApkAsset stores three global string resource pools for quick lookups. The object corresponding to the Java layer is usually StringBlock
- 5. In order to quickly find resources that match the current environment configuration (screen density, locale, etc.), there is a FilteredConfigGroup object in the filter build resource phase to provide fast lookup.
- 6. The cache BagID
Analyzing resource management systems, what can be concluded about Android performance optimization?
1. Package volume optimization, we can confuse the resource file, so that the package size is smaller. Why is that? Because by obfuscating resources, you can reduce the size of the string resource pool in resource-.arsc, thereby reducing the resource size.
2. Resource management system Searching for resources is a time-consuming process by nature, so The Android system does six layers of caching. Ensure that resources can be found relatively quickly. This is why resource resolution method stalling is often reported when the method stalling test is first started. The solution is to open a thread pool to properly parse resources ahead of time.
3. After reading the source code, we can also understand why the StringBlock must be updated whenever a plugin, hot fix, or resource fix is involved. StringBlock holds a global string resource pool, and if you don’t update the resource pool after fixing it, you’re going to get a resource lookup exception. Of course, Tinker is correct in saying that “system resources should be loaded in advance, otherwise the exception will be caused”, but the root cause of the error is wrong analysis.
4. Of course, from this process, we can actually realize that the entire Android resource system can be further optimized: 1. The resource files such as Asset are not compressed, we actually get the ZipEntry corresponding to the asset folder in APK. In fact, we can do a compression ourselves, take the data stream and decompress it further. It’s just a strategy to replace space with time. 2. In the whole process, string resource pool is a complete resource, in fact, we can use Huffman encoding to further compress the data in the string resource pool, of course, so we need to invade the compilation process, now I do not have this level.
Author: yjy239 links: www.jianshu.com/p/02a253989… The copyright of the book belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.