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:
- 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 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:
- 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"); 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:
- 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;
}
/ / 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:
- 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 {
// 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:
- 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 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:
- 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.