Preliminary knowledge

  1. Understand basic Android development
  2. Understand the basic principles of the four components of Android
  3. Learn about ClassLoader

How far can I go after reading this article

  1. Understand the common implementation principles of plug-ins

Prepare before reading

  1. Clone CommonTec project

The article gives an overview of

History of plugin frameworks

The whole history of plug-in framework refers to bao jianqiang’s speech in the 2016GMTC global development conference. In 2012, AndroidDynamicLoader implemented a plug-in framework for fragments, which can dynamically load fragments in plug-ins to achieve page switching. 23Code 2013 provides a shell in which plug-ins can be dynamically downloaded and run. In the Alibaba Technology Salon in 2013, Boquet shared the Atlas plug-in framework, indicating that At that time, Ali was already doing the application and development of plug-ins. In 2014, Ren Yugang opened the source of Dynamic-load-APK and realized the dynamic through agent distribution. If you have read the book Art Exploration of Android Development, you should know about this method. In 2015, Zhang Yong released DroidPlugin, using hook system method to realize plug-in. In 2015, Ctrip released DynamicApk between 2015 and 2016 (the time is uncertain), Lody released VirtualApp, which can directly run the uninstalled APK. Basically, it is implemented by hook system, but the implementation is much more delicate. Implement your own set of AMS to manage plug-in activities, and so on. In 2017, Alibaba launched Atlas. In 2017, 360 launched RePlugin. In 2017, Didi launched VirtualApk. In 2019, Tencent launched Shadow. Essentially, you use the broker to distribute the lifecycle to make the four components dynamic, and then abstract the interface to make the framework dynamic. We’ll have a chance to analyze it later.

This is basically the history of the plug-in framework. From 2012 to now, it can be said that the plug-in technology has been basically formed, mainly in two ways: agent and hook system (there is no statistical development of hotfix here. In fact, there is some understanding between hotfix and plug-in, and the following articles will introduce hotfix). Looking to the future, I would venture to predict that the principle of plug-in technology will not change much.

Two, the noun explanation

In pluginization, there are some proper terms, if you are the first contact may not be familiar, here to explain. The host is responsible for loading the plug-in’s APK, which is generally the installed application itself. StubActivity A placeholder Activity in the host that is registered in the host Manifest file and is responsible for loading plug-in activities. PluginActivity: The PluginActivity in the plugin APK is not registered in the Manifest file and requires StubActivity to load.

3. Use Gradle to simplify the plug-in development process

When learning and developing plug-ins, we need to dynamically load the plug-in APK, so there are generally two APKs in the development process, one is the host APK, the other is the plug-in APK, and the corresponding host project and plug-in project are required. In CommonTec, you create app as the host project and Plugin as the plug-in project. For convenience, we put the generated plug-in APK directly into assets in the host APK, which is placed directly into internal storage space for loading when the APK is started. With such a project structure, our process for debugging problems is as follows: Modify the plugin project -> compile and generate the plugin apk -> Copy the plugin apK to host assets -> Modify the plugin project -> compile and generate the plugin APk -> Install the plugin APK -> Verify the problem If we go through this long process every time we fix a small problem, Patience will soon run out. It is better to automatically package the plug-in APK and copy it to the host assets directory when compiling the host APK directly, so that we can compile the host project directly, no matter what changes we make. How does that work? Remember the Gradle series we talked about before? Now is the time to put what you have learned into practice. First, add the following code to the plugin project’s build.gradle:

project.afterEvaluate {
    project.tasks.each {
        if (it.name == "assembleDebug") {
            it.doLast {
                copy {
                    from new File(project.getBuildDir(), 'outputs/apk/debug/plugin-debug.apk').absolutePath
                    into new File(project.getRootProject().getProjectDir(), 'app/src/main/assets')
                    rename 'plugin-debug.apk'.'plugin.apk'
                }
            }
        }
    }
}
Copy the code

After afterEvaluate, the code iterates through the project’s tasks, finds the packaged task, which is assembleDebug, and then copies the generated APK into the assets directory of the host project and renaming it to plugin.apk. Then add the following code to your app project’s build.gradle:

project.afterEvaluate {
    project.tasks.each {
        if (it.name == 'mergeDebugAssets') {
            it.dependsOn ':plugin:assembleDebug'}}}Copy the code

Find the mergeDebugAssets task of the host package, which depends on the package of the plug-in project, so that every time the host project is compiled, the plug-in project will be compiled first, and then the plug-in APK will be copied to the assets directory of the host APK. Every time you change it, you just need to compile the host project.

Four, this

ClassLoader is a must in pluginization, because the plug-in is an uninstalled APK, the system will not process the classes in it, so we need to handle it ourselves.

4.1 ClassLoader in Java

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

The ExtensionClassLoader is responsible for loading the JVM’s extension classes, such as the JAR packages under JAVA_HOME/lib/ext

The AppClassLoader is responsible for loading jar packages and directories in the CLASspath

4.2 Android ClassLoader

The APK file and JAR file containing the dex are the dex file. The PathClassLoader is used to load system classes and application classes. It can load the dex file in the apK directory that has been installed

The DexClassLoader is used to load the dex file. You can load the dex file from the storage space.

We usually use a DexClassLoader in plugins.

4.3 Parent delegation mechanism

Each ClassLoader has a parent object, which represents the parent ClassLoader. When a class is loaded, it will first use the parent ClassLoader to load it. If the parent ClassLoader is not found, it will load the class itself. This mechanism ensures that the 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 not found, load it by yourselfc = findClass(name); }}return c;
    }
Copy the code

4.4 How Do I Load Classes in a Plug-in

To load the classes in the plug-in, we first create a DexClassLoader. Let’s take a look at what parameters the constructor of DexClassLoader takes.

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

The constructor takes four arguments: OptimizedDirectory is the location where the dex is stored after the dex is optimized. On ART, oAT will be executed to optimize the dex and generate machine code. This is where the optimized odex file will be stored. LibrarySearchPath is the location of the native dependency. Parent is the parent class loader

Once an instance of DexClassLaoder has been created, simply call its loadClass(className) method to load the classes in the plug-in. The concrete implementation is as follows:

    // Take the plugin APK from assets and place it in internal storage
    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 plug-in classes
        pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this: :class.java.classLoader)
    }
Copy the code

Five, plug-ins need to solve the difficulties

If the class is a normal class, then use the DexClassLoader mentioned above to load it directly. If the class is a special class, such as Activity or four major components, then some special processing is required. Because there are four components that need to interact with the system. In the plug-in, the four components need to solve the following difficulties:

  • Activity
  1. How is the lifecycle invoked
  2. How do I use the resources in the plug-in
  • Service
  1. How is the lifecycle invoked
  • BroadcastReceiver
  1. Registering static and dynamic broadcasts
  • ContentProvider
  1. How do I register a plug-in Provider with the system

6. Plug-in implementation of Activity

6.1 Analysis of Difficulties

We talked earlier about the difficulty of pluginizing activities. Let’s first clarify why these two problems occur. Because the plug-in is loaded dynamically, the four components of the plug-in cannot be registered in the Manifest file of the host, and the four components that are not registered in the Manifest cannot interact with the system directly. Some may ask, why not just register the plug-in’s Activity in the host Manifest? This is fine, but it loses the dynamic nature of pluginization. If the host Manifest is modified and repackaged every time an Activity is added to the plug-in, it is no different than writing it directly in the host. Let’s talk about why an unregistered Activity cannot interact with the system

  1. If we start an Activity that is not registered in the Manifest, we will find an error like this:
android.content.ActivityNotFoundException: Unable to find explicit activity class {com.zy.commontec/com.zy.plugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?
Copy the code

This log can be seen in the Instrumentation’s checkStartActivityResult method:

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. In fact, an Activity’s main work is called in its lifecycle methods. Since the system detected the Manifest registration file and the Activity was rejected, then its lifecycle methods will definitely not be called. The plug-in Activity will not work properly.

In fact, both of the above questions ultimately point to the same difficulty, which is how the lifecycle of the Activity in the plug-in is invoked. Before we solve this problem, let’s look at how a normal system starts an Activity. Here is a brief introduction to the Activity startup process, the specific process code is not analyzed, because the analysis of the words can probably write an article, and in fact, there are many articles about the Activity startup process analysis. Here’s a schematic to illustrate:

The entire call path is as follows:

Activity.startActivity -> Instrumentation.execStartActivity -> Binder -> AMS.startActivity -> ActivityStarter.startActivityMayWait -> startActivityLocked -> startActivityUnChecked -> ActivityStackSupervisor.resumeFocusedStackTopActivityLocked -> ActivityStatk.resumeTopAcitivityUncheckLocked -> resumeTopActivityInnerLocked -> ActivityStackSupervisor.startSpecificActivityLocked -> realStartActivityLocked -> Binder  -> ApplictionThread.scheduleLauchActivity -> H -> ActivityThread.scheduleLauchActivity -> handleLaunchActivity -> PerformLaunchActivity - > Instrumentation. NewActivity create Activity - > callActivityOnCreate series of life cycleCopy the code

In fact, we can understand AMS as the Boss behind a company. Activities are equivalent to small employees who have no authority to directly talk to the Boss. Everything they want to do must be reported to the secretary, and then the secretary will convey the orders of AMS, the Boss. Besides, the Boss has a list of all the employees, which is impossible if you want to infiltrate the illegal employees. There are only two ways to let the non-registered staff perform the task. One is for the official staff to pick up the task and distribute it to the non-registered staff, and the other is to trick the Boss into thinking that the staff is registered.

The corresponding practical solution is:

  1. We manually invoke the plug-in Activity lifecycle
  2. Trick the system into thinking the Activity is registered in the Manifest

After the lifecycle issue, let’s look at the resource issue. In an Activity, you will almost always display an interface, and the display interface will almost always use resources. In the Activity, there is an mResources variable, which is of type Resources. This variable can be understood to represent the resources of the entire APK.

When an Activity is invoked in the host, mResources naturally represent the host’s resources, so we need to treat the plug-in’s resources in a special way. Let’s first look at how to generate the Resources class that represents plug-in Resources. You first generate an instance of AssetManager, then add the plug-in’s path through its addAssetPath method, so that AssetManager contains the plug-in’s resources. The plug-in Resources are then generated through the Resources constructor. The specific code is as follows:

private fun handleResources(a) {
    try {
        // First generate an AssetManager instance by reflection
        pluginAssetManager = AssetManager::class.java.newInstance(a)// Then call its addAssetPath to add the plug-in path.
        valaddAssetPathMethod = pluginAssetManager? .javaClass? .getMethod("addAssetPath", String::class.java)addAssetPathMethod? .invoke(pluginAssetManager, pluginPath) }catch (e: Exception) {
    }
    // Call the Resources constructor to generate an instance
    pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
}
Copy the code

The preliminary preparation knowledge point is almost finished, we then look at the specific implementation method.

6.2 Manually Calling the Activity Lifecycle

The life cycle principle of manual invocation is as follows:

When we manually call the plug-in Activity lifecycle, we need to call it at the right time. How to call it at the right time? That is to start a real Activity, commonly known as a StubActivity (StubActivity), and then call the corresponding lifecycle of the plug-in Activity within the StubActivity lifecycle, thus indirectly starting the plug-in Activity. There are two methods to call the plug-in Activity lifecycle in StubActivity. One is to directly reflect its lifecycle method, which is rude and simple. The only drawback is the efficiency of reflection. Another approach is to generate an interface that contains the lifecycle methods, and let the plug-in Activity implement the interface. StubActivity can call the interface methods directly, thus avoiding the inefficiency of reflection.

The specific code implementation can be found in the CommonTec project, here is the main implementation (the implementation here and CommonTec may have some differences, CommonTec some code to do some encapsulation, here is the main principle of explanation).

6.2.1 Invoking the Activity Lifecycle through reflection

See reflection call Lifecycle for specific implementation, and the key code is listed below.

class StubReflectActivity : Activity() {
    protected var activityClassLoader: ClassLoader? = null
    protected var activityName = ""
    private var pluginPath = ""
    private var nativeLibDir: String? = null
    private var dexOutPath: String? = null

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        pluginPath = intent.getStringExtra("pluginPath")
        activityName = intent.getStringExtra("activityName")
        // Create a plug-in ClassLoader
        activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this: :class.java.classLoader)
    }

    // Take the onCreate method as an example. Other lifecycle methods such as onStart are similar
    fun onCreate(savedInstanceState: Bundle?). {
        // Get the onCreate method of the plug-in Activity and call it
        getMethod("onCreate", Bundle::class.java)? .invoke(activity, savedInstanceState)
    }

    fun getMethod(methodName: String.vararg params: Class< * >): Method? {
        returnactivityClassLoader? .loadClass(activity)? .getMethod(methodName, *params) } }Copy the code
6.2.2 Invoking an Activity Lifecycle through an Interface

See the interface call lifecycle for specific implementation, and the key code is listed below. To invoke the Activity lifecycle through an interface, you need to define an IPluginActivity interface

interface IPluginActivity {
    fun attach(proxyActivity: Activity)
    fun onCreate(savedInstanceState: Bundle?).
    fun onStart(a)
    fun onResume(a)
    fun onPause(a)
    fun onStop(a)
    fun onDestroy(a)
}
Copy the code

Then implement this interface in the plug-in Activity

open class BasePluginActivity : Activity(), IPluginActivity {
    var proxyActivity: Activity? = null

    override fun attach(proxyActivity: Activity) {
        this.proxyActivity = proxyActivity
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        if (proxyActivity == null) {
            super.onCreate(savedInstanceState)
        }
    }
    // ...
}
Copy the code

Call the plug-in Activity lifecycle through the interface in StubActivity

class StubInterfaceActivity : StubBaseActivity() {
    protected var activityClassLoader: ClassLoader? = null
    protected var activityName = ""
    private var pluginPath = ""

    private var activity: IPluginActivity? = null

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        pluginPath = intent.getStringExtra("pluginPath")
        activityName = intent.getStringExtra("activityName")
        // Generate a plug-in ClassLoader
        activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this: :class.java.classLoader)
        // Load the plug-in Activity class and convert it to the IPluginActivity interfaceactivity = activityClassLoader? .loadClass(activityName)? .newInstance()asIPluginActivity? activity? .attach(this)
        // Call the corresponding lifecycle method directly through the interfaceactivity? .onCreate(savedInstanceState) } }Copy the code
6.2.3 Resource Processing Mode

Since calling the lifecycle manually overrides a large number of Activity lifecycle methods, we can simply override the getResources method and return the plug-in’s resource instance. Here is the code.

open class StubBaseActivity : Activity() {

    protected var activityClassLoader: ClassLoader? = null
    protected var activityName = ""
    private var pluginPath = ""
    private var pluginAssetManager: AssetManager? = null
    private var pluginResources: Resources? = null
    private var pluginTheme: Resources.Theme? = null
    private var nativeLibDir: String? = null
    private var dexOutPath: String? = null

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        pluginPath = intent.getStringExtra("pluginPath")
        activityName = intent.getStringExtra("activityName")
        activityClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this: :class.java.classLoader)
        handleResources()
    }

    override fun getResources(a): Resources? {
        // Return the resources of the plug-in, so that the plug-in Activity uses the resources of the plug-in
        returnpluginResources ? :super.getResources()
    }

    override fun getAssets(a): AssetManager {
        returnpluginAssetManager ? :super.getAssets()
    }

    override fun getClassLoader(a): ClassLoader {
        returnactivityClassLoader ? :super.getClassLoader()
    }

    private fun handleResources(a) {
        try {
            / / generated AssetManager
            pluginAssetManager = AssetManager::class.java.newInstance(a)// Add the plug-in apK path
            valaddAssetPathMethod = pluginAssetManager? .javaClass? .getMethod("addAssetPath", String::class.java)addAssetPathMethod? .invoke(pluginAssetManager, pluginPath) }catch (e: Exception) {
        }
        // Generate plug-in resources
        pluginResources = Resources(pluginAssetManager, super.getResources().displayMetrics, super.getResources().configuration)
    }
}
Copy the code

6.3 Hook System related implementation way to trick the system into calling the life cycle

6.3.1 hook Instrumentation

Now that we’ve seen how to start a plug-in Activity by manually calling its lifecycle method, let’s look at how to trick the system. AMS is a system service. Applications interact with AMS with Binder, which is similar to how clients interact with servers in daily development. But Binder is used here as a means of interaction, and a brief look at Binder is available in this article. For now, we just need to know that applications can talk to AMS through Binder. The way the architecture is designed also presents some opportunities for us. In theory, we can trick the system by modifying the Activity information before the message that starts the Activity reaches AMS, and then restoring the information after the message comes back. In this process, there are a lot of hook points can be carried out, and different plug-in frameworks for the choice of hook points are different, here we choose the way of hook Instrumentation to introduce (the reason is that I feel this way is a little simpler). The simplified process is as follows:

Instrumentation is equivalent to the administrator of the Activity, the creation of the Activity, and the invocation of the lifecycle are called by Instrumentation after the AMS notification. As we said above, AMS is equivalent to the Boss behind a company, and Instrumentation is equivalent to the secretary, Activity is equivalent to small staff, no authority to directly talk to the Boss, want to do anything must be reported by the secretary, Then Instrumentation again to the Boss AMS command down. Besides, the Boss has a list of all the employees, which is impossible if you want to infiltrate the illegal employees. However, during the whole process, due to the Java language features, the big Boss in the dialogue with the secretary Instrumentation, not care who the secretary is, only to confirm that this person is a secretary (whether the Instrumentation type). When we load the Activity in the plug-in, we ask an extra employee who is not on the Boss list to apply for the task. In normal circumstances, the Boss will check the list of employees, to confirm the legitimacy of the staff, must not be passed. We quietly replaced the secretary. When the secretary reported to the Boss, we changed the name of the staff to the staff on the Boss list. After the Boss arranged the work, the secretary changed the name back and asked the extra staff to perform the task. The way we hook is to replace Instrumentation and modify the Activity class name to hide THE AMS effect.

Hook mode schematic diagram

Let’s look at the code implementation. Specific implementation see hook implementation plug-in, the following mainly explains the key code. Before replacing Instrumentation, we first need to implement our own Instrumentation. The implementation is as follows:

class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) :
    Instrumentation() {
    private val KEY_COMPONENT = "commontec_component"

    companion object {
        fun inject(activity: Activity, pluginContext: PluginContext) {
            Hook system. Replace Instrumentation with our own AppInstrumentation. Reflect is a copy of VirtualApp's reflection utility class
            var reflect = Reflect.on(activity)
            var activityThread = reflect.get<Any>("mMainThread")
            var base = Reflect.on(activityThread).get<Instrumentation>("mInstrumentation")
            var appInstrumentation = AppInstrumentation(activity, base, pluginContext)
            Reflect.on(activityThread).set("mInstrumentation", appInstrumentation)
            Reflect.on(activity).set("mInstrumentation", appInstrumentation)
        }
    }

    override fun newActivity(cl: ClassLoader, className: String, intent: Intent): Activity? {
        // This method is called when an Activity is created, and returns an instance of the plug-in Activity
        val componentName = intent.getParcelableExtra<ComponentName>(KEY_COMPONENT)
        var clazz = pluginContext.classLoader.loadClass(componentName.className)
        intent.component = componentName
        return clazz.newInstance() as Activity?
    }

    private fun injectIntent(intent: Intent?). {
        var component: ComponentName? = null
        varoldComponent = intent? .componentif (component == null || component.packageName == realContext.packageName) {
            // Replace the name of the class in the intent with the name of the Activity's class, so that the system can find the Activity when it looks in the Manifest
            component = ComponentName("com.zy.commontec"."com.zy.commontec.activity.hook.HookStubActivity") intent? .component = component intent?.putExtra(KEY_COMPONENT, oldComponent) } }fun execStartActivity(
        who: Context,
        contextThread: IBinder,
        token: IBinder,
        target: Activity,
        intent: Intent,
        requestCode: Int
    ): Instrumentation.ActivityResult? {
        // This method is called when an activity is started. It replaces the ClassName in the Intent with the registered host activity
        injectIntent(intent)
        return Reflect.on(base)
            .call("execStartActivity", who, contextThread, token, target, intent, requestCode).get()}// ...
}
Copy the code

There are two key points in AppInstrumentation, execStartActivity and newActivity. ExecStartActivity is a process required to start an Activity that has not yet reached AMS, so replace the Activity with a registered StubActivity in the host. In this way, AMS will assume that the Activity is registered when it detects it. NewActivity creates an instance of the Activity and returns the actual plug-in Activity that you want to run, so that the system will then invoke the corresponding lifecycle based on that instance.

6.3.2 Resource processing mode of hook system

Since we hook the Instrumentation implementation, we still call the Activity lifecycle to the system, so our resource handling is not the same as calling the lifecycle manually. Direct reflection replaces the mResource variable in the Activity. Here is the code.

class AppInstrumentation(var realContext: Context, var base: Instrumentation, var pluginContext: PluginContext) : Instrumentation() {
    private fun injectActivity(activity: Activity?). {
        valintent = activity? .intentvalbase = activity? .baseContexttry {
            // Reflect to replace mResources
            Reflect.on(base).set("mResources", pluginContext.resources)
            Reflect.on(activity).set("mResources", pluginContext.resources)
            Reflect.on(activity).set("mBase", pluginContext)
            Reflect.on(activity).set("mApplication", pluginContext.applicationContext)
            // for native activity
            valcomponentName = intent!! .getParcelableExtra<ComponentName>(KEY_COMPONENT)val wrapperIntent = Intent(intent)
            wrapperIntent.setClassName(componentName.packageName, componentName.className)
            activity.intent = wrapperIntent
        } catch (e: Exception) {
        }
    }

    override fun callActivityOnCreate(activity: Activity? , icicle:Bundle?). {
        // Replace the resource here
        injectActivity(activity)
        super.callActivityOnCreate(activity, icicle)
    }
}

public class PluginContext extends ContextWrapper {
    private void generateResources() {
        try {
            // Reflection generates an AssetManager instance
            assetManager = AssetManager.class.newInstance();
            // Call addAssetPath to add the plug-in path
            Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
            method.invoke(assetManager, pluginPath);
            // Generate the Resources instance
            resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
        } catch(Exception e) { e.printStackTrace(); }}}Copy the code

After talking about the above two methods, let’s compare the advantages and disadvantages of these two methods:

Implementation method advantages disadvantages
Manual call 2. The implementation is relatively simple, and there is no need to know much about the internal implementation of the system Reflection is inefficient, and there are many methods to implement through the interface
Hook system 1. There is no need to implement a large number of interface methods. 2 It needs to adapt to different systems and devices. 2. It has high requirements for developers and requires in-depth understanding of system implementation

Plug-in implementation of Service

A Service is much simpler than an Activity. A Service does not have a complex lifecycle to deal with. Similar onCreate or onStartCommand can be distributed directly through an agent. You can simply add a placeholder Service to the host app and then call the lifecycle method of the plug-in Service in the corresponding lifecycle.

class StubService : Service() {
    var serviceName: String? = null
    var pluginService: Service? = null

    companion object {
        var pluginClassLoader: ClassLoader? = null
        fun startService(context: Context, classLoader: ClassLoader, serviceName: String) {
            pluginClassLoader = classLoader
            val intent = Intent(context, StubService::class.java)
            intent.putExtra("serviceName", serviceName)
            context.startService(intent)
        }
    }

    override fun onStartCommand(intent: Intent? , flags:Int, startId: Int): Int {
        val res = super.onStartCommand(intent, flags, startId) serviceName = intent? .getStringExtra("serviceName") pluginService = pluginClassLoader? .loadClass(serviceName)? .newInstance()asService pluginService? .onCreate()returnpluginService? .onStartCommand(intent, flags, startId) ? : res }override fun onDestroy(a) {
        super.onDestroy() pluginService? .onDestroy() }override fun onBind(intent: Intent?).: IBinder? {
        return null}}Copy the code

Plug-in implementation of BroadcastReceiver

Dynamic broadcast processing is also relatively simple, there is no complex life cycle, there is no need to register in the Manifest, the use of direct registration can be. So just load the broadcast classes in the plug-in APK through the ClassLoader and register them directly.

class BroadcastUtils {
    companion object {
        private val broadcastMap = HashMap<String, BroadcastReceiver>()

        fun registerBroadcastReceiver(context: Context, classLoader: ClassLoader, action: String, broadcastName: String) {
            val receiver = classLoader.loadClass(broadcastName).newInstance() as BroadcastReceiver
            val intentFilter = IntentFilter(action)
            context.registerReceiver(receiver, intentFilter)
            broadcastMap[action] = receiver
        }

        fun unregisterBroadcastReceiver(context: Context, action: String) {
            val receiver = broadcastMap.remove(action)
            context.unregisterReceiver(receiver)
        }
    }
}
Copy the code

Static Broadcast is a little more troublesome, here you can parse the Manifest file to find the static registered Broadcast and dynamic registration, here is not to parse the Manifest, know how it works.

Nine, ContentProvider plug-in implementation

In fact, in daily development, the use of ContentProvider in plug-in is still relatively small. Here only introduces a relatively simple plug-in implementation method of ContentProvider, which is similar to Service. Register the placeholder ContentProvider in the host app and forward the actions to the plug-in ContentProvider. The code is as follows:

class StubContentProvider : ContentProvider() {

    private var pluginProvider: ContentProvider? = null
    private var uriMatcher: UriMatcher? = UriMatcher(UriMatcher.NO_MATCH)

    override fun insert(uri: Uri? , values:ContentValues?).: Uri? {
        loadPluginProvider()
        returnpluginProvider? .insert(uri, values) }override fun query(uri: Uri? , projection:Array<out String>? , selection:String? , selectionArgs:Array<out String>? , sortOrder:String?).: Cursor? {
        loadPluginProvider()
        if (isPlugin1(uri)) {
            returnpluginProvider? .query(uri, projection, selection, selectionArgs, sortOrder) }return null
    }

    override fun onCreate(a): Boolean{ uriMatcher? .addURI("com.zy.stubprovider"."plugin1".0) uriMatcher? .addURI("com.zy.stubprovider"."plugin2".0)
        return true
    }

    override fun update(uri: Uri? , values:ContentValues? , selection:String? , selectionArgs:Array<out String>?: Int {
        loadPluginProvider()
        returnpluginProvider? .update(uri, values, selection, selectionArgs) ? :0
    }

    override fun delete(uri: Uri? , selection:String? , selectionArgs:Array<out String>?: Int {
        loadPluginProvider()
        returnpluginProvider? .delete(uri, selection, selectionArgs) ?:0
    }

    override fun getType(uri: Uri?).: String {
        loadPluginProvider()
        returnpluginProvider? .getType(uri) ? :""
    }

    private fun loadPluginProvider(a) {
        if (pluginProvider == null) { pluginProvider = PluginUtils.classLoader? .loadClass("com.zy.plugin.PluginContentProvider")? .newInstance()asContentProvider? }}private fun isPlugin1(uri: Uri?).: Boolean {
        if(uriMatcher? .match(uri) ==0) {
            return true
        }
        return false}}Copy the code

The solution is to define a different plug-in path in the Uri. Such as plugin1 corresponding is the Uri of the content: / / com. Zy. Stubprovider/plugin1, plugin2 corresponding Uri is the content: / / com. Zy. Stubprovider/plugin2, The plugin providers are then distributed in StubContentProvider according to the plugin.

Ten,

This article introduces the implementation of plug-ins, focusing on the implementation of activities. The highlights are as follows:

In the end, it is recommended that you learn some of the four major components and their system implementations as well as plug-in

Recommend study Materials

Plugin history www.infoq.cn/article/and… The Activity start process blog.csdn.net/AndrLin/art… Gityuan.com/2016/03/12/…

About me