Public number: [Android old skin] hope to write things can help you 🤣🤣
1. Introduction
Virtual APK is an excellent plug-in framework developed by Didi Chuxing, and its main developer is Teacher Ren Yugang
Speaking of Teacher Ren Yugang, he can be said to be my enlightening teacher of Android FrameWork layer. When I first got into Android, after dragging around controls for a few years and writing some CURD operations, I came to the conclusion that the client was boring and I am now fully proficient in Android development. Until one day, I read a book called “Exploring the Art of Android Development” and couldn’t help feeling that Android development could be played like this. My previous knowledge was really shallow
Without further ado, the features and usage of VirtualAPK are not the focus of this article. If you need to learn more, please go to the features and usage of VirtualAPK. This article mainly for the realization of Virtual APK to explain.
2. Important knowledge points
- Activity Startup process (AMS)
- DexClassLoader
- A dynamic proxy
- reflection
- Dynamic registration of broadcasts
3. Implementation of host App
Central idea:
- The plug-in APK is parsed to obtain the information of the plug-in APK
- During the framework initialization, a series of system components and interfaces are replaced, so as to modify and monitor the startup and life cycle of Activity, Service and ContentProvider, so as to start the corresponding components of the plug-in Apk by deceiving the system or hijacking the system.
3.1 Analysis and loading of plug-in Apk
The plugin Apk is loaded in the PluginManager#loadPlugin method. After loading, a LoadedPlugin object is generated and stored in the Map. LoadedPlugin holds most of the important information in the plugin Apk and a DexClassLoader that acts as a class loader for the plugin Apk.
Looking at the implementation of the LoadedPlugin, the comments indicate the meaning of each attribute:
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception { // PluginManager this.mPluginManager = pluginManager; // HostContext this.mHostContext = Context; // Plugin apk path this.mlocation = apk.getabsolutePath (); this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK); / / the plugin apk metadata this. MPackage. ApplicationInfo. Metadata = this. MPackage. MAppMetaData; This.mpackageinfo = new PackageInfo(); this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo; this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath(); / / the plugin apk signature information if (Build) VERSION) SDK_INT > = 28 | | (Build) VERSION) SDK_INT = = 27 && Build. VERSION. PREVIEW_SDK_INT! = 0)) { // Android P Preview try { this.mPackageInfo.signatures = this.mPackage.mSigningDetails.signatures; } catch (Throwable e) { PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); this.mPackageInfo.signatures = info.signatures; } } else { this.mPackageInfo.signatures = this.mPackage.mSignatures; } / / plugin apk package name enclosing mPackageInfo. PackageName = this. MPackage. PackageName; / / if you have already loaded the same apk, throw an exception 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]; this.mPackageManager = createPluginPackageManager(); this.mPluginContext = createPluginContext(null); this.mNativeLibDir = getDir(context, Constants.NATIVE_DIR); this.mPackage.applicationInfo.nativeLibraryDir = this.mNativeLibDir.getAbsolutePath(); This.mresources = createResources(context, getPackageName(), apk); MClassLoader = createClassLoader(context, apk, this.mnativelibDir, context.getClassLoader()); tryToCopyNativeLib(apk); // Cache instrumentations 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()]); ActivityInfos = new HashMap<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>(); for (PackageParser.Activity activity : this.mPackage.activities) { activity.info.metaData = activity.metaData; activityInfos.put(activity.getComponentName(), activity.info); } this.mActivityInfos = Collections.unmodifiableMap(activityInfos); this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]); <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()]); Providers = new HashMap<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()]); Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>(); for (PackageParser.Activity receiver : this.mPackage.receivers) { receivers.put(receiver.getComponentName(), receiver.info); BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance()); for (PackageParser.ActivityIntentInfo aii : receiver.intents) { this.mHostContext.registerReceiver(br, aii); } } this.mReceiverInfos = Collections.unmodifiableMap(receivers); this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]); InvokeApplication (); // invokeApplication(); }Copy the code
3.2 Activity startup processing and life cycle management
The overall scheme of Activity in Virtual APK startup plug-in APK:
- The Hook Instrumentaion and the callback of the main Halder Instrumentaion are used to replace the Intent or Activity at the important startup node
- Pre-set some in the host APP
Put the Activity of pile
, these petting activities do not actually start, but deceive AMS. If the Activity started is in the plug-in APK, the appropriate petted Activity is selected according to the startup mode of the Actiivty. After AMS processes the petted Activity in the startup stage, the Activity to be started in the plug-in APK is actually created in the Activity instance creation stage.
3.2.1 Declaration of piling Activity:
There are many activities for inserting piles. Pick some and have a look:
<! -- Stub Activities --> <activity android:exported="false" android:name=".A$1" android:launchMode="standard"/> <activity android:exported="false" android:name=".A$2" android:launchMode="standard" android:theme="@android:style/Theme.Translucent" /> <! -- Stub Activities --> <activity android:exported="false" android:name=".B$1" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$2" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$3"Copy the code
3.2.2 hook Instrumentation
- Replace the Instrumentation provided by the system with custom VAInstrumentation, Replace the Callback of the main Handler with VAInstrumentation as well (VAInstrumentation implements the handler. Callback interface)
Protected void hookInstrumentationAndHandler () {try {/ / get the current process of activityThread activityThread activityThread = ActivityThread.currentActivityThread(); / / get the current process of Instrumentation Instrumentation baseInstrumentation = activityThread. GetInstrumentation (); // Create custom Instrumentation final VAInstrumentation = createInstrumentation(baseInstrumentation); // Replace the existing Instrumentation object of the current process with a custom Reflector. With (activityThread). Field ("mInstrumentation"). MainHandler = Reflector. With (activityThread).method("getHandler").call(); Reflector.with(mainHandler).field("mCallback").set(instrumentation); this.mInstrumentation = instrumentation; Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation); } catch (Exception e) { Log.w(TAG, e); }}Copy the code
3.2.3 AMS is spooked when the Activity is started
If we are familiar with the Activity startup process, we must know that Activity startup and lifecycle management are managed indirectly through Instrumentation. If you are not familiar with AMS, you can read the AMS series I wrote before, and I am sure you will understand it in a second. VAInstrumentation overrides some of the important methods of this class, one by one, according to the Activity launch process
3.2.3.1 execStartActivity
This method has many overloads, pick one of them:
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intents intents, int requestCode) {// Intents intents (intents); return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode); }Copy the code
InjectIntent () {ComponentsHandler#markIntentIfNeeded (); If the package name of the target Activity is different from that of the current process, and the LoadedPlugin object corresponding to the package name exists, then it is an Activity in the APK that we loaded.
public void markIntentIfNeeded(Intent intent) { ... String targetPackageName = intent.getComponent().getPackageName(); String targetClassName = intent.getComponent().getClassName(); // Start the Activity if (! targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) ! = null) { ... // Replace the target Acitivy of the original Intent with a dispatchStubActivity(Intent) in the default peg Activity; }}Copy the code
The dispatchStubActivity method selects the appropriate stapled Activity based on the launch mode of the original Intent and changes the class name of the stapled Activity from the original Intent. Example code:
case ActivityInfo.LAUNCH_SINGLE_TOP: {
usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_TASK: {
usedSingleTaskStubActivity = usedSingleTaskStubActivity % MAX_COUNT_SINGLETASK + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETASK, corePackage, usedSingleTaskStubActivity);
break;
}
case ActivityInfo.LAUNCH_SINGLE_INSTANCE: {
usedSingleInstanceStubActivity = usedSingleInstanceStubActivity % MAX_COUNT_SINGLEINSTANCE + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLEINSTANCE, corePackage, usedSingleInstanceStubActivity);
break;
}
Copy the code
3.2.3.2 newActivity
If the original Intent is replaced, then the Acitivty in Apk will eventually be launched. This does not meet the purpose of Acitivty in Apk. In the Activity instance creation stage, the actual Actiivty will be replaced. Methods in VAInstrumentation# newActivity:
@Override public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { try { cl.loadClass(className); Log.i(TAG, String.format("newActivity[%s]", className)); } catch (ClassNotFoundException e) { ComponentName component = PluginUtil.getComponent(intent); String targetClassName = component.getClassName(); Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName)); LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component); // Use the DexClassLoader created in the LoadedPlugin object for class loading. Activity Activity = mbase.newActivity (plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent); // After the plugin Activity instance is created, Replace the Resource for the resources of the plugin APK Reflector. QuietReflector. With (activity). The field (" mResources "). The set (plugin. GetResources ()); return newActivity(activity); } return newActivity(mBase.newActivity(cl, className, intent)); }Copy the code
If we start an Activity in the APK plugin, the Catch block of this method will be executed because the className parameter has been replaced with a pinned Activity, However, we only define these Actiivty in the host App’s Androidmanifest.xml, and there is no actual implementation. After entering the Catch block, the Activity is created using the DexClassloader saved in the LoadedPlugin.
3.2.3.3 AMS manages activities in plug-in APK
You have replaced the Activity you want to start, but AMS still records the staked Actiivty. What about the subsequent interaction between this Activity instance and AMS? Is it not that the records in AMS cannot be found? B: Don’t worry, that won’t happen. As you’ll see from the previous AMS articles, Activity management in AMS is based on a Binder instance called appToken, The corresponding token on the client will be passed to Activity#attach after the Instrumentation#newActivity is executed.
This is why spoofing AMS with plugins is possible, because the subsequent management is token, and if Android uses a className or something like that, I’m afraid this kind of solution is not easy to implement.
3.2.3.4 Replace Context, Applicaiton, and Resources
After the system creates a plug-in Activity Context, replace it with a PluginContext. The difference between PluginContext and Context is that a LoadedPlugin object is stored inside the PluginContext, which is convenient for replacing resources in the Context. The code is in VAInstrumentaiton#injectActivity, and the call is in VAInstrumentaiton#callActivityOnCreate
protected void injectActivity(Activity activity) { final Intent intent = activity.getIntent(); if (PluginUtil.isIntentFromPlugin(intent)) { Context base = activity.getBaseContext(); try { LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent); Reflector.with(base).field("mResources").set(plugin.getResources()); Reflector reflector = Reflector.with(activity); reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext())); reflector.field("mApplication").set(plugin.getApplication()); // set screenOrientation ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent)); if (activityInfo.screenOrientation ! = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { activity.setRequestedOrientation(activityInfo.screenOrientation); } // for native activity ComponentName component = PluginUtil.getComponent(intent); Intent wrapperIntent = new Intent(intent); wrapperIntent.setClassName(component.getPackageName(), component.getClassName()); wrapperIntent.setExtrasClassLoader(activity.getClassLoader()); activity.setIntent(wrapperIntent); } catch (Exception e) { Log.w(TAG, e); }}}Copy the code
3.3 Service Processing
The overall scheme of Activity in Virtual APK startup plug-in APK:
- Use dynamic proxies to host all requests for services in your APP
- Check whether it is a Service in the plug-in APK. If not, it indicates that it is a Service in the host APP. Open it directly
- If it is a Service in the plug-in APK, it determines whether it is a RemoteService. If it is a RemoteService, it starts RemoteService and processes it in its StartCommand method based on the propped lifecycle method
- If it is a LocalService, start LocalService and process it according to the proxied lifecycle method in its StartCommand method
Proxy IActivityManager for the system during initialization of the plug-in framework
IActivityManager is the implementation interface for AMS. Its implementation classes are ActivityManagerService and its Proxy
Here we need the Proxy, which is implemented in PluginManager#hookSystemServices
Protected void hookSystemServices() {try {Singleton<IActivityManager object > defaultSingleton; // Get the IActivityManager object if (build.version.sdk_int >= build.version_codes.o) {defaultSingleton = Reflector.on(ActivityManager.class).field("IActivityManagerSingleton").get(); } else { defaultSingleton = Reflector.on(ActivityManagerNative.class).field("gDefault").get(); } IActivityManager origin = defaultSingleton.get(); IActivityManager Dynamic proxy for activityManager = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class }, createActivityManagerProxy(origin)); // Replace the IActivityManager instance Reflector. With (defaulsingleton).field("mInstance").set(activityManagerProxy); if (defaultSingleton.get() == activityManagerProxy) { this.mActivityManager = activityManagerProxy; Log.d(TAG, "hookSystemServices succeed : " + mActivityManager); } } catch (Exception e) { Log.w(TAG, e); }}Copy the code
By replacing the proxy of ActivityManager created by the system with a dynamic proxy, the AMS method is referred to the Invoke method of ActivityManagerProxy, and the Service life cycle is managed according to the method name. Pick one:
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("startService".equals(method.getName())) { try { return startService(proxy, method, args); } catch (Throwable e) { Log.e(TAG, "Start service error", e); }}Copy the code
StartService:
protected Object startService(Object proxy, Method method, Object[] args) throws Throwable {
IApplicationThread appThread = (IApplicationThread) args[0];
Intent target = (Intent) args[1];
ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
if (null == resolveInfo || null == resolveInfo.serviceInfo) {
// 插件中没找到,说明是宿主APP自己的Service
return method.invoke(this.mActivityManager, args);
}
// 启动插件APK中的Service
return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}
Copy the code
StartDelegateServiceForTarget wrapperTargetIntent will call processing, and ultimately in RemoteService or LocalService onStartCommand the life cycle of Service processing.
Note that the APK needs to be parsed and reloaded in RemoteService to generate the LoadedPlugin because it is running in a different process.
This also means that APK Service processes that declare more than one Service are invalid because they all end up running in the same process that hosts RemoteService.
3.4 Processing of ContentProvider
ContentProvicer is handled similarly to a Service, without further ado.
4. Implementation of plug-in App
In theory, the plugin APP does not need to do anything special. The only thing that needs to be noticed is the conflict of resource files. Therefore, you need to add the following code to the build.gradle directory of the plugin project APP:
virtualApk { packageId = 0x6f // the package id of Resources. targetHost = '.. /.. /VirtualAPK/app' // the path of application module in host project. applyHostMapping = true //optional, default value: true. }Copy the code
It overrides the resource ID when the plugin APK is compiled, handled in the collect method of the ResourceCollector. Groovy file:
Parsing () {// First, collect all resources by parsing the R symbol file. parseResEntries(allRSymbolFile, parsing) {// First, collect all resources by parsing the R symbol file. // Parsing the host apk R symbol file. should be stripped. parseResEntries(hostRSymbolFile, hostResources, //3, Compute the resources that should be retained in the plugin apk.filterpluginresources () //4, Reassign the resource ID. If the resource entry exists in host apk, the reassign ID // should be same with value in host apk; If the resource entry is owned by plugin project, // Then we should recalculate the ID value. reassignPluginResourceId() // 4, Collect all the resources in the retained resource AARs, to regenerate the R java file that uses the new resource ID vaContext.retainedAarLibs.each { gatherReservedAarResources(it) } }Copy the code
Obtain the resource set of the plug-in app and the host app first, and then find the resource ID of the conflict to modify the ID is reassignPluginResourceId method:
Private void reassignPluginResourceId() {// Sort the resource by typeId resourceidList.sort {t1, TypeId - t2.typeId} int lastType = 1 // Rewrite resource ID resourceidList. each {if (it. TypeId < 0) {return} def typeId = 0 def entryId = 0 typeId = lastType++ pluginResources.get(it.resType).each { it.setNewResourceId(virtualApk.packageId, typeId, entryId++) } } }Copy the code
Here are the components of a resource ID:
The resource ID is a 32-bit hexadecimal integer. The first eight digits represent App. The next eight digits represent typeId(string, layout, ID, etc.). To decompile a random APK, take a look at the structure of part of it:
The outer loop iterates over typeId from 01, the inner loop iterates over typeId from 0000, and the inner loop overrides the resource ID by calling setNewResourceId:
public void setNewResourceId(packageId, typeId, entryId) {
newResourceId = packageId << 24 | typeId << 16 | entryId
}
Copy the code
PackageId is the Virtualapk. packageId that we defined in build.gradle. Move it 24 bits to the left, corresponding to the first 8 bits of the resource ID, typeId to bits 9-16, followed by the resource ID
In this way, the replacement of the conflicting resource ID is completed during the plug-in app compilation process, and there will be no conflict problems later
5. To summarize
The realization of the review of the whole Virtual APK, actually logic is not particularly complicated, but you can see the authors to AMS, resource loading and class loader API familiarity, if not very proficient in the knowledge system, it is difficult to achieve, even can’t have idea, which we learn the meaning of source.