推薦閱讀:
LeakCanary 與 鵝場Matrix ResourceCanary對比分析
Android插件化已經出來好幾年了,各大廠都出了各自方案,引用Wiki中VirtualAPK和其他開源框架的對比如下:
VirtualAPK
VirtualAPK是滴滴出行自研的一款優秀的插件化框架,主要有如下幾個特性。
功能完備
- 支持幾乎所有的Android特性;
- 四大組件方面
四大組件均不需要在宿主manifest中預注冊,每個組件都有完整的生命周期。
- Activity:支持顯示和隱式調用,支持Activity的
theme
和LaunchMode
,支持透明主題; - Service:支持顯示和隱式調用,支持Service的
start
、stop
、bind
和unbind
,並支持跨進程bind插件中的Service; - Receiver:支持靜態注冊和動態注冊的Receiver;
- ContentProvider:支持provider的所有操作,包括
CRUD
和call
方法等,支持跨進程訪問插件中的Provider。
- 自定義View:支持
自定義View
,支持自定義屬性和style
,支持動畫; - PendingIntent:支持
PendingIntent
以及和其相關的Alarm
、Notification
和AppWidget
; - 支持插件
Application
以及插件manifest中的meta-data
; - 支持插件中的
so
。
VirtualAPK對插件沒有額外的約束,原生的apk即可作為插件。插件工程編譯生成apk后,即可通過宿主App加載,每個插件apk被加載后,都會在宿主中創建一個單獨的LoadedPlugin對象。如下圖所示,通過這些LoadedPlugin對象,VirtualAPK就可以管理插件並賦予插件新的意義,使其可以像手機中安裝過的App一樣運行。
一、Application支持
通常情況,我們可能在Applicaton中做的事情,如下:
1.onCreate中做三方庫的初始化
2.registerActivityLifeCycleCallbacks監控
3.ComponentCallbacks2支持,做些資源清理動作
4.attachBaseContext() 中 multiDex install(插件不需要考慮)
從前3方面,看看VitualAPK支持哪些?
在PluginManager加載Plugin,構建LoadedPlugin事,就構建了插件Application,代碼如下:
protected Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) throws Exception { if (null != this.mApplication) { return this.mApplication; } String appClass = this.mPackage.applicationInfo.className; if (forceDefaultAppClass || null == appClass) { appClass = "android.app.Application"; } this.mApplication = instrumentation.newApplication(this.mClassLoader, appClass, this.getPluginContext()); // inject activityLifecycleCallbacks of the host application
//宿主Application 監控插件Activity的生命周期 mApplication.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacksProxy());
//插件Application.onCreate()回調 instrumentation.callApplicationOnCreate(this.mApplication); return this.mApplication; }
從源碼可以看到,在構建插件Appliation后就回調了其onCreate方法,ActivityLifecycleCallbacksProxy反射獲取了宿主Application的mActivityLifecycleCallbacks,在插件Activity生命周期監控回調也派發到宿主Activity生命周期監控中。
先看看ActivityLifecycleCallbacks機制,Activity生命周期關鍵代碼如下:
public class Activity { protected void onCreate(@Nullable Bundle savedInstanceState) { ...
//派發ActivityLifeCallbacks.onActivityCreated()方法 getApplication().dispatchActivityCreated(this, savedInstanceState); ... } protected void onStart() { ...
//派發ActivityLifeCallbacks.onActivityStarted()方法 getApplication().dispatchActivityStarted(this); ... } ... }
顯而易見,Activity的生命周期監控都是其Activity生命周期函數中派發。從后文Activity支持分析中,顯然插件Application可以監控到插件的Activity生命周期。
Application,Activity,Service,ContentProvider都實現了ComponentCallbacks2
//ComponentCallbacks2的接口函數 void onTrimMemory() void onConfigurationChanged(Configuration newConfig); void onLowMemory();
而這些函數都由ActivtyThread.mH發送異步消息,調用相應對應函數,而ActvityThread維護Application,Service,Activity,ContentProvider的相關記錄,能夠回調ComponentCallbacks2接口相應方法,但是插件Application並沒有在ActivityThread記錄過,而在LoadedPlugin中,並沒有對ComponentCallbacks2進行相應的處理,所以VitualAPK並不支持插件Application的ComponentCallbacks2,而插件Activity,Service,ContentProvider是否支持ComponentCallbacks2,見后文。
二、Activity 支持
需要考慮如下問題:
1.怎樣啟動Activity
2.怎樣加載activity
3.加載資源
4.怎樣保證生命周期
5.是否支持多進程
6.ComponentCallbacks2支持,做些資源清理動作
1.啟動Activity
正常情況下,Activity啟動調用了 Instrumentation.execStartActivity方法,完成AMS遠程Activity,再由mh發送異步activity啟動消息,從binder線程池環境切換到ActivityThread主線程環境,開始正真的activity啟動。插件里activity沒有宿主AndroidManifest.xml注冊,常規方法是沒法啟動插件activity的。
對於插件 Activity 啟動,VitualApk采用的是宿主 manifest 中占坑的方式來繞過系統校驗,然后再加載真正的activity。
什么是占坑?就是構造一系列假的 Activity 替身,在 AndroidMainfest.xml 里面進行注冊,以繞過檢測,然后到了真正啟動 Activity 的時候,再把它變回,去啟動真正的目標 Activity。那么這一步是怎么做的呢?
坑位Activity注冊如下:
<application> <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" /> <!-- Stub Activities --> <activity android:exported="false" android:name=".B$1" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$2" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$3" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$4" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$5" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$6" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$7" android:launchMode="singleTop"/> <activity android:exported="false" android:name=".B$8" android:launchMode="singleTop"/> <!-- Stub Activities --> <activity android:exported="false" android:name=".C$1" android:launchMode="singleTask"/> <activity android:exported="false" android:name=".C$2" android:launchMode="singleTask"/> <activity android:exported="false" android:name=".C$3" android:launchMode="singleTask"/> <activity android:exported="false" android:name=".C$4" android:launchMode="singleTask"/> <activity android:exported="false" android:name=".C$5" android:launchMode="singleTask"/> <activity android:exported="false" android:name=".C$6" android:launchMode="singleTask"/> <activity android:exported="false" android:name=".C$7" android:launchMode="singleTask"/> <activity android:exported="false" android:name=".C$8" android:launchMode="singleTask"/> <!-- Stub Activities --> <activity android:exported="false" android:name=".D$1" android:launchMode="singleInstance"/> <activity android:exported="false" android:name=".D$2" android:launchMode="singleInstance"/> <activity android:exported="false" android:name=".D$3" android:launchMode="singleInstance"/> <activity android:exported="false" android:name=".D$4" android:launchMode="singleInstance"/> <activity android:exported="false" android:name=".D$5" android:launchMode="singleInstance"/> <activity android:exported="false" android:name=".D$6" android:launchMode="singleInstance"/> <activity android:exported="false" android:name=".D$7" android:launchMode="singleInstance"/> <activity android:exported="false" android:name=".D$8" android:launchMode="singleInstance"/> </application>
可以發現,在清單里面注冊了一堆假的 StubActivity
。 ABCD分別對應不同的啟動模式,那么,我們啟動插件的 Activity 的時候,是如何把它改為清單里面已注冊的這些假的 Activity
名呢?
構建PluginManager時,Hook 了一個 VAInstrumentation 以替代系統的 Instrumentation,並ActivityThread.mH設置了callback,攔截處理Activity啟動請求。
protected void hookInstrumentationAndHandler() {
try {
ActivityThread activityThread = ActivityThread.currentActivityThread();
Instrumentation baseInstrumentation = activityThread.getInstrumentation();
//Hook instrumentation,重設ActivityThread的mInstrmentation
final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
//Hook Handler mh,設置其mCallback,攔截Activity啟動消息
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);
}
}
VAInstrumentation 相關的代碼如下:
public ActivityResult execStartActivity(...) { injectIntent(intent); return mBase.execStartActivity(...); } protected void injectIntent(Intent intent) {
//確定Intent Component targetActivity的packageName,從已經加載的插件中檢索 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 用那些注冊的假的StubActivity來替換真實的Activity,以繞過檢測 this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent); } }
繞過了系統的檢測,通過mH發送LAUNCH_ACTIVITY的異步消息,由於Hook的時候設置了callback,攔截了LAUNCH_ACTIVITY,給Intent設置了插件的Theme,classloader,然后按照mH原有的邏輯走。
提個問題,通過adb shell dumpsys activity activities可以看到插件Activity? 顯然只能看到坑位Activity,因為在AMS登記的是坑位Activity,驗證如下:
2.加載activity
當ActivityThread使用Instrumentation.newActivity,構造activity,自然是調用VAInstrumentation的newActivity方法,代碼如下:
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { try {
//先使用host 的 classloader加載類 cl.loadClass(className); Log.i(TAG, String.format("newActivity[%s]", className)); } catch (ClassNotFoundException e) {
//根據intent 從加載的插件中檢索到 插件Activity ComponentName component = PluginUtil.getComponent(intent); if (component == null) { return newActivity(mBase.newActivity(cl, className, intent)); }
String targetClassName = component.getClassName(); Log.i(TAG, String.format("newActivity[%s : %s/%s]", className, component.getPackageName(), targetClassName)); 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)); }
//插件的Activity,使用插件自己的classloader,用插件Activity類,替換坑位Activity類名 Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent); // for 4.1+ 把插件Activity的Resources設置為Resources Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources()); return newActivity(activity); } return newActivity(mBase.newActivity(cl, className, intent)); }
優先使用宿主host的classloader加載Activity,找不到,若是插件Activity,替換坑位Activity,使用插件的classloader加載。
那么插件的classloader是怎樣構建的,代碼如下:
protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception { File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR); String dexOutputPath = dexOutputDir.getAbsolutePath();
//插件classloder的 parent 為宿主host的classloader
//插件plugin可以加載宿主host的class,宿主host不能插件plugin的class DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);
//開啟加載器組合,插件的dex,so文件都設置host classloader的dexPathList(見BaseDexClassLoader)
//這樣host與plugin,plugin與plugin之間可以互相加載對方的class了 if (Constants.COMBINE_CLASSLOADER) { DexUtil.insertDex(loader, parent, libsDir); } return loader; }
VirtualApk始終開啟COMBINE_CLASSLOADER,也就是說host與plugin,plugin與plugin之間可以互相加載對方的class了。
3.資源加載
在VAInstrumentation.newActivity中,插件Activty的mResources 被設置為對應插件的Resources
Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
那么看看插件的Resources是怎樣構建的,代碼如下:
if (Constants.COMBINE_RESOURCES) {
//資源組合開啟,則把插件的apk路徑添加宿主Resources的相關的參數,並把插件resources替換為宿主的resrouces,
//這個過程很復雜,需要同步所有應用Resources地方,需要兼容 系統版本api和rom return ResourcesManager.createResources(context, packageName, apk); } else {
//插件只能訪問自身的Resource資源 Resources hostResources = context.getResources(); AssetManager assetManager = createAssetManager(context, apk); return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); }
VirtualApk始終開啟COMBINE_RESOURCES,也就是說host與plugin,plugin與plugin之間可以互相加載對方的Reources資源了。
4.生命周期回調
ActivtyThread維護這啟動的activity集合ArrayMap<IBinder, ActivityClientRecord> mActivities,自然而然生命周期也就同步了,需要注意的是:在調度插件的onCreate生命周期函數需要需要設置插件Activity的mBase,mResource,mApplication等,因為在Activity實例化后,進行attch動作,需要重置為插件對應的配置。
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); } }
}
5.是否多進程
插件Activity在其他進程中啟動
<activity android:name=".OtherProcessActivity" android:process=":other"、>
打開該插件Activity,會創建該界面,adb查看相關進程,結果如下:
沒發現:other相關的進程,想想也是,坑位Activity都沒有設置android:Process,只能運行在主進程中。
6.支持ComponentCallbacks2,做些資源清理動作
插件Activity在ActivityThread有登記,收集的ComponentCallbacks2接口,包括了插件Activity,所以能派發到插件Activity。
三、Service支持
需要考慮如下問題:
1.Service啟動
2.是否支持多進程
3.Service生命周期
4.是否支持ComponentCallbacks2,做些資源清理動作
1.Service啟動
動態代理AMS,攔截service相關的請求,將其中轉給Service Runtime
去處理,Service Runtime
會接管系統的所有操作
public class ActivityManagerProxy implements InvocationHandler { 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); } } else if ("bindService".equals(method.getName())) { try { return bindService(proxy, method, args); } catch (Throwable e) { Log.w(TAG, e); } } else if ("unbindService".equals(method.getName())) { try { return unbindService(proxy, method, args); } catch (Throwable e) { Log.w(TAG, e); } } else if ("getIntentSender".equals(method.getName())) { try { getIntentSender(method, args); } catch (Exception e) { Log.w(TAG, e); } } else if ("overridePendingTransition".equals(method.getName())){ try { overridePendingTransition(method, args); } catch (Exception e){ Log.w(TAG, e); } } try { // sometimes system binder has problems. return method.invoke(this.mActivityManager, args); } catch (Throwable th) { ... }
protected ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
//把插件Service相關操作的Intent 轉為為 坑位Service相關命令派發的Intent
Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
return mPluginManager.getHostContext().startService(wrapperIntent);
}
}
Service runtime也注冊兩個坑位Service,由坑位Service分發插件Service的相關操作,一個LocalService,用來派發與宿主同進程的插件service操作,一個是RemoteService,用來派發與宿主不同進程的插件service操作,插件service並沒有在AMS、ActivityThread登記過,由Service runtime自身維護。
2.是否支持多進程
RemoteService用來派發與宿主不同進程的插件service操作,只不過插件Service運行在RemoteService所在的:daemon,沒有運行在插件Service指定的進程上。
3.Service生命周期
坑位Service在派發插件Service操作的時候,會回調相應的生命周期的周期函數。
需要注意以下兩點:
1)通常情況下bindService啟動Service,該Service的生命周期跟綁定着生命周期一致,但是插件Service有VitualAPK Service runtime維護,runtime沒有做插件Serice跟綁定者生命周期一致處理,需要手動調用unbindService,關閉該Service。
2)通常情況下startService和bindService或者使用,關閉Service需要調用stop和unbind方法,而插件Service混合啟動,只需要調用stop或者unbind方法就可以關閉Service|。
4.不支持ComponentCallbacks2,做些資源清理動作
插件Service沒有在ActivityThread登記,收集的ComponentCallbacks2沒有包括插件Service,所以插件不支持ComponentCallbacks2。
四、BroadcastReceiver支持
BroadcastRecevier注冊分為動態注冊和靜態注冊。
插件BroadcastRecevier的動態注冊,只需要能夠加載到插件BroadcastReceiver類即可,在前文分析中插件與宿主、插件與插件的classloader都可以加載對方的類,顯然VitaulAPK是支持插件的動態注冊。
插件BroadcastRecevier的靜態注冊,即在插件的AndroidManifest.xml中注冊,VituaAPK在加載插件時,通過PackageParserCompat的parsePackage方法解析了AndroidManifest.xml中Application和四大組件信息,
然后反射實例化插件BroadcastRecevier把插件的靜態注冊改為動態注冊,代碼如下:
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception { this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK); this.mPackageInfo.packageName = this.mPackage.packageName; if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) { throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName); } // 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 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()]); }
當然插件的靜態注冊,在喚醒app的廣播接收中會失效。
五、ContentProvider支持
ContentProvider是數據共享型組件,進程間和進程內均可共享,由ContentResolver統一管理訪問ContentProvider,我們通過使用context.getContentResolver(),context的實例為contextImpl,ContentResolver通過IContentProvider遠程服務代理訪問ContentProvider服務接口增刪改查。IContentProvider服務代理從ActivityThread中獲取,如下:
public final IContentProvider acquireProvider( Context c, String auth, int userId, boolean stable) {
//從本進程中獲取 final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable); if (provider != null) { return provider; } // There is a possible race here. Another thread may try to acquire // the same provider at the same time. When this happens, we want to ensure // that the first one wins. // Note that we cannot hold the lock while acquiring and installing the // provider since it might take a long time to run and it could also potentially // be re-entrant in the case where the provider is in the same process. IActivityManager.ContentProviderHolder holder = null; try {
//從ams中獲取 holder = ActivityManagerNative.getDefault().getContentProvider( getApplicationThread(), auth, userId, stable); } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } if (holder == null) { Slog.e(TAG, "Failed to find provider info for " + auth); return null; } // Install provider will increment the reference count for us, and break // any ties in the race. holder = installProvider(c, holder, holder.info, true /*noisy*/, holder.noReleaseNeeded, stable); return holder.provider; }
可以IContentProvider的來源,ActivityThread和AMS,優先從本進程中獲取。在應用的啟動時候,ContentProvider也跟着啟動,在AMS中有相應的記錄,
插件ContentProvider在AMS中沒有記錄,VitualApk使用代理轉發,宿主坑位ContentProvider攔截了ContentProvider操作,由坑位ContentProvider派發到具體的插件ContentProvider,代碼如下:
public class RemoteContentProvider extends ContentProvider { private static final String TAG = Constants.TAG_PREFIX + "RemoteContentProvider"; public static final String KEY_PKG = "pkg"; public static final String KEY_PLUGIN = "plugin"; public static final String KEY_URI = "uri"; public static final String KEY_WRAPPER_URI = "wrapper_uri"; private static Map<String, ContentProvider> sCachedProviders = new HashMap<>(); @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
//獲取插件的ContentProvider 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; } @Override public Uri insert(Uri uri, ContentValues values) {
//獲取插件的ContentProvider ContentProvider provider = getContentProvider(uri); Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI)); if (provider != null) { return provider.insert(pluginUri, values); } return uri; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) {
//獲取插件的ContentProvider ContentProvider provider = getContentProvider(uri); Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI)); if (provider != null) { return provider.delete(pluginUri, selection, selectionArgs); } return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
//獲取插件的ContentProvider ContentProvider provider = getContentProvider(uri); Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI)); if (provider != null) { return provider.update(pluginUri, values, selection, selectionArgs); } return 0; } @Override public int bulkInsert(Uri uri, ContentValues[] values) { ContentProvider provider = getContentProvider(uri); Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI)); if (provider != null) { return provider.bulkInsert(pluginUri, values); } return 0; } ... }
插件中ContentProvider沒有經過Android Framework層,由坑位ContentProvider維護。
插件有自己的ContentResolver:PluginContentResolver,插件訪問ContentProvider時,先從插件管理器中找對應ProviderInfo,找到了則用插件的IContentProvider,否則使用宿主的IContentProvider。
@Override protected IContentProvider acquireProvider(Context context, String auth) { if (mPluginManager.resolveContentProvider(auth, 0) != null) { return mPluginManager.getIContentProvider(); } return super.acquireProvider(context, auth); } @Override protected IContentProvider acquireExistingProvider(Context context, String auth) { if (mPluginManager.resolveContentProvider(auth, 0) != null) { return mPluginManager.getIContentProvider(); } return super.acquireExistingProvider(context, auth); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) @Override protected IContentProvider acquireUnstableProvider(Context context, String auth) { if (mPluginManager.resolveContentProvider(auth, 0) != null) { return mPluginManager.getIContentProvider(); } return super.acquireUnstableProvider(context, auth); }
那么插件中IContentProvider,怎來的了?宿主坑位ContentProvider,在ActivityThread中肯定存在對應IContentProvider,插件IContentProvider,對其進行動態代理構建了一個。
protected void hookIContentProviderAsNeeded() { Uri uri = Uri.parse(RemoteContentProvider.getUri(mContext)); mContext.getContentResolver().call(uri, "wakeup", null, null); try { Field authority = null; Field provider = null; ActivityThread activityThread = ActivityThread.currentActivityThread(); Map providerMap = Reflector.with(activityThread).field("mProviderMap").get(); Iterator iter = providerMap.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(RemoteContentProvider.getAuthority(mContext))) { if (provider == null) { provider = val.getClass().getDeclaredField("mProvider"); provider.setAccessible(true); }
//插件IContentProvider IContentProvider rawProvider = (IContentProvider) provider.get(val); IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider); mIContentProvider = proxy; Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider); break; } } } catch (Exception e) { Log.w(TAG, e); } }
IContentProviderProxyi的nvoke函數首先將訪問插件的Uri轉到宿主占坑Uri
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.v(TAG, method.toGenericString() + " : " + Arrays.toString(args));
//插件Uri轉為宿主占坑Uri
wrapperUri(method, args); try { return method.invoke(mBase, args); } catch (InvocationTargetException e) { throw e.getTargetException(); }
}
插件的Uri轉到宿主占坑Uri,代碼如下:
//
public static Uri wrapperUri(LoadedPlugin loadedPlugin, Uri pluginUri) { String pkg = loadedPlugin.getPackageName(); String pluginUriString = Uri.encode(pluginUri.toString()); StringBuilder builder = new StringBuilder(RemoteContentProvider.getUri(loadedPlugin.getHostContext())); builder.append("/?plugin=" + loadedPlugin.getLocation()); builder.append("&pkg=" + pkg); builder.append("&uri=" + pluginUriString); Uri wrapperUri = Uri.parse(builder.toString()); return wrapperUri; }
總結
VitualAPK 四大組件實現原理如下:
1.Activity 采用宿主manifest中占坑的方式來繞過系統校驗,然后再加載真正的activity;
2.Service 動態代理AMS,攔截service相關的請求,將其中轉給Service Runtime
去處理,Service Runtime
會接管系統的所有操作;
3.Receiver 將插件中靜態注冊的receiver重新注冊一遍;
4.ContentProvider 動態代理IContentProvider,攔截provider相關的請求,將其中轉給Provider Runtime
去處理,Provider Runtime
會接管系統的所有操作。
插件組件需要注意的地方:
參考資料:
VirtualApk源碼分析-BroadcastReceiver插件化
VirtualApk源碼分析-ContentProvider插件化
如果您對博主的更新內容持續感興趣,請關注公眾號!