First, understanding plug-in

1.1 Plug-in origins

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.

As we all know, in the Android system, applications exist in the form of Apk, applications need to be installed to use. In fact, the Android system installation is quite simple, in fact, the application Apk copy to a different directory on the system, and then extract so.

Common application installation directories are as follows:

  • /system/app: System application
  • /system/priv-app: System application
  • /data/app: User application

You may wonder, since the installation process is so easy, how does Android run the application code, let’s first look at the composition of Apk, a common Apk will contain the following several parts:

  • classes.dex:JavaCode bytecode
  • res: Resource file
  • lib:sofile
  • assets: static asset file
  • AndroidManifest.xml: manifest file

When you open the application on Android, you just open the process and use the ClassLoader to load classs. dex into the process and execute the corresponding components.

Why can’t we execute code in Apk since Android itself uses a similar form of reflection to load code?

1.2 Plug-in Advantages

Plugins allow code in Apk (mainly Android components) to run without installation, which brings many benefits:

  • To reduce the installationApkVolume, download modules on demand
  • Dynamic update plug-in
  • Hosts and plug-ins are compiled separately to improve development efficiency
  • The number of solutions exceeds 65535

Imagine that your app has the same high performance as a Native app, but still gets the same revenue as a Web app.

Well, it’s nice, isn’t it?

1.3 The difference with componentization

  • componentization: Will be oneAppDivided into multiple modules, each module is a component (module), the development process can make these components dependent on each other or independent compilation, debugging part of the components, but these components will eventually merge into a completeApkPublish to the app market.
  • Plug-in: The whole App is divided into many modules, each module is an Apk (componentized module is a lib), and the host Apk and plug-in Apk are packaged separately in the final packaging process. Only the host Apk is released to the application market, and the plug-in Apk is dynamically delivered to the host Apk on demand.

Second, the technical difficulties of plug-in

To make the plug-in Apk work, we need to find the location of the plug-in Apk first, and then we need to be able to parse and load the code in the Apk.

But it doesn’t make sense to execute Java code with light. There are four major components that need to be registered in the Android system. ActivityManagerService (AMS) and PackageManagerService (PMS) are registered in the Android system, and the four components depend on AMS and PMS for parsing and startup. Get him to acknowledge a component in an uninstalled Apk, How to make the host dynamically load Android components (Activity, Service, BroadcastReceiver, ContentProvider, Fragment) in Apk is the biggest difficulty of plug-in.

Application resource references are also a problem. Imagine what happens when you load a plug-in Apk using reflection in the host process and the id corresponding to R cannot refer to the correct resource.

To sum up, in fact, the main points of plug-in are these:

  • How do I load and execute a plug-inApkCode (ClassLoader Injection)
  • Enable the system to call plug-insApkComponents in (Runtime Container)
  • Correctly identify plug-insApkResources (Resource Injection)

There are a few other minor issues, but they may not apply to all scenarios, and we’ll talk about them separately later.

Third, this Injection

ClassLoader is a must to master in plug-in, because we know that the Android application itself is based on the modified Java VIRTUAL machine, and because the plug-in is not installed apK, the system will not process the classes, so we need to use ClassLoader to load APK. And then reflect the code inside.

3.1 ClassLoader in Java

  • The BootstrapClassLoader is responsible for loading core classes for the JVM runtime, such as JAVA_HOME/lib/rt.jar, and so on

  • ExtensionClassLoader is responsible for loading JVM extension classes, such as jar packages under JAVA_HOME/lib/ext

  • AppClassLoader is responsible for loading jars and directories in the classpath

3.2 ClassLoader in Android

In Android system, ClassLoader is used to load dex files, including APK files and JAR files containing dex. Dex file is a product of class file optimization. When an Application is packaged in Android, it merges and optimizes all the class files (keeping only one duplicate of the different class files) to produce a final class.dex file

  • The PathClassLoader is used to load system classes and application classes. It can load the dex file in the installed APK directory

    public class PathClassLoader extends BaseDexClassLoader {
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null.null, parent);
        }
    
        public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
            super(dexPath, null, libraryPath, parent); }}Copy the code
  • DexClassLoader is used to load dex files. You can load dex files from storage space.

    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
            super(dexPath, newFile(optimizedDirectory), libraryPath, parent); }}Copy the code

We generally use DexClassLoader in plug-in.

3.3 Parent delegation mechanism

Each ClassLoader has a parent object, which represents the parent ClassLoader. When loading a class, the parent ClassLoader is used first. If the parent object is not found in the parent ClassLoader, the system ClassLoader is used to load the class. This mechanism ensures that all system classes are loaded by the system class loader. The following is a concrete implementation of the loadClass method of ClassLoader.

    protectedClass<? > loadClass(String name,boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loadedClass<? > c = findLoadedClass(name);if (c == null) {
                try {
                    if(parent ! =null) {
                        // Load from the parent class loader first
                        c = parent.loadClass(name, false);
                    } else{ c = findBootstrapClassOrNull(name); }}catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If it is not found, load it againc = findClass(name); }}returnc; } Duplicate codeCopy the code

3.4 How Do I Load classes in a Plug-in

To load the classes in the plug-in, we first need to create a DexClassLoader by taking a look at the parameters required by the DexClassLoader constructor.

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        // ...}} Copy the codeCopy the code

The constructor takes four arguments: DexPath is the dex/APK/JAR file path to be loaded. OptimizedDirectory is the location where dex is stored after optimization. On ART, OAT will be executed to optimize dex and generate machine code. This is where the optimized Odex file is stored. LibrarySearchPath is a native dependent location. Parent is the parent class loader, which loads the corresponding class from parent by default

Once the DexClassLaoder instance is created, you can simply call its loadClass(className) method to load the classes in the plug-in. The specific implementation is as follows:

    // Take the apK from assets and put it into the internal storage space
    private fun extractPlugin(a) {
        var inputStream = assets.open("plugin.apk")
        File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init(a) {
        extractPlugin()
        pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        // Generate a DexClassLoader to load the plug-in class
        pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this: :class.java.classLoader)
    }
Copy the code

3.5 Execute the methods of the plug-in class

Class methods are executed by reflection

val loadClass = pluginClassLoader.loadClass(activityName)
loadClass.getMethod("test".null).invoke(loadClass)
Copy the code

We call this process ClassLoader injection. After injection, all classes from the host are loaded using the host’s ClassLoader, all classes from the plug-in Apk are loaded using the plug-in ClassLoader, and due to the parent delegate mechanism of the ClassLoader, In fact, the system classes are not affected by the ClassLoader’s class isolation mechanism, so that the host Apk can use the component classes from the plug-in in the host process.

Fourth, the Runtime Container

As we mentioned earlier, the biggest difficulty in pluginizing an Activity is tricking the system into acknowledging an uninstalled component in Apk. Because the plug-in is dynamically loaded, the four components of the plug-in cannot be registered in the host Manifest file, and the four components that are not registered in the Manifest file cannot interact with the system directly. Registering a plug-in’s Activity directly in the host Manifest loses the dynamic nature of the plug-in, because every time a new Activity is added to the plug-in, the host Manifest is modified and repackaged, and it is no different from writing directly to the host.

4.1 Why Unregistered Activities cannot Interact with the System

There are two meanings of not interacting directly

  1. If we start an Activity that is not registered in the Manifest, we will find the following error:

    android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zyg.commontec/com.zyg.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?
    
    Copy the code

This log can be seen in the checkStartActivityResult method of Instrumentation:

public class Instrumentation {
    public static void checkStartActivityResult(int res, Object intent) {
        if(! ActivityManager.isStartResultFatalError(res)) {return;
        }

        switch (res) {
            case ActivityManager.START_INTENT_NOT_RESOLVED:
            case ActivityManager.START_CLASS_NOT_FOUND:
                if (intent instanceofIntent && ((Intent)intent).getComponent() ! =null)
                    throw new ActivityNotFoundException(
                            "Unable to find explicit activity class "
                            + ((Intent)intent).getComponent().toShortString()
                            + "; have you declared this activity in your AndroidManifest.xml?");
                throw new ActivityNotFoundException(
                        "No Activity found to handle "+ intent); . }}}Copy the code
  1. The life cycle of an Activity cannot be called. In fact, the main work of an Activity is called in its life cycle method. Since the system detected the Manifest file in the previous step, the Activity was rejected, so its life cycle method will definitely not be called. Therefore, the plug-in Activity will not run properly.

4.2 Runtime container technology

The components in Android (activities, Services, BroadcastReceiver, and ContentProviders) are created by the system, and the system manages the life cycle. It is useless to simply construct instances of these classes; you also need to manage the life cycle of the components. Among them, Activity is the most complex, and different frameworks take different approaches. How plug-ins support component lifecycle management. There are roughly two ways:

  • Runtime container Technology (ProxyActivity proxy)
  • Buried StubActivity, hook system to start the Activity process

Our solution is very simple. It’s called runtime container technology. Basically, we embed empty Android components in the host Apk. And register it in androidmanifest.xml.

What it does is simply help us act as a container for the plug-in Activity. It accepts several parameters from the Intent, each containing information about the plug-in, such as:

  • pluginName
  • pluginApkPath
  • pluginActivityName

The pluginApkPath and pluginActivityName are important. When ContainerActivity starts, we load the ClassLoader and Resource of the plug-in. And reflect the Activity class corresponding to the pluginActivityName. When it’s finished loading, ContainerActivity does two things:

  • Forward all lifecycle callbacks from the system to the plug-inActivity
  • acceptActivityMethod and forward it back to the system

We can do the first step by copying ContainerActivity’s lifecycle method, and the second step is to define a PluginActivity, and then when writing the Activity component in the plug-in Apk, Instead of integrating with Android.app. Activity, integrate with our PluginActivity.

public class ContainerActivity extends Activity {
    private PluginActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        String pluginActivityName = getIntent().getString("pluginActivityName"."");
        pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
        if (pluginActivity == null) {
            super.onCreate(savedInstanceState);
            return;
        }

        pluginActivity.onCreate();
    }

    @Override
    protected void onResume(a) {
        if (pluginActivity == null) {
            super.onResume();
            return;
        }
        pluginActivity.onResume();
    }

    @Override
    protected void onPause(a) {
        if (pluginActivity == null) {
            super.onPause();
            return;
        }
        pluginActivity.onPause();
    }

    // ...
}
Copy the code
public class PluginActivity {
    private ContainerActivity containerActivity;

    public PluginActivity(ContainerActivity containerActivity) {
        this.containerActivity = containerActivity;
    }

    @Override
    public <T extends View> T findViewById(int id) {
        return containerActivity.findViewById(id);
    }
    // ...
}
Copy the code
// Plugin 'Apk' actually writes components
public class TestActivity extends PluginActivity {
    / /...
}
Copy the code

Is feeling a little understand, although actually there are many small pits, but the principle is so simple, probably start plug-in components need to rely on the container, the container is responsible for loading the plug-in components and complete two-way forward, forward from system lifecycle callback to plug-in components, from the plug-in components are also forwarded system call to the system.

4.3 Bytecode replacement

Although this method can well realize the purpose of starting the plug-in Activity, but because of the highly invasive development, the Activity in the plug-in must inherit the PluginActivity. If you want to transform the previous module into a plug-in, it needs a lot of extra work.

class TestActivity extends Activity {} - >class TestActivity extends PluginActivity {}
Copy the code

Is there any way to write a plug-in component exactly the same as before?

Shadow is a bytecode replacement plug-in, which is a great idea. To put it simply, Android provides some Gradle plug-in development kits, including a feature called Transform Api, which can be involved in the construction process of a project. After bytecode generation and before dex file generation, Some transformation of the code, the specific how to do not say, you can see the documentation.

Gradle plugin: Gradle plugin: Gradle plugin

class TestActivity extends Activity {}
Copy the code

Then, after compiling, the final bytecode displays:

class TestActivity extends PluginActivity {}
Copy the code

That’s pretty much the end of the basic framework.

Fifth, the Resource Injection

The last point is resource injection, which is quite important. Android application development advocates the idea of separating logic and resources. All resources (layout, values, etc.) are packaged into Apk, and a corresponding R class is generated, which contains the reference ids of all resources.

It’s not easy to inject resources, but The good news is that Android gives us a way back, the most important being these two interfaces:

  • PackageManager#getPackageArchiveInfo: according to theApkPath resolution of an uninstalledApkPackageInfo
  • PackageManager#getResourcesForApplication: according to theApplicationInfoTo create aResourcesThe instance

All we need to do is create an instance of the plugin resource using these two methods when we load the plugin Apk in ContainerActivity#onCreate above. PackageManager#getPackageArchiveInfo = PackageInfo (Apk) Then through PackageManager# getResourcesForApplication to create instances of resources, probably code like this:

PackageManager packageManager = getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
    pluginApkPath,
    PackageManager.GET_ACTIVITIES
    | PackageManager.GET_META_DATA
    | PackageManager.GET_SERVICES
    | PackageManager.GET_PROVIDERS
    | PackageManager.GET_SIGNATURES
);
packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;

Resources injectResources = null;
try {
    injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
} catch (PackageManager.NameNotFoundException e) {
    // ...
}
Copy the code

Once we have the resource instance, we need to Merge the host Resources with the plug-in Resources, write a new Resources class, and do the automatic proxy like this:

public class PluginResources extends Resources {
    private Resources hostResources;
    private Resources injectResources;

    public PluginResources(Resources hostResources, Resources injectResources) {
        super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
        this.hostResources = hostResources;
        this.injectResources = injectResources;
    }

    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
        try {
            return injectResources.getString(id, formatArgs);
        } catch (NotFoundException e) {
            returnhostResources.getString(id, formatArgs); }}// ...
}
Copy the code

Then we create a Merge resource after ContainerActivity has loaded the plug-in component, duplicate ContainerActivity#getResources, and replace it with the obtained resources:

public class ContainerActivity extends Activity {
    private Resources pluginResources;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // ...
        pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
        // ...
    }

    @Override
    public Resources getResources(a) {
        if (pluginActivity == null) {
            return super.getResources();
        }
        returnpluginResources; }}Copy the code

This completes the injection of resources.

[Reference article]

Android Plugin hack Technology implementation Principle