VirtualAPK:滴滴 Android 插件化的實踐之路
一、前言
在 Android 插件化技術日新月異的今天,開發并落地一款插件化框架到底是簡單還是困難,這個問題不同人會有不同的答案。但是我相信,完成一個插件化框架的 Demo 并不是多難的事兒,然而要開發一款完善的插件化框架卻并非易事,尤其在國內,各大 ROM 廠商都對 Android 系統做了一定程度的定制,這更進一步加劇了 Android 本身的碎片化問題。
滴滴出行在插件化上的探索起步較晚,由于業務發展較快,迭代占據了大量的時間,這使得我們在2016年才開始研究這方面的技術。經過半年的開發、測試、適配和線上驗證,今天,我們正式推出一款較為完善的插件化框架——VirtualAPK。之所以現在推出,是因為 VirtualAPK 在內部已經得到了很好的驗證,我們在迭代過程中不斷地做機型適配和細節特性的支持,目前已達到一個非常穩定的狀況,足以支撐滴滴部分乃至全部業務的動態發版需求。目前滴滴出行最新版本(v5.0.4)上,小巴和接送機業務均為插件,大家可以去體驗。
二、插件化的現狀
到目前為止,業界已經有很多優秀的開源項目,比如早期的基于靜態代理思想的 DynamicLoadApk,隨后的基于占坑思想的 DynamicApk、Small,還有360手機助手的 DroidPlugin。它們都是優秀的開源項目,很大程度上促進了國內插件化技術的發展。
盡管有如此多的優秀框架存在,但是兼容性問題仍然是制約插件化發展的一大難題。一款插件化框架,也許可以在一款手機上完美運行,但是在數以千萬的設備上卻總是容易存在這樣那樣的兼容性問題。我相信上線過插件化的工程師應該深有體會。滴滴為什么還要自研一款新的插件化框架?因為我們需要一款功能完備、兼容性優秀、適用于滴滴業務的插件化框架,目前市面上的開源不能滿足我們的需求,所以必須重新造輪子,于是 VirtualAPK 誕生了。
三、VirtualAPK 的誕生
VirtualAPK 是滴滴出行自研的一款優秀的插件化框架,主要有如下幾個特性。
1. 功能完備
- 支持幾乎所有的 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 。 </ul>
- 兼容市面上幾乎所有的 Android 手機,這一點已經在滴滴出行客戶端中得到驗證;
- 資源方面適配小米、Vivo、Nubia 等,對未知機型采用自適應適配方案;
- 極少的 Binder Hook,目前僅僅 hook 了兩個 Binder: AMS 和 IContentProvider ,Hook過程做了充分的兼容性適配;
- 插件運行邏輯和宿主隔離,確保框架的任何問題都不會影響宿主的正常運行。
- 插件開發等同于原生開發,四大組件無需繼承特定的基類;
- 精簡的插件包,插件可以依賴宿主中的代碼和資源,也可以不依賴;
- 插件的構建過程簡單,通過Gradle插件來完成插件的構建,整個過程對開發者透明。
- 耦合形態 :插件對宿主可以有代碼或者資源的依賴,也可以沒有依賴。這種模式下,插件中的類不能和宿主重復,資源 id 也不能和宿主沖突。這是 VirtualAPK 的默認形態,也是適用于大多數業務的形態。
- 獨立形態 :插件對宿主沒有代碼或者資源的依賴。這種模式下,插件和宿主沒有任何關系,所以插件中的類和資源均可以和宿主重復。這種形態的主要作用是用于運行一些第三方 apk。
2. 優秀的兼容性
3. 入侵性極低
四、VirtualAPK 的工作過程
VirtualAPK 對插件沒有額外的約束,原生的 apk 即可作為插件。插件工程編譯生成 apk 后,即可通過宿主 App 加載,每個插件 apk 被加載后,都會在宿主中創建一個單獨的 LoadedPlugin 對象。如下圖所示,通過這些 LoadedPlugin 對象,VirtualAPK 就可以管理插件并賦予插件新的意義,使其可以像手機中安裝過的App一樣運行。
1. VirtualAPK 的運行形態
我們計劃賦予 VirtualAPK 兩種工作形態,耦合形態和獨立形態。目前 VirtualAPK 對耦合形態已經有了很好的支持,接下來將計劃支持獨立形態。
2. 如何使用
第一步: 初始化插件引擎
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
PluginManager.getInstance(base).init();
}
第二步:加載插件
public class PluginManager {
public void loadPlugin(final File apk);
public void loadPlugin(final String moduleCode);
}
我們對上述加載過程進行了一些封裝,通過如下方式即可異步地去加載一個插件。
// 示例:啟動插件中的Activity
DownloadManager downloadManager = DownloadManager.getInstance(this);
downloadManager.loadModule("com.ryg.test", true, this, new ILoadListener() {
@Override
public void onLoadEnd(int resultCode) {
if (resultCode == ILoadListener.LOAD_SUCCESS) {
Intent intent = new Intent();
intent.setClassName("com.ryg.test", "com.ryg.test.MainActivity");
startActivity(intent);
} else {
// todo load plugin failed
}
}
});
當插件入口被調用后,插件的后續邏輯均不需要宿主干預,均走原生的 Android 流程。比如,在插件內部,如下代碼將正確執行:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_book_manager);
LinearLayout holder = (LinearLayout)findViewById(R.id.holder);
TextView imei = (TextView)findViewById(R.id.imei);
imei.setText(IDUtil.getUUID(this));
// bind service in plugin
Intent service = new Intent(this, BookManagerService.class);
bindService(service, mConnection, Context.BIND_AUTO_CREATE);
// start activity in plugin
Intent intent = new Intent(this, TCPClientActivity.class);
startActivity(intent);
}</code></pre>
五、探究原理
1. 基本原理
- 合并宿主和插件的ClassLoader :需要注意的是,插件中的類不可以和宿主重復;
- 合并插件和宿主的資源 :重設插件資源的 packageId ,將插件資源和宿主資源合并;
- 去除插件包對宿主的引用 :構建時通過 Gradle 插件去除插件對宿主的代碼以及資源的引用。
2. 四大組件的實現原理

- Activity :采用宿主 manifest 中占坑的方式來繞過系統校驗,然后再加載真正的 Activity;
- Service :動態代理 AMS,攔截 Service 相關的請求,將其中轉給一個虛擬空間(Matrix)去處理,Matrix 會接管系統的所有操作;
- Receiver :將插件中靜態注冊的 Receiver 重新注冊一遍;
- ContentProvider :動態代理 IContentProvider ,攔截 Provider 相關的請求,將其中轉給一個虛擬空間(Matrix)去處理,Matrix 會接管系統的所有操作。
以下是 VirtualAPK 的整體結構圖。

六、填坑之路
在實踐中我們遇到了很多很多的問題,比如機型適配、API 版本適配、Binder Hook 的穩定性保證等問題,這里拿一個典型的資源適配問題來說明。
其實這是一個很無奈的問題,由于國內各大 ROM 廠商喜歡深度定制 Android 系統,所以就出現了這種適配問題。
正常情況下我們通過如下代碼去創建插件的 Resources 對象:
Resources newResources = new Resources(assetManager,
hostResources.getDisplayMetrics(), hostResources.getConfiguration());
然后在 Vivo 手機上,竟然出現了如下的類型轉換錯誤,看起來是 Vivo 自己派生了 Resources 的子類。
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.sdu.didi.psnger/com.didi.virtualapk.core.A$1}: java.lang.ClassCastException: android.content.res.Resources cannot be cast to android.content.res.VivoResources
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2196)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2245)
at android.app.ActivityThread.access$800(ActivityThread.java:140)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1202)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:136)
at android.app.ActivityThread.main(ActivityThread.java:5143)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:602)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.ClassCastException: android.content.res.Resources cannot be cast to android.content.res.VivoResources
at android.app.ResourcesManager.getTopLevelResources(ResourcesManager.java:236)
at android.app.ContextImpl.<init>(ContextImpl.java:2057)
at android.app.ContextImpl.createActivityContext(ContextImpl.java:2008)
at android.app.ActivityThread.createBaseContextForActivity(ActivityThread.java:2207)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2140)
... 11 more
于是反編譯了下 Vivo 的 Framework 代碼,果不其然,在如下代碼中進行了類型轉換,所以在加載插件資源的時候就報錯了。
@VivoHook(hookType = VivoHookType.NEW_METHOD)
public Resources getTopLevelResources(String pkgName, String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
Resources resources = getTopLevelResources(resDir, displayId, overrideConfiguration, compatInfo, token);
if (resources != null) {
((VivoResources) resources).init(pkgName);
}
return resources;
}
為了解決這個問題,我們分析了 VivoResources 的代碼實現,然后在創建插件資源的時候,采用了如下的代碼。
private static final class VivoResourcesCompat {
private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
Class resourcesClazz = Class.forName("android.content.res.VivoResources");
Resources newResources = (Resources)ReflectUtil.invokeConstructor(resourcesClazz,
new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class},
new Object[]{assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()});
return newResources;
}
}
除了 Vivo 以外,有類似問題的還有 MIUI、Nubia 以及其他不知名的機型。而且在 Vivo 手機上,除了類型轉換錯誤的問題,還有其他很坑的問題。
事實上我們還處理了很多其他的坑,這里無法一一說明,所以說如何保證插件化的穩定性是一件很有技術挑戰的事情。
七、一些暫時不支持的特性
由于種種原因,VirtualAPK 目前未能支持所有的 Android 的特性,已知的有如下幾點:
- 不支持 Activity 的部分屬性,比如 process 、 configChanges 等;
- 暫不支持 overridePendingTransition(int enterAnim, int exitAnim) 這種形式的轉場動畫;
- 插件中彈通知,不能使用插件中的資源,比如圖片。
八、開源計劃
我們的目標是打造一款功能完備的插件化框架,使得各個業務線都能以插件的形式集成,從而實現 Android App 的熱更新能力。
目前 VirtualAPK 還有一些特性需要進一步完善,待完善后,將會進行開源計劃。我們期望 VirtualAPK 開源后,可以讓其他 App 能夠無縫集成,無需考慮細節實現和兼容性問題即可輕松擁有熱更新能力。
來自:http://geek.csdn.net/news/detail/130917