滴滴插件化方案 VirtualApk 源碼解析
一、概述
之前一直沒有寫過插件化相關的博客,剛好最近滴滴和360分別開源了自家的插件化方案,趕緊學習下,寫兩篇博客,第一篇是滴滴的方案:
那么其中的難點很明顯是對四大組件支持,因為大家都清楚,四大組件都是需要在AndroidManifest中注冊的,而插件apk中的組件是不可能預先知曉名字,提前注冊中宿主apk中的,所以現在基本都采用一些hack方案類解決,VirtualAPK大體方案如下:
- Activity:在宿主apk中提前占幾個坑,然后通過“欺上瞞下”(這個詞好像是360之前的ppt中提到)的方式,啟動插件apk的Activity;因為要支持不同的launchMode以及一些特殊的屬性,需要占多個坑。
- Service:通過代理Service的方式去分發;主進程和其他進程,VirtualAPK使用了兩個代理Service。
- BroadcastReceiver:靜態轉動態
- ContentProvider:通過一個代理Provider進行分發。
這些占坑的數量并不是固定的,比如Activity想支持某個屬性,該屬性不能動態設置,只能在Manifest中設置,那就需要去占坑支持。所以占坑數量這些,可以根據自己的需求進行調整。
下面就逐一去分析代碼啦~
注:本篇博客涉及到的framework邏輯,為API 22.
分期版本為 com.didi.virtualapk:core:0.9.0
二、Activity的支持
這里就不按照某個流程一行行代碼往下讀了,針對性的講一些關鍵流程,可能更好閱讀一些。
首先看一段啟動插件Activity的代碼:
final String pkg = "com.didi.virtualapk.demo";
if (PluginManager.getInstance(this).getLoadedPlugin(pkg) == null) {
Toast.makeText(this, "plugin [com.didi.virtualapk.demo] not loaded", Toast.LENGTH_SHORT).show();
return;
}
// test Activity and Service
Intent intent = new Intent();
intent.setClassName(pkg, "com.didi.virtualapk.demo.aidl.BookManagerActivity");
startActivity(intent);</code></pre>
可以看到優先根據包名判斷該插件是否已經加載,所以在插件使用前其實還需要調用
pluginManager.loadPlugin(apk);
加載插件。
這里就不贅述源碼了,大致為調用 PackageParser.parsePackage 解析apk,獲得該apk對應的PackageInfo,資源相關(AssetManager,Resources),DexClassLoader(加載類),四大組件相關集合(mActivityInfos,mServiceInfos,mReceiverInfos,mProviderInfos),針對Plugin的PluginContext等一堆信息,封裝為LoadedPlugin對象。
詳細可以參考 com.didi.virtualapk.internal.LoadedPlugin 類。
ok,如果該插件以及加載過,則直接通過startActivity去啟動插件中目標Activity。
(1)替換Activity
這里大家肯定會有疑惑,該Activity必然沒有在Manifest中注冊,這么啟動不會報錯嗎?
正常肯定會報錯呀,所以我們看看它是怎么做的吧。
跟進startActivity的調用流程,會發現其最終會進入Instrumentation的execStartActivity方法,然后再通過ActivityManagerProxy與AMS進行交互。
而Activity是否存在的校驗是發生在AMS端,所以我們在于AMS交互前,提前將Activity的ComponentName進行替換為占坑的名字不就好了么?
這里可以選擇hook Instrumentation,或者ActivityManagerProxy都可以達到目標,VirtualAPK選擇了hook Instrumentation.
打開 PluginManager 可以看到如下方法:
private void hookInstrumentationAndHandler() {
try {
Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext);
if (baseInstrumentation.getClass().getName().contains("lbe")) {
// reject executing in paralell space, for example, lbe.
System.exit(0);
}
final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation);
Object activityThread = ReflectUtil.getActivityThread(this.mContext);
ReflectUtil.setInstrumentation(activityThread, instrumentation);
ReflectUtil.setHandlerCallback(this.mContext, instrumentation);
this.mInstrumentation = instrumentation;
} catch (Exception e) {
e.printStackTrace();
}
}</code></pre>
可以看到首先通過反射拿到了原本的 Instrumentation 對象,拿的過程是首先拿到ActivityThread,由于ActivityThread可以通過靜態變量 sCurrentActivityThread 或者靜態方法 currentActivityThread() 獲取,所以拿到其對象相當輕松。拿到ActivityThread對象后,調用其 getInstrumentation() 方法,即可獲取當前的Instrumentation對象。
然后自己創建了一個VAInstrumentation對象,接下來就直接反射將VAInstrumentation對象設置給ActivityThread對象即可。
這樣就完成了hook Instrumentation,之后調用Instrumentation的任何方法,都可以在VAInstrumentation進行攔截并做一些修改。
這里還hook了ActivityThread的mH類的Callback,暫不贅述。
剛才說了,可以通過Instrumentation的execStartActivity方法進行偷梁換柱,所以我們直接看對應的方法:
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
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);
}
ActivityResult result = realExecStartActivity(who, contextThread, token, target,
intent, requestCode, options);
return result;
}</code></pre>
首先調用transformIntentToExplicitAsNeeded,這個主要是當component為null時,根據啟動Activity時,配置的action,data,category等去已加載的plugin中匹配到確定的Activity的。
本例我們的寫法ComponentName肯定不為null,所以直接看 markIntentIfNeeded() 方法:
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);
}
}</code></pre>
在該方法中判斷如果啟動的是插件中類,則將啟動的包名和Activity類名存到了intent中,可以看到這里存儲明顯是為了后面恢復用的。
然后調用了 dispatchStubActivity(intent)
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);
String stubActivity = mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj);
Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
intent.setClassName(mContext, stubActivity);
}
可以直接看最后一行,intent通過setClassName替換啟動的目標Activity了!這個stubActivity是由 mStubActivityInfo.getStubActivity(targetClassName, launchMode, themeObj) 返回。
很明顯,傳入的參數launchMode、themeObj都是決定選擇哪一個占坑類用的。
public String getStubActivity(String className, int launchMode, Theme theme) {
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);
}
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
switch (launchMode) {
case ActivityInfo.LAUNCH_MULTIPLE: {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
if (windowIsTranslucent) {
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, 2);
}
break;
}
case ActivityInfo.LAUNCH_SINGLE_TOP: {
usedSingleTopStubActivity = usedSingleTopStubActivity % MAX_COUNT_SINGLETOP + 1;
stubActivity = String.format(STUB_ACTIVITY_SINGLETOP, corePackage, usedSingleTopStubActivity);
break;
}
// 省略LAUNCH_SINGLE_TASK,LAUNCH_SINGLE_INSTANCE
}
mCachedStubActivity.put(className, stubActivity);
return stubActivity;
}</code></pre>
可以看到主要就是根據launchMode去選擇不同的占坑類。
例如:
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
STUB_ACTIVITY_STANDARD值為:"%s.A$%d" , corePackage值為 com.didi.virtualapk.core ,usedStandardStubActivity為數字值。
所以最終類名格式為: com.didi.virtualapk.core.A$1
再看一眼,CoreLibrary下的AndroidManifest中:
<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"/>
// 省略很多...</code></pre>
就完全明白了。
到這里就可以看到,替換我們啟動的Activity為占坑Activity,將我們原本啟動的包名,類名存儲到了Intent中。
這樣做只完成了一半,為什么這么說呢?
(2) 還原Activity
因為欺騙過了AMS,AMS執行完成后,最終要啟動的不可能是占坑Activity,還應該是我們的啟動的目標Activity呀。
這里需要知道Activity的啟動流程:
AMS在處理完啟動Activity后,會調用: app.thread.scheduleLaunchActivity ,這里的thread對應的server端未我們ActivityThread中的ApplicationThread對象(binder可以理解有一個client端和一個server端),所以會調用 ApplicationThread.scheduleLaunchActivity 方法,在其內部會調用mH類的sendMessage方法,傳遞的標識為 H.LAUNCH_ACTIVITY ,進入調用到ActivityThread的handleLaunchActivity方法->ActivityThread#handleLaunchActivity->mInstrumentation.newActivity()。
ps:這里流程不清楚沒關系,暫時理解為最終會回調到Instrumentation的newActivity方法即可,細節可以自己去查看結合老羅的blog理解。
關鍵的來了,最終又到了Instrumentation的newActivity方法,還記得這個類我們已經改為VAInstrumentation啦:
直接看其newActivity方法:
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
try {
cl.loadClass(className);
} catch (ClassNotFoundException e) {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
String targetClassName = PluginUtil.getTargetActivity(intent);
if (targetClassName != null) {
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
// 省略兼容性處理代碼
return activity;
}
}
return mBase.newActivity(cl, className, intent);
}</code></pre>
核心就是首先從intent中取出我們的目標Activity,然后通過plugin的ClassLoader去加載(還記得在加載插件時,會生成一個LoadedPlugin對象,其中會對應其初始化一個DexClassLoader)。
這樣就完成了Activity的“偷梁換柱”。
還沒完,接下來在 callActivityOnCreate 方法中:
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
final Intent intent = activity.getIntent();
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent);
ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources());
ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext());
ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication());
ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext());
// set screenOrientation
ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
} catch (Exception e) {
e.printStackTrace();
}
}
mBase.callActivityOnCreate(activity, icicle);
}</code></pre>
設置了修改了mResources、mBase(Context)、mApplication對象。以及設置一些可動態設置的屬性,這里僅設置了屏幕方向。
這里提一下,將mBase替換為PluginContext,可以修改Resources、AssetManager以及攔截相當多的操作。
看一眼代碼就清楚了:
原本Activity的部分get操作
# ContextWrapper
@Override
public AssetManager getAssets() {
return mBase.getAssets();
}
@Override
public Resources getResources()
{
return mBase.getResources();
}
@Override
public PackageManager getPackageManager() {
return mBase.getPackageManager();
}
@Override
public ContentResolver getContentResolver() {
return mBase.getContentResolver();
}</code></pre>
直接替換為:
# PluginContext
@Override
public Resources getResources() {
return this.mPlugin.getResources();
}
@Override
public AssetManager getAssets() {
return this.mPlugin.getAssets();
}
@Override
public ContentResolver getContentResolver() {
return new PluginContentResolver(getHostContext());
}</code></pre>
看得出來還是非常巧妙的。可以做的事情也非常多,后面對ContentProvider的描述也會提現出來。
好了,到此Activity就可以正常啟動了。
下面看Service。
三、Service的支持
Service和Activity有點不同,顯而易見的首先我們也會將要啟動的Service類替換為占坑的Service類,但是有一點不同,在Standard模式下多次啟動同一個占坑Activity會創建多個對象來對象我們的目標類。而Service多次啟動只會調用onStartCommond方法,甚至常規多次調用bindService,seviceConn對象不變,甚至都不會多次回調bindService方法(多次調用可以通過給Intent設置不同Action解決)。
還有一點,最明顯的差異是,Activity的生命周期是由用戶交互決定的,而Service的聲明周期是我們主動通過代碼調用的。
也就是說,start、stop、bind、unbind都是我們顯示調用的,所以我們可以攔截這幾個方法,做一些事情。
Virtual Apk的做法,即將所有的操作進行攔截,都改為startService,然后統一在onStartCommond中分發。
下面看詳細代碼:
(1) hook IActivityManager
再次來到PluginManager,發下如下方法:
private void hookSystemServices() {
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();
}
}</code></pre>
首先拿到ActivityManagerNative中的gDefault對象,該對象返回的是一個 Singleton<IActivityManager> ,然后拿到其mInstance對象,即IActivityManager對象(可以理解為和AMS交互的binder的client對象)對象。
然后通過動態代理的方式,替換為了一個代理對象。
那么重點看對應的InvocationHandler對象即可,該代理對象調用的方法都會輾轉到其invoke方法:
@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);
}
} else if ("stopService".equals(method.getName())) {
try {
return stopService(proxy, method, args);
} catch (Throwable e) {
Log.e(TAG, "Stop Service error", e);
}
} else if ("stopServiceToken".equals(method.getName())) {
try {
return stopServiceToken(proxy, method, args);
} catch (Throwable e) {
Log.e(TAG, "Stop service token error", e);
}
}
// 省略bindService,unbindService等方法
}
當我們調用startService時,跟進代碼,可以發現調用流程為:
startService->startServiceCommon->ActivityManagerNative.getDefault().startService
這個getDefault剛被我們hook,所以會被上述方法攔截,然后調用: startService(proxy, method, args)
private 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) {
// is host service
return method.invoke(this.mActivityManager, args);
}
return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
}</code></pre>
先不看代碼,考慮下我們這里唯一要做的就是通過Intent保存關鍵數據,替換啟動的Service類為占坑類。
所以直接看最后的方法:
private ComponentName startDelegateServiceForTarget(Intent target,
ServiceInfo serviceInfo,
Bundle extras, int command) {
Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
return mPluginManager.getHostContext().startService(wrapperIntent);
}
最后一行就是啟動了,那么替換的操作應該在wrapperTargetIntent中完成:
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();
// start delegate service to run plugin service inside
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);
intent.putExtra(RemoteService.EXTRA_COMMAND, command);
intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
if (extras != null) {
intent.putExtras(extras);
}
return intent;
}</code></pre>
果不其然,重新初始化了Intent,設置了目標類為LocalService(多進程時設置為RemoteService),然后將原本的Intent存儲到 EXTRA_TARGET ,攜帶command為 EXTRA_COMMAND_START_SERVICE ,以及插件apk路徑。
(2)代理分發
那么接下來代碼就到了LocalService的onStartCommond中啦:
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// 省略一些代碼...
Intent target = intent.getParcelableExtra(EXTRA_TARGET);
int command = intent.getIntExtra(EXTRA_COMMAND, 0);
if (null == target || command <= 0) {
return START_STICKY;
}
ComponentName component = target.getComponent();
LoadedPlugin plugin = mPluginManager.getLoadedPlugin(component);
switch (command) {
case EXTRA_COMMAND_START_SERVICE: {
ActivityThread mainThread = (ActivityThread)ReflectUtil.getActivityThread(getBaseContext());
IApplicationThread appThread = mainThread.getApplicationThread();
Service service;
if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
service = this.mPluginManager.getComponentsHandler().getService(component);
} else {
try {
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();
attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
service.onCreate();
this.mPluginManager.getComponentsHandler().rememberService(component, service);
} catch (Throwable t) {
return START_STICKY;
}
}
service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
break;
}
// 省略下面的代碼
case EXTRA_COMMAND_BIND_SERVICE:break;
case EXTRA_COMMAND_STOP_SERVICE:break;
case EXTRA_COMMAND_UNBIND_SERVICE:break;
}</code></pre>
這里代碼很簡單了,根據command類型,比如 EXTRA_COMMAND_START_SERVICE ,直接通過plugin的ClassLoader去load目標Service的class,然后反射創建實例。比較重要的是,Service創建好后,需要調用它的attach方法,這里湊夠參數,然后反射調用即可,最后調用onCreate、onStartCommand收工。然后將其保存起來,stop的時候取出來調用其onDestroy即可。
bind、unbind以及stop的代碼與上述基本一致,不在贅述。
唯一提醒的就是,剛才看到還hook了一個方法叫做: stopServiceToken ,該方法是什么時候用的呢?
主要有一些特殊的Service,比如IntentService,其stopSelf是由自身調用的,最終會調用 mActivityManager.stopServiceToken 方法,同樣的中轉為STOP操作即可。
四、BroadcastReceiver的支持
這個比較簡單,直接解析Manifest后,靜態轉動態即可。
相關代碼在LoadedPlugin的構造方法中:
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();
}
}</code></pre>
可以看到解析到receiver信息后,直接通過pluginClassloader去loadClass拿到receiver對象,然后調用this.mHostContext.registerReceiver即可。
開心,最后一個了~
五、ContentProvider的支持
(1)hook IContentProvider
ContentProvider的支持依然是通過代理分發。
看一段CP使用的代碼:
Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);
這里用到了PluginContext,在生成Activity、Service的時候,為其設置的Context都為PluginContext對象。
所以當你調用getContentResolver時,調用的為PluginContext的getContentResolver。
@Override
public ContentResolver getContentResolver() {
return new PluginContentResolver(getHostContext());
}
返回的是一個PluginContentResolver對象,當我們調用query方法時,會輾轉調用到
ContentResolver.acquireUnstableProvider 方法。該方法被PluginContentResolver中復寫:
protected IContentProvider acquireUnstableProvider(Context context, String auth) {
try {
if (mPluginManager.resolveContentProvider(auth, 0) != null) {
return mPluginManager.getIContentProvider();
}
return (IContentProvider) sAcquireUnstableProvider.invoke(mBase, context, auth);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}</code></pre>
如果調用的auth為插件apk中的provider,則直接返回 mPluginManager.getIContentProvider() 。
public synchronized IContentProvider getIContentProvider() {
if (mIContentProvider == null) {
hookIContentProviderAsNeeded();
}
return mIContentProvider;
}</code></pre>
咦,又看到一個hook方法:
private void hookIContentProviderAsNeeded() {
Uri uri = Uri.parse(PluginContentResolver.getUri(mContext));
mContext.getContentResolver().call(uri, "wakeup", null, null);
try {
Field authority = null;
Field mProvider = null;
ActivityThread activityThread = (ActivityThread) ReflectUtil.getActivityThread(mContext);
Map mProviderMap = (Map) ReflectUtil.getField(activityThread.getClass(), activityThread, "mProviderMap");
Iterator iter = mProviderMap.entrySet().iterator();
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);
IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
mIContentProvider = proxy;
Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
前兩行比較重要,第一行是拿到了占坑的provider的uri,然后主動調用了其call方法。
如果你跟進去,會發現,其會調用acquireProvider->mMainThread.acquireProvider->ActivityManagerNative.getDefault().getContentProvider->installProvider。簡單來說,其首先調用已經注冊provider,得到返回的IContentProvider對象。
這個IContentProvider對象是在ActivityThread.installProvider方法中加入到mProviderMap中。
而ActivityThread對象又容易獲取,mProviderMap又是它成員變量,那么也容易獲取,所以上面的一大坨(除了前兩行)代碼,就為了拿到占坑的provider對應的IContentProvider對象。
然后通過動態代理的方式,進行了hook,關注InvocationHandler的實例IContentProviderProxy。
IContentProvider能干嗎呢?其實就能攔截我們正常的query、insert、update、delete等操作。
攔截這些方法干嘛?
當然是修改uri啦,把用戶調用的uri,替換為占坑provider的uri,再把原本的uri作為參數拼接在占坑provider的uri后面即可。
好了,直接看invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.v(TAG, method.toGenericString() + " : " + Arrays.toString(args));
wrapperUri(method, args);
try {
return method.invoke(mBase, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}</code></pre>
直接看wrapperUri
private void wrapperUri(Method method, Object[] args) {
Uri uri = null;
int index = 0;
if (args != null) {
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Uri) {
uri = (Uri) args[i];
index = i;
break;
}
}
}
// 省略部分代碼
PluginManager pluginManager = PluginManager.getInstance(mContext);
ProviderInfo info = pluginManager.resolveContentProvider(uri.getAuthority(), 0);
if (info != null) {
String pkg = info.packageName;
LoadedPlugin plugin = pluginManager.getLoadedPlugin(pkg);
String pluginUri = Uri.encode(uri.toString());
StringBuilder builder = new StringBuilder(PluginContentResolver.getUri(mContext));
builder.append("/?plugin=" + plugin.getLocation());
builder.append("&pkg=" + pkg);
builder.append("&uri=" + pluginUri);
Uri wrapperUri = Uri.parse(builder.toString());
if (method.getName().equals("call")) {
bundleInCallMethod.putString(KEY_WRAPPER_URI, wrapperUri.toString());
} else {
args[index] = wrapperUri;
}
}
}</code></pre>
從參數中找到uri,往下看,搞了個StringBuilder首先加入占坑provider的uri,然后將目標uri,pkg,plugin等參數等拼接上去,替換到args中的uri,然后繼續走原本的流程。
假設是query方法,應該就到達我們占坑provider的query方法啦。
(2)代理分發
占坑如下:
<provider
android:name="com.didi.virtualapk.delegate.RemoteContentProvider"
android:authorities="${applicationId}.VirtualAPK.Provider"
android:process=":daemon" />
打開RemoteContentProvider,直接看query方法:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
ContentProvider provider = getContentProvider(uri);
Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
if (provider != null) {
return provider.query(pluginUri, projection, selection, selectionArgs, sortOrder);
}
return null;
}</code></pre>
可以看到通過傳入的生成了一個新的provider,然后拿到目標uri,在直接調用provider.query傳入目標uri即可。
那么這個provider實際上是這個代理類幫我們生成的:
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();
// 省略了緩存管理
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();
}
}
final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0);
if (providerInfo != null) {
RunUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
try {
LoadedPlugin loadedPlugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
ContentProvider contentProvider = (ContentProvider) Class.forName(providerInfo.name).newInstance();
contentProvider.attachInfo(loadedPlugin.getPluginContext(), providerInfo);
sCachedProviders.put(auth, contentProvider);
} catch (Exception e) {
e.printStackTrace();
}
}
}, true);
return sCachedProviders.get(auth);
}
return null;
}</code></pre>
很簡單,取出原本的uri,拿到auth,在通過加載plugin得到providerInfo,反射生成provider對象,在調用其attachInfo方法即可。
其他的幾個方法:insert、update、delete、call邏輯基本相同,就不贅述了。
感覺這里其實通過hook AMS的getContentProvider方法也能完成上述流程,感覺好像可以更徹底,不需要依賴PluginContext了。
六、總結
總結下,其實就是文初的內容,可以看到VritualApk大體方案如下:
- Activity:在宿主apk中提前占幾個坑,然后通過“欺上瞞下”(這個詞好像是360之前的ppt中提到)的方式,啟動插件apk的Activity;因為要支持不同的launchMode以及一些特殊的屬性,需要占多個坑。
- Service:通過代理Service的方式去分發;主進程和其他進程,VirtualAPK使用了兩個代理Service。
- BroadcastReceiver:靜態轉動態。
- ContentProvider:通過一個代理Provider進行分發。
整體代碼看起來還是很輕松的~
當然如果你要選擇某一個插件化方案進行使用,一定要了解其中的實現原理,文檔上描述的并不是所有細節,很多一些屬性什么的,以及由于其實現的方式造成一些特性的不支持。了解源碼,可以方便自己排查問題,擴展,甚至寫一套根據自己業務需求的插件化方案~~
再多嘴一句,還是建議大多多在某一方面深入了解,不要癡迷于UI特效(上班路上看看我的推文就好啦~玩笑~,很多特效的,了解下原理即可)~~其實我早期浪費了很多時間在上面,在你掌握了自定義View的詳細細節、事件分發機制這些機制后,大部分UI的編寫都是時間問題。
不要在上面浪費過多時間,比別人多研究幾個特效并不會對自己的提升有巨大的幫助,過來人,忠言逆耳~。
支持我的話可以關注下我的公眾號,每天都會推送新知識~
歡迎關注我的微信公眾號:hongyangAndroid
(可以給我留言你想學習的文章,支持投稿)

來自:http://blog.csdn.net/lmj623565791/article/details/75000580