About the author

Guo Xiaoxing, programmer and guitarist, is mainly engaged in the infrastructure of Android platform. Welcome to exchange technical questions. You can go to my Github to raise an issue or send an email to [email protected] to communicate with me.

The article directories

  • VirtualAPK initialization process
  • VirtualAPK load process
  • Three VirtualAPK startup components flow
    • 3.1 the Activity
    • 3.2 the Service
    • 3.3 Broadcast Receiver
    • 3.4 the Content Provider

See Android Open Framwork Analysis for more Android open Framework source code analysis articles.

Since 2012, pluggable technology got great development, investigate its reason, mainly because as the growth of the business, the main project is becoming more and more difficult to maintain, and as the expansion of the company’s business, the original primary also gradually differentiation more child application, research and development team by one become more also, but child still needs to be the main application of traffic entrance advantage, The requirements of various business scenarios greatly promote the development of plug-in technology.

Currently, there are several mainstream plug-in frameworks:

As can be seen from the comparison in the figure above, 360’s DroidPlugin framework and Didi’s VirtualAPK framework have achieved good performance. The two companies have different business types, resulting in different emphasis of the two frameworks. To be specific,

  • DroidPlugin: DroidPlugin focuses on loading third-party independent plug-ins, such as wechat, and plug-ins cannot access the host code and resources. This also fits the business characteristics of the 260 app market.
  • VirtualAPK: VirtualAPK focuses on loading service modules. Service modules usually have a certain coupling relationship with the host. For example, service modules need to access the data information such as orders and accounts provided by the host.

That said, if we need to load an internal business module that is difficult to decouple completely from the main project, we would prefer VirtualAPK.

A powerful and lightweight plugin framework for Android

The official website: https://github.com/didi/VirtualAPK

Source code version: 0.9.1

In accordance with international convention, before analyzing the source code implementation of VirtualAPK, first blow a wave of its advantages 😎. As follows:

Complete functionality

  • Activity: supports display and implicit invocation, supports Activity theme and LaunchMode, and supports transparent themes;
  • Service: support explicit and implicit calls, support the start, stop, bind and unbind of services, and support cross-process bind plug-in Service;
  • Receiver: Supports static registration and dynamic registration.
  • ContentProvider: Supports all operations of the provider, including CRUD and Call methods, and supports cross-process access to the provider in the plug-in.
  • Custom View: support custom View, support custom attributes and style, support animation;
  • PendingIntent: Supports PendingIntent and its associated Alarm, Notification, and AppWidget.
  • Support meta-data in the plug-in manifest and Application;
  • Support for SO in plug-ins.

Excellent compatibility

  • It is compatible with almost all Android phones on the market, which has been proven in Didi Chuxing client.
  • In terms of resources, it ADAPTS xiaomi, Vivo, Nubia, etc., and adopts adaptive adaptation scheme for unknown models;
  • Few Binder hooks. Currently, only two Binder hooks are hooked: AMS and IContentProvider. The Hook process is fully compatible and adaptable.
  • The plug-in runtime logic is isolated from the host to ensure that any problems with the framework do not affect the normal operation of the host.

Very low invasiveness

  • Plug-in development is the same as native development, the four components do not need to inherit a specific base class;
  • A compact plug-in package that can depend on or not depend on code and resources in the host;
  • The process of building plug-ins is simple. Gradle plug-ins are used to build plug-ins. The whole process is transparent to developers.

👉 note: blowing so much, in fact, this framework is still flawed, specific problems, in the analysis of the source code when we will talk.

To understand a set of frameworks, it is necessary to grasp it as a whole, understand its structure and hierarchy, and then analyze it one by one. The overall architecture of VirtualAPK is shown below:

The overall source code structure is not complicated, as shown below:

VirtualAPK initialization process

Before using VirtualAPK, we need to initialize multiple VirtualapKs, as shown below:

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    PluginManager.getInstance(base).init();
}
Copy the code

Let’s take a look at what happens during VirtualAPK initialization, as follows:

public class PluginManager {
     private PluginManager(Context context) {
         
           Context app = context.getApplicationContext();
           / / get the Context
           if (app == null) {
               this.mContext = context;
           } else {
               this.mContext = ((Application)app).getBaseContext();
           }
           / / initialization
           prepare();
       }
   
       private void prepare(a) {
           Systems.sHostContext = getHostContext();
           //1. Hook object Instrumentation.
           this.hookInstrumentationAndHandler();
           //2. Hook IActivityManager from ActivityManagerNative according to different Android versions.
           if (Build.VERSION.SDK_INT >= 26) {
               this.hookAMSForO();
           } else {
               this.hookSystemServices(); }}}}Copy the code

VirtualAPK hooks two main objects during initialization, as shown below:

  1. Hook object Instrumentation.
  2. Hook IActivityManager from ActivityManagerNative according to different Android versions.

The first is the Instrumentation object. Why hook this object? 🤔 this is because the Instrumentation object will have a verification process when starting an Activity, one of which is to check whether the Activity is registered in the Manifest file, as shown below:

public class Instrumentation {
      public static void checkStartActivityResult(int res, Object intent) {
           if (res >= ActivityManager.START_SUCCESS) {
               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);
               case ActivityManager.START_PERMISSION_DENIED:
                   throw new SecurityException("Not allowed to start activity "
                           + intent);
               case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
                   throw new AndroidRuntimeException(
                           "FORWARD_RESULT_FLAG used while also requesting a result");
               case ActivityManager.START_NOT_ACTIVITY:
                   throw new IllegalArgumentException(
                           "PendingIntent is not an activity");
               case ActivityManager.START_NOT_VOICE_COMPATIBLE:
                   throw new SecurityException(
                           "Starting under voice control not allowed for: " + intent);
               case ActivityManager.START_NOT_CURRENT_USER_ACTIVITY:
                   // Fail silently for this case so we don't break current apps.
                   // TODO(b/22929608): Instead of failing silently or throwing an exception,
                   // we should properly position the activity in the stack (i.e. behind all current
                   // user activity/task) and not change the positioning of stacks.
                   Log.e(TAG,
                           "Not allowed to start background user activity that shouldn't be displayed"
                           + " for all users. Failing silently...");
                   break;
               default:
                   throw new AndroidRuntimeException("Unknown error code "
                           + res + " when starting "+ intent); }}}Copy the code

Have you declared this activity in your Androidmanifest.xml, hook object Instrumentation, Then replace the corresponding method inside to achieve the purpose of bypassing the check. Let’s look at the hook process, as shown below:

public class PluginManager {
    
       private void hookInstrumentationAndHandler(a) {
           try {
               Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
               if (baseInstrumentation.getClass().getName().contains("lbe")) {
                   // reject executing in paralell space, for example, lbe.
                   System.exit(0);
               }
   
               // The custom VAInstrumentation overrides logic such as newActivity(). The Instrumentation object is also saved for download
               final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
               // Get the ctivityThread instance
               Object activityThread = ReflectUtil.getActivityThread(this.mContext);
               // Replace the Instrumentation object in the ActivityThread with a custom VAInstrumentation heavy object
               ReflectUtil.setInstrumentation(activityThread, instrumentation);
               ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
               this.mInstrumentation = instrumentation;
           } catch(Exception e) { e.printStackTrace(); }}}Copy the code

In article 03Android Component Framework: In the Android view container Activity, we mentioned that the Instrumentation object is used to monitor the interaction between the application and the system. The creation of activities is also done in the Instrumentation object. The reason to hook this object is to modify the Activity creation logic.

Replace the Instrumentation object in the ActivityThread with a custom VAInstrumentation heavy object, This calls the custom newActivity() method inside the VAInstrumentation when the system starts the Activity and calls the newActivity() method of the Instrumentation.

public class PluginManager {
           
       // Android API 26 and above
       private void hookAMSForO(a) {
           try {
               Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManager.class, null."IActivityManagerSingleton");
               IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());
               ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);
           } catch(Exception e) { e.printStackTrace(); }}// Android API 26 below
       private void hookSystemServices(a) {
             try {
                 Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null."gDefault");
                 IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get());
     
                 // Hook IActivityManager from ActivityManagerNative
                 ReflectUtil.setField(defaultSingleton.getClass().getSuperclass(), defaultSingleton, "mInstance", activityManagerProxy);
     
                 if (defaultSingleton.get() == activityManagerProxy) {
                     this.mActivityManager = activityManagerProxy; }}catch(Exception e) { e.printStackTrace(); }}}Copy the code

In addition to Instrumentation objects, it also hooks IActivityManager from ActivityManagerNative according to different Android versions, so what is this IActivityManager object? 🤔

We previously covered android Component Frameworks in article 02: ActivityManager, the Android component manager, has mentioned that it is a proxy object for ActivityManagerService, through which it can communicate IPC with ActivityManagerService and request it to do some component management work. Components like startActivity(), startService(), and bindService() call methods that end up in ActivityManagerService.

This is the initialization process of VIrtualAPK. Let’s look at how VIrtualAPK loads an APK file. 👇

VirtualAPK load process

VirtualAPK has no additional constraints on loaded APK files, just add VirtualAPK plug-ins to compile, as shown below:

apply plugin: 'com.didi.virtualapk.plugin'

virtualApk {
    packageId = 0x6f             // The package id of Resources.
    targetHost='source/host/app' // The path of application module in host project.
    applyHostMapping = true      // [Optional] Default value is true. 
}
Copy the code

VirtualAPK uses these LoadedPlugin objects to manage the APK. These apKs also work like apps installed directly on your phone.

String pluginPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/Test.apk");
File plugin = new File(pluginPath);
PluginManager.getInstance(base).loadPlugin(plugin);
Copy the code

The loading process of APK is as follows:

We can see that the loadPlugin() method is called above to load an APK. Let’s look at its implementation.

public class PluginManager {
    
     public void loadPlugin(File apk) throws Exception {
          if (null == apk) {
              throw new IllegalArgumentException("error : apk is null.");
          }
  
          if(! apk.exists()) {throw new FileNotFoundException(apk.getAbsolutePath());
          }
  
          1. Load the APK file
          LoadedPlugin plugin = LoadedPlugin.create(this.this.mContext, apk);
          if (null! = plugin) {this.mPlugins.put(plugin.getPackageName(), plugin);
              // try to invoke plugin's application
              // 2. Try calling APK
              plugin.invokeApplication();
          } else {
              throw  new RuntimeException("Can't load plugin which is invalid: "+ apk.getAbsolutePath()); }}}Copy the code

The LoadedPlugin’s create() method is called to build a LoadedPlugin object, so all initialization is done in the LoadedPlugin constructor, as shown below:

public final class LoadedPlugin {
    
    LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws PackageParser.PackageParserException {
            this.mPluginManager = pluginManager;
            this.mHostContext = context;
            this.mLocation = apk.getAbsolutePath();
            // 1. Call PackageParser to parse APK and obtain the PackageParser.Package object.
            this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
            this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
            // 2. Build the PackageInfo object.
            this.mPackageInfo = new PackageInfo();
            this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
            this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
            this.mPackageInfo.signatures = this.mPackage.mSignatures;
            this.mPackageInfo.packageName = this.mPackage.packageName;
            if(pluginManager.getLoadedPlugin(mPackageInfo.packageName) ! =null) {
                throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
            }
            this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
            this.mPackageInfo.versionName = this.mPackage.mVersionName;
            this.mPackageInfo.permissions = new PermissionInfo[0];
            // 3. Build PluginPackageManager objects.
            this.mPackageManager = new PluginPackageManager();
            this.mPluginContext = new PluginContext(this);
            this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
            // 4. Build the Resouces object.
            this.mResources = createResources(context, apk);
            // 5. Build a ClassLoader object.
            this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
    
            // 6. Copy so library.
            tryToCopyNativeLib(apk);
    
            // 7. Cache Instrumentation objects.
            Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
            for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {
                instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
            }
            this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
            this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);
    
            // 8. Cache Activity information in APK.
            Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
            for (PackageParser.Activity activity : this.mPackage.activities) {
                activityInfos.put(activity.getComponentName(), activity.info);
            }
            this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
            this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);
            
            // 9. Cache Service information in APK.
            Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
            for (PackageParser.Service service : this.mPackage.services) {
                serviceInfos.put(service.getComponentName(), service.info);
            }
            this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
            this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);
    
            Caches Content Provider information in APK.
            Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
            Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
            for (PackageParser.Provider provider : this.mPackage.providers) {
                providers.put(provider.info.authority, provider.info);
                providerInfos.put(provider.getComponentName(), provider.info);
            }
            this.mProviders = Collections.unmodifiableMap(providers);
            this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
            this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);
    
            // 11. Change the static broadcast to dynamic.
            Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
            for (PackageParser.Activity receiver : this.mPackage.receivers) {
                receivers.put(receiver.getComponentName(), receiver.info);
    
                try {
                    BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
                    for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
                        this.mHostContext.registerReceiver(br, aii); }}catch(Exception e) { e.printStackTrace(); }}this.mReceiverInfos = Collections.unmodifiableMap(receivers);
            this.mPackageInfo.receivers = receivers.values().toArray(newActivityInfo[receivers.size()]); }}Copy the code

The entire process of building the LoadedPlugin object is to parse the component information in APK and cache it, specifically:

  1. Call PackageParser to parse THE APK and get the PackageParser.Package object.
  2. Build the PackageInfo object.
  3. Build the PluginPackageManager object.
  4. Build the Resouces object.
  5. Build a ClassLoader object.
  6. Copy the SO library.
  7. Cache Instrumentation objects.
  8. Caches Activity information in APK.
  9. Caches Service information in APK.
  10. Caches Content Provider information in APK.
  11. Convert a static broadcast to a dynamic one.

Let’s look at how the four major components are launched that are not registered in the host App’s Manifest. 👇

Three VirtualAPK startup components flow

3.1 the Activity

Earlier we said that during VirtualAPK initialization, the native Instrumentation object is replaced by the custom VAInstrumentation in the ActivityThread to hook into the Activity launch process. Bypass the validation process for starting activities with Instrumentation.

So how does VirtualAPK bypass system validation? 🤔

Virtual circumvents checksumming by using the pit capture method, which is defined in the Manifest file of the library as follows:

 <application>
        <! -- Stub Activities -->
        <activity android:name=".A$1" android:launchMode="standard"/>
        <activity android:name=".A$2" android:launchMode="standard"
            android:theme="@android:style/Theme.Translucent" />

        <! -- Stub Activities -->
        <activity android:name=".B$1" android:launchMode="singleTop"/>
        <activity android:name=".B$2" android:launchMode="singleTop"/>
        <activity android:name=".B$3" android:launchMode="singleTop"/>
        <activity android:name=".B$4" android:launchMode="singleTop"/>
        <activity android:name=".B$5" android:launchMode="singleTop"/>
        <activity android:name=".B$6" android:launchMode="singleTop"/>
        <activity android:name=".B$7" android:launchMode="singleTop"/>
        <activity android:name=".B$8" android:launchMode="singleTop"/>

        <! -- Stub Activities -->
        <activity android:name=".C$1" android:launchMode="singleTask"/>
        <activity android:name=".C$2" android:launchMode="singleTask"/>
        <activity android:name=".C$3" android:launchMode="singleTask"/>
        <activity android:name=".C$4" android:launchMode="singleTask"/>
        <activity android:name=".C$5" android:launchMode="singleTask"/>
        <activity android:name=".C$6" android:launchMode="singleTask"/>
        <activity android:name=".C$7" android:launchMode="singleTask"/>
        <activity android:name=".C$8" android:launchMode="singleTask"/>

        <! -- Stub Activities -->
        <activity android:name=".D$1" android:launchMode="singleInstance"/>
        <activity android:name=".D$2" android:launchMode="singleInstance"/>
        <activity android:name=".D$3" android:launchMode="singleInstance"/>
        <activity android:name=".D$4" android:launchMode="singleInstance"/>
        <activity android:name=".D$5" android:launchMode="singleInstance"/>
        <activity android:name=".D$6" android:launchMode="singleInstance"/>
        <activity android:name=".D$7" android:launchMode="singleInstance"/>
        <activity android:name=".D$8" android:launchMode="singleInstance"/>
 </application>
Copy the code

A, B, C, and D represent standard, singleTop, singleTask, and singleInstance startup modes respectively.

VirtualAPK creates a dummy for an Activity that registers a pit in the Manifest file, and then fills the pit with the Activity when it starts the real Activity. Let’s look at the specific implementation process:

  1. execStartActivity()
  2. realExecStartActivity()
  3. newActivity()
  4. callActivityOnCreate()

The above four methods are all necessary to start an Activity.

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
    
     public ActivityResult execStartActivity(
             Context who, IBinder contextThread, IBinder token, Activity target,
             Intent intent, int requestCode, Bundle options) {
         / / 1. Converts implicit Intent to explicit Intent, Virtual is through Intent setClassName (this, "com. Guoxiaoxing. Plugin. MainActivity"); This kind of
         // to start the Activity, encapsulating the package name into a real ComponentName object.
         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()));
             //2. Replace the real Activity with the registered StubActivity to bypass validation.
             this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent);
         }
 
         //3. Generate the Intent that occupies the pit of StubActivity. Call the realExecStartActivity() method to continue the Activity's start, bypassing validation.
         ActivityResult result = realExecStartActivity(who, contextThread, token, target,
                     intent, requestCode, options);
 
         return result;
     }
     
     private ActivityResult realExecStartActivity(
             Context who, IBinder contextThread, IBinder token, Activity target,
             Intent intent, int requestCode, Bundle options) {
         ActivityResult result = null;
         try {
             Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
             int.class, Bundle.class};
             
             // Start the Activity with the Intent of the spotty StubActivity to bypass validation.
             result = (ActivityResult)ReflectUtil.invoke(Instrumentation.class, mBase,
                     "execStartActivity", parameterTypes,
                     who, contextThread, token, target, intent, requestCode, options);
         } catch (Exception e) {
             if (e.getCause() instanceof ActivityNotFoundException) {
                 throw (ActivityNotFoundException) e.getCause();
             }
             e.printStackTrace();
         }
 
         returnresult; }}Copy the code

The method does three main things, as follows:

  1. Converts implicit Intent to explicit Intent, Virtual is through Intent setClassName (this, “com. Guoxiaoxing. Plugin. MainActivity”); To start an Activity in this way, encapsulate the package name into a real ComponentName object.
  2. Replace the real Activity with the registered StubActivity to bypass detection.
  3. Generated the Intent for the spotty StubActivity. Call the realExecStartActivity() method to continue the Activity’s start, bypassing validation.

The main point is that the registered StubActivity replaces the real Activity to bypass detection. Let’s take a look at its implementation, as shown below:

public class ComponentsHandler {
    
      public void markIntentIfNeeded(Intent intent) {
            if (intent.getComponent() == null) {
                return;
            }
    
            / / package name
            String targetPackageName = intent.getComponent().getPackageName();
            / / the name of the class
            String targetClassName = intent.getComponent().getClassName();
            // Search for spotty StubActivity for the corresponding startup mode
            if(! targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) ! =null) {
                // Make a plugin tag
                intent.putExtra(Constants.KEY_IS_PLUGIN, true);
                / / save the package name
                intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
                The purpose of storing this information is to get the Intent information of the real Activity to start the real Activity.
                intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
                / / find StubActivitydispatchStubActivity(intent); }}private void dispatchStubActivity(Intent intent) {
            ComponentName component = intent.getComponent();
            String targetClassName = intent.getComponent().getClassName();
            // Get the LoadedPlugin object corresponding to the intent
            LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
            // Obtain the corresponding ActivityInfo based on the ComponentName information
            ActivityInfo info = loadedPlugin.getActivityInfo(component);
            if (info == null) {
                throw new RuntimeException("can not find " + component);
            }
            // Start mode
            int launchMode = info.launchMode;
            Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
            // Replace the corresponding theme
            themeObj.applyStyle(info.theme, true);
            // Get the corresponding StubActivity
            String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
            Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
            // Set the full path name of StubActivityintent.setClassName(mContext, stubActivity); }}Copy the code

Let’s see how to query StubActivity in detail, as follows:

class StubActivityInfo {
    
    // Maximum number of activities in standard mode
    public static final int MAX_COUNT_STANDARD = 1;
    // The maximum number of activities to reuse at the top of the stack
    public static final int MAX_COUNT_SINGLETOP = 8;
    // The maximum number of mode activities to reuse in the stack
    public static final int MAX_COUNT_SINGLETASK = 8;
    // Maximum number of activities in singleton mode
    public static final int MAX_COUNT_SINGLEINSTANCE = 8;

    // The full pathname of the activities that occupy the pit
    public static final String corePackage = "com.didi.virtualapk.core";
    public static final String STUB_ACTIVITY_STANDARD = "%s.A$%d";
    public static final String STUB_ACTIVITY_SINGLETOP = "%s.B$%d";
    public static final String STUB_ACTIVITY_SINGLETASK = "%s.C$%d";
    public static final String STUB_ACTIVITY_SINGLEINSTANCE = "%s.D$%d";

    public final int usedStandardStubActivity = 1;
    public int usedSingleTopStubActivity = 0;
    public int usedSingleTaskStubActivity = 0;
    public int usedSingleInstanceStubActivity = 0;

    private HashMap<String, String> mCachedStubActivity = new HashMap<>();
    
    public String getStubActivity(String className, int launchMode, Theme theme) {
            // 1. First look for StuActivity in the cache.
            String stubActivity= mCachedStubActivity.get(className);
            if(stubActivity ! =null) {
                return stubActivity;
            }
    
            TypedArray array = theme.obtainStyledAttributes(new int[]{
                    android.R.attr.windowIsTranslucent,
                    android.R.attr.windowBackground
            });
            boolean windowIsTranslucent = array.getBoolean(0.false);
            array.recycle();
            if (Constants.DEBUG) {
                Log.d("StubActivityInfo"."getStubActivity, is transparent theme ? " + windowIsTranslucent);
            }
            
            / / standard boot mode: com. Didi. Virtualapk. Core. A $1
            stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
            switch (launchMode) {
                case ActivityInfo.LAUNCH_MULTIPLE: {
                   / / standard boot mode: com. Didi. Virtualapk. Core. $1, each time from 1-6
                    stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
                    if (windowIsTranslucent) {
                        stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
                    }
                    break;
                }
                case ActivityInfo.LAUNCH_SINGLE_TOP: {
                    / reuse/stack mode: com. Didi. Virtualapk. Core. $, since every time 1, ranging from 1 to 8.
                    usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
                    stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
                    break;
                }
                case ActivityInfo.LAUNCH_SINGLE_TASK: {
                     / / in the stack mode: com. Didi. Virtualapk. Core. C $, since every time 1, ranging from 1 to 8.
                    usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
                    stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
                    break;
                }
                case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
                    / / the singleton pattern: com. Didi. Virtualapk. Core. D $, since every time 1, ranging from 1 to 8.
                    usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
                    stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
                    break;
                }
    
                default:break;
            }
    
            // Place the found Activity in the cache
            mCachedStubActivity.put(className, stubActivity);
            returnstubActivity; }}Copy the code

As you can see here, the StubActivity lookup is completed, as shown below:

  • Standard boot mode: com. Didi. Virtualapk. Core. $1, each time from 1-6.
  • The stack multiplexing mode: com. Didi. Virtualapk. Core. $, since every time 1, ranging from 1 to 8.
  • Stack in multiplexing mode: com. Didi. Virtualapk. Core. C $, since every time 1, ranging from 1 to 8.
  • Singleton pattern: com. Didi. Virtualapk. Core. D $, since every time 1, ranging from 1 to 8.

Now that we’ve changed the Activity we want to start to StubActivity for the sake of dye check. Then when you actually start the Activity, you have to change it back again.

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
        @Override
        public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
            try {
                cl.loadClass(className);
            } catch (ClassNotFoundException e) {
                // 1. Find the corresponding LoadedPlugin according to the Intent.
                LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
                // 2. Extract the actual targetClassName from the Intent.
                String targetClassName = PluginUtil.getTargetActivity(intent);
    
                Log.i(TAG, String.format("newActivity[%s : %s]", className, targetClassName));
    
                if(targetClassName ! =null) {
                    // 3. This mBase is the native Instrumentation object we saved above. Call its newActivity() method to complete the construction of the Activity
                    // This is a dynamic proxy mode. GetClassLoader () is a self-built DexClassLoader class used to load classes in APK.
                    Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
                    activity.setIntent(intent);
    
                    try {
                        / / for 4.1 +
                        ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
                    } catch (Exception ignored) {
                        // ignored.
                    }
    
                    returnactivity; }}returnmBase.newActivity(cl, className, intent); }}Copy the code

VAInstrumentation overwraps the newActivity method of Instrumentation, which does three things:

  1. Find the corresponding LoadedPlugin based on the Intent.
  2. Extract the real targetClassName from the Intent.
  3. The mBase is the native Instrumentation object we saved above, and its newActivity() method is called to complete the construction of the Activity, which is equivalent to a dynamic proxy mode. GetClassLoader () is a self-built DexClassLoader class used to load classes in APK.

Through the above analysis, the overall idea is very clear, as follows:

Register more than one StubActivity in the Manifest file in advance. In the validation phase, replace the className in the Intent with StubActivity and save the original Activity information to pass the validation. In the launch phase, the real Activity information is retrieved from the Intent, and the newActivity() method of the Instrumentation is called to continue the Activity.

The overall idea is still quite a mechanism 👍, of course, the idea of pit has long been put forward by Android students, which is also a kind of idea to achieve plug-in. Having introduced the Activity startup process, let’s move on to the Service startup process. 👇

3.2 the Service

Service is started using dynamic proxy AMS, which intercepts Service operation requests and forwards them to ActivityManagerProxy.

public class ActivityManagerProxy implements InvocationHandler {
    
       private Object startService(Object proxy, Method method, Object[] args) throws Throwable {
            // Get the IApplicationThread object.
            IApplicationThread appThread = (IApplicationThread) args[0];
            // Get a jump Intent.
            Intent target = (Intent) args[1];
            // Check the Service information
            ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
            if (null == resolveInfo || null == resolveInfo.serviceInfo) {
                // is host service
                return method.invoke(this.mActivityManager, args);
            }
            
            return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
        }
        
        private ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
            Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
            return mPluginManager.getHostContext().startService(wrapperIntent);
        }
    
        private Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
            // fill in service with ComponentName
            target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
            String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();
    
            // Determine whether to send the command to LocalService or RemoteService
            // Start the Service in the new process.
            boolean local = PluginUtil.isLocalService(serviceInfo);
            Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
            Intent intent = new Intent();
            intent.setClass(mPluginManager.getHostContext(), delegate);
            intent.putExtra(RemoteService.EXTRA_TARGET, target);
            // Save the command for each operation.
            intent.putExtra(RemoteService.EXTRA_COMMAND, command);
            intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
            if(extras ! =null) {
                intent.putExtras(extras);
            }
    
            returnintent; }}Copy the code

So essentially, when you start, bind, or close an Intent, you end up calling LocalService or RemoteService’s onStartCommand() method to distribute the action request.

LocalService and RemoteService are registered in the VirtualAPK Manifest file, as shown below:

<application>
    <! -- Local Service running in main process -->
    <service android:name="com.didi.virtualapk.delegate.LocalService" />
    
    <! -- Daemon Service running in child process -->
    <service android:name="com.didi.virtualapk.delegate.RemoteService" android:process=":daemon">
        <intent-filter>
            <action android:name="${applicationId}.intent.ACTION_DAEMON_SERVICE" />
        </intent-filter>
    </service>
</application>
Copy the code

Let’s look at the implementation of both of them.

3.2.1 LocalService

public class LocalService extends Service {
    private static final String TAG = "LocalService";

    // Target Service in plugin APK
    public static final String EXTRA_TARGET = "target";
    public static final String EXTRA_COMMAND = "command";
    public static final String EXTRA_PLUGIN_LOCATION = "plugin_location";

    public static final int EXTRA_COMMAND_START_SERVICE = 1;
    public static final int EXTRA_COMMAND_STOP_SERVICE = 2;
    public static final int EXTRA_COMMAND_BIND_SERVICE = 3;
    public static final int EXTRA_COMMAND_UNBIND_SERVICE = 4;

    private PluginManager mPluginManager;

    @Override
    public IBinder onBind(Intent intent) {
        return new Binder();
    }

    @Override
    public void onCreate(a) {
        super.onCreate();
        // Get the PluginManager singleton
        mPluginManager = PluginManager.getInstance(this);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (null== intent || ! intent.hasExtra(EXTRA_TARGET) || ! intent.hasExtra(EXTRA_COMMAND)) {return START_STICKY;
        }


        Intent target = intent.getParcelableExtra(EXTRA_TARGET);
        // Get command information
        int command = intent.getIntExtra(EXTRA_COMMAND, 0);
        if (null == target || command <= 0) {
            return START_STICKY;
        }

        // Get component information
        ComponentName component = target.getComponent();
        // Get the corresponding LoadedPlugin based on the component information
        LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
        // ClassNotFoundException when unmarshalling in Android 5.1
        target.setExtrasClassLoader(plugin.getClassLoader());
        switch (command) {
            / / start the Service
            case EXTRA_COMMAND_START_SERVICE: {
                // Get the ActivityThread object.
                ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
                // Get the IApplicationThread object.
                IApplicationThread appThread = mainThread.getApplicationThread();
                Service service;

                if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                    // Try to get a Service from ComponentsHandler,
                    service = this.mPluginManager.getComponentsHandler().getService(component);
                } else {
                    try {
                        // Call DexClassLoader to load the Service class.
                        service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                        Application app = plugin.getApplication();
                        IBinder token = appThread.asBinder();
                        Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                        IActivityManager am = mPluginManager.getActivityManager();
                        // Call attch() to bind the application context.
                        attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                        // Call the onCreate() method of the Service.
                        service.onCreate();
                        / / insert the Service
                        this.mPluginManager.getComponentsHandler().rememberService(component, service);
                    } catch (Throwable t) {
                        returnSTART_STICKY; }}// Call the onStartCommand() method of the Service.
                service.onStartCommand(target, 0.this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
                break;
            }
            
            // Bind the service
            case EXTRA_COMMAND_BIND_SERVICE: {
                ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
                IApplicationThread appThread = mainThread.getApplicationThread();
                Service service = null;

                if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                    // Try to get a Service from ComponentsHandler,
                    service = this.mPluginManager.getComponentsHandler().getService(component);
                } else {
                    try {
                        // Call DexClassLoader to load the Service class.
                        service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                        Application app = plugin.getApplication();
                        IBinder token = appThread.asBinder();
                        Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                        IActivityManager am = mPluginManager.getActivityManager();
                        // Call attch() to bind the application context.
                        attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                        // Call the onCreate() method of the Service.
                        service.onCreate();
                        / / insert the Service
                        this.mPluginManager.getComponentsHandler().rememberService(component, service);
                    } catch(Throwable t) { t.printStackTrace(); }}try {
                    // Call the onBind() method of the Service.
                    IBinder binder = service.onBind(target);
                    IBinder serviceConnection = PluginUtil.getBinder(intent.getExtras(), "sc");
                    IServiceConnection iServiceConnection = IServiceConnection.Stub.asInterface(serviceConnection);
                    if (Build.VERSION.SDK_INT >= 26) {
                        ReflectUtil.invokeNoException(IServiceConnection.class, iServiceConnection, "connected".new Class[]{ComponentName.class, IBinder.class, boolean.class},
                                new Object[]{component, binder, false});
                    } else{ iServiceConnection.connected(component, binder); }}catch (Exception e) {
                    e.printStackTrace();
                }
                break;
            }
            // Stop the service
            case EXTRA_COMMAND_STOP_SERVICE: {
                // Remove the Service record from ComponentsHandler
                Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
                if (null! = service) {try {
                        // Calls the onDestroy() method of Service
                        service.onDestroy();
                    } catch (Exception e) {
                        Log.e(TAG, "Unable to stop service " + service + ":"+ e.toString()); }}else {
                    Log.i(TAG, component + " not found");
                }
                break;
            }
            case EXTRA_COMMAND_UNBIND_SERVICE: {
                 // Remove the Service record from ComponentsHandler
                Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
                if (null! = service) {try {
                        // Call the onUnbind() method of the Service
                        service.onUnbind(target);
                        // Calls the onDestroy() method of Service
                        service.onDestroy();
                    } catch (Exception e) {
                        Log.e(TAG, "Unable to unbind service " + service + ":"+ e.toString()); }}else {
                    Log.i(TAG, component + " not found");
                }
                break; }}returnSTART_STICKY; }}Copy the code

You can see that the entire implementation of the class rewrites part of the Service startup process, including context binding and some lifecycle method callbacks. See our previous article on android Component Framework. ComponentsHandler is used to manage services. After all, we only registered a LocalService in the Manifest. ComponentsHandler is used to insert and remove Services and manage ServiceConnection. This allows you to start multiple services in the plug-in APK even if only one LocalService is registered.

Let’s move on to RemoteService, 👇

3.2.2 RemoteService

public class RemoteService extends LocalService {

    @Override
    public IBinder onBind(Intent intent) {
        // the onBind() method returns null, indicating that it cannot be bound.
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null) {
            return super.onStartCommand(intent, flags, startId);
        }

        Intent target = intent.getParcelableExtra(EXTRA_TARGET);
        if(target ! =null) {
            String pluginLocation = intent.getStringExtra(EXTRA_PLUGIN_LOCATION);
            ComponentName component = target.getComponent();
            LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(component);
            if (plugin == null&& pluginLocation ! =null) {
                try {
                    // There is one more operation to load the APK plug-in from a file than LocalService
                    PluginManager.getInstance(this).loadPlugin(new File(pluginLocation));
                } catch(Exception e) { e.printStackTrace(); }}}return super.onStartCommand(intent, flags, startId); }}Copy the code

RemoteService inherits from LocalService. The difference is that the onBind() and onStartCommand() methods are implemented as follows:

  • RemoteService’s onBind() method returns null, indicating that it cannot be bound.
  • RemoteService’s onStartCommand() method has one more function than LocalService that loads the APK plug-in from a file, meaning it can load services from other APKs.

3.3 Broadcast Receiver

Broadcast receivers are simpler, converting static broadcasts directly to dynamic broadcasts without the need for registration.

// Change the static broadcast to dynamic.
Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity receiver : this.mPackage.receivers) {
    receivers.put(receiver.getComponentName(), receiver.info);

    try {
        BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
        for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
            this.mHostContext.registerReceiver(br, aii); }}catch(Exception e) { e.printStackTrace(); }}Copy the code

3.4 the Content Provider

VirtualAPK uses the dynamic proxy IContentProvider to intercept requests for ContentProvider operations and forward them to PluginContentResolver. The IContentProvider object hooks are actually done in PluginManager, as shown below:

public class PluginManager {
     private void hookIContentProviderAsNeeded(a) {
         
            // Get the Content Provider and call its call() method
            // The RemoteContentProvider getContentProvider is called to build a RemoteContentProvider.
            Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
            mContext.getContentResolver().call(uri, "wakeup".null.null);
            try {
                Field authority = null;
                Field mProvider = null;
                
                // Get the ActivityThread object
                ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);
                // Get the ContentProvider Map
                Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");
                Iterator iter = mProviderMap.entrySet().iterator();
                // The variable queries the corresponding ContentProvider
                while (iter.hasNext()) {
                    Map.Entry entry = (Map.Entry) iter.next();
                    Object key = entry.getKey();
                    Object val = entry.getValue();
                    String auth;
                    if (key instanceof String) {
                        auth = (String) key;
                    } else {
                        if (authority == null) {
                            authority = key.getClass().getDeclaredField("authority");
                            authority.setAccessible(true);
                        }
                        auth = (String) authority.get(key);
                    }
                    if (auth.equals(PluginContentResolver.getAuthority(mContext))) {
                        if (mProvider == null) {
                            mProvider = val.getClass().getDeclaredField("mProvider");
                            mProvider.setAccessible(true);
                        }
                        IContentProvider rawProvider = (IContentProvider) mProvider.get(val);
                        // Obtain the corresponding IContentProvider
                        IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                        mIContentProvider = proxy;
                        Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                        break; }}}catch(Exception e) { e.printStackTrace(); }}}Copy the code

ContentProvider is also registered in the Manifest file as follows:


<application>
    <provider
            android:name="com.didi.virtualapk.delegate.RemoteContentProvider"
            android:authorities="${applicationId}.VirtualAPK.Provider"
            android:process=":daemon" />
</application>
Copy the code

After obtaining the IContentProvider object, you can dynamically proxy it to intercept operations in it, for example: Query, insert, update, delete, etc. In these operations, we cache the URI of the pit Provider called by the user and concatenate the original URI after the pit Provider as a parameter. This substitution and concatenation is done by the PluginContentResolver wrapperUri() method, as shown below:

public class PluginContentResolver extends ContentResolver {
        @Keep
        public static Uri wrapperUri(LoadedPlugin loadedPlugin, Uri pluginUri) {
            String pkg = loadedPlugin.getPackageName();
            String pluginUriString = Uri.encode(pluginUri.toString());
            StringBuilder builder = new StringBuilder(PluginContentResolver.getUri(loadedPlugin.getHostContext()));
            // Add the URI of the trap Provider first
            builder.append("/? plugin=" + loadedPlugin.getLocation());
            // Concatenate the destination URI and packageName
            builder.append("&pkg=" + pkg);
            builder.append("&uri=" + pluginUriString);
            Uri wrapperUri = Uri.parse(builder.toString());
            returnwrapperUri; }}Copy the code

RemoteContentProvider: RemoteContentProvider: RemoteContentProvider: RemoteContentProvider: RemoteContentProvider: RemoteContentProvider: RemoteContentProvider: RemoteContentProvider

public class RemoteContentProvider extends ContentProvider {
    
       private ContentProvider getContentProvider(final Uri uri) {
            final PluginManager pluginManager = PluginManager.getInstance(getContext());
            Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
            final String auth = pluginUri.getAuthority();
            // 1. Try to fetch the ContentProvider from the cache.
            ContentProvider cachedProvider = sCachedProviders.get(auth);
            if(cachedProvider ! =null) {
                return cachedProvider;
            }
    
            synchronized (sCachedProviders) {
                // 2. Obtain LoadedPlugin object.
                LoadedPlugin plugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
                if (plugin == null) {
                    try {
                        pluginManager.loadPlugin(new File(uri.getQueryParameter(KEY_PLUGIN)));
                    } catch(Exception e) { e.printStackTrace(); }}Obtain ProviderInfo from the LoadedPlugin object.
                final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0);
                if(providerInfo ! =null) {
                    RunUtil.runOnUiThread(new Runnable() {
                        @Override
                        public void run(a) {
                            try {
                                LoadedPlugin loadedPlugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
                                // 4. Create a ContentProvider object using reflection.
                                ContentProvider contentProvider = (ContentProvider) Class.forName(providerInfo.name).newInstance();
                                contentProvider.attachInfo(loadedPlugin.getPluginContext(), providerInfo);
                                // 5. Store the ContentProvider object in the cache.
                                sCachedProviders.put(auth, contentProvider);
                            } catch(Exception e) { e.printStackTrace(); }}},true);
                    returnsCachedProviders.get(auth); }}return null; }}Copy the code

The entire process of building a ContentProvider object looks like this:

  1. Try to fetch the ContentProvider from the cache.
  2. Get the LoadedPlugin object.
  3. Get ProviderInfo from the LoadedPlugin object.
  4. Create a ContentProvider object using reflection.
  5. Store the ContentProvider object in the cache.

Let’s take a look at the add, delete, change and check operations in RemoteContentProvider, as shown below:

public class RemoteContentProvider extends ContentProvider {
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // 1. Generate a new Provider using the URI passed in.
        ContentProvider provider = getContentProvider(uri);
        // 2. Get the target URI.
        Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
        if(provider ! =null) {
            // 3. Perform the final query operation.
            return provider.query(pluginUri, projection, selection, selectionArgs, sortOrder);
        }

        return null; }}Copy the code

The logic of the query() method is also quite simple, as follows:

  1. Generate a new Provider from the URI passed in.
  2. Get the target URI.
  3. Perform the final query operation.

Ok, the startup process of the four components has been analyzed, let’s summarize again:

  • Activity: Preempts the pit in the host APK, and then starts the Activity in the plug-in APK by bypassing the verification by “cheating the top and hiding the bottom”.
  • VirtualAPK uses two proxy services, LocalService and RemoteService.
  • BroadcastReceiver: changes static broadcast to dynamic broadcast.
  • ContentProvider: Distribution of operations through a proxy Provider.

Above is the whole VrtualAPK framework principle analysis.