那麼其中的難點很明顯是對四大組件支持,由於你們都清楚,四大組件都是須要在AndroidManifest中註冊的,而插件apk中的組件是不可能預先知曉名字,提早註冊中宿主apk中的,因此如今基本都採用一些hack方案類解決,VirtualAPK大致方案以下:android
Activity:在宿主apk中提早佔幾個坑,而後經過「欺上瞞下」(這個詞好像是360以前的ppt中提到)的方式,啓動插件apk的Activity;由於要支持不一樣的launchMode以及一些特殊的屬性,須要佔多個坑。
Service:經過代理Service的方式去分發;主進程和其餘進程,VirtualAPK使用了兩個代理Service。
BroadcastReceiver:靜態轉動態
ContentProvider:經過一個代理Provider進行分發。
這些佔坑的數量並非固定的,好比Activity想支持某個屬性,該屬性不能動態設置,只能在Manifest中設置,那就須要去佔坑支持。因此佔坑數量這些,能夠根據本身的需求進行調整。緩存
下面就逐一去分析代碼啦~app
注:本篇博客涉及到的framework邏輯,爲API 22. ide
分期版本爲 com.didi.virtualapk:core:0.9.0
2、Activity的支持ui
這裏就不按照某個流程一行行代碼往下讀了,針對性的講一些關鍵流程,可能更好閱讀一些。this
首先看一段啓動插件Activity的代碼:spa
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
pluginManager.loadPlugin(apk);
1
1
加載插件。
這裏就不贅述源碼了,大體爲調用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 = www.yszx11.cn instrumentation;
} catch (Exception e) {
e.printStackTrace();
能夠看到首先經過反射拿到了本來的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(www.ei66yule.cn/) != 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;
首先調用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( www.sb45475.com 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);
在該方法中判斷若是啓動的是插件中類,則將啓動的包名和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(" www.hbwfjx.cn/ 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"www.yuheng119.com , "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;
能夠看到主要就是根據launchMode去選擇不一樣的佔坑類。
例如:
stubActivity = String.format(STUB_ACTIVITY_STANDARD, corePackage, usedStandardStubActivity);
1
1
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" www.caihonyule.com 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爲佔坑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;
核心就是首先從intent中取出咱們的目標Activity,而後經過plugin的ClassLoader去加載(還記得在加載插件時,會生成一個LoadedPlugin對象,其中會對應其初始化一個DexClassLoader)。
這樣就完成了Activity的「偷樑換柱」。
還沒完,接下來在callActivityOnCreate方法中:
@Override
public void callActivityOnCreate(www.lieqibiji.com 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);
設置了修改了mResources、mBase(Context www.yihuanyule.cn/ )、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();
直接替換爲:
# PluginContext
@Override
public Resources getResources(www.tangrenyule11.cn) {
return this.mPlugin.getResources(www.ysylcsvip.cn);
}
@Override
public AssetManager getAssets() {
return this.mPlugin.getAssets();
}
@Override
public ContentResolver getContentResolver() {
return new PluginContentResolver(getHostContext());
看得出來仍是很是巧妙的。能夠作的事情也很是多,後面對ContentProvider的描述也會提現出來。
好了,到此Activity就能夠正常啓動了。
下面看Service。
3、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();
首先拿到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(www.wmyl11.com/ 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 www.wmyl15.com/ Service error", e);
}
} else if ("stopServiceToken".equals(method.getName())) {
try {
return stopServiceToken(proxy, method, args);
} catch (Throwable e) {
Log.e(TAG, "Stop www.wmyl88.com service token error", e);
}
}
// 省略bindService,unbindService等方法
當咱們調用startService時,跟進代碼,能夠發現調用流程爲:
startService->startServiceCommon->ActivityManagerNative.getDefault().startService
1
1
這個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);
先不看代碼,考慮下咱們這裏惟一要作的就是經過Intent保存關鍵數據,替換啓動的Service類爲佔坑類。
因此直接看最後的方法:
private ComponentName startDelegateServiceForTarget(Intent target,
ServiceInfo serviceInfo,
Bundle extras, int command) {
Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
最後一行就是啓動了,那麼替換的操做應該在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);
果不其然,從新初始化了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;
這裏代碼很簡單了,根據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操做便可。
4、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();
能夠看到解析到receiver信息後,直接經過pluginClassloader去loadClass拿到receiver對象,而後調用this.mHostContext.registerReceiver便可。
開心,最後一個了~
5、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();
若是調用的auth爲插件apk中的provider,則直接返回mPluginManager.getIContentProvider()。
public synchronized IContentProvider getIContentProvider() {
if (mIContentProvider == null) {
hookIContentProviderAsNeeded();
}
return mIContentProvider;
咦,又看到一個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();
直接看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;
從參數中找到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;
能夠看到經過傳入的生成了一個新的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;
很簡單,取出本來的uri,拿到auth,在經過加載plugin獲得providerInfo,反射生成provider對象,在調用其attachInfo方法便可。
其餘的幾個方法:insert、update、delete、call邏輯基本相同,就不贅述了。
感受這裏其實經過hook AMS的getContentProvider方法也能完成上述流程,感受好像能夠更完全,不須要依賴PluginContext了。
6、總結
總結下,其實就是文初的內容,能夠看到VritualApk大致方案以下:
Activity:在宿主apk中提早佔幾個坑,而後經過「欺上瞞下」(這個詞好像是360以前的ppt中提到)的方式,啓動插件apk的Activity;由於要支持不一樣的launchMode以及一些特殊的屬性,須要佔多個坑。
Service:經過代理Service的方式去分發;主進程和其餘進程,VirtualAPK使用了兩個代理Service。
BroadcastReceiver:靜態轉動態。
ContentProvider:經過一個代理Provider進行分發。
總體代碼看起來仍是很輕鬆的~
固然若是你要選擇某一個插件化方案進行使用,必定要了解其中的實現原理,文檔上描述的並非全部細節,不少一些屬性什麼的,以及因爲其實現的方式形成一些特性的不支持。瞭解源碼,能夠方便本身排查問題,擴展,甚至寫一套根據本身業務需求的插件化方案~~
再多嘴一句,仍是建議大多多在某一方面深刻了解,不要癡迷於UI特效(上班路上看看個人推文就好啦~玩笑~,不少特效的,瞭解下原理便可)~~其實我早期浪費了不少時間在上面,在你掌握了自定義View的詳細細節、事件分發機制這些機制後,大部分UI的編寫都是時間問題。
不要在上面浪費過多時間,比別人多研究幾個特效並不會對本身的提高有巨大的幫助,過來人,忠言逆耳~。