How the Resources class is initialized in the Java layer and how to obtain the corresponding Resources.

Virtually every resource is managed by ResourcesManager. But Resources is like a proxy class, and the actual operations are done by ResourcesImpl.

What does ResourcesImpl manage? It generally manages various resource files in an APK, among which it has a very core class AssetManager, which is really connected to the native layer for parsing. Although the name of AssetManager seems to refer to managing the Asset folder, its scope of management is generally ApkAsset, the resource abstraction object.

With that in mind, let’s take a look at how the entire Android system manages ApksAsset and AssetManager. Basically understand the Android system resource management system.

The body of the

From the sequence diagram above, we can focus on how ContextImpl is initialized. To get access to resources, look at the createActivityContext method.

static ContextImpl createActivityContext(ActivityThread mainThread, LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId, Configuration overrideConfiguration) { if (packageInfo == null) throw new IllegalArgumentException("packageInfo"); String[] splitDirs = packageInfo.getSplitResDirs(); ClassLoader classLoader = packageInfo.getClassLoader(); if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) { try { classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName); splitDirs = packageInfo.getSplitPaths(activityInfo.splitName); } catch (NameNotFoundException e) { // Nothing above us can handle a NameNotFoundException, better crash. throw new RuntimeException(e); } finally { } } ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName, activityToken, null, 0, classLoader); // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY. displayId = (displayId ! = Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY; final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY) ? packageInfo.getCompatibilityInfo() : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; final ResourcesManager resourcesManager = ResourcesManager.getInstance(); // Create the base resources for which all configuration contexts for this Activity // will be rebased upon. context.setResources(resourcesManager.createBaseActivityResources(activityToken, packageInfo.getResDir(), splitDirs, packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, classLoader)); context.mDisplay = resourcesManager.getAdjustedDisplay(displayId, context.getResources()); return context; }Copy the code

Here we can see the LoadApk object we were familiar with in the add-in talk analysis article. This object represents the memory object of the APK package in Android.

Let’s not go into the details of where this object came from. But we can get a rough idea of what the method name does:

  • 1. Read the resource folder saved in LoadApk
  • 2. Read the classLoader in LoadApk as the main classLoader of the current application.
  • 3. Instantiate ContextImpl, which in most cases is the Context we use for application development.
  • 4. Initialize ResourceManager
    1. Set resource management to the Context so that the Context has the ability to access resources.

This article mainly focuses on the loading of resources, so we only need to study ResourceManager, can understand the principle of resource loading, pay attention to the following two lines of code:

        final ResourcesManager resourcesManager = ResourcesManager.getInstance();

        // Create the base resources for which all configuration contexts for this Activity
        // will be rebased upon.
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
Copy the code

Therefore, resource initialization is placed on this function:

  • CreateBaseActivityResources open Resource mapping, preliminary analytical resources in the Resource

Open the resource mapping createBaseActivityResources

public @Nullable Resources createBaseActivityResources(@NonNull IBinder activityToken, @Nullable String resDir, @Nullable String[] splitResDirs, @Nullable String[] overlayDirs, @Nullable String[] libDirs, int displayId, @Nullable Configuration overrideConfig, @NonNull CompatibilityInfo compatInfo, @Nullable ClassLoader classLoader) { try { final ResourcesKey key = new ResourcesKey( resDir, splitResDirs, overlayDirs, libDirs, displayId, overrideConfig ! = null ? new Configuration(overrideConfig) : null, // Copy compatInfo); classLoader = classLoader ! = null ? classLoader : ClassLoader.getSystemClassLoader(); synchronized (this) { // Force the creation of an ActivityResourcesStruct. getOrCreateActivityResourcesStructLocked(activityToken); } // Update any existing Activity Resources references. updateResourcesForActivity(activityToken, overrideConfig, displayId, false /* movedToDifferentDisplay */); // Now request an actual Resources object. return getOrCreateResources(activityToken, key, classLoader); } finally { } }Copy the code
  • 1. Configure and generate a ResourcesKey based on all resource directories and display ids.
    1. GetOrCreateActivityResourcesStructLocked ActivityResources generated and stored in the cache to the map.
    1. Based on ResourcesKey, generate the actual ResourceImpl of a resource.

Cache ActivityResources into the list

    private ActivityResources getOrCreateActivityResourcesStructLocked(
            @NonNull IBinder activityToken) {
        ActivityResources activityResources = mActivityResourceReferences.get(activityToken);
        if (activityResources == null) {
            activityResources = new ActivityResources();
            mActivityResourceReferences.put(activityToken, activityResources);
        }
        return activityResources;
    }

    private static class ActivityResources {
        public final Configuration overrideConfig = new Configuration();
        public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
    }
Copy the code

This cache object essentially controls all of the Resources in Apk. With the cache, you don’t need to reopen the resource directory to read the Resources later. Essentially, as discussed in the View instantiation section, to reduce the number of reflections, the reflected View constructor is saved for next use.

Based on ResourcesKey, generate a ResourceImpl

updateResourcesForActivity

Let’s look at updateResourcesForActivity method, update the Resource configuration.

public void updateResourcesForActivity(@NonNull IBinder activityToken, @Nullable Configuration overrideConfig, int displayId, boolean movedToDifferentDisplay) { try { synchronized (this) { final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(activityToken); . // Rebase each Resources associated with this Activity. final int refCount = activityResources.activityResources.size();  for (int i = 0; i < refCount; i++) { WeakReference<Resources> weakResRef = activityResources.activityResources.get( i); Resources resources = weakResRef.get(); if (resources == null) { continue; } // Extract the ResourcesKey that was last used to create the Resources for this // activity. final ResourcesKey oldKey  = findKeyForResourceImplLocked(resources.getImpl()); if (oldKey == null) { continue; } // Build the new override configuration for this ResourcesKey. final Configuration rebasedOverrideConfig = new Configuration(); if (overrideConfig ! = null) { rebasedOverrideConfig.setTo(overrideConfig); } if (activityHasOverrideConfig && oldKey.hasOverrideConfiguration()) { // Generate a delta between the old base Activity override configuration and // the actual final override configuration that was used to figure out the // real delta this Resources object wanted. Configuration overrideOverrideConfig = Configuration.generateDelta( oldConfig, oldKey.mOverrideConfiguration); rebasedOverrideConfig.updateFrom(overrideOverrideConfig); } // Create the new ResourcesKey with the rebased override config. final ResourcesKey newKey = new ResourcesKey(oldKey.mResDir, oldKey.mSplitResDirs, oldKey.mOverlayDirs, oldKey.mLibDirs, displayId, rebasedOverrideConfig, oldKey.mCompatInfo); ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(newKey); if (resourcesImpl == null) { resourcesImpl = createResourcesImpl(newKey); if (resourcesImpl ! = null) { mResourceImpls.put(newKey, new WeakReference<>(resourcesImpl)); } } if (resourcesImpl ! = null && resourcesImpl ! = resources.getImpl()) { // Set the ResourcesImpl, updating it for all users of this Resources // object. resources.setImpl(resourcesImpl); } } } } finally { } }Copy the code

This method essentially updates the Resource instance saved in activityResources. You can see that each time the combined ResourceKey is tried to find if the old ResourceKey existed before.

If not, it will not continue. If not, it will generate a new ResourceKey from the old ResourceKey. If not, it will create a new ResourceImpl from the new ResourceKey. And set key and ResourceImpl to mResourceImpls.

You can see that there are two cores in the middle:

  • Search for corresponding ResourcesImpl findResourcesImplForKeyLocked
  • CreateResourcesImpl Create a ResourcesImpl and pause here, so let’s go to getOrCreateResources first, and then go back to these two methods.

getOrCreateResources

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) { synchronized (this) { if (activityToken ! = null) { final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(activityToken); // Clean up any dead references so they don't pile up. ArrayUtils.unstableRemoveIf(activityResources.activityResources, sEmptyReferencePredicate); // Rebase the key's override config on top of the Activity's base override. ... ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); if (resourcesImpl ! = null) { return getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); } // We will create the ResourcesImpl object outside of holding this lock. } else { // Clean up any dead references so they don't pile up. ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate); // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); if (resourcesImpl ! = null) { return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } // We will create the ResourcesImpl object outside of holding this lock. } // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now. ResourcesImpl resourcesImpl = createResourcesImpl(key); if (resourcesImpl == null) { return null; } // Add this ResourcesImpl to the cache. mResourceImpls.put(key, new WeakReference<>(resourcesImpl)); final Resources resources; if (activityToken ! = null) { resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); } else { resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } return resources; }}Copy the code

There are two cases:

  • 1. The presence of activityToken means that the application layer is developed
    1. The absence of activityToken refers to the system application

When activityToken exists

When activityToken exists, this is what you do when the application starts.

  • 1. First try to find if ResourcesImpl is cached as follows:
private ResourcesImpl findResourcesImplForKeyLocked(@NonNull ResourcesKey key) { WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.get(key); ResourcesImpl impl = weakImplRef ! = null ? weakImplRef.get() : null; if (impl ! = null && impl.getAssets().isUpToDate()) { return impl; } return null; }Copy the code

You can see that each ResourcesImpl will be saved to the mResourceImpls ArrayMap.

In the presence of ResourceImpl getOrCreateResourcesLocked will call, when through ResourcesImpl, in turn, to find the Resource proxy class, not found, would be to generate a new Resource, Add to the mResourceReferences weak reference cache.

The creation of ResourcesImpl

The ResourcesImpl needs to be created when it does not exist.

    private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }
Copy the code

You can see that the AssetManager is created when the ResourcesImpl is created. The AssetManager is the manager of asset resources in the APK package, and we have to deal with it whenever we need to access resources.

AssetManager creation preparation

protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { final AssetManager.Builder builder = new AssetManager.Builder(); if (key.mResDir ! = null) { try { builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/, false /*overlay*/)); } catch (IOException e) { Log.e(TAG, "failed to add asset path " + key.mResDir); return null; } } if (key.mSplitResDirs ! = null) { for (final String splitResDir : key.mSplitResDirs) { try { builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/, false /*overlay*/)); } catch (IOException e) { ... return null; } } } if (key.mOverlayDirs ! = null) { for (final String idmapPath : key.mOverlayDirs) { try { builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/, true /*overlay*/)); } catch (IOException e) { ... } } } if (key.mLibDirs ! = null) { for (final String libDir : key.mLibDirs) { if (libDir.endsWith(".apk")) { // Avoid opening files we know do not have resources, // like code-only .jar files. try { builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/, false /*overlay*/)); } catch (IOException e) { .... } } } } return builder.build(); }Copy the code

In Android 9.0, you need to use a core method called addApkAssets; In older versions, this method is replaced by assets. AddAssetPath. The reason we know how to load resources this way is because this is how resources load resources into AssetManager.

The steps here can be divided into two steps:

  • LoadApkAssets reads the resources in the resource directory to generate the ApkAsset object
  • AddApkAssets adds all the objects to the AssetManager builder and finally generates the AssetManager object

So there are three core methods, one is to create an AssetManager in build mode, one addApkAssets, one loadApkAssets to read the resources in the directory and then, let’s see once again what does this accomplish?

LoadApkAssets reads the resources of the directory and generates an ApkAsset object

private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay) throws IOException { final ApkKey newKey = new ApkKey(path, sharedLib, overlay); ApkAssets apkAssets = mLoadedApkAssets.get(newKey); if (apkAssets ! = null) { return apkAssets; } // Optimistically check if this ApkAssets exists somewhere else. final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(newKey); if (apkAssetsRef ! = null) { apkAssets = apkAssetsRef.get(); if (apkAssets ! = null) { mLoadedApkAssets.put(newKey, apkAssets); return apkAssets; } else { // Clean up the reference. mCachedApkAssets.remove(newKey); } } if (overlay) { apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path), false /*system*/); } else { apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib); } mLoadedApkAssets.put(newKey, apkAssets); mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets)); return apkAssets; }Copy the code
Resource Caching

We can see that an ApkAssets object is generated in all the resource directory paths and is cached as a level 2 cache.

  • Level 1 cache: mLoadedApkAssets holds all strong references to ApkAssets that have been loaded.
  • Level 2 cache: mCachedApkAssets holds weak references to all loaded ApkAssets.

First, check whether loaded resources already exist from mLoadedApkAssets. If not, try to check from mCachedApkAssets. If found, remove it from mCachedApkAssets and add it to mLoadedApkAssets.

This idea is actually reflected in Glide, where we can think of this cache as an in-memory cache and split it into two parts, active cache and inactive cache. Active caches hold strong references to avoid GC destruction, whereas inactive active caches hold weak references and are fine if the GC is destroyed.

When nothing can be found, resources have to be read from disk.

Create an ApkAssets resource object

ApkAssets can be created in two ways:

  • ApkAssets. LoadOverlayFromPath apk when using the additional corresponding ApkAsset overlapping resource directory
  • ApkAssets. LoadFromPath When APK uses general resources such as value resources and third-party libraries to create the corresponding ApkAsset.
   public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
            throws IOException {
        return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/);
    }

    public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system,
            boolean forceSharedLibrary) throws IOException {
        return new ApkAssets(path, system, forceSharedLibrary, false /*overlay*/);
    }

    public static @NonNull ApkAssets loadFromPath(@NonNull String path, boolean system)
            throws IOException {
        return new ApkAssets(path, system, false /*forceSharedLib*/, false /*overlay*/);
    }

    private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
            throws IOException {
        mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
        mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
    }
Copy the code

You can see that for each static method, the constructor’s nativeLoad eventually generates a pointer to the address at native and creates a StringBlock. What on earth is going on here? Let’s take a look at nativeLoad first.

NativeLoad of ApkAssets creates native objects

static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, jstring java_path, jboolean system, jboolean force_shared_lib, jboolean overlay) { ScopedUtfChars path(env, java_path); . std::unique_ptr<const ApkAssets> apk_assets; if (overlay) { apk_assets = ApkAssets::LoadOverlay(path.c_str(), system); } else if (force_shared_lib) { apk_assets = ApkAssets::LoadAsSharedLibrary(path.c_str(), system); } else { apk_assets = ApkAssets::Load(path.c_str(), system); } if (apk_assets == nullptr) { ... return 0; } return reinterpret_cast<jlong>(apk_assets.release()); }Copy the code

It can be seen that this native method is also divided into three situations to read resource data and generate ApkAssets native objects to return to the Java layer.

  • LoadOverlay Loads overlapping resources
  • LoadAsSharedLibrary loads third-party library resources
  • Load Loads common resources

What are overlapping resources, to quote Luo Shengyang’s explanation?

Suppose we are compiling package-1, we can set another package-2 to tell AAPT that if package-2 defines the same resources as package-1, Replace the resources defined in package-1 with those defined in package-2. With this Overlay mechanism, we can customize resources without losing generality.

For example, when we download a theme and replace it, we will replace the entire Android-related resource. In this case, the overlay folder contains the APK, which has only resources but no dex, and writes the relevant replaceable ids in a file. At this point, the AssetManager initializes and replaces all resources with this ID. This is a framework level replacement compared to a skin framework.

Let’s first look at the logic for loading a generic resource, Load.

ApkAssets::Load Reads the resources of the disk
static const std::string kResourcesArsc("resources.arsc"); std::unique_ptr<const ApkAssets> ApkAssets::Load(const std::string& path, bool system) { return LoadImpl({} /*fd*/, path, nullptr, nullptr, system, false /*load_as_shared_library*/); } std::unique_ptr<const ApkAssets> ApkAssets::LoadImpl( unique_fd fd, const std::string& path, std::unique_ptr<Asset> idmap_asset, std::unique_ptr<const LoadedIdmap> loaded_idmap, bool system, bool load_as_shared_library) { ::ZipArchiveHandle unmanaged_handle; int32_t result; if (fd >= 0) { result = ::OpenArchiveFd(fd.release(), path.c_str(), &unmanaged_handle, true /*assume_ownership*/); } else { result = ::OpenArchive(path.c_str(), &unmanaged_handle); }... std::unique_ptr<ApkAssets> loaded_apk(new ApkAssets(unmanaged_handle, path)); // Find the resource table. ::ZipString entry_name(kResourcesArsc.c_str()); ::ZipEntry entry; result = ::FindEntry(loaded_apk->zip_handle_.get(), entry_name, &entry); if (result ! = 0) {... loaded_apk->loaded_arsc_ = LoadedArsc::CreateEmpty(); return std::move(loaded_apk); } if (entry.method == kCompressDeflated) { ... } loaded_apk->resources_asset_ = loaded_apk->Open(kResourcesArsc, Asset::AccessMode::ACCESS_BUFFER); if (loaded_apk->resources_asset_ == nullptr) { ... return {}; } loaded_apk->idmap_asset_ = std::move(idmap_asset); const StringPiece data( reinterpret_cast<const char*>(loaded_apk->resources_asset_->getBuffer(true /*wordAligned*/)), loaded_apk->resources_asset_->getLength()); loaded_apk->loaded_arsc_ = LoadedArsc::Load(data, loaded_idmap.get(), system, load_as_shared_library); if (loaded_apk->loaded_arsc_ == nullptr) { ... return {}; } return std::move(loaded_apk); }Copy the code

In LoadImpl, it can be seen that the compression algorithm of resources is zip algorithm, so we can see that in this core method, resource reading is roughly divided into the following four steps:

  • 1.OpenArchive Open the zip file and generate the ApkAssets object \
  • 2. Use FindEntry to search for the resource. Arsc file in the APK package
  • 3. Read the resource. Arsc file in the APK package and the map containing the ID and the resource Asset folder.
  • 4. Generate a StringPiece object and use LoadedArsc::Load to read the data in it.

Zip algorithm is essentially a lossless compression, through phrase compression, coding compression (Huffman coding) compression. We also see that Android uses Libziparchive, the built-in system library that does not support ZIP64, which limits the size of package compression (it must be less than 32 bits (4G)). This is fundamentally because the system limits size, not just because the market limits apK size. In other words, even if you force a large APK package to be generated, Android will reject parsing. Here’s the core check:

if (file_length > static_cast<off64_t>(0xffffffff)) {
....
}
Copy the code

If you’re familiar with this process, you’ll know that Resource. Arsc is the core file in a ResTable, so let’s see what happens in LoadedArsc::Load.

Now we just need to worry about what the Open method reads after ApkAssets are generated and what StringPiece refers to.

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 (result ! = 0) { return {}; } if (entry.method == kCompressDeflated) { std::unique_ptr<FileMap> map = util::make_unique<FileMap>(); if (! map->create(path_.c_str(), ::GetFileDescriptor(zip_handle_.get()), entry.offset, entry.compressed_length, true /*readOnly*/)) { ... return {}; } std::unique_ptr<Asset> asset = Asset::createFromCompressedMap(std::move(map), entry.uncompressed_length, mode); if (asset == nullptr) { ... return {}; } return asset; } else { std::unique_ptr<FileMap> map = util::make_unique<FileMap>(); if (! map->create(path_.c_str(), ::GetFileDescriptor(zip_handle_.get()), entry.offset, entry.uncompressed_length, true /*readOnly*/)) { .... return {}; } std::unique_ptr<Asset> asset = Asset::createFromUncompressedMap(std::move(map), mode); if (asset == nullptr) { ... return {}; } return asset; }}Copy the code

The open method means that it checks the current Entry of the Zip that is passed in and whether the current Entry is compressed.

  • For compressed modules, FileMap maps the ZipEntry to virtual memory via Mmap (see Binder’s MMap principles for details). Then through Asset: : createFromCompressedMap through _CompressedAsset: : openChunk get StreamingZipInflater, return _CompressedAsset object.
  • If there is no compression module, through the FileMap ZipEntry through mmap mapped to the virtual memory, the last Asset: : createFromUncompressedMap, obtain FileAsset object.

In this case, resource. Arsc is not compressed in APK, so go below and return the corresponding FileAsset directly.

From this, you can see that ApkAsset will manage FileMap Asset objects mapped from ZipEntry.

Resource. Arsc stores content

This method is the way to parse the entire Android resource table, and once you understand this method, you can understand how Android finds the ID resource. It may be hard to get an intuitive view of the data structure just by looking at the source code, but let’s take a look at what’s in the APK package, resource. Arsc. Let’s look inside with the help of the AS parser:

From this table, you can see the type of resource on the left and the id of the resource and its specific path (or specific resource content) on the right. In general, we refer to the resource mapping tables stored in resource. Arsc as ResTable. Of course, if it is similar to String, id will be a String content:

When we use apK internal resources, we usually import them in the same way as R.id.xx, which is essentially an int corresponding to this. During the packaging process, the corresponding ID will be packaged into resource. Arsc. During the runtime, the file will be parsed and the corresponding path will be found through the mapping ID to correctly find the resources we need.

The component structure of each resource ID was briefly discussed earlier in the Plug-in Infrastructure article, so we’ll go into details here. When you mention data structures in the resource. Arsc file, you’ll be sure to mention the following figure.

Android resource packaging process

Before talking about the resource. Arsc, I first talk about the resources in the directory in the Android package tools/frameworks/base/tools/aapt /

Aapt is a tool we deal with a lot in development, but never pay attention to. This tool mainly collects and packages resource files for our APK and generates resource. Arsc files. In this process, all XML resource files are converted from text format to binary format when packaged. There are two reasons for this:

  • Binary XML files take up less space. All strings are collected into the string dictionary (also known as the string resource pool). All strings are de-duplicated, and repeated strings are indexed, essentially similar to zip compression.
  • 2. The binary reading and parsing speed is faster than the text speed, because the string is deduplicated, the data to be read is much smaller.

The following resources are available throughout the APK package:

  • 1. Binary XML files
  • 2. Resource. Arsc file
  • 3. Uncompressed Asset files and so libraries

Then the whole APK resource packaging must include these processes. It can be roughly divided into the following three steps:

  • 1. Collect resources
  • 2. Collect Xml resources, press the Xml file, and convert it to binary Xml
  • 3. Collect resources and generate the resource. Arsc file

The whole package can be roughly divided into the following steps:

Collecting resources:
  • 1. Parse androidmanifest.xml and create ResourcesTable according to the package tag
  • 2. Add the referenced resource package. For example, the layout_width of the system, or the resources defined by the application, these reference resource bundles will be added.
  • 3. Collect resource files
  • 4. Add the collected resource files to the resource table
  • 5. Compile the value class resource. At this time, an entry for each resource type will be added. Each String is called an entry, and strings map to different real strings in different languages. These are called config.
  • 6. Assign an ID to the Bag resource. There are many other types of resources besides string for resources of type VALUES, some of which are special, such as those of the bag, Style, Plurals, and Array classes. These resources define some special values for themselves. These resources with special values are collectively called Bag resources
Collecting Xml Resources
  • 7. Compile Xml resource files: Parse Xml files to generate XMLNode
  • 8. Compile Xml resource files: Assign attribute names resource IDS. Each Xml file assigns resource ids to attribute names starting from the root node, and then to attribute names recursively assigned to each child node, until the attribute names of each node obtain resource ids. The following

  • 9. Compile Xml resource files: parse attribute values; The previous step resolves the name of the attribute of the Xml element, and this step resolves the value of the attribute of the Xml element. Find the corresponding string in BAG using the resource ID of the previous step as the result of parsing. (“@+id/XXX”+ symbol means create one if there is no corresponding resource ID)
Flatten Xml resources

With Xml parsing resources ready, start flattening the Xml file, converting the text file into a binary file.

  • 10. Collect property names and strings with resource IDS; In addition to collecting the name strings of Xml element attributes with resource ids, this step also collects the corresponding resource ids in an array. The property name strings collected here are stored in a string resource pool, and they correspond one to one with the collected resource ID array.
  • Collect other strings, such as control names, namespaces, and so on
  • 12. Write the Xml header. Contains the type representing the header (RES_XML_TYPE), the header size, and the entire XML file size. The resulting Xml binary is a series of chunks, each with a header that describes the chunk’s meta information. At the same time, the entire Xml binary can be viewed as a total chunk with a header of type ResXMLTree_header.
  • 13. Write the strings in step 10 and step 11 into the string resource pool in strict sequence. Write the header size and type RES_STRING_POOL_TYPE
  • 14. Write resource ids. The ids collected in Step 10 will be written to the XML file in sequence as a single chunk, following the string pool.
  • 15. Flatten the Xml file and replace all strings with indexes in the string pool
Generate the resource. Arsc resource table

From the first step, a large amount of data about the resource is collected and stored in the resource table (in memory at this point), where you actually need to generate a file.

  • 16. Collect type strings such as layout and ID
  • 17. Collect resource Item Name String Retrieves the name of each item in the type string
  • 18. Collect the resource item value string to obtain the specific value of each resource.
  • 14. Write type RES_TABLE_PACKAGE_TYPE to the header of the Package resource metadata block
  • 15. Write type string resource pool to refer to (layout, menu,strings, etc. XML file names)
  • String resource pool, which refers to the name of the data item in each resource type (e.g. Layout has a main.xml file name)
  • 17. Write type specification data block, type is RES_TABLE_TYPE_SPEC_TYPE; The type specification refers to this (folder layout, various data in menu).
    1. Write type resource item data block, type RES_TABLE_TYPE_TYPE, used to describe a type resource item header. Each resource item data block will point to a resource entry, which contains the real data of the current resource item in various situations, such as MIpmap and drawable, specific file paths under folders of different resolutions.
    1. Type is RES_TABLE_TYPE, where size refers to the resource. Arsc size
  • 20. Write the value of the resource item string resource pool
  • 21. Write the Package data block

This completes the generation of the resource. Arsc file.

Finally, a few additional steps are needed to complete the resources that APK has not yet packaged.

  • 1. Convert AndroidMainfest.xml to binary files
  • 2. Generate the R.java file
  • 3. Package assets, resources. Arsc, and binary Xml files into APK.

So that’s how Android packaging works.

Arsc file data structure analysis

Analyze the Resource. Arsc file based on the packaging process shown above and in the previous section.

At the top of the entire table is a flag bit for RES_TABLE_TYPE to indicate where the entire resource map starts parsing. This is followed by the size of the header, the size of the entire file, and how many of the package’s resources are held.

In the entire resource mapping table, the first chunk is the string pool. The type is RES_STRING_POOL_TYPE. Throughout the file generation process all the resource value strings are collected, put into this pool, and indexed.

The last chunk is the Package data block written at the end of the generation of the resource. Arsc file. Packge data is roughly divided into the following chunks:

  • 1. The header of Package, type is RES_TABLE_PACKAGE_TYPE.
  • 2. Type specification of Package Name string, resource type value name string Resource pool
  • 3. The header of the RES_TABLE_TYPE_SPEC_TYPE type specification of the Package
  • 4. The header of a resource entry of Package RES_TABLE_TYPE_TYPE type, which contains a pointer to entry.

With an overview of the entire resource. Arsc file, take a look at how LoadedArsc::Load parses.

LoadedArsc::Load

std::unique_ptr<const LoadedArsc> LoadedArsc::Load(const StringPiece& data, const LoadedIdmap* loaded_idmap, bool system, bool load_as_shared_library) { std::unique_ptr<LoadedArsc> loaded_arsc(new LoadedArsc()); loaded_arsc->system_ = system; ChunkIterator iter(data.data(), data.size()); while (iter.HasNext()) { const Chunk chunk = iter.Next(); switch (chunk.type()) { case RES_TABLE_TYPE: if (! loaded_arsc->LoadTable(chunk, loaded_idmap, load_as_shared_library)) { return {}; } break; default: ... break; }}... }Copy the code

The first thing to do is to iterate over the resource. Arsc file’s flag header, RES_TABLE_TYPE, after parsing all the chunks of the zip. After that, we start reading the data, looking for the following structure:

LoadedArsc::LoadTable

bool LoadedArsc::LoadTable(const Chunk& chunk, const LoadedIdmap* loaded_idmap, bool load_as_shared_library) { const ResTable_header* header = chunk.header<ResTable_header>(); . const size_t package_count = dtohl(header->packageCount); size_t packages_seen = 0; packages_.reserve(package_count); 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: if (global_string_pool_.getError() == NO_INIT) { status_t err = global_string_pool_.setTo(child_chunk.header<ResStringPool_header>(), child_chunk.size()); } else { ... } break; case RES_TABLE_PACKAGE_TYPE: { if (packages_seen + 1 > package_count) { .... return false; } packages_seen++; std::unique_ptr<const LoadedPackage> loaded_package = LoadedPackage::Load(child_chunk, loaded_idmap, system_, load_as_shared_library); if (! loaded_package) { return false; } packages_.push_back(std::move(loaded_package)); } break; default: ... break; }}... }Copy the code

In the LoadPackage method, two large areas of data are loaded:

  • 1.RES_STRING_POOL_TYPE represents all strings in the resource, the resource pool style (excluding the resource type name and the resource data item name). Parse the following:

  • For example, string.xml, a value in an R.string.xxx, or a specific path to a file in the Drawable folder

  • RES_TABLE_PACKAGE_TYPE represents the entire Package data block, which is parsed as follows:

ResStringPool parsing process

Let’s first look at the structure of the Xml string resource pool loaded into memory:

struct ResStringPool_header
{
    struct ResChunk_header header;

    // Number of strings in this pool (number of uint32_t indices that follow
    // in the data).
    uint32_t stringCount;

    // Number of style span arrays in the pool (number of uint32_t indices
    // follow the string indices).
    uint32_t styleCount;

    // Flags.
    enum {
        // If set, the string index is sorted by the string values (based
        // on strcmp16()).
        SORTED_FLAG = 1<<0,

        // String pool is encoded in UTF-8
        UTF8_FLAG = 1<<8
    };
    uint32_t flags;

    // Index from header of the string data.
    uint32_t stringsStart;

    // Index from header of the style data.
    uint32_t stylesStart;
};
Copy the code

This data structure is actually this part of the string resource pool:

From this header we can parse to the size of the entire resource pool, the number of strings, the number of styles, the tag, and the string pool starting position offset and the style pool starting position offset.

The calculation is actually quite simple:

String pool start position = header ADDRESS +stringsStart style Resource pool start position = header address +stylesStart

Note that the number of strings/styles is not the number of strings/styles written. Char is used to calculate how much of the entire resource StringPool/StylePool is occupied by the offset in the setTo method.

It is also worth noting that there are two more important Entrys in the entire string resource pool that have yet to be discussed. The positions of these two entries (offset arrays) are shown below, after the header:

The important thing that these two offset arrays do is, when we try to find the contents of a String through index, we access the offset array to find the position of the corresponding String in the entire pool. The calculation method is as follows:

String offset array start = header + header.size style Offset array start = string offset array start + string size

Since resources are written strictly sequentially, it is possible to find each other’s resources by index. Let’s look at how string8At finds strings:

const char* ResStringPool::string8At(size_t idx, size_t* outLen) const
{
    if (mError == NO_ERROR && idx < mHeader->stringCount) {
        if ((mHeader->flags&ResStringPool_header::UTF8_FLAG) == 0) {
            return NULL;
        }
        const uint32_t off = mEntries[idx]/sizeof(char);
        if (off < (mStringPoolSize-1)) {
            const uint8_t* strings = (uint8_t*)mStrings;
            const uint8_t* str = strings+off;

            decodeLength(&str);

            const size_t encLen = decodeLength(&str);
            *outLen = encLen;

            if ((uint32_t)(str+encLen-strings) < mStringPoolSize) {
                return stringDecodeAt(idx, str, encLen, outLen);

            } else {
                ...
            }
        } else {
            ...
        }
    }
    return NULL;
}
Copy the code

At the bottom of Android, there is a layer of caching mCache that holds parsed resource strings of uINT_16 length.

The algorithm for parsing String is as follows:

Uint32_t off = Entries[index]

If the offset element off is not set, this is the offset of the current string from the start of the resource pool. If the offset element off is set, the current offset is cleared and len is added to the next character. This gives you flexibility to combine strings.

Corresponding to the string string starting address (unit uint8_t) = mString(starting address of the string resource pool) + off

Finally, the following method is called to parse the string in the resource pool:

const char* ResStringPool::stringDecodeAt(size_t idx, const uint8_t* str,
                                          const size_t encLen, size_t* outLen) const {
    const uint8_t* strings = (uint8_t*)mStrings;

    size_t i = 0, end = encLen;
    while ((uint32_t)(str+end-strings) < mStringPoolSize) {
        if (str[end] == 0x00) {
            if (i != 0) {
                ...
            }

            *outLen = end;
            return (const char*)str;
        }

        end = (++i << (sizeof(uint8_t) * 8 * 2 - 1)) | encLen;
    }

    // Reject malformed (non null-terminated) strings
 ...
    return NULL;
}
Copy the code

You can see that the algorithm is as follows:

Within the size limit of the String resource pool, unit_8, the String is written over and over again, and the entire data is moved 15 bits to the left, stopping parsing at 0x00 and setting the result to outLen. The usual outLen is a pointer to the encLen encLen is the content, and the encLen is built by parsing the content of the STR, so this method essentially writes to the STR.

It’s tricky, but it’s not this hard to understand.

Global_string_pool_ = global_string_pool_ = global_string_pool_

Package data block parsing, LoadedPackage::Load

std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, const LoadedIdmap* loaded_idmap, bool system, bool load_as_shared_library) { ATRACE_NAME("LoadedPackage::Load"); std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage()); // typeIdOffset was added at some point, but we still must recognize apps built before this // was added. constexpr size_t kMinPackageSize = sizeof(ResTable_package) - sizeof(ResTable_package::typeIdOffset); const ResTable_package* header = chunk.header<ResTable_package, kMinPackageSize>(); if (header == nullptr) { ... return {}; } 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) { ... 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); if (type_id_offset > std::numeric_limits<uint8_t>::max()) { ... return {}; } loaded_package->type_id_offset_ = static_cast<int>(type_id_offset); } util::ReadUtf16StringFromDevice(header->name, arraysize(header->name), &loaded_package->package_name_); 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: { break; case RES_TABLE_TYPE_SPEC_TYPE: { ... break; case RES_TABLE_TYPE_TYPE: ... break; case RES_TABLE_LIBRARY_TYPE: ... 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

According to type, we can distinguish the following types:

  • 1.RES_TABLE_PACKAGE_TYPE Parses the header and parses the following data:

  • RES_STRING_POOL_TYPE Resolves all resource type names and strings in resource data item names from resource type string pools and resource item name string pools

  • RES_TABLE_TYPE_SPEC_TYPE Parses all resource type specifications

  • RES_TABLE_TYPE_TYPE Resolves all resource types

  • RES_TABLE_LIBRARY_TYPE resolves all third-party library resources.

summary

Limited to the length of the article, this article will analyze here, the next chapter will analyze the resource type specification, resource data items, the core principle of AssetManager. Within this, this article describes the following: Resource is controlled by ResourcesImpl. ApkAssets are objects in memory for each resource folder. The AssetManager exists along with the ResourcesImpl initialization to better manage each ApkAssets. There are quad caches in the Java layer of the entire Android resource architecture:

  • 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. Native load disk resources (there is some cache in the process of loading disk resources)

For Android, the resources.arsc file is especially important. It acts as a guide for the Android system to parse resources. Without it, Android applications cannot parse data properly.

The file is roughly divided into the following sections. Note that there are multiple string resource pools that store different data:

  • 1. Resources. Arsc header information. Type is RES_TABLE_TYPE
  • 2. Parse all the strings in the resource. The style string is RES_STRING_POOL_TYPE
  • 3. The remaining data blocks are Package data blocks whose type is RES_TABLE_PACKAGE_TYPE
    1. RES_TABLE_PACKAGE_TYPE represents the header of this Package data block
  • 5. There is also a resource pool in the Package data block, but this resource pool contains both the resource type specification string and the resource data string. The type of RES_STRING_POOL_TYPE
    1. RES_TABLE_TYPE_SPEC_TYPE represents all of these resource type specifications data blocks (chunks)
  • RES_TABLE_TYPE_TYPE represents all resource data blocks
  • RES_TABLE_LIBRARY_TYPE represents all third-party repositories.

Three different string resource pools, using the Layout folder as an example:

  • The leftmost subscript 1 refers to the resource type name, that is, the data in the package data block, typeString offset array, and typeString resource pool, from which RES_TABLE_TYPE_SPEC_TYPE is found
  • Subscript 2 refers to the resource data item name, which is located in the Package data block, the String offset array, and the resource data item String resource pool from which RES_TABLE_TYPE_TYPE is found.
  • Subscript 3 refers to the resource string, the largest pool of string resources outside the Package data block.

Using these resource pools, and the data in the Config data item pointed to by the resource data item, resources can be correctly recovered from the resource. Arsc file.

In order to speed up the loading of resources, Android does not directly read resource information through File read and write operations. Instead, file addresses are mapped to virtual memory via FileMap, or Mmap, ready to be read and written. The advantage of this is that mmap returns the address of the file and can operate on the file, saving the overhead of system calls. The disadvantage is that Mmap maps to virtual memory, which increases the virtual memory. This is discussed in more detail in Binder’s MMAP principles.

Author: yjy239 links: www.jianshu.com/p/817a78791… The copyright of the book belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.