(demo address)

Let me introduce you

VirtualApk is an open source plug-in solution of Didi. It supports four components, supports interaction between plug-in hosts, and has strong compatibility. It has been applied in Didi Chuxing APP. Here’s how the official documentation compares to other major plug-in frameworks (see article) :

features DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK
Supports four major components Only support Activity Only support Activity Only support Activity Full support Full support
Components do not need to be pre-registered in the host manifest Square root x Square root Square root Square root
Plug-ins can depend on the host Square root Square root Square root x Square root
Support the PendingIntent x x x Square root Square root
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

A, configuration,

1.1 Connecting to the main program

  1. Adding Gradle depends on adding plug-ins in the root directory build.gradle
    buildscript {
        dependencies {
            ...
            classpath 'com. Didi. Virtualapk: gradle: 0.9.8.6'. }}Copy the code
  1. Add apply plugin: ‘com.didi.virtualapk.host’ to build.gradle of app module

  2. Add dependent on app module build. Gradle dependencies of adding implementation ‘com. Didi. Virtualapk: core: 0.9.8’

  3. Initialize the SDK Select an appropriate time to initialize the SDK, typically in the attachBaseContext method of the project’s Application class.

    override fun attachBaseContext(base: Context?). {
        super.attachBaseContext(base)
        PluginManager.getInstance(base).init()
    }
Copy the code

1.2 Adding plug-in modules

  1. Add gradle dependencies as above in the main program step 1 configuration, if the plug-in module and the main program are in the same project can be ignored

  2. The introduction of the plugin In the build plug-in modules. Add the apply gradle plugin: ‘com. Didi. Virtualapk. The plugin is: plug-in module is also an application project rather than a library project, which apply the plugin: ‘com.android.application’ instead of apply plugin: ‘com.android.library’

  3. Declare the virtualApk configuration at the bottom of build.gradle in the plug-in module

    virtualApk {
        packageId = 0x6f // Resource prefix.
        targetHost = '.. /app' // The file path of the host module. The generated plug-in will check the dependencies, analyze and exclude co-dependencies with the host APP.
        applyHostMapping = true //optional, default value: true.
    }
    Copy the code

    PackageId is the prefix of the resource ID, which is used to distinguish plug-in resources. Therefore, plug-ins must use different prefixes. This prefix does not have to be 0x6f. Normally, R files compiled by our APP generally look like the following, which can be seen as the prefix 0x7f. Theoretically, the value range of this packageId should be [0x00,0x7f], but 0x01, 0x02, etc., have been occupied by the system application. So try to choose a number that is large enough to be allocated to all plug-ins.

    public final class R { public static final class anim { public static final int abc_fade_in=0x7f010000; public static final int abc_fade_out=0x7f010001; public static final int abc_grow_fade_in_from_bottom=0x7f010002; }}Copy the code

Official description of packageId

Here we have completed the configuration of the VirtualApk host and plug-in module. It is very simple, and we can see that there is no need to modify our existing project completely. We can still use the modular development method we are accustomed to.

At the time of writing, the latest version is 0.9.8.6, I suggest you try to use the latest version, after all, Android fragmentation is so serious, and hook solution is somewhat imperfect, I believe didi and gay friends at GayHub will continue to improve it in the new version, and the old version will probably not be maintained. You can usually find the current version from the official GitHub project releases.

Here is mvnrepository.com/, a repository of maven artifacts, such as VirtualApk, which makes it easy to view versions and reference syntax for generating maven, Gradle, and other build tools.

Second, the application of

Here is an example of a typical scenario: the Activity in the host APP startup plug-in.

2.1 Writing plug-ins

Plug-in modules and normal module development is exactly the same, completely unaware of the development of a plug-in, so existing project modules can be relatively easy to convert into plug-ins.

  1. Create a new application module called pluginA and configure Gradle according to the configuration method mentioned above. Note that the apply plugin: ‘com.android.application’ is used.

  2. Use a unique applicationId. The applicationId “com.huangMb.plugin. a” is used as an example.

  3. To create a new Activity, for the sake of simplicity here chose Studio built-in scroll view template directly. Com huangmb. Plugin. A.S crollingActivity

    Since it is an application module, you can also run the module directly and see the familiar interface below.

    This directly-run approach is very convenient for us to develop debugging plug-ins, but it is not our ultimate goal. We want to make it a plug-in.

  4. Generating plug-ins is as simple as running the command./gradlew assemblePlugin or double-click the assemblePlugin in the Gradle panel.

    In practice, I have encountered many generated plug-in flashbacks during runtime, mainly due to the problem of ID prefix. It is recommended that you clean it before Assemble.

    After the operation will be in the build/outputs/plugins/release folder can find the generated plug-in package, name of the file format is generally “{applicationId} _yyyyMMddHHmmss. Apk”. I can’t find anywhere to configure the output file name, I personally prefer a fixed file name, such a dynamic file name causes a file to be added every time it is compiled.

  5. Installing a plug-in Installing a plug-in is essentially placing the plug-in APK in a file path that the host plug-in can access for loading. Instead of designing the logic for installing the plugin, rename the plugin to plugina.apk and copy it to the host app folder using Android Studio’s Device Explorer tool. The Android/data / {app_applicationId} / cache. The host APP will then read the plug-in from this directory.

2.2 Host APP

What the host APP does is simply a button that launches the ScrollingActivity in plugina.apk in its click event.

  1. Complete the initialization of the plug-in on the host as described in The first part, Section 1.1.

  2. Loading a plug-in is important to ensure that the plug-in is loaded at some point before the plug-in code is launched (otherwise there is no plug-in code), such as in the onCreate of the Application (for cases where the plug-in location is known, such as built-in plug-ins or installed plug-ins), or dynamically loaded before the plug-in code is executed. To facilitate the rest of the code, three constants are defined: the plug-in file name, the plug-in package name, and the plug-in Activity class name.

      private const val PLUGIN_NAME = "pluginA.apk"
      private const val PLUGIN_PKG = "com.huangmb.plugin.a"
      private const val PLUGIN_ACTIVITY = "com.huangmb.plugin.a.ScrollingActivity"
    Copy the code

    The plug-in is loaded as

    val apk = File(externalCacheDir, PLUGIN_NAME)
    PluginManager.getInstance(this).loadPlugin(apk)
    Copy the code

    In VirtualApk, plug-ins are not allowed to be reloaded, so you can encapsulate the plug-in loading method and verify the plug-in loading before loading it

      // Check whether the plug-in is installed. If not, install it by loadPlugin
      private fun checkPlugin(a): Boolean {
         PluginManager.getInstance(this).getLoadedPlugin(PLUGIN_PKG) ? :return loadPlugin()
         return true
      }
      private fun loadPlugin(a): Boolean {
         val apk = File(externalCacheDir, PLUGIN_NAME)
         if (apk.exists()) {
             // Load the plug-in
             val manager = PluginManager.getInstance(this)
             manager.loadPlugin(apk)
             PluginUtil.hookActivityResources(this, PLUGIN_PKG)
             return true
         }
         // The plug-in does not exist
         return false
    
     }
    Copy the code

    The checkPlugin method can be called before the plug-in code is called, which returns true if the plug-in is loaded normally and false otherwise. The getLoadedPlugin method returns a LoadedPlugin object, which is a useful object that the host APP uses to retrieve the AndroidManifest information from the plugin. If null is returned, the plugin is not installed.

  3. An Activity is also launched with an Intent, but it is launched with the name of the plug-in package and the name of the Activity class. Because the host project does not rely on the plug-in, there is no direct reference to scrollingActivity.class.

   val i = Intent()
   i.setClassName(PLUGIN_PKG, PLUGIN_ACTIVITY)
   startActivity(i)
Copy the code

This completes a plug-in practice and lets see how it works:

Third, the principle of

In the example above, we did not register the ScrollingActivity in the host’s AndroidManifest, but we can still start it with startActivity.

Here is a brief introduction to the principle of Activity plug-in, there is time to open a separate introduction to the principle of the plug-in of the four components.

VirtualApk actually hooks into the system API to simulate the Activity lifecycle. In the PluginManager source code, we can see that such code replaces the Instrument of the system with reflection.

   protected void hookInstrumentationAndHandler(a) {
        try {
            ActivityThread activityThread = ActivityThread.currentActivityThread();
            Instrumentation baseInstrumentation = activityThread.getInstrumentation();
    
            final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
            Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
            Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
            Reflector.with(mainHandler).field("mCallback").set(instrumentation);
            this.mInstrumentation = instrumentation;
            Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
        } catch(Exception e) { Log.w(TAG, e); }}Copy the code

Instrument is often used in automated tests. For example, in this unit test, Instrument starts an Activity and simulates an Activity environment.

   Intent intent = new Intent();
        intent.setClassName("com.sample", Sample.class.getName());
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        sample = (Sample) getInstrumentation().startActivitySync(intent);
        text = (TextView) sample.findViewById(R.id.text1);
        button = (Button) sample.findViewById(R.id.button1);
Copy the code

VirtualApk is also based on this principle, with a custom VAInstrumentation that overloads each execStartActivity method to recognize and mark the Intent that launched the plug-in Activity, the injectIntent method.

  public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, String target, Intent intent, int requestCode, Bundle options) {
        injectIntent(intent);
        return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode, options);
    }
    
    protected void injectIntent(Intent intent) {
        mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent);
        // null component is an implicitly intent
        if(intent.getComponent() ! =null) {
            Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
            // resolve intent with Stub Activity if needed
            this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent); }}Copy the code

In the newActivity method, we do the logic to load the Activity from the plug-in. In the injectActivity method, we replace the resources object in the plug-in Activity with reflection. The alternative Resources object comes from the LoadedPlugin’s createResources method and adds the plug-in installation package folder to the AssetManager path:

  protected Resources createResources(Context context, String packageName, File apk) throws Exception {
        if (Constants.COMBINE_RESOURCES) {
            return ResourcesManager.createResources(context, packageName, apk);
        } else {
            Resources hostResources = context.getResources();
            AssetManager assetManager = createAssetManager(context, apk);
            return newResources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); }}Copy the code

The getResources.getxxx method in the plug-in Activity reads resources from the plug-in. The overall idea is similar to an automated test of an Activity.

Four,

The introduction of VirtualApk is generally relatively easy, less intrusive to the project, especially plug-in engineering and common application engineering development is basically the same, the existing module to do the necessary adjustment and business isolation, can be relatively easy to convert into plug-in, migration cost is small. For plug-in developers, a plug-in is a single application, which is conducive to independent development testing, less interference from the development environment, and finally to the host.

Of course, in most business scenarios, the plug-in is not completely independent, as in the demo above, a button to start an Activity and everything is fine. Many times, plug-ins need to be injected through some extension interface logic, and there may be some interaction between plug-ins and between plug-ins and hosts. VirtualApk also has some advanced gameplay to support these scenarios, such as host plug-in dependency de-loading, which allows plug-ins to rely on an SDK provided by the host rather than being compiled into the final plug-in, so that plug-ins can interact with the host interface. Have time to further unlock more gameplay and share with you.

Five, the problem of

The following is a summary of some problems encountered in the process of developing demo and solutions. Welcome to share your usual pit and solution in the comments. You can also go to official issues for questions and answers.

  • Compilation fails
[INFO][VAPlugin] Evaluating VirtualApk's configurations... FAILURE: Build failed with an exception. * What went wrong: A problem occurred configuring project ':plugina'. > Failed to notify project evaluation listener. > Can't using incremental dexing mode, please add 'android.useDexArchive=false' in gradle.properties of :plugina.
   > Cannot invoke method onProjectAfterEvaluate() on null object

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
Copy the code

Solution: Create a new gradle.properties file and add configuration android.useDexArchive=false

  • Compilation fails
FAILURE: Build failed with an exception. * What went wrong: Failed to notify The task execution listener. > The dependencies [com. Android. Support: design: 28.0.0, Com. Android. Support: recyclerview - v7:28.0.0, com. Android. Support: the transition: 28.0.0, Com. Android. Support: cardview - v7:28.0.0] that will be 2in the current plugin must be included in the host app first. Please add it in the host app as well.

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
Copy the code

Solve this problem because the plugin: engineering design reference in the library and not in the host, will need to com. Android. Support: design: 28.0.0 added to the host and the host APP assembleRelease APP. VirtualApk does not support the introduction of separate dependencies in plug-ins. Is the support package special?

  • Compilation fails
FAILURE: Build failed with an exception.

* What went wrong:
Failed to notify task execution listener.
> com/android/build/gradle/internal/scope/TaskOutputHolder$TaskOutputType

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
Copy the code

Solution: Gradle plugin version may be too high, the construction principle of VirtualApk is strongly dependent on gradle plugin. It is recommended to use the official gradle plugin version used by the demo project. The classpath ‘com. Android. Tools. Build: gradle: 3.0.0’

  • Plug-in unsigned
Caused by: android.content.pm.PackageParser$PackageParserException: Package /storage/emulated/0/Android/data/com.huangmb.virtualapkdemo/cache/pluginA.apk has no certificates at entry AndroidManifest.xml
Copy the code

Solution: The plug-in must have an official signature.

signingConfigs {
    release {
        storeFile file("...")
        storePassword "..."
        keyAlias "..."
        keyPassword "..."} } buildTypes { release { ... signingConfig signingConfigs.release ... }}Copy the code
  • Reloading plug-ins
java.lang.RuntimeException: plugin has already been loaded : xxx
        at com.didi.virtualapk.internal.LoadedPlugin.<init>(LoadedPlugin.java:172)
        at com.didi.virtualapk.PluginManager.createLoadedPlugin(PluginManager.java:177)
        at com.didi.virtualapk.PluginManager.loadPlugin(PluginManager.java:318)
Copy the code

Solution: A plug-in can be loaded only once. You can check whether a plug-in has been loaded before loading it.

val hasLoaded = PluginManager.getInstance(this).getLoadedPlugin(PLUGIN_PKG) ! =null
Copy the code

PLUGIN_PKG is the name of the plugin package to be verified, which is the applicationId in Gradle (possibly different from the package in AndroidManifest).