1. Plug-in features
1, the advantages of
- Package specific features as plug-ins that can be downloaded and turned on when the user needs to use a particular feature
- More flexible version, can be issued at any time
- The organizational structure is more flexible, with each team responsible for its own plug-in development
- The debugging speed is faster in the development, and the plug-in is directly pushed into the phone to run
2. Limitations
- The stability is not enough. There are compatibility problems through hook mode
- Plug-in development may require a release if it changes too much
Second, the Activity starts Hook point analysis
The Activity startup process focuses on the application process communicating with AMS. After the processing is complete, AMS will hand over the application process to continue processing. The point where Hook is needed is before AMS call and after MAS call.
1, execStartActivity
#Instrumentation
public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, String resultWho, Intent intent, int requestCode, Bundle options, UserHandle user) { ..... try { intent.migrateExtraStreamToClipData(); intent.prepareToLeaveProcess(who); / / call the AMS continue to launch the Activity int result = ActivityManager. GetService () startActivityAsUser (whoThread, who getBasePackageName (), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, resultWho, requestCode, 0, null, options, user.getIdentifier()); CheckStartActivityResult (result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e);
}
return null;
}
Copy the code
When an Activity is started, check the result of the Activity with the checkStartActivityResult in Instrumentation. If the Activity of the plug-in is not registered in the manifest file, Will throw ActivityNotFoundException. So the question is how do you pass validation?
2, ActivityThread
#ActivityThread
private class H extends Handler {
...
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart"); final ActivityClientRecord r = (ActivityClientRecord) msg.obj; r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); HandleLaunchActivity (r, null,"LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break; . }... }Copy the code
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ... / / to start the creation Activity context ContextImpl appContext = createBaseContextForActivity (r); Activity activity = null; try { java.lang.ClassLoader cl = appContext.getClassLoader(); / / to create the Activity with the class loader instance of the Activity. = mInstrumentation newActivity (cl, component getClassName (), r.i ntent); / / 1... } catch (Exception e) { ... }...return activity;
}
Copy the code
The problem is to create the plug-in Activity that needs to be loaded
Three, the principle of VirtualApk analysis
VirtualApk
1. Initialization
Perform initialization in Application
PluginManager.getInstance(base).init();
Copy the code
Instrumentation, Callback of mH class of ActivityThread, IActivityManager, and DataBindingUtil are hooked during initialization
#PluginManager
protected PluginManager(Context context) {
......
hookCurrentProcess();
}
protected void hookCurrentProcess() {
hookInstrumentationAndHandler();
hookSystemServices();
hookDataBindingUtil();
}
Copy the code
(1)hookInstrumentationAndHandler
#PluginManager
protected void hookInstrumentationAndHandler() {
try {
ActivityThread activityThread = ActivityThread.currentActivityThread();
Instrumentation baseInstrumentation = activityThread.getInstrumentation();
final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
Reflector.with(mainHandler).field("mCallback").set(instrumentation);
this.mInstrumentation = instrumentation;
Log.d(TAG, "hookInstrumentationAndHandler succeed : "+ mInstrumentation); } catch (Exception e) { Log.w(TAG, e); } } public class VAInstrumentation extends Instrumentation implements Handler.Callback {...... }Copy the code
- Create VAInstrumentation, a subclass of Instrumentation that implements the handler. Callback method
- Set the VAInstrumentation to the ActivityThread by reflection, hook the Instrumentation
- Callback is set by reflection, which intercepts the Callback of ActivityThread H
(2) hookSystemServices
protected void hookSystemServices() {
try {
Singleton<IActivityManager> defaultSingleton;
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 activityManagerProxy = (IActivityManager) Proxy.newProxyInstance(mContext.getClassLoader(), new Class[] { IActivityManager.class },
createActivityManagerProxy(origin));
// Hook IActivityManager from ActivityManagerNative
Reflector.with(defaultSingleton).field("mInstance").set(activityManagerProxy);
if (defaultSingleton.get() == activityManagerProxy) {
this.mActivityManager = activityManagerProxy;
Log.d(TAG, "hookSystemServices succeed : "+ mActivityManager); } } catch (Exception e) { Log.w(TAG, e); } } public class ActivityManagerProxy implements InvocationHandler {...... }Copy the code
- The dynamic proxy object ActivityManagerProxy for IActivityManager is created
- Replace the AMS proxy object IActivityManager with reflection to take over operations such as Activity startup
2. Plug-in loading
(1) loadPlugin generally generates a jar or APK file for a function plug-in, and then delivers it to the main project for loading through loadPlugin of PluginManager
#PluginManagerpublic void loadPlugin(File apk) throws Exception { ...... // Convert the plugin file to a LoadedPlugin object LoadedPlugin plugin = createLoadedPlugin(apk);if (null == plugin) {
throw new RuntimeException("Can't load plugin which is invalid: "+ apk.getAbsolutePath()); } // Save the plugin to this.mplugins.put (plugin.getPackagename (), plugin); . }Copy the code
(2) Build LoadedPlugin object
#LoadedPluginpublic LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception { this.mPluginManager = pluginManager; this.mHostContext = context; this.mLocation = apk.getAbsolutePath(); this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK); this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData; This.mpackageinfo = new PackageInfo(); this.mpackageInfo = new PackageInfo(); this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo; this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath(); . this.mPackageManager = createPluginPackageManager(); this.mPluginContext = createPluginContext(null); this.mNativeLibDir = getDir(context, Constants.NATIVE_DIR); this.mPackage.applicationInfo.nativeLibraryDir = this.mNativeLibDir.getAbsolutePath(); // createResource this.mResources = createResources(context, getPackageName(), apk); This.mclassloader = createClassLoader(context, apk, this.mnativelibDir, context.getClassLoader()); // copy so tryToCopyNativeLib(apk); 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()]); Activity Map<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()]); Service 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()]); Providers 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()]);
// Register broadcast receivers dynamically
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()]);
// try to invoke plugin's application invokeApplication(); }Copy the code
Create PackageInfo, Resouces, and ClassLoader objects to store information about Instrumentation, Activity, Service, and Content Provider. (3) Create a ClassLoader object
#LoadedPluginprotected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception { File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR); String dexOutputPath = dexOutputDir.getAbsolutePath(); DexClassLoader = new DexClassLoader(apk.getabsolutePath (), dexOutputPath, libsDir.getAbsolutePath(), parent);if (Constants.COMBINE_CLASSLOADER) {
DexUtil.insertDex(loader, parent, libsDir);
}
return loader;
}
#DexUtilpublic static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception { Object baseDexElements = getDexElements(getPathList(baseClassLoader)); Object newDexElements = getDexElements(getPathList(dexClassLoader)); Object allDexElements = combineArray(baseDexElements, newDexElements); Object pathList = getPathList(baseClassLoader); // Assign the combined dex file to dexElements reflect.with (pathList).field("dexElements").set(allDexElements);
insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
Copy the code
- Create a DexClassLoader object
- Merge the host and plug-in Dex files and assign values to dexElements via reflection
- Files such as the Activity in the plug-in can then be loaded
3. Define placeholder activities
<activity android:exported="false" android:name="com.didi.virtualapk.delegate.StubActivity" android:launchMode="standard"/ > <! -- Stub Activities --> <activity android:exported="false" android:name=".AThe $1" android:launchMode="standard"/>
<activity android:exported="false" android:name=".A$2" android:launchMode="standard"
android:theme="@android:style/Theme.Translucent"/ >... <! -- Local Service runningin main process -->
<service android:exported="false" android:name="com.didi.virtualapk.delegate.LocalService"/ > <! -- Daemon Service runningin child process -->
<service android:exported="false" android:name="com.didi.virtualapk.delegate.RemoteService" android:process=":daemon">
<intent-filter>
<action android:name="${applicationId}.intent.ACTION_DAEMON_SERVICE" />
</intent-filter>
</service>
<provider
android:exported="false"
android:name="com.didi.virtualapk.delegate.RemoteContentProvider"
android:authorities="${applicationId}.VirtualAPK.Provider"
android:process=":daemon" />
Copy the code
There are placeholders for various startup modes in the manifest file: Activities, Services, and ContentProviders
4. Replace the plug-in Activity with a placeholder Activity
(1) The execStartActivity method is walked to the VAInstrumentation when the Activity is launched
#VAInstrumentation
@Override
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, Int requestCode) {// Replace the Activity injectIntent(intent); // Continue to InstrumentationexecStartActivity methodreturnmBase.execStartActivity(who, contextThread, token, target, intent, requestCode); } protected void injectIntent(Intent Intent) {// Use the Intent to match the Activity pit in the PluginManager mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent); // null component is an implicitly intentif(intent.getComponent() ! = null) { Log.i(TAG, String.format("execStartActivity[%s : %s]", intent.getComponent().getPackageName(), intent.getComponent().getClassName()));
// resolve intent with Stub Activity ifneeded this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent); }}Copy the code
(2) Store the related information of the plug-in Activity
public void markIntentIfNeeded(Intent intent) {
if (intent.getComponent() == null) {
return;
}
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
// search map and return specific launchmode stub activity
if(! targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) ! = null) { intent.putExtra(Constants.KEY_IS_PLUGIN,true); Intent.putextra (Constants.KEY_TARGET_PACKAGE, targetPackageName); // Intent.putextra (Constants. intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName); dispatchStubActivity(intent); }}Copy the code
(3) Start the plug-in Activity by replacing it with a placeholder Activity
private void dispatchStubActivity(Intent intent) {
ComponentName component = intent.getComponent();
String targetClassName = intent.getComponent().getClassName();
LoadedPlugin loadedPlugin = mPluginManager.getLoadedPlugin(intent);
ActivityInfo info = loadedPlugin.getActivityInfo(component);
if (info == null) {
throw new RuntimeException("can not find " + component);
}
int launchMode = info.launchMode;
Resources.Theme themeObj = loadedPlugin.getResources().newTheme();
themeObj.applyStyle(info.theme, true); / / by launchMode etc information to find the right placeholder Activity String stubActivity = mStubActivityInfo. GetStubActivity (targetClassName launchMode, themeObj); Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
intent.setClassName(mContext, stubActivity);
}
Copy the code
The Activity then holds the placeholder and continues to communicate with AMS
5. Replace the Activity of the target plug-in
(1) VAInstrumentation receives the message sent by the ApplicationThread
#VAInstrumentation
@Override
public boolean handleMessage(Message msg) {
if (msg.what == LAUNCH_ACTIVITY) {
// ActivityClientRecord r
Object r = msg.obj;
try {
Reflector reflector = Reflector.with(r);
Intent intent = reflector.field("intent").get(); intent.setExtrasClassLoader(mPluginManager.getHostContext().getClassLoader()); ActivityInfo ActivityInfo = reflector. Field ("activityInfo").get();
if (PluginUtil.isIntentFromPlugin(intent)) {
int theme = PluginUtil.getTheme(mPluginManager.getHostContext(), intent);
if(theme ! = 0) { Log.i(TAG,"resolve theme, current theme:" + activityInfo.theme + " after :0x"+ Integer.toHexString(theme)); Thme activityinfo. theme = theme; } } } catch (Exception e) { Log.w(TAG, e); }}return false;
}
Copy the code
(2) Create an Activity object by the newActivity of VAInstrumentation
#VAInstrumentation
@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) {// The placeholder Activity does not exist, ComponentName component = pluginutil.getComponent (intent);if (component == null) {
returnnewActivity(mBase.newActivity(cl, className, intent)); } // Get the Activity of the target plug-in String targetClassName = component.getClassName(); LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(component);if (plugin == null) {
// Not found then goto stub activity.
boolean debuggable = false; try { Context context = this.mPluginManager.getHostContext(); debuggable = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) ! = 0; } catch (Throwable ex) { }if (debuggable) {
throw new ActivityNotFoundException("error intent: " + intent.toURI());
}
Log.i(TAG, "Not found. starting the stub activity: " + StubActivity.class);
returnnewActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent)); } // With Instrumentation newActivity to create the target Activity Activity Activity = mbase.newActivity (plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent); //for4.1 + Reflector. QuietReflector. With (activity). The field ("mResources").set(plugin.getResources());
return newActivity(activity);
}
return newActivity(mBase.newActivity(cl, className, intent));
}
Copy the code
Gets the ComponentName for the target Activity
#PluginUtil
public static ComponentName getComponent(Intent intent) {
if (intent == null) {
return null;
}
if (isIntentFromPlugin(intent)) {
return new ComponentName(intent.getStringExtra(Constants.KEY_TARGET_PACKAGE),
intent.getStringExtra(Constants.KEY_TARGET_ACTIVITY));
}
return intent.getComponent();
}
Copy the code
Retrieves the parameter information previously stored in the placeholder Activity and returns ComponentName to continue the Activity start operation
6, callActivityOnCreate
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
injectActivity(activity);
mBase.callActivityOnCreate(activity, icicle, persistentState);
}
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); } / /fornative activity ComponentName component = PluginUtil.getComponent(intent); Intent wrapperIntent = new Intent(intent); wrapperIntent.setClassName(component.getPackageName(), component.getClassName()); activity.setIntent(wrapperIntent); } catch (Exception e) { Log.w(TAG, e); }}}Copy the code
Set and modify mResources, mBase (Context), and mApplication objects, and finally execute the onCreate method of the Activity
7, the Activity plug-in summary
(1) Hook the Callback of activityThread.mh during initialization (2) define placeholder Activity in the host project manifest file (3) when loading the plug-in, merge the plug-in dex file with the host dex file, Reflect the dexElements assigned to the PathList in order to be loaded by the ClassLoader. (4) Launching the target Activity, VAInstrumentation replaces the target Activity with a placeholder Activity, The target Activity information is stored as a parameter. Through the Activity verification, and then continue to communicate with AMS. (5) After AMS processing is complete, the message is passed to the ApplicationThread, intercepted by the VAInstrumentation, the placeholder Activity is replaced by the target Activity, and the target Activity is created. (6) Set the mResources, mBase (Context), and mApplication objects, and finally call the onCreate method of the Activity
References:
- A brief analysis of Android plug-in
- 360 open source plug-in framework Replugin in-depth analysis
- Didi plug-in program VirtualApk source code analysis
- Android Plug-in Development Guide
- Android Advanced Decryption