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
Official website: github.com/didi/Virtua…
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:
A VirtualAPK initialization path
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(); If (app == null) {this.mcontext = Context; } else { this.mContext = ((Application)app).getBaseContext(); } // Prepare (); } private void prepare() { 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:
- Hook object Instrumentation.
- 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 instanceof Intent && ((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() { 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(). Instrumentation objects are also saved download Final VAInstrumentation = new VAInstrumentation(this, baseInstrumentation); / / get ctivityThread instance Object activityThread = ReflectUtil. GetActivityThread (enclosing mContext); / / replace ActivityThread with custom VAInstrumentation heavy objects in the Instrumentation 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.
Private void hookAMSForO() {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(); Private void hookSystemServices() {try {Singleton<IActivityManager> defaultSingleton = (Singleton<IActivityManager>) ReflectUtil.getField(ActivityManagerNative.class, null, "gDefault"); IActivityManager activityManagerProxy = ActivityManagerProxy.newInstance(this, defaultSingleton.get()); . / / Hook objects from the ActivityManagerNative IActivityManager 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()); 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(new ActivityInfo[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:
- Call PackageParser to parse THE APK and get the PackageParser.Package object.
- Build the PackageInfo object.
- Build the PluginPackageManager object.
- Build the Resouces object.
- Build a ClassLoader object.
- Copy the SO library.
- Cache Instrumentation objects.
- Caches Activity information in APK.
- Caches Service information in APK.
- Caches Content Provider information in APK.
- 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:
- execStartActivity()
- realExecStartActivity()
- newActivity()
- 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"); To start an Activity, wrap 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(); } return result; }}Copy the code
The method does three main things, as follows:
- 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.
- Replace the real Activity with the registered StubActivity to bypass detection.
- 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; } String targetPackageName = intent.getComponent().getPackagename (); // Class name String targetClassName = intent.getComponent().getClassName(); // Search StubActivity if (! targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) ! Intent.putextra (Constants.KEY_IS_PLUGIN, true); Intent.putextra (Constants.KEY_TARGET_PACKAGE, targetPackageName); // Intent.putextra (Constants. 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 StubActivity dispatchStubActivity(Intent); } } private void dispatchStubActivity(Intent intent) { ComponentName component = intent.getComponent(); String targetClassName = intent.getComponent().getClassName(); / / to get corresponding LoadedPlugin intent object LoadedPlugin LoadedPlugin = mPluginManager. GetLoadedPlugin (intent); / / according to the ComponentName information for the corresponding ActivityInfo ActivityInfo info. = loadedPlugin getActivityInfo (component); if (info == null) { throw new RuntimeException("can not find " + component); } // launchMode int launchMode = info.launchmode; Resources.Theme themeObj = loadedPlugin.getResources().newTheme(); Themeobj. applyStyle(info.theme, true); // Replace 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 StubActivity intent.setClassName(mContext, StubActivity); }}Copy the code
Let’s see how to query StubActivity in detail, as follows:
Class StubActivityInfo {public static final int MAX_COUNT_STANDARD = 1; Public static final int MAX_COUNT_SINGLETOP = 8; Public static final int MAX_COUNT_SINGLETASK = 8; Public static final int MAX_COUNT_SINGLEINSTANCE = 8; Public static final String corePackage = "com.dide.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. Start by looking 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 startup 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, Format (STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity); if (windowIsTranslucent) { stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2); } break; } case ActivityInfo.LAUNCH_SINGLE_TOP: {// Top of stack reuse mode: Com. Didi. Virtualapk. Core. $, since every time 1, The range from 1 to 8. UsedSingleTopStubActivity = MAX_COUNT_SINGLETOP usedSingleTopStubActivity % + 1; stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity); break; } case ActivityInfo.LAUNCH_SINGLE_TASK: {// stack mode: Com. Didi. Virtualapk. Core. C $, since every time 1, The range from 1 to 8. UsedSingleTaskStubActivity = MAX_COUNT_SINGLETASK usedSingleTaskStubActivity % + 1; stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity); break; } case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {// singleton mode mode: Com. Didi. Virtualapk. Core. D $, since every time 1, The range from 1 to 8. UsedSingleInstanceStubActivity = MAX_COUNT_SINGLEINSTANCE usedSingleInstanceStubActivity % + 1; stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity); break; } default:break; } // Put the found Activity in the cache McAchedstubactivity. put(className, stubActivity); return stubActivity; }}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 based on 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, and its newActivity() method is called to complete the Activity construction, which is equivalent to 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, the activity, "mResources", the plugin, getResources ()); } catch (Exception ignored) { // ignored. } return activity; } } return mBase.newActivity(cl, className, intent); }}Copy the code
VAInstrumentation overwraps the newActivity method of Instrumentation, which does three things:
- Find the corresponding LoadedPlugin based on the Intent.
- Extract the real targetClassName from the Intent.
- 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 {// Obtains the IApplicationThread Object. IApplicationThread appThread = (IApplicationThread) args[0]; // Get a jump Intent. Intent target = (Intent) args[1]; / / check 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(); LocalService and RemoteService respectively determine whether to start Service in a 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); } return intent; }}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";
// 插件APK里的目标Service
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() {
super.onCreate();
// 获取PluginManager单例
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);
// 获取command信息
int command = intent.getIntExtra(EXTRA_COMMAND, 0);
if (null == target || command <= 0) {
return START_STICKY;
}
// 获取组件信息
ComponentName component = target.getComponent();
// 根据组件信息获取对应的LoadedPlugin
LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
// ClassNotFoundException when unmarshalling in Android 5.1
target.setExtrasClassLoader(plugin.getClassLoader());
switch (command) {
// 启动Service
case EXTRA_COMMAND_START_SERVICE: {
// 获取ActivityThread对象。
ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
// 获取IApplicationThread对象。
IApplicationThread appThread = mainThread.getApplicationThread();
Service service;
if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
// 尝试从ComponentsHandler里获取Service、
service = this.mPluginManager.getComponentsHandler().getService(component);
} else {
try {
// 调用DexClassLoader加载Service类。
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();
// 调用attch()方法,绑定应用上下文。
attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
// 回调Service的onCreate()方法。
service.onCreate();
// 插入Service
this.mPluginManager.getComponentsHandler().rememberService(component, service);
} catch (Throwable t) {
return START_STICKY;
}
}
// 回调Service的onStartCommand()方法。
service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
break;
}
// 绑定服务
case EXTRA_COMMAND_BIND_SERVICE: {
ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
IApplicationThread appThread = mainThread.getApplicationThread();
Service service = null;
if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
// 尝试从ComponentsHandler里获取Service、
service = this.mPluginManager.getComponentsHandler().getService(component);
} else {
try {
// 调用DexClassLoader加载Service类。
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();
// 调用attch()方法,绑定应用上下文。
attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
// 回调Service的onCreate()方法。
service.onCreate();
// 插入Service
this.mPluginManager.getComponentsHandler().rememberService(component, service);
} catch (Throwable t) {
t.printStackTrace();
}
}
try {
// 回调Service的onBind()方法。
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;
}
// 停止服务
case EXTRA_COMMAND_STOP_SERVICE: {
// 从ComponentsHandler移除Service的记录
Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
if (null != service) {
try {
// 回调Service的onDestroy()方法
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: {
// 从ComponentsHandler移除Service的记录
Service service = this.mPluginManager.getComponentsHandler().forgetService(component);
if (null != service) {
try {
// 回调Service的onUnbind()方法
service.onUnbind(target);
// 回调Service的onDestroy()方法
service.onDestroy();
} catch (Exception e) {
Log.e(TAG, "Unable to unbind service " + service + ": " + e.toString());
}
} else {
Log.i(TAG, component + " not found");
}
break;
}
}
return START_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) { Note 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 {/ / a loaded from a File more than LocalService APK plug-in operation 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 () {/ / get a Content Provider of pit, and then take the initiative to call () method to call it, The Call () method builds a RemoteContentProvider by calling RemoteContentProvider's getContentProvider. Uri uri = Uri.parse(PluginContentResolver.getUri(mContext)); mContext.getContentResolver().call(uri, "wakeup", null, null); try { Field authority = null; Field mProvider = null; / / get ActivityThread object ActivityThread ActivityThread = (ActivityThread) ReflectUtil. GetActivityThread (mContext); MProviderMap = (Map) reflectutil. getField(ActivityThread.getClass (), activityThread, "mProviderMap"); Iterator iter = mProviderMap.entrySet().iterator(); // Query the corresponding ContentProvider while (iter.hasNext()) {map.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); / / get 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 Builder.append ("/? plugin=" + loadedPlugin.getLocation()); Builder.append ("&pkg=" + PKG); builder.append("&uri=" + pluginUriString); Uri wrapperUri = Uri.parse(builder.toString()); return wrapperUri; }}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. Get the 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(); }} // 3. Obtain ProviderInfo from the LoadedPlugin object. final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0); if (providerInfo ! = null) { RunUtil.runOnUiThread(new Runnable() { @Override public void run() { 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); return sCachedProviders.get(auth); } } return null; }}Copy the code
The entire process of building a ContentProvider object looks like this:
- Try to fetch the ContentProvider from the cache.
- Get the LoadedPlugin object.
- Get ProviderInfo from the LoadedPlugin object.
- Create a ContentProvider object using reflection.
- 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 from 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. 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:
- Generate a new Provider from the URI passed in.
- Get the target URI.
- 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.