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

JustinShapi 7年前發布 | 15K 次閱讀 安卓開發 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。
    </li>
  • 自定義View:支持自定義 View,支持自定義屬性和 style,支持動畫;

  • PendingIntent:支持 PendingIntent 以及和其相關的 Alarm 、 Notification 和 AppWidget ;
  • 支持插件 Application 以及插件 manifest 中的 meta-data ;
  • 支持插件中的 so 。
  • </ul>

    2. 優秀的兼容性

    • 兼容市面上幾乎所有的 Android 手機,這一點已經在滴滴出行客戶端中得到驗證;
    • 資源方面適配小米、Vivo、Nubia 等,對未知機型采用自適應適配方案;
    • 極少的 Binder Hook,目前僅僅 hook 了兩個 Binder: AMS 和 IContentProvider ,Hook過程做了充分的兼容性適配;
    • 插件運行邏輯和宿主隔離,確保框架的任何問題都不會影響宿主的正常運行。

    3. 入侵性極低

    • 插件開發等同于原生開發,四大組件無需繼承特定的基類;
    • 精簡的插件包,插件可以依賴宿主中的代碼和資源,也可以不依賴;
    • 插件的構建過程簡單,通過Gradle插件來完成插件的構建,整個過程對開發者透明。

    四、VirtualAPK 的工作過程

    VirtualAPK 對插件沒有額外的約束,原生的 apk 即可作為插件。插件工程編譯生成 apk 后,即可通過宿主 App 加載,每個插件 apk 被加載后,都會在宿主中創建一個單獨的 LoadedPlugin 對象。如下圖所示,通過這些 LoadedPlugin 對象,VirtualAPK 就可以管理插件并賦予插件新的意義,使其可以像手機中安裝過的App一樣運行。

    1. VirtualAPK 的運行形態

    我們計劃賦予 VirtualAPK 兩種工作形態,耦合形態和獨立形態。目前 VirtualAPK 對耦合形態已經有了很好的支持,接下來將計劃支持獨立形態。

    • 耦合形態 :插件對宿主可以有代碼或者資源的依賴,也可以沒有依賴。這種模式下,插件中的類不能和宿主重復,資源 id 也不能和宿主沖突。這是 VirtualAPK 的默認形態,也是適用于大多數業務的形態。
    • 獨立形態 :插件對宿主沒有代碼或者資源的依賴。這種模式下,插件和宿主沒有任何關系,所以插件中的類和資源均可以和宿主重復。這種形態的主要作用是用于運行一些第三方 apk。

    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

     

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