The introduction

In the last section, we learned sample-Manager (I) of Shadow source code parsing, mainly explaining the host plug-in script/sample-Manager. apk plug-in design concept/host to sample-Manager. apk plug-in entry process, etc

Next, we will learn a series of processes after being hosted in the sample-manager.apk plug-in entrance to help us understand the functions of the plug-in sample-Manager to dynamically manage other business plug-ins (such as loading other plug-ins/updating plug-in logic, etc.).

The code analysis

1. Review of the previous section

As you can see from the host code, mpluginManager.enter enters the plug-in

2. Location of plug-in entry code

After entering the plug-in, the implementation of Enter is shown in the figure above, where you can see the core implementation is onStartActivity

3. OnStartActivity implementation

 private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
        //0) Prepare parameters
        final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
        final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
        final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
        final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
        Log.i(TAG, "SamplePluginManager, onStartActivity,pluginZipPath = " + pluginZipPath);
        Log.i(TAG, "SamplePluginManager, onStartActivity,partKey = " + partKey);
        Log.i(TAG, "SamplePluginManager, onStartActivity,className = " + className);

        executorService.execute(() -> {
            try {
                //1) Plug-in optimization etc, then return the first plug-in list (default)
                InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null.true);

                //2) Intent wrapper
                Intent pluginIntent = new Intent();
                pluginIntent.setClassName(context.getPackageName(), className);
                if(extras ! =null) {
                    pluginIntent.replaceExtras(extras);
                }

                //3) Load the framework plug-in (such as loader/ Runtime) and service plug-in, and start the plug-in activity
                startPluginActivity(installedPlugin, partKey, pluginIntent);
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "SamplePluginManager, the plug-in is started, so I'm not going to expand it, I'm going to expand it in the next stage."); }}); }Copy the code

As can be seen from the code, the following things are mainly done:

1) Parameter analysis

2) Do something asynchronously

2.1) Plug-in optimization, etc. Then return the first (default) plug-in list. 2.2) Intent wrapper 2.3) Load the framework plug-in (e.g. loader/ Runtime) and business plug-in, and start the plug-in activity at the same timeCopy the code

3. Parameter analysis

Several parameters are parsed here:

1) pluginZipPath: pluginZipPath: pluginZipPath: pluginZipPath: pluginZipPath: pluginZipPath

2) partKey: The name of the plug-in to launch

3) className: The name of the activity to start the plug-in

4. Optimization of plug-ins for asynchronous processing of some things

 InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null.true);
Copy the code

Simple 1 sentence code call, completed the plug-in optimization and other series of work

You can see that the input parameter is pluginZipPath and the output is the InstalledPlugin object

InstalledPlugin is a pluginZipPath memory abstraction.

Let’s take a look at the implementation code:

public InstalledPlugin installPlugin(String zip, String hash, boolean odex)
            throws IOException, JSONException, InterruptedException, ExecutionException {

        //1) Convert zip to PluginConfig configuration
        final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);

        final String uuid = pluginConfig.UUID;
        List<Future> futures = new LinkedList<>();

        //2) Odex optimization for plugins runTime/pluginLoader
        if(pluginConfig.runTime ! =null&& pluginConfig.pluginLoader ! =null) {
            //runTime
            Future odexRuntime = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME, pluginConfig.runTime.file);
                return null;
            });
            futures.add(odexRuntime);

            //pluginLoader
            Future odexLoader = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER, pluginConfig.pluginLoader.file);
                return null;
            });
            futures.add(odexLoader);
        }

        //3) Business plug-in so decompression /odex optimization, etc
        for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
            final String partKey = plugin.getKey();
            final File apkFile = plugin.getValue().file;

            // Business plug-in, plug-in APk so decompression
            Future extractSo = mFixedPool.submit((Callable) () -> {
                // Plugin apk so decompress
                extractSo(uuid, partKey, apkFile);
                return null;
            });
            futures.add(extractSo);

            // Business plug-in, odex optimization
            if (odex) {
                Future odexPlugin = mFixedPool.submit((Callable) () -> {
                    oDexPlugin(uuid, partKey, apkFile);
                    return null; }); futures.add(odexPlugin); }}//4) Task execution
        for (Future future : futures) {
            The /** * get () method can: * 1) return a result value when the task is finished, * 2) block the current thread until the task is finished ** / if the work is not finished
            future.get();
        }

        //5) Persist the plug-in information to the database (such as soDir/oDexDir, etc.)
        onInstallCompleted(pluginConfig);

        //6) Get the installed plug-ins. The last one installed is at the top of the returned List
        return getInstalledPlugins(1).get(0);
    }
Copy the code

The code is longer and does the following:

1) Convert zip to PluginConfig configuration

2) Odex optimization of framework plug-in runTime/pluginLoader

3) Business plug-in so decompression /odex optimization, etc

4) Persistence of plug-in information to database (e.g. SoDir /oDexDir, etc.)

5) Return the first in the business plug-in

So how did each of these things happen? And then what about Android? The following will explain one by one

4.1 Zip conversion to PluginConfig configuration

//1) Convert zip to PluginConfig configuration
final PluginConfig pluginConfig = installPluginFromZip(new File(zip), hash);
Copy the code

You can see that the input parameter is pluginZipPath and the output is the PluginConfig object

Here is to memory zip, the specific stamp here >>>

4.2 Odex optimization of framework plug-in runTime/pluginLoader

//2) Odex optimization for framework plug-in runTime/pluginLoader
if(pluginConfig.runTime ! =null&& pluginConfig.pluginLoader ! =null) {

            //runTime
            Future odexRuntime = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_RUNTIME, pluginConfig.runTime.file);
                return null;
            });
            futures.add(odexRuntime);

            //pluginLoader
            Future odexLoader = mFixedPool.submit((Callable) () -> {
                oDexPluginLoaderOrRunTime(uuid, InstalledType.TYPE_PLUGIN_LOADER, pluginConfig.pluginLoader.file);
                return null;
            });
            
            futures.add(odexLoader);
}
Copy the code

Here are the optimizations for the plugin framework runTime and pluginLoader. Let’s look at further implementations

public final void oDexPluginLoaderOrRunTime(String uuid, int type, File apkFile) throws InstallPluginException {
        try {
            File root = mUnpackManager.getAppDir();
            File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
            String key = type == InstalledType.TYPE_PLUGIN_LOADER ? "loader" : "runtime";
            // Core implementation
            ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, key));
        } catch (InstallPluginException e) {
            throwe; }}Copy the code

Create some directories above to prepare for odex optimization later

Let’s look at the specific optimization implementation

public static void oDexPlugin(File apkFile, File oDexDir, File copiedTagFile) throws InstallPluginException {
        String key = apkFile.getAbsolutePath();
        //key: sample-loader-debug.apk/sample-Run-time debug.apk etc
        / / value: the Object
        Object lock = sLocks.get(key);
        if (lock == null) {
            lock = new Object();
            sLocks.put(key, lock);
        }

        synchronized (lock) {
            if (copiedTagFile.exists()) {
                return;
            }

            // If the odex directory exists but is a file, not a directory, that is unexpected. It doesn't always work if you delete it.
            if (oDexDir.exists() && oDexDir.isFile()) {
                throw new InstallPluginException("oDexDir=" + oDexDir.getAbsolutePath() + "It already exists, but it's a file. I'm afraid to delete it.");
            }

            // Create the oDex directory
            oDexDir.mkdirs();

            new DexClassLoader(
                    apkFile.getAbsolutePath(),//dexPath
                    oDexDir.getAbsolutePath(),//optimizedDirectory
                    null.//librarySearchPath
                    ODexBloc.class.getClassLoader());//ClassLoader parent

            // Execute successfully, create tag file
            try {
                copiedTagFile.createNewFile();
            } catch (IOException e) {
                throw new InstallPluginException("ODexPlugin failed to create tag file:"+ copiedTagFile.getAbsolutePath(), e); }}}Copy the code

The core operation here is to instantiate DexClassLoader to optimize dex

How do you instantiate it?

Here’s a primer on loading android Dex:

The odex file structure will be generated in /dalvik/dalvik-cache when the application starts the app for the first time

However, our plug-in APK has not been installed, and the startup is not system startup, so the odex optimization of the plug-in needs to be done by ourselves, that is, the new DexClassLoader object can be directly used.

The specific reason why the system needs to do odex involves the knowledge of the system. Here is a passage from the Internet:

Then why can dex be optimized to Odex by directly new DexClassLoader? The details of the process can be found on the Internet.

4.3 Business plug-in so decompression/Odex optimization, etc

for (Map.Entry<String, PluginConfig.PluginFileInfo> plugin : pluginConfig.plugins.entrySet()) {
            final String partKey = plugin.getKey();
            final File apkFile = plugin.getValue().file;

            // Business plug-in, plug-in APk so decompression
            Future extractSo = mFixedPool.submit((Callable) () -> {
                // Plugin apk so decompress
                extractSo(uuid, partKey, apkFile);
                return null;
            });
            futures.add(extractSo);

            // Business plug-in, odex optimization
            if (odex) {
                Future odexPlugin = mFixedPool.submit((Callable) () -> {
                    oDexPlugin(uuid, partKey, apkFile);
                    return null; }); futures.add(odexPlugin); }}Copy the code

The first is so decompression

public final void extractSo(String uuid, String partKey, File apkFile) throws InstallPluginException {
        try {
            File root = mUnpackManager.getAppDir();
            String filter = "lib/" + getAbi() + "/";
            File soDir = AppCacheFolderManager.getLibDir(root, uuid);
            CopySoBloc.copySo(apkFile, soDir
                    , AppCacheFolderManager.getLibCopiedFile(soDir, partKey), filter);
        } catch (InstallPluginException e) {
            throwe; }}Copy the code

The code is relatively simple, copy so to the specified directory folder

Then there is the optimization of dex

 public final void oDexPlugin(String uuid, String partKey, File apkFile) throws InstallPluginException {
        try {
            File root = mUnpackManager.getAppDir();
            File oDexDir = AppCacheFolderManager.getODexDir(root, uuid);
            ODexBloc.oDexPlugin(apkFile, oDexDir, AppCacheFolderManager.getODexCopiedFile(oDexDir, partKey));
        } catch (InstallPluginException e) {
            throwe; }}Copy the code

Same optimization principle as above, so no longer redundant

4.4 Persisting Plug-in Information to the Database (for example, soDir/oDexDir)

//5) Persist the plug-in information to the database (such as soDir/oDexDir, etc.)
onInstallCompleted(pluginConfig);
Copy the code
public final void onInstallCompleted(PluginConfig pluginConfig) {
        File root = mUnpackManager.getAppDir();
        String soDir = AppCacheFolderManager.getLibDir(root, pluginConfig.UUID).getAbsolutePath();
        String oDexDir = AppCacheFolderManager.getODexDir(root, pluginConfig.UUID).getAbsolutePath();

        mInstalledDao.insert(pluginConfig, soDir, oDexDir);
}
Copy the code

This is simply to persist some information about the plug-in package ZIP database for future business use

4.5 Return the first business plug-in

//6) Get the installed plug-ins. The last one installed is at the top of the returned List
return getInstalledPlugins(1).get(0);
Copy the code
 /** * gets the installed plug-ins, with the last installed at the top of the return List **@paramLimit Maximum number of entries */
public final List<InstalledPlugin> getInstalledPlugins(int limit) {
    return mInstalledDao.getLastPlugins(limit);
}
Copy the code

This one is also relatively simple, in the persistent database just remove the first business plug-in information return load

5. Intent wrappers that handle something asynchronously

 //2) Intent wrapper
Intent pluginIntent = new Intent();
pluginIntent.setClassName(context.getPackageName(), className);
if(extras ! =null) {
   pluginIntent.replaceExtras(extras);
}
Copy the code

The name of the activity class of the plug-in to launch is given to the intent and so on

6. Asynchronous processing of some things to load/start series of plug-ins, etc

//3) Load the framework plug-in (such as loader/ Runtime) and service plug-in, and start the plug-in activity
startPluginActivity(installedPlugin, partKey, pluginIntent);
Copy the code

 public void startPluginActivity(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
        //1) Intent wrapper
        Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

        / / 2)
        mPluginLoader.startActivityInPluginProcess(intent);
 }
Copy the code

There are two things to do in this link:

1) The intent is wrapped twice

2) Start the activity of the plug-in

6.1 Wrapping intEnts twice

So let’s look at the first point, wrapper around intent 2

 //1) Intent wrapper
Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Copy the code
 public Intent convertActivityIntent(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) {
        //1) Load framework plug-ins (such as loader/ Runtime) and service plug-ins
        loadPlugin(installedPlugin.UUID, partKey);

        //2) Wrap the intent of the plugin
        return mPluginLoader.convertActivityIntent(pluginIntent);
 }
Copy the code

As you can see from the code, the framework plug-in is loaded before the intent is wrapped and then wrapped

Since an intent is intended to start a business activity with a business plug-in, the framework plug-in and the business plug-in must be prepared first. Then, information in the plug-in must be extracted and encapsulated in the intent to start the activity successfully

Let’s take a closer look at the implementation

A) Load framework plug-ins (such as Loader/Runtime) and service plug-ins

private void loadPlugin(String uuid, String partKey) {
        //1) Load framework plug-ins: loader and Runtime
        loadPluginLoaderAndRuntime(uuid, partKey);

        //2) Load the service plug-in: load the plug-in through the frame loader;
        // Example: partKey = sample-plugin-app
        mPluginLoader.loadPlugin(partKey);
}
Copy the code

Load the frame plug-in Loader and Runtime first, and then use the frame loader to load the service plug-in

Now let’s look at how to load the framework plug-in

private void loadPluginLoaderAndRuntime(String uuid, String partKey) {
        /*** * sample-runtime-release.apk * */
        loadRunTime(uuid);
        /** *sample-loader-release.apk * */
        loadPluginLoader(uuid);
}
Copy the code

To load the framework plugin runtime, you need to load sample-Runtimerelease.apk from the plugin zip.

public final void loadRunTime(String uuid) {
        InstalledApk installedApk;
        // 1) Loader plug-in memory
        installedApk = getRuntime(uuid);
        InstalledApk installedRuntimeApk = new InstalledApk(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath);
        //2) Load the frame plug-in loader according to the memorized object
        boolean loaded = DynamicRuntime.loadRuntime(installedRuntimeApk);
        if(loaded) { DynamicRuntime.saveLastRuntimeInfo(mHostContext, installedRuntimeApk); }}Copy the code

This implementation is emasculated because the original version is called remotely through IPC BInder, which omits IPC calls to avoid long links

Here is mainly “loader plug-in memory” and “according to the memory object loading framework plug-in loader”

Loader plug-in memory, this section is not good to say, so omit

Load the framework plug-in loader based on the memorized object

 public static boolean loadRuntime(InstalledApk installedRuntimeApk) {
        // Host ClassLoader
        ClassLoader contextClassLoader = DynamicRuntime.class.getClassLoader();

        HackParentToRuntime (installedRuntimeApk, contextClassLoader);
        RuntimeClassLoader runtimeClassLoader = getRuntimeClassLoader();

        if(runtimeClassLoader ! =null) {
            String apkPath = runtimeClassLoader.apkPath;
            if (TextUtils.equals(apkPath, installedRuntimeApk.apkFilePath)) {
                // The runtime of the same version is already loaded, no need to load
                Log.i(TAG, "DynamicRuntime has been loaded with the same apkPath runtime, do not need to load");
                return false;
            } else {
                // If the version is different, restore the normal classLoader structure before updating the RuntimerecoveryClassLoader(); }}// Attach runtime to pathclassLoader
        try {
            Log.i(TAG, DynamicRuntime, normal processing, attach Runtime to pathclassLoader);
            hackParentToRuntime(installedRuntimeApk, contextClassLoader);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return true;
 }
Copy the code
 /* * contextClassLoader is the loader of the host */
  private static void hackParentToRuntime(InstalledApk installedRuntimeApk,ClassLoader contextClassLoader) throws Exception {
        RuntimeClassLoader runtimeClassLoader = new RuntimeClassLoader(
                installedRuntimeApk.apkFilePath,
                installedRuntimeApk.oDexPath,
                installedRuntimeApk.libraryPath,
                contextClassLoader.getParent());

        hackParentClassLoader(contextClassLoader, runtimeClassLoader);
   }
Copy the code
static void hackParentClassLoader(ClassLoader classLoader, ClassLoader newParentClassLoader) throws Exception {
      
        Field field = getParentField();
        if (field == null) {
            throw new RuntimeException("No parent domain of type ClassLoader found in classLoader. class");
        }
        field.setAccessible(true);
        field.set(classLoader, newParentClassLoader);
  }
Copy the code

This looks like a lot of code, but it’s actually pretty simple

Build a Classloader to load the plugin, using the “replace the parent PathClassloader” solution.

So we’re done loading the framework runtime (Sample-Runtime-release.apk);

Loader (sample-loader-release.apk

public final void loadPluginLoader(String uuid) {
        InstalledApk installedApk;
        installedApk = getPluginLoader(uuid);
        File file = new File(installedApk.apkFilePath);
        if(! file.exists()) { Log.e(TAG, file.getAbsolutePath() +"File does not exist");
        }
        LoaderImplLoader implLoader = new LoaderImplLoader();
        mPluginLoader = implLoader.load(installedApk, uuid, mHostContext);
 }
Copy the code
 public PluginLoaderImpl load(InstalledApk installedApk, String uuid, Context appContext) {
        ApkClassLoader pluginLoaderClassLoader = new ApkClassLoader(
                installedApk,
                LoaderImplLoader.class.getClassLoader(),
                loadWhiteList(installedApk),
                1
        );

        // From apK, read the implementation of the interface
        //plugin-debug.zip/sample-loader-debug.apk
        LoaderFactory loaderFactory = null;
        try {
            loaderFactory = pluginLoaderClassLoader.getInterface(
                    LoaderFactory.class,
                    sLoaderFactoryImplClassName
            );
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (loaderFactory == null) {
            return null;
        }

        //buildLoader
        return loaderFactory.buildLoader(uuid, appContext);
}
Copy the code

Loader is implemented in sample-loader-release.apk

The plug-in loading framework uses the loader

class ApkClassLoader extends DexClassLoader {

    static final String TAG = "daviAndroid";
    private ClassLoader mGrandParent;
    private final String[] mInterfacePackageNames;

    ApkClassLoader(InstalledApk installedApk,
                   ClassLoader parent,////parent = host ClassLoader
                   String[] mInterfacePackageNames,
                   int grandTimes) {
        super(installedApk.apkFilePath, installedApk.oDexPath, installedApk.libraryPath, parent);

        / / by default
        ClassLoader grand = parent;//parent = host ClassLoader

        // How many generations are there
        for (int i = 0; i < grandTimes; i++) {
            grand = grand.getParent();
        }
        mGrandParent = grand;

        this.mInterfacePackageNames = mInterfacePackageNames;
    }

    @Override
    protectedClass<? > loadClass(String className,boolean resolve) throws ClassNotFoundException {
        String packageName;
        int dot = className.lastIndexOf('. ');
        if(dot ! = -1) {
            packageName = className.substring(0, dot);
        } else {
            packageName = "";
        }
        boolean isInterface = false;
        for (String interfacePackageName : mInterfacePackageNames) {
            if (packageName.equals(interfacePackageName)) {
                isInterface = true;
                break; }}//apkFilePath = /data/user/0/com.tencent.shadow.sample.host/files/pluginmanager.apk
        //oDexPath = /data/user/0/com.tencent.shadow.sample.host/files/ManagerImplLoader/ksi9pl9k
        if (isInterface) {
            // Case 1: The plugin can load the host class implementation:
            return super.loadClass(className, resolve);
        } else {
            // Case 2: The plug-in does not need to load the host class implementationClass<? > clazz = findLoadedClass(className);//1) System search
            if (clazz == null) {
                ClassNotFoundException suppressed = null;
                try {
                    // If not, look in your dexPath first
                    clazz = findClass(className);//2) find in the dexPath of your own
                } catch (ClassNotFoundException e) {
                    suppressed = e;
                }
                if (clazz == null) {
                    // If no parent ClassLoader is found, check the parent ClassLoader.
                    //BootClassLoader
                    try {
                        clazz = mGrandParent.loadClass(className);/ / to find his father
                    } catch (ClassNotFoundException e) {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            e.addSuppressed(suppressed);
                        }
                        throwe; }}}return clazz;
        }
    }

    <T> T getInterface(Class<T> clazz, String className) throws Exception {
        try{ Class<? > interfaceImplementClass = loadClass(className); Object interfaceImplement = interfaceImplementClass.newInstance();return clazz.cast(interfaceImplement);
        } catch (ClassNotFoundException | InstantiationException
                | ClassCastException | IllegalAccessException e) {
            throw newException(e); }}}Copy the code

The principle of this loader is actually described in the past.

After loading the framework plug-in Runtime and loader, the framework plug-in Loader is used to load the business plug-in

 //2) Load the service plug-in: load the plug-in through the frame loader;
 // Example: partKey = sample-plugin-app
 mPluginLoader.loadPlugin(partKey);
Copy the code

Some people may wonder why the business plug-in loader is used as a frame plug-in to load the business plug-in.

This is the dynamic design of shadow. All modules are dynamic (e.g. business plug-in download logic/business plug-in load etc.).

Because the explanation at the present stage is about the source code parsing of the sample-Manager module, that is, the sample-Manager. apk in the framework plug-in, how to load the service plug-in in the framework loader is not expanded here, and will be explained later when the module is parsed to loader.apk. Just be aware of it

B) Wrapped intents of plug-ins, etc

 //2) Wrap the intent of the plugin
return mPluginLoader.convertActivityIntent(pluginIntent);
Copy the code

This implementation is also in the loader, in fact, the content of the business plug-in information wrapped in the intent

The specific implementation will not be expanded for the time being, and will be explained later in the loader.apk module

6.2 Starting the Activity of the Plug-in

/ / 2)
mPluginLoader.startActivityInPluginProcess(intent);
Copy the code

This implementation is also in loader. The specific implementation will not be expanded for the time being, and will be explained later in the loader.apk module

At the end

Haha, that’s all for this article (systematic learning and growing together)

Tips

More exciting content, please pay attention to “Android hot repair technology” wechat public number