We know that every View subclass can set backgroud, so how does this background load?
Find the way to construct View
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { ...... case com.android.internal.R.styleable.View_background: background = a.getDrawable(attr); break; ...... }
@Nullable public Drawable getDrawable(@StyleableRes int index) { return getDrawableForDensity(index, 0); } @Nullable public Drawable getDrawableForDensity(@StyleableRes int index, int density) { ...... return mResources.loadDrawable(value, value.resourceId, density, mTheme); ...... }
See this line
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
Enter Resources
@NonNull Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException { return mResourcesImpl.loadDrawable(this, value, id, density, theme); }
As you can see, the background is ultimately loaded into Drawable by ResourcesImpl in Resources, which is created in the Resources construct.
@Deprecated public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { this(null); mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments()); } private Resources() { ...... mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config, new DisplayAdjustments()); }
Let's look at how Resources are created. Usually when we get some resource files, we get Resources in this way.
context.getResources()
We know that Context is an abstract class, so go directly to ContextImpl, a subclass of Context.
@Override public Resources getResources() { return mResources; }
So when was this mResources created, find this method
void setResources(Resources r) { if (r instanceof CompatResources) { ((CompatResources) r).setContext(this); } mResources = r; }
Then you see that this method is invoked in many places.
c.setResources(createResources(mActivityToken, pi, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); c.setResources(createResources(mActivityToken, pi, null, displayId, null, getDisplayAdjustments(displayId).getCompatibilityInfo())); context.setResources(packageInfo.getResources()); context.setResources(ResourcesManager.getInstance().getResources( mActivityToken, mPackageInfo.getResDir(), paths, mPackageInfo.getOverlayDirs(), mPackageInfo.getApplicationInfo().sharedLibraryFiles, displayId, null, mPackageInfo.getCompatibilityInfo(), classLoader)); context.setResources(resourcesManager.createBaseActivityResources(activityToken, packageInfo.getResDir(), splitDirs, packageInfo.getOverlayDirs(), packageInfo.getApplicationInfo().sharedLibraryFiles, displayId, overrideConfiguration, compatInfo, classLoader));
There are several ways to do this, and we can't find any other place to assign value to mResources. Preliminary judgment can be made that this is how Resources were created. Follow these ways, you will find that, although setResources methods have several forms, they will eventually enter into ResourcesManger class. The annotations of this method can be seen that Resources will be slowed down. In storage, the life cycle of a resource is the same as that of the Activity. When the classloader changes, the Resources will also change.
/** * Gets or creates a new Resources object associated with the IBinder token. References returned * by this method live as long as the Activity, meaning they can be cached and used by the * Activity even after a configuration change. If any other parameter is changed * (resDir, splitResDirs, overrideConfig) for a given Activity, the same Resources object * is updated and handed back to the caller. However, changing the class loader will result in a * new Resources object. * <p/> * If activityToken is null, a cached Resources object will be returned if it matches the * input parameters. Otherwise a new Resources object that satisfies these parameters is * returned. * * @param activityToken Represents an Activity. If null, global resources are assumed. * @param resDir The base resource path. Can be null (only framework resources will be loaded). * @param splitResDirs An array of split resource paths. Can be null. * @param overlayDirs An array of overlay paths. Can be null. * @param libDirs An array of resource library paths. Can be null. * @param displayId The ID of the display for which to create the resources. * @param overrideConfig The configuration to apply on top of the base configuration. Can be * null. Mostly used with Activities that are in multi-window which may override width and * height properties from the base config. * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}. * @param classLoader The class loader to use when inflating Resources. If null, the * {@link ClassLoader#getSystemClassLoader()} is used. * @return a Resources object from which to access resources. */ 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); } }
/** * Gets an existing Resources object set with a ResourcesImpl object matching the given key, * or creates one if it doesn't exist. * * @param activityToken The Activity this Resources object should be associated with. * @param key The key describing the parameters of the ResourcesImpl object. * @param classLoader The classloader to use for the Resources object. * If null, {@link ClassLoader#getSystemClassLoader()} is used. * @return A Resources object that gets updated when * {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)} * is called. */ private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken, @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) { synchronized (this) { ...... //Below are two cases, when the activityToken (IBinder) is null or not, the ResourcesImpl is obtained according to the key. //As long as the ResourcesImpl exists, it will either be returned directly from the Resources cache or a new Resources return will be created. if (activityToken != null) { ...... //Get the corresponding ResourcesImpl cache based on key ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); if (resourcesImpl != null) { ...... // As long as the ResourcesImpl obtained according to the key is not null, the cache is retrieved according to the ResourcesImpl. //Resources, if you have this Resource cache, it returns, and if it doesn't, it creates. Look at this method in detail. return getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); } // We will create the ResourcesImpl object outside of holding this lock. } else { ...... ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); if (resourcesImpl != null) { ...... return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } } //When the program comes here, it means that ResourcesImpl is not found and Resources are not available. So here is the basis. //key creates a ResourcesImpl, and the first time the program runs, it's bound to come here first, so the code above can //Next, let's look at how ResourcesImpl was created. See the method createResourcesImpl. // 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; } synchronized (this) { ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key); if (existingResourcesImpl != null) { //Get from the cache ...... resourcesImpl.getAssets().close(); resourcesImpl = existingResourcesImpl; } else { // Cache the created ResourcesImpl mResourceImpls.put(key, new WeakReference<>(resourcesImpl)); } final Resources resources; //Here, for activityToken to be null or not, respectively, in getOrCreateResourcesForActivityLocked and getOrCreateResourcesLocked //In these two methods, we focus on the fact that Resources do not have caching, so we will eventually see the creation of Resources. //See the method below. if (activityToken != null) { resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader, resourcesImpl, key.mCompatInfo); } else { resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo); } return resources; } //There are two ways to get Resources according to different conditions. One is new CompatResources, the other is new CompatResources. //new Resources, entering the CompatResources class, we see that this construct will eventually call a construct of Resources as well. //The method public Resources(@Nullable ClassLoader classLoader) returns Resources, so this Resource is new //Coming out Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) : new Resources(classLoader); //Set ResourcesImpl for Resources resources.setImpl(impl); //Add Cache mResourceReferences.add(new WeakReference<>(resources));
getOrCreateResourcesForActivityLocked
/** * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist * or the class loader is different. */ private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken, @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) { ...... //Cached access cache Resources resources = weakResourceRef.get(); ...... return resources; ...... //No cache is created and added to the cache Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader) : new Resources(classLoader); resources.setImpl(impl); activityResources.activityResources.add(new WeakReference<>(resources)); ...... return resources; }
createResourcesImpl, you can see that the creation of ResourcesImpl depends on these objects AssetManager, DisplayMetrics, Configuration, DisplayAdjustments
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) { final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration); daj.setCompatibilityInfo(key.mCompatInfo); final AssetManager assets = createAssetManager(key); if (assets == null) { return null; } final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj); final Configuration config = generateConfig(key, dm); final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj); ...... return impl; }
Focusing on the creation of Asset Manager, this class is critical
/** * Creates an AssetManager from the paths within the ResourcesKey. * * This can be overridden in tests so as to avoid creating a real AssetManager with * real APK paths. * @param key The key containing the resource paths to add to the AssetManager. * @return a new AssetManager. */ @VisibleForTesting protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { AssetManager assets = new AssetManager(); // 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) { // Add all the resource paths in app to the AssetManager object. Neither of the methods below can be looked at. Let's focus on //Focusing on this approach, it can be said that the reason why an application can load resources is through Asset Manager and by calling addAsset Path. //Resource Path if (assets.addAssetPath(key.mResDir) == 0) { Log.e(TAG, "failed to add asset path " + key.mResDir); return null; } } ..... return assets; }
getDisplayMetrics, which is just a new DisplayMetrics
@VisibleForTesting protected @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) { DisplayMetrics dm = new DisplayMetrics(); final Display display = getAdjustedDisplay(displayId, da); if (display != null) { display.getMetrics(dm); } else { dm.setToDefaults(); } return dm; }
GeneeConfig, Configuration is also new
private Configuration generateConfig(@NonNull ResourcesKey key, @NonNull DisplayMetrics dm) { Configuration config; .... config = new Configuration(getConfiguration()); .... return config; }
One of the key classes in the creation of Resources is ResourcesImpl, which requires several important information. One of them is AssetManager, which talks about an AssetManager object through a direct instance and sets the resource path for the object. This is the basis for #Resources to access file resources. DisplayMetrics or Configuration are equivalent to some fixed settings. The path set in Asset Manager is actually the path of the APK we're going to set up to get resources from the apk.
In this way, we have access to Resources, and we are free to access resource files.
So let's go back to the beginning and see how Resources loads Drawable.
In Resources
@NonNull Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException { return mResourcesImpl.loadDrawable(this, value, id, density, theme); }
In ResourcesImpl
@Nullable Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) throws NotFoundException { ...... Drawable dr; boolean needsNewDrawableAfterCache = false; if (cs != null) { dr = cs.newDrawable(wrapper); } else if (isColorDrawable) { dr = new ColorDrawable(value.data); } else { dr = loadDrawableForCookie(wrapper, value, id, density, null); } ...... return dr; ......
/** * Loads a drawable from XML or resources stream. */ private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value, int id, int density, @Nullable Resources.Theme theme) { ...... final Drawable dr; Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file); try { //If the resource is an xml file if (file.endsWith(".xml")) { final XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme); rp.close(); } else { //If the resource is an image resource, open it to get the flow, and parse it to get drawable final InputStream is = mAssets.openNonAsset( value.assetCookie, file, AssetManager.ACCESS_STREAMING); dr = Drawable.createFromResourceStream(wrapper, value, is, file, null); is.close(); } ...... Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); return dr; }
After the above analysis, the idea of skin switching is already in place. Download the apk file, which contains various resource files of another skin. Load the Resources in the apk through Resources to achieve the effect of skin change. The key code is as follows.
//Click on an apk on the phone to get the image resource and set it to the ImageView display //Obtaining two parameters of the system Resources superResources = getResources(); //Create assetManger (you can't directly use new because it's hidden, so use reflection) AssetManager assetManager = AssetManager.class.newInstance(); //Add Asset Path is also not directly invoked by hide Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); method.setAccessible(true);//If it's private, add something to prevent it from becoming private one day. method.invoke(assetManager,Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"red.skin");//Note that the names of your resources should be consistent //DisplayMetrics and Configuration objects can be directly new out, using Resources obtained from getResources, which is actually new out. Resources resources = new Resources(assetManager,superResources.getDisplayMetrics(),superResources.getConfiguration()); //Use the created Resources to get Resources (notice three parameters, the first is to get the name of the resource, we set the girl, don't forget, the second parameter represents which folder the resource is in, and the third parameter represents the package name of the apk to get the Resources, which is indispensable) int identifier = resources.getIdentifier("girl", "drawable", "com.example.myapplication"); if (identifier != 0){ Drawable drawable = resources.getDrawable(identifier); mImage.setImageDrawable(drawable); }
The following is the initialization process of Asset Manager on the native side. Why can our app call the system to provide good resources and how these resources are loaded can be answered here.
init() of Asset Manager
public AssetManager() { synchronized (this) { if (DEBUG_REFS) { mNumRefs = 0; incRefsLocked(this.hashCode()); } init(false); if (localLOGV) Log.v(TAG, "New asset manager: " + this); ensureSystemAssets(); } } //android_util_AssetManager.cpp //AssetManager.cpp private native final void init(boolean isSystem);
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem) { if (isSystem) { verifySystemIdmaps(); } // AssetManager.cpp 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)); } bool AssetManager::addDefaultAssets() { const char* root = getenv("ANDROID_ROOT"); LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set"); String8 path(root); // Load the framework-res.apk of the system at initialization path.appendPath(kSystemAssets); return addAssetPath(path, NULL); }
AssetManager's addAssetPath(String path) method
bool AssetManager::addAssetPath(const String8& path, int32_t* cookie) { 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()); // Check that the path has an AndroidManifest.xml Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked( kAndroidManifest, Asset::ACCESS_BUFFER, ap); if (manifestAsset == NULL) { // This asset path does not contain any resources. delete manifestAsset; return false; } delete manifestAsset; mAssetPaths.add(ap); // new paths are always added at the end if (cookie) { *cookie = static_cast<int32_t>(mAssetPaths.size()); } #ifdef HAVE_ANDROID_OS // Load overlays, if any asset_path oap; for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) { mAssetPaths.add(oap); } #endif if (mResources != NULL) { appendPathToResTable(ap); } return true; } bool AssetManager::appendPathToResTable(const asset_path& ap) const { // skip those ap's that correspond to system overlays if (ap.isSystemOverlay) { return true; } Asset* ass = NULL; ResTable* sharedRes = NULL; bool shared = true; bool onlyEmptyResources = true; MY_TRACE_BEGIN(ap.path.string()); Asset* idmap = openIdmapLocked(ap); size_t nextEntryIdx = mResources->getTableCount(); ALOGV("Looking for resource asset in '%s'\n", ap.path.string()); if (ap.type != kFileTypeDirectory) { if (nextEntryIdx == 0) { // The first item is typically the framework resources, // which we want to avoid parsing every time. sharedRes = const_cast<AssetManager*>(this)-> mZipSet.getZipResourceTable(ap.path); if (sharedRes != NULL) { // skip ahead the number of system overlay packages preloaded nextEntryIdx = sharedRes->getTableCount(); } } if (sharedRes == NULL) { ass = const_cast<AssetManager*>(this)-> mZipSet.getZipResourceTableAsset(ap.path); if (ass == NULL) { ALOGV("loading resource table %s\n", ap.path.string()); ass = const_cast<AssetManager*>(this)-> openNonAssetInPathLocked("resources.arsc", Asset::ACCESS_BUFFER, ap); if (ass != NULL && ass != kExcludedAsset) { ass = const_cast<AssetManager*>(this)-> mZipSet.setZipResourceTableAsset(ap.path, ass); } } if (nextEntryIdx == 0 && ass != NULL) { // If this is the first resource table in the asset // manager, then we are going to cache it so that we // can quickly copy it out for others. ALOGV("Creating shared resources for %s", ap.path.string()); sharedRes = new ResTable(); sharedRes->add(ass, idmap, nextEntryIdx + 1, false); #ifdef HAVE_ANDROID_OS const char* data = getenv("ANDROID_DATA"); LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set"); String8 overlaysListPath(data); overlaysListPath.appendPath(kResourceCache); overlaysListPath.appendPath("overlays.list"); addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx); #endif sharedRes = const_cast<AssetManager*>(this)-> mZipSet.setZipResourceTable(ap.path, sharedRes); } } } else { ALOGV("loading resource table %s\n", ap.path.string()); ass = const_cast<AssetManager*>(this)-> openNonAssetInPathLocked("resources.arsc", Asset::ACCESS_BUFFER, ap); shared = false; } if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) { ALOGV("Installing resource asset %p in to table %p\n", ass, mResources); if (sharedRes != NULL) { ALOGV("Copying existing resources for %s", ap.path.string()); mResources->add(sharedRes); } else { ALOGV("Parsing resources for %s", ap.path.string()); mResources->add(ass, idmap, nextEntryIdx + 1, !shared); } onlyEmptyResources = false; if (!shared) { delete ass; } } else { ALOGV("Installing empty resources in to table %p\n", mResources); mResources->addEmpty(nextEntryIdx + 1); } if (idmap != NULL) { delete idmap; } MY_TRACE_END(); return onlyEmptyResources; }