Plug-in Skin Change Framework Construction-Resource Loading Source Code Analysis

Keywords: Android Java xml Attribute

1. overview

Most of the controls we can use, but we do not necessarily know the principle of resource loading. At present, there are many skinning frameworks that we can use freely, but as early as a few years ago, these data are relatively small, if you want to make a skinning framework, you can only gnaw at the source code bit by bit.
If we don't use third-party open source frameworks now and want to do a skin-changing function, there is only one problem that we have to solve, that is, how to read the resources in another skin apk.
 
All sharing outlines: The Way to Advance Android in 2017

Video commentary address: http://pan.baidu.com/s/1bC3lAQ

2. Source Code Analysis of Resource Loading

2.1 Let's first look at how the scr attribute of ImageView loads image resources:

    <ImageView
        android:layout_width="wrap_content"
        android:src="@drawable/app_icon"
        android:layout_height="wrap_content" />

    // ImageView.java parses properties
    final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
    // Getting pictures through TypedArray
    final Drawable d = a.getDrawable(R.styleable.ImageView_src);
    if (d != null) {
      setImageDrawable(d);
    }

    // TypedArray.getDrawable() method
    public Drawable getDrawable(@StyleableRes int index) {
       // Eliminate part of the code...
       // Loading resources is actually obtained through mResources
       return mResources.loadDrawable(value, value.resourceId, mTheme);
    }

2.2 Resource creation process analysis:
  
We often use context. get Resources (). getColor (R. ID. title_color) in Activity, so how did this Resource instance be created? We can start with ContextImpl, the implementation class of context.

private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
       ......
       Resources resources = packageInfo.getResources(mainThread);

       if (resources != null) {
       // This branch won't go, because 6.0 does not support multi-screen display, although there are many related codes, 7.0 and officially support multi-screen operation.
          if (displayId != Display.DEFAULT_DISPLAY
            || overrideConfiguration != null
            || (compatInfo != null && compatInfo.applicationScale
                    != resources.getCompatibilityInfo().applicationScale)) {
              ......
            }
       }
       ......
       mResources = resources;
}

// packageInfo.getResources method
public Resources getResources(ActivityThread mainThread) {
       // The caching mechanism returns directly if mResources in Loaded Apk have been initialized.
       // Otherwise, create resources objects through ActivityThread
       if (mResources == null) {
           mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                   mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
       }
       return mResources;
}

Eventually, you'll come to the getResources method of ResourcesManager

    public @NonNull Resources getResources(@Nullable IBinder activityToken,
            @Nullable String resDir, //The app resource folder path is actually the path to the APK file, such as / data/app / package name / base.apk
            @Nullable String[] splitResDirs, //For an app that consists of multiple apks (slicing the original APK into several apks), the resource folders in each sub-apk
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs, // app-dependent shared jar/apk paths
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            // Create key with apk path as parameter
            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);
        }
    }


    private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {
            // .......

            if (activityToken != null) {
                // Find ResourcesImpl from the cache based on key 
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    // If resourcesImpl does, then look for resources from the cache based on resourcesImpl and classLoader
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl);
                }

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

        // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
        // This more important createResourcesImpl passes through key
        ResourcesImpl resourcesImpl = createResourcesImpl(key);

        synchronized (this) {
            // .......
            final Resources resources;
            if (activityToken != null) {
                // Obtain Resources from resourcesImpl and classLoader 
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
            }
            return resources;
        }
    }

    private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);
        // Create Asset Manager 
        final AssetManager assets = createAssetManager(key);
        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        // Creating a Resource Impl based on Asset Manager actually looks for resources: Resources - > Resource Impl - > Asset Manager
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }

    @VisibleForTesting
    protected @NonNull AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        // Create an AssetManager object
        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.
        // Add all resource paths in app to the AssetManager object
        if (key.mResDir != null) {
            // This method is very important. We will use it to load the skin apk later.
            if (assets.addAssetPath(key.mResDir) == 0) {
                throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
            }
        }

        if (key.mLibDirs != null) {
            for (final String libDir : key.mLibDirs) {
                // Select only the apk in the shared dependency because there will be no resource files in the jar
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        return assets;
    }

    /**
     * Gets an existing Resources object if the class loader and ResourcesImpl are the same,
     * otherwise creates a new Resources object.
     */
    private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
            @NonNull ResourcesImpl impl) {
        // Find an existing Resources that has this ResourcesImpl set.
        final int refCount = mResourceReferences.size();
        for (int i = 0; i < refCount; i++) {
            WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
            // Find it from the Soft Reference Cache
            Resources resources = weakResourceRef.get();
            if (resources != null &&
                    Objects.equals(resources.getClassLoader(), classLoader) &&
                    resources.getImpl() == impl) {
                if (DEBUG) {
                    Slog.d(TAG, "- using existing ref=" + resources);
                }
                return resources;
            }
        }

        // Create a new Resources reference and use the existing ResourcesImpl object.
        // To create a Resource, Resource has several constructions, with slightly different versions 
        // Some versions use this constructor Resources(assets, dm, config, compatInfo)
        Resources resources = new Resources(classLoader);
        resources.setImpl(impl);
        // Add cache
        mResourceReferences.add(new WeakReference<>(resources));
        if (DEBUG) {
            Slog.d(TAG, "- creating new ref=" + resources);
            Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
        }
        return resources;
    }

[After looking at so many things, we can summarize the process of creating Resources:]
- PaageInfo. getResources (mainThread) - > mainThread. getTopLevelResources () - > mResourcesManager. getResources () - > getOrCreateResources () Here we first look for the ResourcesImpl cache, if any, and get the Resource cache back.
- If there is no ResourcesImpl cache, go back and create ResourcesImpl, which depends on Asset Manager.
- AssetManager is created by directly instantiating an object and calling an addAssetPath(path) method to add the apk path of the application to AssetManager. See the source code for the addAssetPath() method.
- After creating ResourcesImpl, we will go to the cache to find Resource if not, then we will create Resource and cache it. The source code we see is new Resources(classLoader), resources.setImpl(impl). Different versions may be new Resources(assets, dm, config, compatInfo). See the 6.0 source code specifically.

3. Loading Skin Resources

If we have a general idea of the loading process of resources and the creation process of resources, now we need to load the resources in another apk. We just need to create a Resource object by ourselves. The following code looks for a lot of resources on the Internet. If you have analyzed the source code, you will have a better understanding.

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        try {
            Resources superRes = getResources();
            // Create Asset Manager, but not directly new, so only through reflection
            AssetManager assetManager = AssetManager.class.newInstance();
            // Reflection acquisition addAssetPath method
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            // Skin Pack Path: Local sdcard/plugin.skin
            String skinPath = Environment.getExternalStorageDirectory().getAbsoluteFile()+ File.separator+"plugin.skin";
            // Reflective call addAssetPath method
            addAssetPathMethod.invoke(assetManager, skinPath);
            // Create Resource Objects for Skin
            Resources skinResources = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
            // Get Id by resource name, type, package name
            int bgId = skinResources.getIdentifier("main_bg","drawable","com.hc.skin");
            Drawable bgDrawable = skinResources.getDrawable(bgId);
            // Setting background
            findViewById(R.id.activity_main).setBackgroundDrawable(bgDrawable);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4. Asset Manager Creation Process Analysis

The following analysis hopes that there will be no obsessive-compulsive disorder, and it doesn't matter if you don't understand it because it involves JNI. From the previous analysis, we can see that the actual management of resources in Android system is AssetManager class. Each Resource object will be associated with an AssetManager object, and Resources will delegate most of the operation of resources to AssetManager. Of course, some sources have a layer of ResourcesImpl that we've just seen.
There will also be an AssetManager object in the native layer that corresponds to the AssetManager object in the java layer, and the address of the native layer AssetManager object in memory is stored in the AssetManager.mObject in the java layer. So in the jni method of java layer AssetManager, we can quickly find the corresponding AssetManager object of native layer.

4.1 init() of Asset Manager

     /**
     * Create a new AssetManager containing only the basic system assets.
     * Applications will not generally use this method, instead retrieving the
     * appropriate asset manager with {@link Resources#getAssets}.    Not for
     * use by applications.
     * {@hide}
     */
    public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }

   // Source path of ndk
   // frameworks/base/core/jni/android_util_AssetManager.cpp
   // frameworks/base/libs/androidfw/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);
    // framework/framework-res.apk  
    // Initialization loads the system's framework-res.apk resources
    // That is to say, why can we load system resources such as color, picture, text and so on?
    path.appendPath(kSystemAssets);

    return addAssetPath(path, NULL);
}

4.2 AssetManager's addAssetPath(String path) method

bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
    asset_path ap;

    // Omit some validation code

    // Determine if it has been loaded
    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;
        }
    }

    // Check if the path has an Android manifest. XML
    Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
            kAndroidManifest, Asset::ACCESS_BUFFER, ap);
    if (manifestAsset == NULL) {
        // If no resources are included
        delete manifestAsset;
        return false;
    }
    delete manifestAsset;
    // Add to 
    mAssetPaths.add(ap);

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

    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());
    // Resource coverage mechanism, not yet considered
    Asset* idmap = openIdmapLocked(ap);
    size_t nextEntryIdx = mResources->getTableCount();
    ALOGV("Looking for resource asset in '%s'\n", ap.path.string());
    // The resource bundle path is not a folder, it's an apk file.
    if (ap.type != kFileTypeDirectory) {
        // For app, the first execution must be zero, because mResources was just created and not yet operated on.
        // The following branch command is executed only when the parameter is the system resource bundle path.
        // And the system resource bundle path is resolved for the first time.
        // When appendPathToResTable is executed for the second time, next Entry Idx will not be zero
        if (nextEntryIdx == 0) {
            // The first resource bundle path stored in mAssetPaths is the path of system resources.
            // That is, the path of framework-res.apk, which was loaded when zygote started
            // Its ResTable object can be obtained through mZipSet.getZipResourceTable
            sharedRes = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTable(ap.path);
            // For APP, it's definitely not NULL.
            if (sharedRes != NULL) {
                // Get the number of resources.arsc in the system resource package path
                nextEntryIdx = sharedRes->getTableCount();
            }
        }
        // When the parameter is the resource bundle path other than the first in mAssetPaths,
        // For example, when app has its own resource bundle path, follow the following logic
        if (sharedRes == NULL) {
            // Check that the resource bundle is loaded by other processes, which is related to the ZipSet data structure, which is described in detail later.
            ass = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTableAsset(ap.path);
            // For app's own resource bundles, the following logic is common
            if (ass == NULL) {
                ALOGV("loading resource table %s\n", ap.path.string());
                // To create an Asset object, open resources.arsc
                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);
                }
            }
            // Only when zygote is started will the following logic be executed
            // Create ResTable for system resources and add it to mZipSet.
            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());
                // Create a ResTable object and add the previous Asset object associated with resources.arsc to the ResTabl
                sharedRes = new ResTable();
                sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
                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);
        // System resource bundle time
        if (sharedRes != NULL) {
            ALOGV("Copying existing resources for %s", ap.path.string());
            mResources->add(sharedRes);
        } else {
            // When a non-system resource bundle is in use, the Asset object associated with resources.arsc is added to Restable
            // This process parses the resources.arsc file.
            ALOGV("Parsing resources for %s", ap.path.string());
            mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
        }
        onlyEmptyResources = false;

        if (!shared) {
            delete ass;
        }
    } else {
        mResources->addEmpty(nextEntryIdx + 1);
    }

    if (idmap != NULL) {
        delete idmap;
    }
    MY_TRACE_END();

    return onlyEmptyResources;
}

You should have known the file resources.arsc before. If you haven't, you can look for an article online. The APK generates it when it is packaged, and we should be able to see it when we unzip the apk. This is basically the index of the stored resources, so different resolutions can load different pictures, which is a great contribution.

5. Resource Search Process

Now let's go back to the original loadDrawable() method, where drawable resources have actual resource files. The process of indexing such resources is generally divided into two steps: parsing the path of the resources represented by the resource ID; loading the resource files and caching them.
Draable is cached into Resources.mDrawableCache. When loading drawable, check if there is any in the cache first, and if so, return directly, then you don't need to load it. If there is no cache, it means that the resource file has not been loaded, so it should be loaded first and then cached into mDrawableCache. In the loadDrawable() method, drawable is loaded by loadDrawableForCookie():

    private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
        // The value of the drawable resource item is a string representing the path of the file
        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();

       .
        final Drawable dr;

        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
        try {
            if (file.endsWith(".xml")) {
                final XmlResourceParser rp = loadXmlResourceParser(
                        file, id, value.assetCookie, "drawable");
                dr = Drawable.createFromXml(this, rp, theme);
                rp.close();
            } else {
                // If drawable is a picture file, open it
                // assetCookie-1 is the index of the resource bundle path of the image in the AssetManager.mAssetPaths array of the native layer
                // Here's how to open the file
                final InputStream is = mAssets.openNonAsset(
                        value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                dr = Drawable.createFromResourceStream(this, value, is, file, null);
                is.close();
            }
        } catch (Exception e) {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            final NotFoundException rnf = new NotFoundException(
                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
        }
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);

        return dr;
    }

So far, the process of searching and loading resources has been thoroughly clarified: index + loading + caching.

All sharing outlines: The Way to Advance Android in 2017

Video commentary address: http://pan.baidu.com/s/1bC3lAQ

Posted by alohatofu on Thu, 18 Apr 2019 17:48:33 -0700