當前位置:
首頁 > 最新 > 滴滴插件化方案 VirtualApk 源碼解析

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

二、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);

載入插件。

這裡就不贅述源碼了,大致為調用 解析apk,獲得該apk對應的PackageInfo,資源相關(AssetManager,Resources),DexClassLoader(載入類),四大組件相關集合(mActivityInfos,mServiceInfos,mReceiverInfos,mProviderInfos),針對Plugin的PluginContext等一堆信息,封裝為LoadedPlugin對象。

詳細可以參考 類。

ok,如果該插件以及載入過,則直接通過startActivity去啟動插件中目標Activity。

(1)替換Activity

這裡大家肯定會有疑惑,該Activity必然沒有在Manifest中註冊,這麼啟動不會報錯嗎?

正常肯定會報錯呀,所以我們看看它是怎麼做的吧。

跟進startActivity的調用流程,會發現其最終會進入Instrumentation的execStartActivity方法,然後再通過ActivityManagerProxy與AMS進行交互。

而Activity是否存在的校驗是發生在AMS端,所以我們在於AMS交互前,提前將Activity的ComponentName進行替換為占坑的名字不就好了么?

這裡可以選擇hook Instrumentation,或者ActivityManagerProxy都可以達到目標,VirtualAPK選擇了hook Instrumentation.

打開 可以看到如下方法:

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(); } }

可以看到首先通過反射拿到了原本的 對象,拿的過程是首先拿到ActivityThread,由於ActivityThread可以通過靜態變數 或者靜態方法 獲取,所以拿到其對象相當輕鬆。拿到ActivityThread對象後,調用其 方法,即可獲取當前的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,所以直接看 方法:

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中,可以看到這裡存儲明顯是為了後面恢復用的。

然後調用了

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是由 返回。

很明顯,傳入的參數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);

, corePackage值為 ,usedStandardStubActivity為數字值。

所以最終類名格式為:

再看一眼,CoreLibrary下的AndroidManifest中:

// 省略很多...

就完全明白了。

到這裡就可以看到,替換我們啟動的Activity為占坑Activity,將我們原本啟動的包名,類名存儲到了Intent中。

這樣做只完成了一半,為什麼這麼說呢?

(2) 還原Activity

因為欺騙過了AMS,AMS執行完成後,最終要啟動的不可能是占坑Activity,還應該是我們的啟動的目標Activity呀。

這裡需要知道Activity的啟動流程:

AMS在處理完啟動Activity後,會調用: ,這裡的thread對應的server端未我們ActivityThread中的ApplicationThread對象(binder可以理解有一個client端和一個server端),所以會調用 方法,在其內部會調用mH類的sendMessage方法,傳遞的標識為 ,進入調用到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的「偷梁換柱」。

還沒完,接下來在 方法中:

@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 defaultSingleton = (Singleton) 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對象,該對象返回的是一個 ,然後拿到其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,所以會被上述方法攔截,然後調用:

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

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 推酷 的精彩文章:

深入探索JVM自動資源管理
盤點國外多個APPStore應用屏幕截圖示例
webpack2終極優化
IntelliJ IDEA 2017.1 JDK 8 性能調優
深入理解CSS外邊距摺疊

TAG:推酷 |

您可能感興趣

Jaunt採用AR技術推出Livestreamed體積視頻解決方案
開源項目丨基於 Spring Boot 的分散式和服務化解決方案 Roses
復旦微電子選用新思科技DesignWare Bluetooth IP 解決方案
Thrust Vector用Oculus頭顯展示VR飛行方案
Spark Connected為AR/VR發布高性能線充電解決方案The Griffin
Antycip Simulatio推出最新VR和3D沉浸式解決方案
Imagination推出PowerVR Automotive計劃,專為汽車行業打造GPU解決方案
push to origin/master was rejected錯誤解決方案
VRstudios推出VR線下電競解決方案VRcade PowerPlay
基於Oculus Go,Thrust Vector展示VR飛行解決方案
智能營銷計劃的解決方案Gagapay Network
Google的開源Consent解決方案解析APUS研究院
Kaon Interactive旨在針對行業用戶推出VR/AR解決方案
Kafka Controller Redesign 方案
Flash Player 的兩種開源替代方案
Realtek藉助Cadence Innovus成功開發DTV SoC解決方案
adb devices命令 unauthorized解決方案
Cadence Innovus助力Realtek成功開發DTV SoC解決方案
Microsoft Azure 以太坊節點自動化部署方案漏洞分析
小米Pro/Air筆記本重裝系統教程及開機No Bootable Devices解決方案