Source code analysis | Resource loading resources

Posted by lmhart on Tue, 15 Feb 2022 02:53:27 +0100

Understand the loading process of resources and load the resource file in the skin file

Resource loading

​ How are src images loaded in the imageView layout?

mResources.loadDrawable(value, value.resourceId, density, mTheme)

​ In fact, they are loaded through resources

​ Since the Resource is loaded through the Resource class, if you want to obtain the Resource file in another apk, can you instantiate a Resource to load it yourself?

  • First, we need to find out how resources are instantiated
@Override
public Resources getResources() {
    if (mResources == null && VectorEnabledTintResources.shouldBeUsed()) {
        mResources = new VectorEnabledTintResources(this, super.getResources());
    }
    return mResources == null ? super.getResources() : mResources;
}

​ In AppcompatActivity, there is a getResources method to obtain resources. If mResource is null, super. Is called Resource s().

@Override
public Resources getResources() {
    return getResourcesInternal();
}

private Resources getResourcesInternal() {
    if (mResources == null) {
        if (mOverrideConfiguration == null) {
            //1
            mResources = super.getResources();
        } else {
            final Context resContext = createConfigurationContext(mOverrideConfiguration);
            //2
            mResources = resContext.getResources();
        }
    }
    return mResources;
}

Both 1 and 2 above ultimately call the getResource method of Context, but the getResource of Context is an abstract method, and we must find its implementation class.

//Returns the Resources instance of the application package
public abstract Resources getResources();

Its implementation class is actually ContextImpl, which can't be seen on as, You need to view it from the source code

@Override
    public Resources getResources() {
        return mResources;
    }

Finally, the above method is called. How to instantiate mResource is as follows:

 private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
            int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
        final String[] splitResDirs;
        final ClassLoader classLoader;
        try {
            splitResDirs = pi.getSplitPaths(splitName);
            classLoader = pi.getSplitClassLoader(splitName);
        } catch (NameNotFoundException e) {
            throw new RuntimeException(e);
        }
        return ResourcesManager.getInstance().getResources(activityToken,
                pi.getResDir(),
                splitResDirs,
                pi.getOverlayDirs(),
                pi.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfig,
                compatInfo,
                classLoader);
    }

When you create a Resource, you will go to the createResources method.

You can see that the getResource method of ResourcesManager is finally called,

public @Nullable Resources getResources(@Nullable 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 {
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
        final ResourcesKey key = new ResourcesKey(
                resDir,
                splitResDirs,
                overlayDirs,
                libDirs,
                displayId,
                overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                compatInfo);
        classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
        return getOrCreateResources(activityToken, key, classLoader);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    }
}

The parameter resDir is the path where apk is stored. In this method, the parameters are integrated into ResourceKey, and then the getOrCreateResources method is called.

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
        @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
   ynchronized (this) {
               //Get ResourcesImpl from cache
             ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
            if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    //1
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                }
            } 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) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    //2
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }

                // We will create the ResourcesImpl object outside of holding this lock.
            }

     		//Create if not found
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }

            // Add cache
            mResourceImpls.put(key, new WeakReference<>(resourcesImpl));

            final Resources resources;
            if (activityToken != null) {
                //3
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl, key.mCompatInfo);
            } else {
                //4
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
            return resources;
        }
}

Pay attention to 1, 2, 3 and 4 marked. These four locations call two methods, which will eventually create a Resource through the following sentence

// Create a new resource reference and use the existing ResourcesImpl object
Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
        : new Resources(classLoader);
resources.setImpl(impl);

Here, Resources is created successfully

We just need to know how the Resource is instantiated and how it is new.

  • Constructor for Resources
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
    this(null);
    mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}

@UnsupportedAppUsage
public Resources(@Nullable ClassLoader classLoader) {
    mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
}

Resources construction method. Due to different versions of source code, the construction of new Resources may be different. It looks a little different, but they are all the same. In the next line of new Resources, the setImpl method is called. The incoming ResourcesImpl is actually created through the createResourcesImpl method:

private @Nullable 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);

    return impl;
}

Using the above method to create ResourceImpl is actually similar to that created in the first construction method.

  • ResourceImpl

Structure: new ResourcesImpl(assets, dm, config, daj);

  • assets: AssetManager resource management. It is created as follows. AssetManager objects are created through Builder and various paths are passed in. In some versions of source code, AssetManager is directly new, and addAssetPath method is called to pass in apk path
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
    final AssetManager.Builder builder = new AssetManager.Builder();

    // resDir can be null if the 'android' package is creating a new Resources object.
    // This is fine, since each AssetManager automatically loads the 'android' package
    // already.
    if (key.mResDir != null) {
        	//Mresdir: directory of apk
            builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/, false /*overlay*/));
    }

    if (key.mSplitResDirs != null) {
        for (final String splitResDir : key.mSplitResDirs) {
                builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,
                        false /*overlay*/));
        }
    }

    if (key.mOverlayDirs != null) {
        for (final String idmapPath : key.mOverlayDirs) {
                builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
                        true /*overlay*/));
        }
    }

    if (key.mLibDirs != null) {
        for (final String libDir : key.mLibDirs) {
            if (libDir.endsWith(".apk")) {
           
                    builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
                            false /*overlay*/));
            }
        }
    }

    return builder.build();
}

Summary of Resource loading: all Resource loads are directly new objects when building objects through Resource, Resource - > and there is a very important parameter AssetManager, which is the core instance of Resource.

Finally, it is obtained through AssetManager.

Load the resource file in the skin file by creating Resources yourself

1. Understand skin files

​ The skin file is actually an apk. Add the resource file to the project and generate an apk, then the apk is the skin file. The resources in the skin file must be consistent with those in the project.

2. Obtain the resource file in the skin file through Resources and load it

val superRes = resources
//When creating AssetManager, the structure is hidden and cannot be created directly
val assetManager = AssetManager::class.java.newInstance()
//Reflection method: add the apk directory of locally downloaded resources. The suffix of apk can be changed at will. Here it is changed to skin
val method =
    AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
method.invoke(
    assetManager, "${Environment.getExternalStorageDirectory().absolutePath}${File.separator}red.skin"
)

//To create a Resource, the specific method is in the source code, which has been introduced above.
//The parameters created are in the source code and can be copied directly
val resources = Resources(assetManager, superRes.displayMetrics, superRes.configuration)
//1. Resource name, resource type and package name
val drawableId = resources.getIdentifier("image_src", "drawable", "com.qs.redskin")
 //Get resources
val drawable = resources.getDrawable(drawableId)

mImageView.setImageDrawable(drawable)

Through the above steps, you can load the resource file in the skin. Note: apply for storage permission

The above is just a simple Demo