1 Introduction to plug-in

Plug-in begging

Plug-in technology originally originated from the idea of running APK without installation. This APK without installation can be understood as a plug-in, and the APP that supports plug-ins is generally called the host.

Problems solved by plug-ins

  1. APP has more and more functional modules, and the volume is getting bigger and bigger
  2. The coupling degree between modules is high, and the communication cost of collaborative development is increasing
  3. The number of methods may exceed 65535, and the APP takes up too much memory
  4. Applications call each other

Plug-in and componentization

Componentization: Componentization development is to divide an APP into multiple modules, and each module is a component. In the development process, we can make these components depend on each other or debug some components separately, but in the final release, these components are combined into one APK, which is componentization development.

Plug-in development: Plug-in development is slightly different from componentization. Plug-in development is to split the whole APP into multiple modules, including a host and multiple plug-ins, each module is an APK, and the host APK and plug-in APK are packaged separately in the final package.

Comparison of common plug-in frameworks

features dynamic-load-apk DynamicAPK Small DroidPlugin VirtualAPK
The author Ren Yugang ctrip wequick 360 drops
Supports four major components Only support Activity Only support Activity Only support Activity Full support Full support
Components need/do not need to be pre-registered in the host manifest Don’t need Need to be Don’t need Don’t need Don’t need
Plug-ins can/cannot depend on the host can can can Can not be can
PendingIntent is supported/not supported Does not support Does not support Does not support support support
Android Feature Support Most of the Most of the Most of the Almost all of the Almost all of the
Compatibility and adaptation general general medium high high
The plug-in build There is no The deployment of aapt Gradle plug-in There is no Gradle plug-in

Class loading mechanism

2.1 Android class loading mechanism

We know that Java class loading adopts the loading mechanism of parent delegate, which effectively prevents the repeated loading of classes and protects the loading of core classes. Android’s loading mechanism is similar to Java’s class loading mechanism, but there are differences. The relationship between the various classloaders in Android is shown below.

On Android, at APP runtime, all the APP’s own dex files containing bytecode files (.class) are wrapped as an Element object and placed in an Element[] Elements array, with each Element corresponding to a dex file. When an application looks for a particular class, it iterates through the array from front to back until it finds or iterates to the end.

The simple plug-in implementation in this article uses the Element array: Create an Element[] newElements array and concatenate the hostElement [] hostElements array with the pluginElements array. Replace the host’s existing array with a new Element array.

2.2 Finding the Element array

When the system loads Java classes in the application’s dex file through the PathClassLoader, the PathClassLoader does not override the loadClass() method. Therefore, BaseDexClassLoader, the parent of PathClassLoader, tries to perform the loading task. However, BaseDexClassLoader does not rewrite the loadClass() method, so it calls the loadClass() method of the parent class loader successively. The parent class loader (starting with the top-level parent loader trying to loadClass) tries to load the required classes in their respective loading scope, failing which it calls the subclass loader’s findClass() method (that is, the parent delegate mechanism) in turn. The findClass() method of The BaseDexClassLoader is eventually called.

// PathClassLoader removes all the commented code, only two constructors
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null.null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent); }}Copy the code
public class BaseDexClassLoader extends ClassLoader {
    // ...
	private final DexPathList pathList;
	// ...
	@Override
	protectedClass<? > findClass(String name)throws ClassNotFoundException {
    	List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    	Class c = pathList.findClass(name, suppressedExceptions);
    	if (c == null) {
        	ClassNotFoundException cnfe = new ClassNotFoundException(
            	"Didn't find class \"" + name + "\" on path: " + pathList);
        	for (Throwable t : suppressedExceptions) {
            	cnfe.addSuppressed(t);
        	}
        	throw cnfe;
    	}
    	return c;
	}
    // ...
}
Copy the code

Within this method, the program calls the findClass() method of the DexPathList private member attribute in the BaseDexClassLoader class.

final class DexPathList {
    // ...
    private Element[] dexElements;
    // ...
    publicClass<? > findClass(String name, List<Throwable> suppressed) {for(Element element : dexElements) { Class<? > clazz = element.findClass(name, definingContext, suppressed);if(clazz ! =null) {
            	returnclazz; }}if(dexElementsSuppressedExceptions ! =null) {
        	suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    	}
    	return null;
    }
    // ...
}
Copy the code

In this code, the enhanced for loop iterates backwards through the dexElements array, which is the Element[] array mentioned in the image above.

3 Project Example

3.1 Project Structure

3.2 Generating plug-in APK

3.2.1 Plugin MainActivity class

The mainactivity.java in the plug-in is not handled as follows:

package com.tongbo.plugin;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}Copy the code

3.2.2 Plug-in Test class

The Test class is used to Test after the plug-in is introduced, as follows:

package com.tongbo.plugin;

import android.util.Log;

public class Test {
    public static void print(a) {
        Log.e("TB"."Test class from com.tongbo.plugin"); }}Copy the code

3.2.3 ★ Generate plug-in APK and upload it ★

  • Run the plugin module;
  • inplugin\build\outputs\apk\debugFound in theplugin-debug.apkAnd upload it to the simulator/sdcard/The path.

3.3 Host loads the plug-in

3.3.1 Host MainActivity class

  • In the onCreate() callback, loadUtil.loadClass (this) is called to load the plug-in (see 3.3.2);

  • On the home page of the host, add a button and bind toStartPlugin(View View) to the onClick property, reflecting the test.print () method of the class calling the plug-in.

package com.tongbo.pluginbasic;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.lang.reflect.Method;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LoadUtil.loadClass(this);
    }

    public void toStartPlugin(View view) {
        Log.e("TB"."btn is clicked.");
        try{ Class<? > clazz = Class.forName("com.tongbo.plugin.Test");
            Method method = clazz.getMethod("print");
            method.invoke(null);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

3.3.2 Host the LoadUtil class

The loadClass(Context Context) method of the LoadUtil class is the core plug-in loading method, which is divided into the following steps:

  • The first step, reflected bydalvik.system.BaseDexClassLoaderthepathListField anddalvik.system.DexPathListthedexElementsField and set access permissions;
  • The second stepThe reflection acquires the hostdexElementsField value;
  • The third stepReflection gets the plugin’sdexElementsField value;
  • The fourth step, the use ofArray.newInstance()Method to create a newElement[], in the newly createdElement[]In the use ofSystem.arraycopy()Method to complete array concatenation;
  • Step 5, set the hostdexElementsThe value of the field is newElement[]To complete the plug-in loading.
package com.tongbo.pluginbasic;

import android.content.Context;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class LoadUtil {

    private final static String pluginApkPath = "/sdcard/plugin-debug.apk";

    public static void loadClass(Context context) {

        try {
            //TODO:to get 'pathList' field and 'dexElements' field by reflection.
            //private final DexPathList pathList;Class<? > baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            //private Element[] dexElements;Class<? > dexPathListClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            //TODO:to get the value of host's dexElements field.
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Object hostPathList = pathListField.get(pathClassLoader);
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

            //TODO:to get the value of plugin's dexElements field.
            DexClassLoader dexClassLoader = new DexClassLoader(
                    pluginApkPath,
                    context.getCacheDir().getAbsolutePath(),
                    null,
                    context.getClassLoader()
            );
            Object pluginPathList = pathListField.get(dexClassLoader);
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

            //TODO:to add host's dexElements and plugin's dexElements together in a newly created Element array.
            Object[] dexElements = (Object[]) Array.newInstance(
                    pluginDexElements.getClass().getComponentType(),
                    hostDexElements.length + pluginDexElements.length
            );
            System.arraycopy(
                    hostDexElements,
                    0, dexElements,
                    0,
                    hostDexElements.length
            );
            System.arraycopy(
                    pluginDexElements,
                    0,
                    dexElements,
                    hostDexElements.length,
                    pluginDexElements.length
            );

            //TODO:to set the newly created Element array to the host's dexElements field and done.
            dexElementsField.set(hostPathList, dexElements);
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

3.4 test

Run the host APP, click the button, and the console will print the following (success) :