本文已在我的公眾號hongyangAndroid原創首發,文章合集。
轉載請標明出處.
一、概述
之前一直沒有寫過外掛化相關的部落格,剛好最近滴滴和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);複製程式碼
可以看到優先根據包名判斷該外掛是否已經載入,所以在外掛使用前其實還需要呼叫
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();
}
}複製程式碼
可以看到首先通過反射拿到了原本的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;
}複製程式碼
首先呼叫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);
}
}複製程式碼
在該方法中判斷如果啟動的是外掛中類,則將啟動的包名和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;
}複製程式碼
可以看到主要就是根據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"/>
// 省略很多...複製程式碼
就完全明白了。
到這裡就可以看到,替換我們啟動的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);
}複製程式碼
核心就是首先從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);
}複製程式碼
設定了修改了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();
}複製程式碼
直接替換為:
# 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());
}複製程式碼
看得出來還是非常巧妙的。可以做的事情也非常多,後面對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();
}
}複製程式碼
首先拿到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);
}複製程式碼
先不看程式碼,考慮下我們這裡唯一要做的就是通過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;
}複製程式碼
果不其然,重新初始化了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操作即可。
四、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即可。
開心,最後一個了~
五、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;
}複製程式碼
如果呼叫的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了。
六、總結
總結下,其實就是文初的內容,可以看到VritualApk大體方案如下:
- Activity:在宿主apk中提前佔幾個坑,然後通過“欺上瞞下”(這個詞好像是360之前的ppt中提到)的方式,啟動外掛apk的Activity;因為要支援不同的launchMode以及一些特殊的屬性,需要佔多個坑。
- Service:通過代理Service的方式去分發;主程式和其他程式,VirtualAPK使用了兩個代理Service。
- BroadcastReceiver:靜態轉動態。
- ContentProvider:通過一個代理Provider進行分發。
整體程式碼看起來還是很輕鬆的~
當然如果你要選擇某一個外掛化方案進行使用,一定要了解其中的實現原理,文件上描述的並不是所有細節,很多一些屬性什麼的,以及由於其實現的方式造成一些特性的不支援。瞭解原始碼,可以方便自己排查問題,擴充套件,甚至寫一套根據自己業務需求的外掛化方案~~
再多嘴一句,還是建議大多多在某一方面深入瞭解,不要痴迷於UI特效(上班路上看看我的推文就好啦~玩笑~,很多特效的,瞭解下原理即可)~~其實我早期浪費了很多時間在上面,在你掌握了自定義View的詳細細節、事件分發機制這些機制後,大部分UI的編寫都是時間問題。
不要在上面浪費過多時間,比別人多研究幾個特效並不會對自己的提升有巨大的幫助,過來人,忠言逆耳~。
支援我的話可以關注下我的公眾號,每天都會推送新知識~
歡迎關注我的微信公眾號:hongyangAndroid
(可以給我留言你想學習的文章,支援投稿)