VirtualAPK:滴滴 Android 插件化的實踐之路

2022-01-29 CSDN

本文為CSDN首發,歡迎技術投稿、約稿,給文章糾錯,請發送郵件至

mobile@csdn.net。

在 Android 插件化技術日新月異的今天,開發並落地一款插件化框架到底是簡單還是困難,這個問題不同人會有不同的答案。但是我相信,完成一個插件化框架的 Demo 並不是多難的事兒,然而要開發一款完善的插件化框架卻並非易事,尤其在國內,各大 ROM 廠商都對 Android 系統做了一定程度的定製,這更進一步加劇了 Android 本身的碎片化問題。

滴滴出行在插件化上的探索起步較晚,由於業務發展較快,迭代佔據了大量的時間,這使得我們在2016年才開始研究這方面的技術。經過半年的開發、測試、適配和線上驗證,今天,我們正式推出一款較為完善的插件化框架——VirtualAPK。之所以現在推出,是因為 VirtualAPK 在內部已經得到了很好的驗證,我們在迭代過程中不斷地做機型適配和細節特性的支持,目前已達到一個非常穩定的狀況,足以支撐滴滴部分乃至全部業務的動態發版需求。目前滴滴出行最新版本(v5.0.4)上,小巴和接送機業務均為插件,大家可以去體驗。

到目前為止,業界已經有很多優秀的開源項目,比如早期的基於靜態代理思想的 DynamicLoadApk,隨後的基於佔坑思想的 DynamicApk、Small,還有360手機助手的 DroidPlugin。它們都是優秀的開源項目,很大程度上促進了國內插件化技術的發展。

儘管有如此多的優秀框架存在,但是兼容性問題仍然是制約插件化發展的一大難題。一款插件化框架,也許可以在一款手機上完美運行,但是在數以千萬的設備上卻總是容易存在這樣那樣的兼容性問題。我相信上線過插件化的工程師應該深有體會。滴滴為什麼還要自研一款新的插件化框架?因為我們需要一款功能完備、兼容性優秀、適用於滴滴業務的插件化框架,目前市面上的開源不能滿足我們的需求,所以必須重新造輪子,於是 VirtualAPK 誕生了。

VirtualAPK 是滴滴出行自研的一款優秀的插件化框架,主要有如下幾個特性。

1. 功能完備

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。

2. 優秀的兼容性

兼容市面上幾乎所有的 Android 手機,這一點已經在滴滴出行客戶端中得到驗證;

資源方面適配小米、Vivo、Nubia 等,對未知機型採用自適應適配方案;

極少的 Binder Hook,目前僅僅 hook 了兩個 Binder:AMS 和 IContentProvider,Hook過程做了充分的兼容性適配;

插件運行邏輯和宿主隔離,確保框架的任何問題都不會影響宿主的正常運行。

3. 入侵性極低

插件開發等同於原生開發,四大組件無需繼承特定的基類;

精簡的插件包,插件可以依賴宿主中的代碼和資源,也可以不依賴;

插件的構建過程簡單,通過Gradle插件來完成插件的構建,整個過程對開發者透明。

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

}

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 能夠無縫集成,無需考慮細節實現和兼容性問題即可輕鬆擁有熱更新能力。

作者簡介: 任玉剛,滴滴出行 Android 技術專家,《Android 開發藝術探索》作者,插件化框架 dynamic-load-apk 的發起者,CSDN 移動開發博客專家,曾當選 CSDN 2014、2015年度十大博客之星。熱愛技術,熱愛開源,凡事喜歡刨根問底,長期活躍在 CSDN 和 GitHub。目前就職於滴滴出行 App 架構組,從事熱修復和插件化相關的開發工作。 

博客地址:http://blog.csdn.net/singwhatiwanna; 

GitHub:https://github.com/singwhatiwanna。 

了解最新移動開發相關信息和技術,請關注 mobilehub 公眾微信號(ID: mobilehub)。

相關焦點

  • Webview.apk —— Google 官方的私有插件化方案
    插件化中的資源固定我們經常聽見 Android 插件化方案裡,有一個概念叫 固定ID,這是什麼意思呢?但是這裡又有一個非常顯眼的問題:如果 packageId 永遠是 7f,那麼顯然是不夠用的,我們知道有一定的方案可以更改 packgeId,只要在不同業務包中使用不同的 packageId,這樣能極大避免 id 碰撞的問題,為插件化使用外部資源提供了條件。等等!我們在開頭說到了 webview.apk 的更新 —— 代碼,資源都可以更新。這聽上去不就是插件化的一種嗎?
  • Android組件化和插件化的概念
    一、什麼是組件化和插件化 組件化: 就是將一個app分成多個模塊,每個模塊都是一個組件(Module),開發的過程中我們可以讓這些組件相互依賴或者單獨調試部分組件等,但是最終發布的時候是將這些組件合併成一個apk,這就是組件化開發。
  • Android插件化系列三:技術流派和四大組件支持
    但是耦合插件卻是和宿主運行在一個進程中,所以插件崩潰,宿主也崩潰了。所以一般業務要根據資源和代碼的耦合程度,插件的可靠性等綜合考慮插件類型。我們接下來慢慢講解。2.1 代碼和資源互通插件與dex因為可能看我文章的還有沒接觸過插件化的同學,所以增加這一部分講解插件和dex到底是怎麼一種存在形式,插件,我們可以理解為一個單獨打包出來的apk。
  • 使用frida hook插件化apk
    apk樣本,裡面有視頻、直播和小說,沒有VIP只能試看30秒,剛好最近學習frida,用來練習下,分析過程中發現是一個插件化的apk,本文記錄下分析的過程。從上圖可以看到,真正邏輯所在的apk是plugin-shadow-apk-debug.apk,是在該apk的私有文件目錄中。從代碼中分析也可知道,此apk是插件化apk,使用的是騰訊開源的插件化框架Shadow,感興趣的可以去了解下。既然已經找到真正的apk,那我們就需要定位到關鍵代碼地方。
  • 玩轉APK:實現Android APK瘦身99.99%,厲害了~~
    我們尚未在 App 的build.gradle文件中設置允許最小化(Minification)和資源收縮(Resource Shrinking)。><TextView xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:gravity="center"    android:text="Hello
  • Android徹底組件化方案實踐
    一、模塊化、組件化與插件化項目發展到一定程度,隨著人員的增多,代碼越來越臃腫,這時候就必須進行模塊化的拆分。在我看來,模塊化是一種指導理念,其核心思想就是分而治之、降低耦合。而在Android工程中如何實施,目前有兩種途徑,也是兩大流派,一個是組件化,一個是插件化。  提起組件化和插件化的區別,有一個很形象的圖:
  • Android徹底組件化方案實踐,附Demo
    而在Android工程中如何實施,目前有兩種途徑,也是兩大流派,一個是組件化,一個是插件化。  提起組件化和插件化的區別,有一個很形象的圖:  本文主要集中講的是組件化的實現思路,對於插件化的技術細節不做討論,我們只是從上面的問答中總結出一個結論:組件化和插件化的最大區別(應該也是唯一區別)就是組件化在運行時不具備動態添加和修改組件的功能,但是插件化是可以的。
  • Android Studio打包apk,aar,jar包
    一片楓葉_劉超的博客地址:http://blog.csdn.net/qq_23547831作者編寫了github項目解析、android源碼分析以及產品研發多個專題,有興趣的可以關注下學習學習~文本我們將講解android studio打包apk,aar,jar包的相關知識。
  • android apk 防反編譯技術第一篇-加殼技術
    現在將最近學習成果做一下整理總結。學習的這些成果我會做成一個系列慢慢寫出來與大家分享,共同進步。這篇主要講apk的加殼技術,廢話不多說了直接進入正題。一、加殼技術原理 所謂apk的加殼技術和pc exe的加殼原理一樣,就是在程序的外面再包裹上另外一段代碼,保護裡面的代碼不被非法修改或反編譯,在程序運行的時候優先取得程序的控制權做一些我們自己想做的工作。
  • 破解第一個Android程序
    '@android:style/AlertDialog'./Library/apktool/framework/1.apk, -S, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel/res, -M, /Users/android/Documents/workspace/Crackme0201/app/build/outputs/apk/outdir_rel
  • Android模擬器和安裝APK文件
    好了進入正題,今天要講的是關於android模擬器和apk鏡像文件的一些事情。一.如何正確的啟動模擬器(早於Android 1.5的開發版本跳過此步) :關於在eclipse裡面如何集成android這些問題就不說了,這寫問題我想還是不用在這裡廢話的。
  • 將兩個 Crosswalk* Android* APK 文件提交到 Google Play Store*...
    這兩個 APK 文件的名稱採用以下格式: AppName.android.crosswalk.x86.timestamp.apk, 例如 ExampleApp.android.crosswalk.x86.20140418132640.apk AppName.android.crosswalk.arm.
  • 精選Android中高級面試題 -- 終結篇:高級乾貨
    推薦文章:美團Android自動化之旅—Walle生成渠道包(https://github.com/Meituan-Dianping/walle)參考回答:插件化是指將 APK 分為宿主和插件的部分。DexClassLoader支持加載指定目錄(不限於內部)的dex/jar/apk文件插件通信:通過給插件apk生成相應的DexClassLoader便可以訪問其中的類,可分為單DexClassLoader和多DexClassLoader兩種結構。
  • 一次Android權限刪除經歷
    2.初步定位首先使用android studio查看了打包出來的apk中的Androidmanifest文件,發現其中確實存在RECEIVE_SMS權限,也就是說打包到apk中的Androidmanifest文件並不是app下的該文件,從android開發者官網中合併多個manifest文件的文檔來看,實際上打包到apk中的manifest文件是由多個menifest文件合併而來的,其合併順序如下
  • 寫給 Android 開發者的 Gradle 系列(三)撰寫 plugin
    /gradlew pluginTest將會看到輸出結果——> Task :app:pluginTestHello Gradle項目化到目前為止談及到的東西都還是一個普通的、不可以發布到倉庫的插件,如果想要將插件發布出去供他人和自己在項目中 apply,需要進行以下步驟將插件變成一個 Project——
  • Android徹底組件化(二)-Demo發布
    今年6月份開始,我開始負責對「得到app」的android代碼進行組件化拆分,在動手之前我查閱了很多組件化或者模塊化的文章,雖然有一些收穫,但是很少有文章能夠給出一個整體且有效的方案,大部分文章都只停留在組件單獨調試的層面上,涉及組件之間的交互就很少了,更不用說組件生命周期、集成調試和代碼邊界這些最棘手的問題了。
  • Android在線下載更新功能實踐
    DownloadManager下載完成後,會發送通知。我們在UpdateAppReceiver ,接受到通知後執行安裝操作。context, context.getPackageName() + ".fileprovider", apkFile);                                i.setDataAndType(contentUri, "application/vnd.android.package-archive");                            } else
  • Android破解實戰:遊戲蜂窩3.21版本破解記錄
    .method public isVip()I    .locals 1    .prologue    .line 771   invoke-virtual><LinearLayoutandroid:orientation="vertical" android:layout_width="0.0dip" android:layout_height="0.0dip" xmlns:android="http://schemas.android.com/apk/res/android">
  • 【Android】Apk安裝與管理工具
    APK Editor是一款應用程式,可讓您準確執行其名稱所指示的操作:編輯保存到設備中的任何APK。而且,如果您本身沒有APK,則可以從已安裝的任何應用程式中提取它。有了這個程序,您可以深入到apk結構中,也可以從中提取圖片。-APK編輯器,您無需任何編碼即可輕鬆創建自己的android應用(在安裝前稱為APK)。-APK編輯器將提取/共享/備份您在設備中製作的所有應用程式的APK,並顯示保存在SD卡中的所有APK的列表。-構建自己的較小應用程式(例如,手電筒示例apk僅約2.5萬,比市場上的其他產品要小得多)。