滴滴外掛化方案 VirtualApk 原始碼解析

鴻洋發表於2019-02-27

本文已在我的公眾號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
(可以給我留言你想學習的文章,支援投稿)
滴滴外掛化方案 VirtualApk 原始碼解析

相關文章