preface

In this article, we will analyze the loading mechanism of resources in Android, such as string resources, image resources are loaded. The analysis will be carried out from two types of resources: string and image, as well as a brief analysis of the content loaded by the resources used later.

Resources source code analysis

For the resource loading mechanism, the core classes are Resources, ResourcesImpl, and AssetManager. Resources is a proxy for ResourcesImpl. All calls to Resources are called to ResourcesImpl. Within ResourcesImpl, there is a Cache and an AssetManager for Resources. The load of resources will be searched from the Cache first. When the search fails, the AssetManager will be called to load the corresponding resources. After the load, the resources will be cached in ResourcesImpl.

Resource has an internal static variable

static Resources mSystem = null;
Copy the code

It is initialized in the getSystem method and stored as a holding of internal variables, which is first called when Zygote creates a new process and preloads resources.

public static Resources getSystem() {
    synchronized (sSync) {
        Resources ret = mSystem;
        if (ret == null) {
            ret = new Resources();
            mSystem = ret;
        }
        returnret; }}Copy the code

The creation of the Resrouce object, the various operations in Resrouce, and ultimately the real executor is ResourcesImpl.

private Resources() {
    this(null);

    final DisplayMetrics metrics = new DisplayMetrics();
    metrics.setToDefaults();

    final Configuration config = new Configuration();
    config.setToDefaults();

    mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
            new DisplayAdjustments());
}

Copy the code

Create an instance of ResourcesImpl in the constructor of Resources.

public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
        @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
    mAssets = assets;
    mMetrics.setToDefaults();
    mDisplayAdjustments = displayAdjustments;
    updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
    mAssets.ensureStringBlocks();
}
Copy the code

When you create the ResoucesImpl instance, you get an instance of AssetManager, which is responsible for the interaction between the application layer and the resource files. The Resource object is obtained using the ContextImpl method by returning the mResource variable inside it,

resources = mResourcesManager.getResources(
activityToken,
packageInfo.getResDir(),
packageInfo.getSplitResDirs(),
packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfiguration,
compatInfo,
packageInfo.getClassLoader());
Copy the code

The getOrCreateResources method of ResourcesManager is called. This is implemented by looking from activityResources, and if it can’t find one, it creates a new one, adds it to activityResources, and returns.

Getting string resources

Start with a method to get a resource file, and start with a method to get text.

@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
    CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
    if(res ! = null) {return res;
    }
    throw new NotFoundException("String resource ID #0x"
                                + Integer.toHexString(id));
}
Copy the code
public AssetManager getAssets() {
    return mAssets;
}
Copy the code

Call getResourceText of AssetManager

final CharSequence getResourceText(@StringRes int resId) {
    synchronized (this) {
        final TypedValue outValue = mValue;
        if (getResourceValue(resId, 0, outValue, true)) {
            return outValue.coerceToString();
        }
        returnnull; }}Copy the code

We first get the TypedValue based on the ID, and then we get the resources we need based on the TypedValue.

final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
        boolean resolveRefs) {
    final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs);
    if (block < 0) {
        return false;
    }
    if (outValue.type == TypedValue.TYPE_STRING) {
        outValue.string = mStringBlocks[block].get(outValue.data);
    }
    return true;
}
Copy the code

For a string resource, the value exists in a TypedValue, so once you have a TypedValue, you can use it to get the resource value.

Get image resources

Because of the particularity of picture resources, the acquisition of string resources is more complicated, so the analysis starts from the upper acquisition method.

public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
        throws NotFoundException {
    final TypedValue value = obtainTempTypedValue();
    try {
        final ResourcesImpl impl = mResourcesImpl;
        impl.getValue(id, value, true);
        return impl.loadDrawable(this, value, id, theme, true); } finally { releaseTempTypedValue(value); }}Copy the code

Similar to the loading of string resources, a TypedValue object is first obtained based on the resource ID, and then loaded using the TypedValue instance through AssetManager.

void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
        throws NotFoundException {
    boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
    if (found) {
        return; }}Copy the code
final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
        boolean resolveRefs) {
    final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs);
    if (block < 0) {
        return false;
    }
    if (outValue.type == TypedValue.TYPE_STRING) {
        outValue.string = mStringBlocks[block].get(outValue.data);
    }
    return true;
}
Copy the code

The Drawable resource core code is a call to the ResourcesImpl loadDrawable function.

@Nullable
Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
        boolean useCache) throws NotFoundException {
    try {
        if (TRACE_FOR_PRELOAD) {
            if ((id >>> 24) == 0x1) {
                final String name = getResourceName(id);
                if(name ! = null) { Log.d("PreloadDrawable", name); }}} final Boolean isColorDrawable; final DrawableCache caches; final long key;if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
            caches = mColorDrawableCache;
            key = value.data;
        } else {
            isColorDrawable = false; caches = mDrawableCache; key = (((long) value.assetCookie) << 32) | value.data; } // if Drawable existsif(! mPreloading && useCache) { final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);if(cachedDrawable ! = null) {returncachedDrawable; Drawable final Drawable.ConstantState cs; Drawable final Drawable.if (isColorDrawable) {
            cs = sPreloadedColorDrawables.get(key);
        } else{ cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key); } // create Drawable Drawable Dr;if(cs ! = null) { dr = cs.newDrawable(wrapper); }else if (isColorDrawable) {
            dr = new ColorDrawable(value.data);
        } else{ dr = loadDrawableForCookie(wrapper, value, id, null); } final Boolean canApplyTheme = Dr! = null && dr.canApplyTheme();if(canApplyTheme && theme ! = null) { dr = dr.mutate(); dr.applyTheme(theme); dr.clearMutated(); } // Add the loaded Drawable resource to the cacheif(dr ! = null && useCache) { dr.setChangingConfigurations(value.changingConfigurations); cacheDrawable(value, isColorDrawable, caches, theme, canApplyTheme, key, dr); }returndr; } catch (Exception e) { ... }}Copy the code

loadDrawableForCookie

Builds a Drawable from an XML file or resource stream based on the information stored in TypedValue

private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
        Resources.Theme theme) {
    if (value.string == null) {
        throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
                + Integer.toHexString(id) + ") is not a Drawable (color or path): "+ value); } final String file = value.string.toString();if (TRACE_FOR_MISS_PRELOAD) {
        // Log only framework resources
        if ((id >>> 24) == 0x1) {
            final String name = getResourceName(id);
            if(name ! = null) { Log.d(TAG,"Loading framework drawable #" + Integer.toHexString(id)
                        + ":" + name + " at "+ file); } } } final Drawable dr; // If the file suffix is XML, construct the Drawable object try {with XmlResourceParserif (file.endsWith(".xml")) {
            final XmlResourceParser rp = loadXmlResourceParser(
                    file, id, value.assetCookie, "drawable");
            dr = Drawable.createFromXml(wrapper, rp, theme);
            rp.close();
        } elseFinal InputStream is = mAssets. OpenNonAsset (value.assetcookie, file, Drawable) AssetManager.ACCESS_STREAMING); dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); is.close(); } } catch (Exception e) { ... }return dr;
}
Copy the code
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
        @NonNull String type)
        throws NotFoundException {
    if(id ! = 0) { try { synchronized (mCachedXmlBlocks) { final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies; final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles; final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks; / / to test whether the cache in the resources we need final int num = cachedXmlBlockFiles. Length;for (int i = 0; i < num; i++) {
                    if(cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] ! = null && cachedXmlBlockFiles[i].equals(file)) {returncachedXmlBlocks[i].newParser(); }} / / if the resource is not in the cache, it through the AssetManager to load, and then added to the cache final XmlBlock block. = mAssets openXmlBlockAsset (assetCookie, file);if(block ! = null) { final int pos = (mLastCachedXmlBlockIndex + 1) % num; mLastCachedXmlBlockIndex = pos; final XmlBlock oldBlock = cachedXmlBlocks[pos];if(oldBlock ! = null) { oldBlock.close(); } cachedXmlBlockCookies[pos] = assetCookie; cachedXmlBlockFiles[pos] = file; cachedXmlBlocks[pos] = block;returnblock.newParser(); } } } catch (Exception e) { .... }}}Copy the code

The image resource loading process is to obtain the TypedValue instance according to the ID, then search for the Drawable resource according to the TypedValue, first check whether the resource is in the cache, if not from the preloaded resource, if not from the preloaded resource, determine the type of resource to be loaded, If colorDrawable, this is created according to Typedvalue, otherwise the Drawable object is obtained by loading XML or processing the file input stream.

The first step is to obtain the TypedValue object corresponding to the ID of the resource. For simple resources, TypedValue object is used. For complex resources, the second step is to load the resource file into memory.

AssetManager

The getSystem method of AssetManager is called when the Resources constructor is created and the ResourcesImpl is created, which is used to ensure that a unique instance of AssetManager is created.

public static AssetManager getSystem() {
    ensureSystemAssets();
    return sSystem;
}
Copy the code

Ensure that there is only one AssetManager globally

private static void ensureSystemAssets() {
    synchronized (sSync) {
        if (sSystem == null) {
            AssetManager system = new AssetManager(true); system.makeStringBlocks(null); sSystem = system; }}}Copy the code
private AssetManager(boolean isSystem) {
    if (DEBUG_REFS) {
        synchronized (this) {
            mNumRefs = 0;
            incRefsLocked(this.hashCode());
        }
    }
    init(true);
}

Copy the code

static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
    if (isSystem) {
        verifySystemIdmaps();
    }
    AssetManager* am = new AssetManager();
    if (am == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError"."");
        return;
    }

    am->addDefaultAssets();

    ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
    env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
}
Copy the code
bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    path.appendPath(kSystemAssets);

    return addAssetPath(path, NULL, false /* appAsLib */, true /* isSystemAsset */);
}
Copy the code

Set a resource path that can be dynamically loaded by modifying this value.

public final int addAssetPath(String path) {
    return  addAssetPathInternal(path, false);
}
Copy the code
private final int addAssetPathInternal(String path, boolean appAsLib) {
    synchronized (this) {
        int res = addAssetPathNative(path, appAsLib);
        makeStringBlocks(mStringBlocks);
        returnres; }}Copy the code

Adding a Resource Path

bool AssetManager::addAssetPath(
        const String8& path, int32_t* cookie, bool appAsLib, bool isSystemAsset)
{
    AutoMutex _l(mLock);

    asset_path ap;

    String8 realPath(path);
    if (kAppZipName) {
        realPath.appendPath(kAppZipName);
    }
    ap.type = ::getFileType(realPath.string());
    if (ap.type == kFileTypeRegular) {
        ap.path = realPath;
    } else {
        ap.path = path;
        ap.type = ::getFileType(path.string());
        if(ap.type ! = kFileTypeDirectory && ap.type ! = kFileTypeRegular) { ALOGW("Asset path %s is neither a directory nor file (type=%d).",
                 path.string(), (int)ap.type);
            return false;
        }
    }

    // Skip if we have it already.
    for (size_t i=0; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = static_cast<int32_t>(i+1);
            }
            return true;
        }
    }

    ALOGV("In %p Asset %s path: %s", this,
         ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string());

    ap.isSystemAsset = isSystemAsset;
    mAssetPaths.add(ap);

    // new paths are always added at the end
    if (cookie) {
        *cookie = static_cast<int32_t>(mAssetPaths.size());
    }

#ifdef __ANDROID__
    // Load overlays, if any
    asset_path oap;
    for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) {
        oap.isSystemAsset = isSystemAsset;
        mAssetPaths.add(oap);
    }
#endif

    if(mResources ! = NULL) { appendPathToResTable(ap, appAsLib); }return true;
}
Copy the code

The APK file has a file called resource. Arsc. This file stores APK resource ID and resource type, attribute, filename reading relation table and all strings, load APK, is to parse this file to generate ResRTable object, through the ResTable object to resolve the resource ID. Unzip the corresponding path, get the corresponding resource table from it, and then join it. Each call is called appendPathToResTable to which the resource table for the new path is added.

Get TypedValue based on ID

void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
        throws NotFoundException {
    boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
    if (found) {
        return;
    }
    throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
Copy the code
final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
        boolean resolveRefs) {
    final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs);
    if (block < 0) {
        return false;
    }
    if (outValue.type == TypedValue.TYPE_STRING) {
        outValue.string = mStringBlocks[block].get(outValue.data);
    }
    return true;
}
Copy the code

Get a Value based on ID from AssetManager.

static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
                                                           jint ident,
                                                           jshort density,
                                                           jobject outValue,
                                                           jboolean resolve)
{
    if (outValue == NULL) {
         jniThrowNullPointerException(env, "outValue");
         return 0;
    }
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return 0;
    }
    const ResTable& res(am->getResources());

    Res_value value;
    ResTable_config config;
    uint32_t typeSpecFlags;
    ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);
    if (kThrowOnBadId) {
        if (block == BAD_INDEX) {
            jniThrowException(env, "java/lang/IllegalStateException"."Bad resource!");
            return 0;
        }
    }
    uint32_t ref = ident;
    if (resolve) {
        block = res.resolveReference(&value, block, &ref, &typeSpecFlags, &config);
        if (kThrowOnBadId) {
            if (block == BAD_INDEX) {
                jniThrowException(env, "java/lang/IllegalStateException"."Bad resource!");
                return0; }}}if (block >= 0) {
        return copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config);
    }

    return static_cast<jint>(block);
}
Copy the code

Get the core code for the TypedValue of the resource.

const ResTable& res(am->getResources());
ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);

Copy the code
const ResTable& AssetManager::getResources(bool required) const
{
    const ResTable* rt = getResTable(required);
    return *rt;
}
Copy the code

Find the resource Table based on the resource path set in AssetManager

First check if a ResTable already exists, create it if it doesn’t, and return it if it does. We then look up the corresponding Resources file based on the path and convert it to a ResTable for later lookup.

const ResTable* AssetManager::getResTable(bool required) const { ResTable* rt = mResources; // It already existsif (rt) {
        return rt;
    }

    AutoMutex _l(mLock);

    if(mResources ! = NULL) {return mResources;
    }

    if(mCacheMode ! = CACHE_OFF && ! mCacheValid) { const_cast<AssetManager*>(this)->loadFileNameCacheLocked(); } // create ResTable mResources = new ResTable(); updateResourceParamsLocked(); bool onlyEmptyResources =true; const size_t N = mAssetPaths.size(); AppendPathToResTable iterates through the Asset path, calling the path appendPathToResTablefor(size_t i=0; i<N; i++) { bool empty = appendPathToResTable(mAssetPaths.itemAt(i)); onlyEmptyResources = onlyEmptyResources && empty; } // If empty, the resources.arsc file was not foundif (required && onlyEmptyResources) {
        ALOGW("Unable to find resources file resources.arsc");
        delete mResources;
        mResources = NULL;
    }

    return mResources;
}
Copy the code
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
        String[] libDirs, int displayId, LoadedApk pkgInfo) {
    return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
            displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
}
Copy the code

Get the Resources object through mResourcesManager, or create one if one does not exist.

summary

TypedValue is a ResTable that is built by parsing the resource table when AssetManager adds the resource path. Through this data structure, TypedValue is searched and constructed according to ID as index. Then, according to the type of resource file, the detailed information about resources stored in TypedValue is used to obtain resources, and the loaded resources are cached at the same time. So in a plug-in scenario, a new ResTable is built by creating a new Resource object and adding a new Asset path to it, enabling non-hosted App resources to be loaded by ID.

Supplement: An AssetManager corresponds to a ResTable. Without setting a resource path for AssetTable, the resource path is parsed and read out. All loaded resources are placed in this table, so we can fetch typeValues from it. According to TypeValue to find the corresponding resource.

The resources

Android resource ID generation rule

The Android resource article