bzdww

Get answers and suggestions for various questions from here

The principle of iQiyi componentization exploration

cms

Author: Jiawei

Dynamic solution

The Android dynamic program has been booming for four or five years, and all major manufacturers have their own dynamic programs. For the time being, it is difficult to have a dynamic plan for the masters to dominate the rivers and lakes. The benefits of dynamics are obvious, reducing the size of the main app package, speeding up the release cycle, and fixing bugs online. So, when the app needs to access the dynamic solution, how should we start the technology selection?

Before answering this question, let's take a look at what is currently available for open source dynamization.

The first is a full containerization scheme represented by 360RePlugin and Divide VirtualAPK . In addition, it is a semi-container solution represented by Atlas, which is produced by Hand . Full containerization is also plug-in. This type of dynamization scheme completely isolates the host from the plug-in. The four components of the plug-in need not be declared in the host. A semi-contained solution is also a componentized solution. This type of dynamization scheme requires the four components of the component to be pre-registered into the host through a packaging script.

Atlas currently supports plug-in, but this article will take Atlas as a representative of the semi-container solution. In addition, the mainstream dynamic program is not the focus of this article. You are interested in seeing officials and you can communicate in private.

The iQiyi App has been plugged in early and has been running stably. Recently we started researching sustainable integrated systems and doing semi-container solutions like Atlas. After a survey of Atlas, it was found that its architecture design is more complicated, the packaging script is quite heavy, and the access and maintenance costs are quite high. In addition, we are not at ease with the framework of Ali Open Source, so we decided to develop ourselves.

When I started the semi-container framework research, I started the mainstream dynamization scheme including InstantApp. The InstantApp implementation was completed by GooglePlayer Service under Android 8.0. After Android 8.0, the relevant code was moved to the Framework layer. We decompile the GooglePlayer Service implementation of InstantApp and found that its InstantApp is actually a plugin framework. In combination with iQiyi's own business, we have come to the following principles as a technology selection.

  1. Low intrusion: Developers can develop component development like a native application.
  2. Easy to maintain: the component architecture is clear, the code is simple, and the packaging script is lightweight.
In the development of iQiyi's own semi-container program, we borrowed a lot of the ingenuity of Atlas, standing on the shoulders of giants, we can see farther.

Component scheme

In Atlas's introduction, there is a saying: "Atlas is a component framework, not a multi-process framework. Its job is to complete the installation of components, classes and resources on demand in the runtime environment."

Many vendors' plug-in solutions will start the plug-in in a separate process to prevent the main process from crashing due to plug-in crashes. After all, plug-in requires a lot of "black technology" and there is a certain risk in compatibility. Componentization does not need to spoof the system to start four major components in a way that is like plug-in. This will reduce the compatibility risk. Of course, this also leads to componentization that is not as flexible as plug-in, and can only be said to have advantages and disadvantages.

As with plug-in, componentization faces the problem of loading classes, resources, and so. Therefore, when introducing the iQiyi componentization solution, first introduce the following classes and resource loading. So loading is relatively simple, this article will not be introduced.

Class loading

There are two processing mechanisms for class loading, one is a single class loader mechanism, and the other is a multi-class loader mechanism. The single-class loader mechanism is better understood, that is, the entire App only has a class loader (that is, PathClassLoader) to complete the class loading, and all component classes are appended to the class loader. The multi-class loader mechanism means that each component is loaded by a new class loader instance. The classes between components are completely isolated and cannot be directly accessed from each other.

Droplet VirtualAPK is a single-class loader hex to complete the loading of the plug-in class. For details, see the VirtualAPK single-class loader implementation . The VirtualAPK class loading scheme is similar to MultiDex and Qzone hot fixes. Take the Dex pre-plug or post-plug method. carry out.

Atlas Atlas adopts a multi-class loader mechanism, and each component is loaded by a separate class loader instance. Atlas customizes two types of class loaders, one is DelegateClassLoader, which exists as a class lookup router and does not itself load as a real class. The other one is BundleClassLoader. Whenever a component is started, a BundleClassloader instance is created, which is responsible for loading the component class. The parent class loader of DelegateClassLoader is PathClassLoader, which can find the class loader of all components during class routing. The BundleClassLoader uses the BootClassloader as the parent class loader, and their reference relationships are as follows.

The Android system provides the DexClassLoader class for developers to dynamically load Dex, based on which we can load components as needed. Because the two types of loading mechanisms have their own merits, we also have a long choice about which class of loader mechanism to use. The single class loader mechanism is simple to implement. It is only necessary to append all the components of the component to the current application's PathClassLoader through dex pre-insertion or post-insertion, but there will be a troublesome ClassCastException , because in large-scale team development, We cannot guarantee that all classes are unique. The implementation principle of the multi-class loader mechanism is more complicated, but the Dex of the component is loaded by different DexClassLoader, which can well avoid the ClassCastException problem. There are many iQiyi teams. To avoid class conflicts, we adopt a multi-class loader mechanism.

The condition for class equality is that the class names are the same and are loaded by the same class loader.

Component class loading

Iqiyi's component class loading draws on some of Altas' ideas, but it's not the same. Most of Altas are directly working with DexFile , but in API 26 DexFile has been marked as obsolete and will be removed in subsequent Android versions. Therefore, to avoid compatibility issues, we still use the DexClassLoader method to load components directly. Atlas changes the suffix name of the component from apk to so, places it in the lib directory, and loads the local component (the component that is packaged with the app). The advantage of this is that when the application is installed, it will be automatically copied to the ApplicationInfo.nativeLibraryDir directory, avoiding an IO.

However, in the following system versions of Android 5.0, it is found that ClassNotFoundException occurs when loading component classes with DexClassLoader . Tracking the DexClassLoader source, we found that there is a makeDexElements method in the DexPathList class .

/**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makeDexElements(ArrayList<File> files,
                                             File optimizedDirectory) {
        ArrayList<Element> elements = new ArrayList<Element>();

        /*
         * Open all files and load the (direct or contained) dex files
         * up front.
         */
        for (File file : files) {
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                    || name.endsWith(ZIP_SUFFIX)) {
                zip = file;

                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ignored) {
                                        /*
                     * IOException might get thrown "legitimately" by
                     * the DexFile constructor if the zip file turns
                     * out to be resource-only (that is, no
                     * classes.dex file in it). Safe to just ignore
                     * the exception here, and let dex == null.
                     */
                }
            } else {
                System.logW("Unknown file type for: " + file);
            }

            if ((zip != null) || (dex != null)) {
                elements.add(new Element(file, zip, dex));
            }
        }
        return elements.toArray(new Element[elements.size()]);
    }


The suffix name of the component to be loaded must be one of dex, jar, apk, and zip. Other types of suffix names are unknown types. Because we changed the suffix name of the component to so, this type is unknown under Android 5.0 system. Direct operation of DexFile in Atlas, DexFile will not verify the suffix name, as long as the component is a Zip compression format file. Continue to analyze the makeDexElements method, the loadDexFile method will be called after the suffix name is successfully verified . If loadDexFile is successfully called, the Element instance will be created . Through the above analysis, we can directly call the loadDexFile method to bypass the suffix name verification process, and then create the corresponding Element object and splicing it into the DexPathList.dexElements instance.

As mentioned earlier, DelegateClassLoader exists as a class route finder, which itself is not responsible for directly loading classes. Therefore, you need to replace the application's PathClassLoader with a DelegateClassLoader. In doing so, we can complete the class lookup process based on our established strategy. The application's PathClassLoader instance is referenced by the mClassLoader member variable in LoadedApk , so we only need to create a DelegateClassLoader instance and assign it to mClassLoader .

The above outlines the implementation of the Iqiyi componentized multi-class loader mechanism principle, focusing on how to load the component Dex and the problems encountered. Others such as how to find the component class and the search order are basically consistent with Atlas. This article does not make too many narratives. For those who are interested, please refer to Atlas .

Resource loading

In the process of componentization, the problem of resource loading is extremely prominent. Not only the Android system version compatibility issue, but also the compatibility of different mobile phone manufacturers. Therefore, this article will spend more time on loading component resources.

There are two ways to load the current mainstream. One is resource isolation, that is, resources between different components cannot be directly accessed, and each component resource is accessed by different AssetManager instances. The other is resource partitioning. To avoid resource id conflicts, we can assign a unique packageId to each component. However, this method needs to modify the aapt source code. Atlas is to modify the aapt source code to achieve resource partitioning. If packaging and packaging of application resources is not clear, please refer to Lao Luo article Android application resource compilation and packaging process analysis and Android application resource manager (Asset Manager) creation process analysis .

At present, many plug-in solutions adopt resource isolation, that is, each plug-in is loaded with resources by different AssetManagers. For example, the well-known DroidPlugin is a resource isolation method. Resources between plug-ins cannot be accessed from each other, and resources between plug-ins and hosts cannot be accessed. Generally speaking, in the plug-in solution, each plug-in is generally a relatively independent service, and there is no coupling situation. Therefore, the method of resource isolation is more suitable. If there is a strong need for the plugin to access the host resource, it can be done by some means, such as using public.xml to lock the resource ID.

But if the boundaries between the various businesses are not so clear, there will be reuse of resources and code. So Atlas takes resource partitioning to solve resource conflicts. Atlas assigns a unique packageId to each component. The value of packageId is between 0x00 and 0x7f, so there is no resource ID conflict. Normally, the packageId of all third-party application resources is 0x7f, which is determined by the aapt packaging process. Atlas achieves the purpose of specifying the packageId by modifying the aapt source. Atlas aapt adds the "--forced-package-id" parameter to specify the resource packageId.

--forced-package-id
By default, package id should be 127 in R.java of application,using thie parametor would change the id to other value

Because we are doing componentization, we decided to take resource partitioning to complete resource loading, avoid resource conflicts and facilitate resource exchange.

Android resource loading has different processing logic on different versions, so we will describe how to complete component resource loading according to the three interval versions.

The resource loaded version is less than 5.0

First, start the analysis with the Android 4.2.2 source code. During the development process, the resources object is obtained by the getResources method provided by the Context. Through the Resources object, various resources can be obtained, so we start with the getResources method.

By tracking the source code of the Context resource, the actual implementation of the getResources method is in ContextImpl .

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


Then analyze when mResources is assigned.

final void init(LoadedApk packageInfo, IBinder activityToken, ActivityThread mainThread) {
        init(packageInfo, activityToken, mainThread, null, null, Process.myUserHandle());
    }
    
    final void init(LoadedApk packageInfo, IBinder activityToken, ActivityThread mainThread,
                    Resources container, String basePackageName, UserHandle user) {
        mPackageInfo = packageInfo;
        mBasePackageName = basePackageName != null ? basePackageName : packageInfo.mPackageName;
        mResources = mPackageInfo.getResources(mainThread);

        if (mResources != null && container != null
                && container.getCompatibilityInfo().applicationScale !=
                mResources.getCompatibilityInfo().applicationScale) {
            if (DEBUG) {
                Log.d(TAG, "loaded context has different scaling. Using container's" +
                        " compatiblity info:" + container.getDisplayMetrics());
            }
            mResources = mainThread.getTopLevelResources(
                    mPackageInfo.getResDir(), Display.DEFAULT_DISPLAY,
                    null, container.getCompatibilityInfo());
        }
        mMainThread = mainThread;
        mActivityToken = activityToken;
        mContentResolver = new ApplicationContentResolver(this, mainThread, user);
        mUser = user;
    }


In the init method of ContextImpl, you can find that mResources is assigned by mPackageInfo.getResources(mainThread) , and mPackageInfo is a LoadedApk object. Off-topic, ContextImpl#init(LoadedApk, IBinder, ActivityThread) is called by the ActivityThread class private methods createBaseContextForActivity, handleBindApplication, handleCreateService, etc. If ContextImpl is assigned as Activity in BaseContext, the IBinder parameter of ContextImpl#init(LoadedApk, IBinder, ActivityThread) is not Empty, other cases are assigned null . Closer to home continues to analyze the real scene of mResources created, Loaded#getResources (ActivityThread) source code is as follows.

public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir,
                    Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }


There is also a mResources member variable in LoadedApk , which is assigned by ActivityThread#getToplevelResources(String, displayId, Configuration, LoadedApk).

Resources getTopLevelResources(String resDir,
                                   int displayId, Configuration overrideConfiguration,
                                   LoadedApk pkgInfo) {
        return getTopLevelResources(resDir, displayId, overrideConfiguration,
                pkgInfo.mCompatibilityInfo.get());
    }


In the above method, the overload method ActivityThread#getToplevelResources(String, displayId, Configuration, CompatibilityInfo) is called.

/**
     * Creates the top level Resources for applications with the given compatibility info.
     *
     * @param resDir   the resource directory.
     * @param compInfo the compability info. It will use the default compatibility info when it's
     *                 null.
     */
    Resources getTopLevelResources(String resDir,
                                   int displayId, Configuration overrideConfiguration,
                                   CompatibilityInfo compInfo) {
        ResourcesKey key = new ResourcesKey(resDir,
                displayId, overrideConfiguration,
                compInfo.applicationScale);
        Resources r;
        synchronized (mPackages) {
            // Resources is app scale dependent.
            ......
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            ......
            if (r != null && r.getAssets().isUpToDate()) {
                ......
                return r;
            }
        }

        AssetManager assets = new AssetManager();
        if (assets.addAssetPath(resDir) == 0) {
            return null;
        }
        ......
        ......
        r = new Resources(assets, dm, config, compInfo);
        ......
        synchronized (mPackages) {
            ......
            ......
            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference<Resources>(r));
            return r;
        }
    }

Before analyzing the above methods, take a look at the role of mActiveResources and ResourcesKey .

final HashMap<ResourcesKey, WeakReference<Resources> > mActiveResources = new HashMap<ResourcesKey, WeakReference<Resources> >();


mActiveResources is a HashMap object with ResourcesKey as the key and WeakReference<Resources> as the value.

In the OS version is greater than or equal to 4.4 is less than or equal to 5.0, the new ResourceManager class is added, and mActiveResources is placed in the ResourcesManager.

mResources stores all the Resources objects, and the ResourcesKey can obtain the corresponding Resources object. So what are the conditions for the equalization of two ResourcesKey objects? Take a look at the ResourcesKey#equals(Object) method.

@Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ResourcesKey)) {
            return false;
        }
        ResourcesKey peer = (ResourcesKey) obj;
        if (!mResDir.equals(peer.mResDir)) {
            return false;
        }
        if (mDisplayId != peer.mDisplayId) {
            return false;
        }
        if (mOverrideConfiguration != peer.mOverrideConfiguration) {
            if (mOverrideConfiguration == null || peer.mOverrideConfiguration == null) {
                return false;
            }
            if (!mOverrideConfiguration.equals(peer.mOverrideConfiguration)) {
                return false;
            }
        }
        if (mScale != peer.mScale) {
            return false;
        }
        return true;
    }

Only the two member variables mResDir, mDisplayId, mOverrideConfiguration, and mScale of the ResourcesKey object are equal, indicating that the two ResourcesKey objects are equal. In the ActivityThread#getToplevelResources(String, displayId, Configuration, CompatibilityInfo) method, the following work is done.

  1. Create a ResourcesKey instance. The parameters of the ResourcesKey constructor are the member variables mResDir, mDisplayId, mOverrideConfiguration, and mScale that determine whether the ResourcesKey instance is equal .
  2. According to the created ResourcesKey object, look for the corresponding Resources object in mActiveResources . If there is a Resources object and it has not expired (as judged by the r.getAssets().isUpToDate() method), the object is returned directly. Otherwise go to step 3.
  3. Create an AssetManager object and call its addAssetPath method to load the application resource. Then create the Resources object and place it in mActiveResources .

After analyzing this, you can get a general idea of ​​how Resources is acquired and created. Then, continue to analyze resources, such as how Layout, String, etc. are obtained.

By referring to the Resources source code, Resources#getString(int), Resources#getLayout(int), etc. are all delegated to the AssetManager implementation, which is why the constructor parameters of the Resources need to be passed to the AssetManager instance.

Go back to the ActivityThread#getToplevelResources(String, displayId, Configuration, LoadedApk) method. Before creating the Resources object, create an AssetManager instance and call addAssetPath to load the application resource path.

/**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public native final int addAssetPath(String path);

    /**
     * Add multiple sets of assets to the asset manager at once.  See
     * {@link #addAssetPath(String)} for more information.  Returns array of
     * cookies for each added asset with 0 indicating failure, or null if
     * the input array of paths is null.
     * {@hide}
     */
    public final int[] addAssetPaths(String[] paths) {
        if (paths == null) {
            return null;
        }

        int[] cookies = new int[paths.length];
        for (int i = 0; i < paths.length; i++) {
            cookies[i] = addAssetPath(paths[i]);
        }

        return cookies;
    }


AssetManager provides the addAssetPath and addAssetPaths methods for application resource loading, but the test in 4.2.2 model found that calling the addAssetPath append component by reflection does not take effect, and Resources.NotFoundException appears . This is because under Android 5.0, dynamic resource path is not supported. As long as Resources.cpp is created, it is impossible to dynamically expand the resource table (this part of the logic is in Native code, and interested friends can sneak into Resources.cpp, AssetManager. Cpp and other related source code).

In order to solve this problem, we re-create the AssetManager, Resources instance to achieve the purpose of component loading. The ActivityThread.mActiveResources, ActivityThread.mActivities (Started Activity Collection), ActivityThread.mServices (Started Service Collection) and other non-empty Resources objects are all replaced with our newly created Resources instance, and LoadedApk.mResources, ContextImpl.mResources, etc. The Resources instance is empty.

The LoadsApk.mResources, ContextImpl.mResources and other Resources instances are set to null. When you need to get the Resources instance, you will enter the ActivityThread#getToplevelResources(String, displayId, Configuration, CompatibilityInfo) method. At this time, go to mActiveResources to find out if there is already corresponding. The Resources object, if any, returns the Resources instance that has been replaced by us.

Of course, the real implementation is not so simple, so we have also lie a lot of pits.

Create a new Resource instance, not only to load the resource path of the component, but also to load the resource path that has been loaded. So the first thing to solve is how to get the loaded resource path.

Calling the addAssetPath method will return an int value (greater than or equal to 0). If the value is equal to 0, the resource fails to load. If it is greater than 0, the load is successful. On the Native side there will be an array of all loaded resource paths, and the return value of addAssetPath indicates the index value of the loaded resource path in the array. So does the AssetManager provide a resource path through the index value? The answer is yes. Through AssetManager#getCookieName(int), we can get the corresponding resource path. The method input parameter is the index value of the loaded resource path. The getCookieName method is marked as a hidden method, so it needs to be called. Knowing how to get the loaded resource path, can you know how many resource paths have been loaded? Continue to analyze.

Before answering this question, first look at the constructor of Resources.

public Resources(AssetManager assets, DisplayMetrics metrics,
                     Configuration config, CompatibilityInfo compInfo) {
        mAssets = assets;
        mMetrics.setToDefaults();
        mCompatibilityInfo = compInfo;
        updateConfiguration(config, metrics);
        assets.ensureStringBlocks();
    }


As mentioned before, the acquisition of all resources in Resources is delegated to the AssetManager. The above constructor calls the AssetManager#ensureStringBlocks() method, which is the secret of resource access.

/*package*/
    final void ensureStringBlocks() {
        if (mStringBlocks == null) {
            synchronized (this) {
                if (mStringBlocks == null) {
                    makeStringBlocks(true);
                }
            }
        }
    }

    /*package*/
    final void makeStringBlocks(boolean copyFromSystem) {
        final int sysNum = copyFromSystem ? sSystem.mStringBlocks.length : 0;
        final int num = getStringBlockCount();
        mStringBlocks = new StringBlock[num];
        if (localLOGV) Log.v(TAG, "Making string blocks for " + this
                + ": " + num);
        for (int i = 0; i < num; i++) {
            if (i < sysNum) {
                mStringBlocks[i] = sSystem.mStringBlocks[i];
            } else {
                mStringBlocks[i] = new StringBlock(getNativeStringBlock(i), true);
            }
        }
    }


The ensureStringBlocks method mainly assigns values to mStringBlocks member variables, and mStringBlocks is an array of StringBlock types. The specific process of assignment is done in the makeStringBlocks(boolean) method. The method parameter is boolean. True means that the system resource path is loaded to the current AssetManager object, and fasle means that the system resource is not loaded. The case where the argument is false is called when the system AssetManager object sSystem is created .

/**
     * 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();
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }
    
   private static void ensureSystemAssets() {
        synchronized (sSync) {
            if (sSystem == null) {
                AssetManager system = new AssetManager(true);
                system.makeStringBlocks(false);
                sSystem = system;
            }
        }
    }


The ensureSystemAssets method is called each time an AssetManager instance is created. ensureSystemAssets creates a new AssetManager instance for loading system resource paths, which is done by calling makeStringBlocks(false) . Here we can see that the parameter passed in makeStringBlocks is false.

StringBlock represents the resource item value string resource pool of a resource index table used by the current application. When a string type resource of type String, Text, TextArray, etc. is obtained by StringBlock, StringBlock provides a cache function for quick access of resources. Corresponding to XmlBlock, used to obtain Xml type resources of type Anim, Layout, Drawable.

Going back, continue to analyze the makeStringBlocks method.

  1. Gets the number of system resource paths based on the Boolean value of the incoming parameter.
  2. Get the number of resource paths loaded by the current application through getStringBlockCount().
  3. Create a StringBlock array with a length of getStringBlockCount() return value;
  4. Enter the for loop. If it is a system resource, directly assign the StringBlock object corresponding to the system resource, otherwise create a new StringBlock object.

Through analysis, we can know:

  1. The number of resource paths that have been loaded can be obtained by the getStringBlockCount method or mStringBlock.length .
  2. The index value (0, 1, 2, 3, etc.) corresponding to the mStringBlock array is incremented by 1. The getCookieName(int) method is used to enter the parameter to obtain the resource path. The return value of addAssetPath mentioned above must be greater than 0 integer, so the array index value must be increased by 1.
  3. Each AssetManager instance will load the system resource path first, so we need to remove the system resource path from all loaded resource paths.
public List<String> getLoadedAppResDir() {
       AssetManager systemAsset = getSystemAsset();
       AssetManager appAsset = getAppAsset();
       Object[] sysStringBlocks = systemAsset.getStringBlocks();
       Object[] appStringBlocks = appAsset.getStringBlocks();
       int appResCount = appStringBlocks.length;
       int sysResCount = sysStringBlocks.length;
       List<String> loadedResDirList = new ArrayList<>(appResCount - sysResCount);
       for (int index = sysResCount + 1; index <= appResCount; ++index) {
           final String inApp = appAsset.getCookieName(index);
           loadedResDirList.add(inApp);
       }
       return loadedResDirList;
   }

The above method is how we implement the path to get the loaded application resource.

Note: Some phones will load the application's resource path into the system AssetManager instance.

If you complete the above steps, can you complete the component resources? Of course not, don't forget the Resource.Theme stuff. As mentioned before, we need to replace all the Resources objects created by the system for us, and Resources.Theme as the internal class of Resources will hold the Resources reference. Therefore, you need to handle the existence of the Resources.Theme instance, there are mTheme member variables in ContextImpl and ContextThemeWrapper , we set mTheme to null .

Here, mTheme is directly blanked , and there is no detailed reason. If you are interested, you can source the source code, which is very simple.

a pit loaded by the resource

When testing the system compatibility below Android 5.0, there is a Resource.NotFoundException on a 4.2.2 mobile phone from ZTE. The same test Huawei glory of a 4.2.2 mobile phone does not have this problem. Therefore, it can be seen that ZTE changed the resource loading implementation process. Through debugging, it is found that each time an Activity is started, the Resources object will be re-created instead of being obtained from the ActivityThread.mActiveResources cache. This also causes the ActivityThread.mActiveResources to increase in size by 1.

The solution is to determine whether the component's resources are loaded in the Instrumentation #callActivityOnCreate(Activity, Bundle) method.

In the iQiyi componentization framework, it is necessary to replace the instrumentation created by the system with the IQInstrumentation object we created, which is consistent with the Atlas idea.

How to determine whether the component resources have been loaded, there are rough methods in the past, the specific implementation is as follows.

public boolean checkResources(String resDir) {
       AssetManager appAsset = getAppAsset();
       Object[] appStringBlocks = appAsset.getStringBlocks();
       int appResCount = appStringBlocks.length;
       List<String> loadedResDirList = new ArrayList<>(appResCount);
       for (int index = 1; index <= appResCount; ++index) {
           final String inApp = appAsset.getCookieName(index);
           loadedResDirList.add(inApp);
       }
       return loadedResDirList.contains(resDir);
   }

checkResources differs from the previous introduction to getLoadedAppResDir in that checkResources does not strip system resource paths. If the checkResources return value is false, then re-create the Resources object, replace all the places where the old Resources object reference exists, such as Activty.mResources, ActivtyThread.mActiveResources, etc., and set Activty.mTheme to null.

The resource loaded version is greater than or equal to 5.0 and less than 7.0.

In this interval, the system version resource loading becomes quite simple. When the version of the analysis resource loading is less than 5.0 , it is mentioned that when the Resources.cpp instance is created, the resource table cannot be dynamically expanded. However, in Android 5.0 and above, the system supports dynamic expansion of resource tables. Therefore, we only need to reflect the call to AssetManager#addAssetPath(String) to chase the component resource path to the AssetManager. The Resources instance created by the system for the application will be saved to ResourcesManager.mActiveResources.

public static void loadResourceLollipop(String resDir) throws ReflectException {
       Map<ResourcesKey, WeakReference<Resources>> actives = getResourceManager().getActiveResources();
       for (Map.Entry<ResourcesKey, WeakReference<Resources>> entry : actives.entrySet()) {
           Resources resources = entry.getValue().get();
           if (resources == null) {
               continue;
           }
           int result = ReflectMethod.on(resources.getAssets()).signature(String.class).call("addAssetPath", resDir) 
           if (result == 0) {
               throw new ReflectException("Exception when load res " + resDir);
           }
       }
   }


The above method can complete the loading of component resources, which is much simpler than the version of the resource loaded is less than 5.0 .

The resource loaded version is greater than or equal to 7.0

In Android 7.0 and above, the ResourcesImpl class is added , and Resources is degraded to the packaging class of ResourcesImpl. That is, the Resources resource acquisition method is delegated to the ResourcesImpl implementation, and the ResourcesImpl resource acquisition method is delegated to the AssetManager implementation.

Android 7.0 and above resource loading ideas and resource loading version is greater than or equal to 5.0 and less than 7.0 , you need to find the location where ResourcesImpl is stored, and call AssetManager#addAssetPath(String) to load the component resource path.

Through source code analysis, the ResourcesImpl implementation created by the system for the application is saved to ResourcesManager.mResourceImpls.

public static void loadResourceN(String resDir) throws ReflectException {
       Map<ResourcesKey, WeakReference<ResourcesImpl>> impls = getResourceManagerN().getResourcesImpls();
       for (Map.Entry<ResourcesKey, WeakReference<ResourcesImpl>> entry : actives.entrySet()) {
           ResourcesImpl impl = entry.getValue().get();
           if (impl == null) {
               continue;
           }
           int result = ReflectMethod.on(impl.getAssets()).signature(String.class).call("addAssetPath", resDir) 
           if (result == 0) {
               throw new ReflectException("Exception when load res " + resDir);
           }
       }
   }


The above method can complete the resource loading of system components at 7.0 and above, and the implementation principle is also very simple. However, when the startup component contains the Activity of WebView, the resource will not find an exception. Through debugging analysis, in the WebViewActivity loaded resource path, the component resource path is not included, but the /system/app/WebViewGoogle/WebViewGoogle.apk resource path is increased, and the size of ResourcesManager.mResourceImpls is increased. Note that when starting WebViewActivity, the ResourcesImpl object will be re-created. Therefore, we need to determine whether the Resource object of the Activity contains the component resource path before the Activity starts. If it does not, we call the AssetManager#addAssetPath(String) method to load the component resource path. The specific approach is similar to a pit in which the resource loading version is less than 5.0 mentioned in the resource loading.

At 7.0 and above, the resource path of WebView is dynamically added, that is, the WebView resource path is loaded only when WebViewActivity is started. Starting with 7.0, the two methods of equaling the ResourcesKey instance have changed.

@Override
   public boolean equals(Object obj) {
       if (!(obj instanceof ResourcesKey)) {
           return false;
       }

       ResourcesKey peer = (ResourcesKey) obj;
       if (mHash != peer.mHash) {
           // If the hashes don't match, the objects can't match.
           return false;
       }

       if (!Objects.equals(mResDir, peer.mResDir)) {
           return false;
       }
       if (!Arrays.equals(mSplitResDirs, peer.mSplitResDirs)) {
           return false;
       }
       if (!Arrays.equals(mOverlayDirs, peer.mOverlayDirs)) {
           return false;
       }
       if (!Arrays.equals(mLibDirs, peer.mLibDirs)) {
           return false;
       }
       if (mDisplayId != peer.mDisplayId) {
           return false;
       }
       if (!Objects.equals(mOverrideConfiguration, peer.mOverrideConfiguration)) {
           return false;
       }
       if (!Objects.equals(mCompatInfo, peer.mCompatInfo)) {
           return false;
       }
       return true;
   }


The above method is to determine whether two ResourcesKey objects are equal in the ResourcesKey. One of the conditions for obtaining equality is that the hash values ​​must be equal, and more *mLibDirs, mSplitResDirs*, etc. must be equal.

The figure above is a diagram of the ResourcesKey instance that contains the WebView resource path during debugging. As you can see, the WebView resource path is assigned to mLibDirs , which is why the component resource path is not loaded when WebViewActivity is started.

end

The framework of componentization and plug-in is quite numerous. By analyzing some of the representative frameworks, it is found that the basic implementation methods are similar. Therefore, we have developed a componentized framework in conjunction with our own business. The benefits of redevelopment are ease of maintenance and expansion. Implementing a dynamic framework is not difficult, it is difficult to deal with various compatibility issues and a complete component-based sustainable integration solution, which requires front-end back-end to work together. At present, the first phase of iQiyi componentization has been completed, and the follow-up will evolve around the ecological integration of sustainable integration. This article mainly introduces how to dynamically load components. In the next article, I will introduce the research and encounter problems of Iqiyi packaging components.