滴滴插件化方案 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

 

 本文由用戶 ChelseaSnip 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!