Android進階(九)Activity外掛化和VirtualApk分析
- 將特定功能打包為外掛,當用戶需要使用某個特定功能時,才進行下載並開啟
- 發版更靈活,可隨時發版
- 組織架構更靈活,每個團隊負責自身的外掛開發
- 開發中除錯速度更快,直接將外掛推入手機執行
2、侷限性
- 穩定性不夠,通過hook方式,存在相容問題
- 外掛化開發如果改動過大可能就需要發版
二、Activity啟動Hook點分析
Activity啟動過程重點是應用程序跟AMS進行通訊,處理完成後AMS再交給應用程序繼續處理。需要Hook的點就是在AMS呼叫之前跟MAS呼叫完成之後。
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); //呼叫AMS繼續啟動Activity int result = ActivityManager.getService() .startActivityAsUser(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, resultWho, requestCode, 0, null, options, user.getIdentifier()); //檢查啟動Activity的結果 checkStartActivityResult(result, intent); } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); } return null; } 複製程式碼
在Activity啟動時,通過Instrumentation的checkStartActivityResult去檢查啟動的Activity的結果,如果外掛的Activity未在清單檔案中註冊,則會丟擲ActivityNotFoundException。需要解決的就是如何通過驗證?
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); //呼叫了performLaunchActivity方法 handleLaunchActivity(r, null, "LAUNCH_ACTIVITY"); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } break; ... } ... } 複製程式碼
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ... //建立要啟動Activity的上下文環境 ContextImpl appContext = createBaseContextForActivity(r); Activity activity = null; try { java.lang.ClassLoader cl = appContext.getClassLoader(); //用類載入器來建立Activity的例項 activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent);//1 ... } catch (Exception e) { ... } ... return activity; } 複製程式碼
需要解決的是將需要載入的外掛Activity創建出來
三、VirtualApk原理分析
1、初始化
在Application進行初始化操作
PluginManager.getInstance(base).init(); 複製程式碼
在初始化操作時Hook了Instrumentation、ActivityThread的mH類的Callback、IActivityManager、DataBindingUtil
#PluginManager protected PluginManager(Context context) { ...... hookCurrentProcess(); } protected void hookCurrentProcess() { hookInstrumentationAndHandler(); hookSystemServices(); hookDataBindingUtil(); } 複製程式碼
(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 {......} 複製程式碼
- 建立VAInstrumentation,是Instrumentation的子類,實現了Handler.Callback方法
- 通過反射將VAInstrumentation設定給ActivityThread, hook住了Instrumentation
- 通過反射設定了Handler.Callback,攔截了ActivityThread的H的Callback
(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 {......} 複製程式碼
- 建立了IActivityManager的動態代理物件ActivityManagerProxy
- 通過反射來替換掉AMS的代理物件IActivityManager,來接管Activity啟動等操作
2、外掛載入
(1)loadPlugin
一般會將某個功能外掛生成jar或者apk檔案,然後交給主工程通過PluginManager的loadPlugin進行載入
#PluginManager public void loadPlugin(File apk) throws Exception { ...... //將外掛檔案轉換為一個LoadedPlugin物件 LoadedPlugin plugin = createLoadedPlugin(apk); if (null == plugin) { throw new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath()); } //將外掛LoadedPlugin存入 this.mPlugins.put(plugin.getPackageName(), plugin); ...... } 複製程式碼
(2)構建LoadedPlugin物件
#LoadedPlugin public 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; //建立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(); //建立Resource this.mResources = createResources(context, getPackageName(), apk); //建立ClassLoader this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader()); //拷貝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()]); // 快取activities 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()]); // 快取services 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(); } 複製程式碼
建立PackageInfo、Resouces、ClassLoader物件,儲存Instrumentation、Activity、Service、Content Provider等資訊
(3)建立ClassLoader物件
#LoadedPlugin protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception { File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR); String dexOutputPath = dexOutputDir.getAbsolutePath(); //建立DexClassLoader用來載入外掛 DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent); if (Constants.COMBINE_CLASSLOADER) { DexUtil.insertDex(loader, parent, libsDir); } return loader; } #DexUtil public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception { Object baseDexElements = getDexElements(getPathList(baseClassLoader)); Object newDexElements = getDexElements(getPathList(dexClassLoader)); //將宿主自身的dex檔案和外掛的dex檔案合併 Object allDexElements = combineArray(baseDexElements, newDexElements); Object pathList = getPathList(baseClassLoader); //通過反射將合併後的dex檔案賦值給dexElements Reflector.with(pathList).field("dexElements").set(allDexElements); insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir); } 複製程式碼
- 建立DexClassLoader物件
- 將宿主和外掛Dex檔案合併,並通過反射賦值給dexElements
- 然後外掛中的Activity等檔案就可以被載入了
3、定義佔位Activity
<activity android:exported="false" android:name="com.didi.virtualapk.delegate.StubActivity" android:launchMode="standard"/> <!-- 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" /> ...... <!-- Local Service running in main process --> <service android:exported="false" android:name="com.didi.virtualapk.delegate.LocalService" /> <!-- Daemon Service running in 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" /> 複製程式碼
在清單檔案中定了各種啟動模式的佔位Activity、Service、ContentProvider
4、將外掛Activity替換為佔位的Activity
(1)啟動Activity時會走到VAInstrumentation的execStartActivity方法
#VAInstrumentation @Override public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode) { //替換為佔坑的Activity injectIntent(intent); //繼續走Instrumentation的execStartActivity方法 return mBase.execStartActivity(who, contextThread, token, target, intent, requestCode); } protected void injectIntent(Intent intent) { //通過intent去匹配PluginManager中Activity的坑位 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())); // resolve intent with Stub Activity if needed this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent); } } 複製程式碼
(2)將外掛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.KEY_TARGET_ACTIVITY, targetClassName); dispatchStubActivity(intent); } } 複製程式碼
(3)將外掛Activity替換為佔位的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); //通過launchMode等資訊找到合適的佔位Activity String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj); Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity)); intent.setClassName(mContext, stubActivity); } 複製程式碼
接下來拿著佔位的Activity繼續跟AMS進行通訊
5、替換回目標外掛的Activity
(1)VAInstrumentation接收到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 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; } 複製程式碼
(2)通過VAInstrumentation的newActivity建立一個Activity物件
#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) { //佔位的Activity不存在,進入catch處理 ComponentName component = PluginUtil.getComponent(intent); if (component == null) { return newActivity(mBase.newActivity(cl, className, intent)); } //獲取目標外掛的Activity 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); return newActivity(mBase.newActivity(cl, StubActivity.class.getName(), intent)); } //通過Instrumentation的newActivity實現目標Activity的建立 Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent); // for 4.1+ Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources()); return newActivity(activity); } return newActivity(mBase.newActivity(cl, className, intent)); } 複製程式碼
獲取目標Activity的ComponentName
#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(); } 複製程式碼
獲取之前儲存到佔位Activity的相關引數資訊,並返回ComponentName,繼續執行Activity啟動操作
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); } // for native 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); } } } 複製程式碼
設定了修改了mResources、mBase(Context)、mApplication物件,最終執行了Activity的onCreate方法
7、Activity外掛化總結
(1)初始化時Hook住Instrumentation、ActivityThread.mH的Callback回撥 (2)在宿主工程的清單檔案中定義佔位Activity (3)載入外掛時,將外掛dex檔案和宿主dex檔案合併,反射賦值給PathList的dexElements,以便被ClassLoader載入 (4)啟動目標Activity過程中,VAInstrumentation將目標Activity替換為佔位Activity,並將目標Activity資訊作為引數儲存。從而通過對Activity的校驗,繼而繼續與AMS進行通訊。 (5)AMS處理完成後,傳遞到ApplicationThread中,通過VAInstrumentation攔截到該訊息,將佔位Activity替換為目標Activity,並將目標Activity進行建立,繼而進行後續操作。 (6)設定mResources、mBase(Context)、mApplication物件,最終呼叫到了Activity的onCreate方法
參考資料:
- 淺析Android外掛化
- 360開源的外掛化框架Replugin深度剖析
- 滴滴外掛化方案 VirtualApk 原始碼解析
- 《Android外掛化開發指南》
- 《Android進階解密》