作者:斜槓Allen 連結:https://www.jianshu.com/p/9601c4938d02
最近幾年移動開發業界興起了「 外掛化技術 」的旋風,各個大廠都推出了自己的外掛化框架,各種開源框架都評價自身功能優越性,令人目不暇接。隨著公司業務快速發展,專案增多,開發資源卻有限,如何能在有限資源內滿足需求和專案的增長,同時又能快速響應問題和迭代新需求,這就是一個矛盾點。此時,外掛化技術正好風生水起,去瞭解各個主流框架實現思路,看看能對目前工作是否有幫助,是很有必要的。
主要分為以下幾個部分來講解
-
外掛化介紹
-
入門知識
-
實現原理
-
主流框架
-
實戰
-
小結
外掛化介紹
百度百科裡是這麼定義外掛的:「 是一種遵循一定規範的應用程式介面編寫出來的程式,只能執行在程式規定的系統平臺下,而不能脫離指定的平臺單獨執行。」,也就是說,外掛可以提供一種動態擴充套件能力,使得應用程式在執行時載入原本不屬於該應用的功能,並且做到動態更新和替換。
那麼在 Android 中,何為「 外掛化 」,顧名思義,就是把一些核心複雜依賴度高的業務模組封裝成獨立的外掛,然後根據不同業務需求進行不同組合,動態進行替換,可對外掛進行管理、更新,後期對外掛也可進行版本管理等操作。在外掛化中有兩個概念需要講解下:
-
宿主
所謂宿主,就是需要能提供執行環境,給資源呼叫提供背景關係環境,一般也就是我們主 APK ,要執行的應用,它作為應用的主工程所在,實現了一套外掛的載入和管理的框架,外掛都是依託於宿主的APK而存在的。 -
外掛
外掛可以想象成每個獨立的功能模組封裝為一個小的 APK ,可以透過線上配置和更新實現外掛 APK 在宿主 APK 中的上線和下線,以及動態更新等功能。
那麼為何要使用外掛化技術,它有何優勢,能給我們帶來什麼樣好處,這裡簡單列舉了以下幾點:
1、讓使用者不用重新安裝 APK 就能升級應用功能,減少發版本頻率,增加使用者體驗。
2、提供一種快速修複線上 BUG 和更新的能力。
3、按需載入不同的模組,實現靈活的功能配置,減少伺服器對舊版本介面相容壓力。
4、模組化、解耦合、並行開發、 65535 問題。
入門知識
首先我們要知道外掛化技術是屬於比較複雜一個領域,複雜點在於它涉及知識點廣泛,不僅僅是上層做應用架構能力,還要求我們對 Android 系統底層知識需要有一定的認知,這裡簡單羅列了其中會涉及的知識點:
首先,要介紹的是 Binder ,我們都知道 Android 多行程通訊核心就是 Binder ,如果沒有它真的寸步難行。 Binder 涉及兩層技術,你可以認為它是一個中介者樣式,在客戶端和伺服器端之間, Binder 就起到中介的作用。如果要實現四大元件的外掛化,就需要在 Binder 上做修改, Binder 服務端的內容沒辦法修改,只能改客戶端的程式碼,而且四大元件的每個元件的客戶端都不一樣,這個就需要深入研究了。學習Binder的最好方式是 AIDL ,這方面在網上有很多資料,最簡單的方式就是自己寫個 aidl 檔案自動生成一個 Java 類,然後去檢視這個Java類的每個方法和變數,然後再去看四大元件,其實都是跟 AIDL 差不多的實現方式。
其次,是 App 打包的流程。程式碼寫完了,執行一次打包操作,中途經歷了資源打包、 Dex 生成、簽名等過程。其中最重要的就是資源的打包,即 AAPT 這一步,如果宿主和外掛的資源id衝突,一種解決辦法就是在這裡做修改。
第三, App 在手機上的安裝流程也很重要。熟悉安裝流程不僅對外掛化有幫助,在遇到安裝 Bug 的時候也非常重要。手機安裝 App 的時候,經常會有下載異常,提示資源包不能解析,這時需要知道安裝 App 的這段程式碼在什麼地方,這隻是第一步。第二步需要知道, App 下載到本地後,具體要做哪些事情。手機有些目錄不能訪問, App 下載到本地之後,放到哪個目錄下,然後會生成哪些檔案。外掛化有個增量更新的概念,如何下載一個增量包,從本地具體哪個位置取出一個包,這個包的具體命名規則是什麼,等等。這些細節都必須要清楚明白。
第四,是 App 的啟動流程。 Activity 啟動有幾種方式?一種是寫一個 startActivity ,第二種是點選手機 App ,透過手機系統裡的 Launcher 機制,啟動 App 裡預設的 Activity 。通常, App 開發人員喜聞樂見的方式是第二種。那麼第一種方式的啟動原理是什麼呢?另外,啟動的時候,Main 函式在哪裡?這個 Main 函式的位置很重要,我們可以對它所在的類做修改,從而實現外掛化。
第五點更重要,做 Android 外掛化需要控制兩個地方。首先是外掛 Dex 的載入,如何把外掛 Dex 中的類載入到記憶體?另外是資源載入的問題。外掛可能是 Apk 也可能是 so 格式,不管哪一種,都不會生成 R.id ,從而沒辦法使用。這個問題有好幾種解決方案。一種是是重寫 Context 的 getAsset 、 getResource 之類的方法,偷換概念,讓外掛讀取外掛裡的資源,但缺點就是宿主和外掛的資源 id 會衝突,需要重寫 AAPT 。另一種是重寫 AMS中儲存的外掛串列,從而讓宿主和外掛分別去載入各自的資源而不會衝突。第三種方法,就是打包後,執行一個指令碼,修改生成包中資源id。
第六點,在實施外掛化後,如何解決不同外掛的開發人員的工作區問題。比如,外掛1和外掛2,需要分別下載哪些程式碼,如何獨立執行?就像機票和火車票,如何只執行自己的外掛,而不執行別人的外掛?這是協同工作的問題。火車票和機票,這兩個 Android 團隊的各自工作區是不一樣的,這時候就要用到 Gradle 指令碼了,每個專案分別有各自的倉庫,有各自不同的打包指令碼,只需要把自己的外掛跟宿主專案一起打包執行起來,而不用引入其他外掛,還有更厲害的是,也可以把自己的外掛當作一個 App 來打包並執行。
上面介紹了外掛化的入門知識,一共六點,每一點都需要花大量時間去理解。否則,在面對外掛化專案的時候,很多地方你會一頭霧水。而只要理解了這六點核心,一切可迎刃而解。
實現原理
在Android中應用外掛化技術,其實也就是動態載入的過程,分為以下幾步:
-
把可執行檔案( .so/dex/jar/apk 等)複製到應用 APP 內部。
-
載入可執行檔案,更換靜態資源
-
呼叫具體的方法執行業務邏輯
Android 專案中,動態載入技術按照載入的可執行檔案的不同大致可以分為兩種:
-
動態載入 .so 庫
-
動態載入 dex/jar/apk檔案(現在動態載入普遍說的是這種)
第一點, Android 中 NDK 中其實就使用了動態載入,動態載入 .so 庫並透過 JNI 呼叫其封裝好的方法。後者一般是由 C/C++ 編譯而成,執行在 Native 層,效率會比執行在虛擬機器層的 Java 程式碼高很多,所以 Android 中經常透過動態載入 .so 庫來完成一些對效能比較有需求的工作(比如 Bitmap 的解碼、圖片高斯模糊處理等)。此外,由於 .so 庫是由 C/C++ 編譯而來的,只能被反編譯成彙編程式碼,相比中 dex 檔案反編譯得到的 Smali 程式碼更難被破解,因此 .so 庫也可以被用於安全領域。
其二,“基於 ClassLoader 的動態載入 dex/jar/apk 檔案”,就是我們指在 Android 中 動態載入由 Java 程式碼編譯而來的 dex 包並執行其中的程式碼邏輯,這是常規 Android 開發比較少用到的一種技術,目前說的動態載入指的就是這種。
Android 專案中,所有 Java 程式碼都會被編譯成 dex 檔案,Android 應用執行時,就是透過執行 dex 檔案裡的業務程式碼邏輯來工作的。使用動態載入技術可以在 Android 應用執行時載入外部的 dex 檔案,而透過網路下載新的 dex 檔案並替換原有的 dex 檔案就可以達到不安裝新 APK 檔案就升級應用(改變程式碼邏輯)的目的。
所以說,在 Android 中的 ClassLoader 機制主要用來載入 dex 檔案,系統提供了兩個 API 可供選擇:
PathClassLoader:只能載入已經安裝到 Android 系統中的 APK 檔案。因此不符合外掛化的需求,不作考慮。
DexClassLoader:支援載入外部的 APK、Jar 或者 dex 檔案,正好符合檔案化的需求,所有的外掛化方案都是使用 DexClassloader 來載入外掛 APK 中的 .class檔案的。
主流框架
在 Android 中實現外掛化框架,需要解決的問題主要如下:
-
資源和程式碼的載入
-
Android 生命週期的管理和元件的註冊
-
宿主 APK 和外掛 APK 資源取用的衝突解決
下麵分析幾個目前主流的開源框架,看看每個框架具體實現思路和優缺點。
DL 動態載入框架 ( 2014 年底)
是基於代理的方式實現外掛框架,對 App 的表層做了處理,透過在 Manifest 中註冊代理元件,當啟動外掛元件時,首先啟動一個代理元件,然後透過這個代理元件來構建,啟動外掛元件。 需要按照一定的規則來開發外掛 APK,外掛中的元件需要實現經過改造後的 Activity、FragmentActivity、Service 等的子類。
優點如下:
-
外掛需要遵循一定的規則,因此安全方面可控制。
-
方案簡單,適用於自身少量程式碼的外掛化改造。
缺點如下:
-
不支援透過 This 呼叫元件的方法,需要透過 that 去呼叫。
-
由於 APK 中的 Activity 沒有註冊,不支援隱式呼叫 APK 內部的 Activity。
-
外掛編寫和改造過程中,需要考慮相容性問題比較多,聯調起來會比較費時費力。
DroidPlugin ( 2015 年 8 月)
DroidPlugin 是 360 手機助手實現的一種外掛化框架,它可以直接執行第三方的獨立 APK 檔案,完全不需要對 APK 進行修改或安裝。一種新的外掛機制,一種免安裝的執行機制,是一個沙箱(但是不完全的沙箱。就是對於使用者來說,並不知道他會把 apk 怎麼樣), 是模組化的基礎。
實現原理:
-
共享行程:為android提供一個行程執行多個 apk 的機制,透過 API 欺騙機制瞞過系統。
-
佔坑:透過預先佔坑的方式實現不用在 manifest 註冊,透過一帶多的方式實現服務管理。
-
Hook 機制:動態代理實現函式 hook ,Binder 代理繞過部分系統服務限制,IO 重定向(先獲取原始 Object –> Read ,然後動態代理 Hook Object 後–> Write 回去,達到瞞天過海的目的)。
外掛 Host 的程式架構:
優點如下:
-
支援 Android 四大元件,而且外掛中的元件不需要在宿主 APK 中註冊。
-
支援 Android 2.3 及以上系統,支援所有的系統 API。
-
外掛與外掛之間,外掛與宿主之間的程式碼和資源完全隔閡。
-
實現了行程管理,外掛的空行程會被及時回收,佔用記憶體低。
缺點如下:
-
外掛 APK 中不支援自定義資源的 Notification,通知欄限制。
-
外掛 APK 中無法註冊具有特殊的 IntentFilter 的四大元件。
-
缺乏對 Native 層的 Hook 操作,對於某些帶有 Native 程式碼的外掛 APK 支援不友好,可能無法正常執行。
-
由於外掛與外掛,外掛與宿主之間的程式碼完全隔離,因此,外掛與外掛,外掛與宿主之間的通訊只能透過 Android 系統級別的通訊方式。
-
安全性擔憂(可以修改,hook一些重要資訊)。
-
機型適配(不是所有機器上都能行,因為大量用反射相關,如果rom廠商深度定製了framework層,反射的方法或者類不在,容易外掛運用失敗)
Small ( 2015 年底)
Small 是一種實現輕巧的跨平臺外掛化框架,基於“輕量、透明、極小化、跨平臺”的理念,實現原理有以下三點。
-
動態載入類:我們知道外掛化很多都從 DexClassLoader 類有個 DexPathList 清單,支援 dex/jar/zip/apk 檔案格式,卻沒有支援 .so 檔案格式,因此 Small 框架則是把 .so 檔案包裝成 zip 檔案格式,插入到 DexPathList 集合中,改寫動態載入的程式碼。
-
資源分段:由於 Android 資源的格式是 0xPPTTNNNN ,PP 是包 ID ,00-02 是屬於系統,7f 屬於應用程式,03-7e 則保留,可以在這個範圍內做文章 , TT 則是 Type 比如,attr 、layout 、string 等等,NNNN 則是資源全域性 ID。那麼這個框架則是對資源包進行重新打包,每個外掛重新分配資源 ID ,這樣就保證了宿主和外掛的資源不衝突。
-
動態代理註冊:在 Android 中要使用四大元件,都是需要在 manifest 清單中註冊,這樣才可以使用,那如何在不註冊情況也能使用呢,這裡就是用到動態代理機制進行 Hook ,在傳送 AMS 之前用佔坑的元件來欺騙系統,透過認證後,再把真正要呼叫的元件還原回來,達到瞞天過海目的。
架構圖:
優點如下:
-
所有外掛支援內建宿主包中。
-
外掛的編碼和資源檔案的使用與普通開發應用沒有差別。
-
透過設定 URI ,宿主以及 Native 應用外掛,Web 外掛,線上網頁等能夠方便進行通訊。
支援 Android 、 iOS 、和 Html5 ,三者可以透過同一套 Javascript 介面實現通訊。
缺點如下:
-
暫不支援 Service 的動態註冊,不過這個可以透過將 Service 預先註冊在宿主的 AndroidManifest.xml 檔案中進行規避,因為 Service 的更新頻率通常非常低。
與其他主流框架的區別:
DyLA : Dynamic-load-apk @singwhatiwanna
DiLA : Direct-Load-apk @FinalLody
APF : Android-Plugin-Framework @limpoxe
ACDD : ACDD @bunnyblue
DyAPK : DynamicAPK @TediWang
DPG : DroidPlugin @cmzy, 360
-
功能
-
透明度
VirtualAPK (2017年 6 月 )
VirtualAPK 是滴滴開源的一套外掛化框架,支援幾乎所有的 Android 特性,四大元件方面。
架構圖:
實現思路:
VirtualAPK 對外掛沒有額外的約束,原生的 apk 即可作為外掛。外掛工程編譯生成 apk後,即可透過宿主 App 載入,每個外掛 apk 被載入後,都會在宿主中建立一個單獨的 LoadedPlugin 物件。如下圖所示,透過這些 LoadedPlugin 物件,VirtualAPK 就可以管理外掛並賦予外掛新的意義,使其可以像手機中安裝過的 App 一樣執行。
-
合併宿主和外掛的ClassLoader 需要註意的是,外掛中的類不可以和宿主重覆
-
合併外掛和宿主的資源 重設外掛資源的 packageId,將外掛資源和宿主資源合併
-
去除外掛包對宿主的取用 構建時透過 Gradle 外掛去除外掛對宿主的程式碼以及資源的取用
特性如下:
四大元件均不需要在宿主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。
優秀的相容性
-
相容市面上幾乎所有的Android手機,這一點已經在滴滴出行客戶端中得到驗證。
-
資源方面適配小米、Vivo、Nubia 等,對未知機型採用自適應適配方案。
-
極少的 Binder Hook,目前僅僅 hook了兩個Binder:AMS和IContentProvider,hook 過程做了充分的相容性適配。
-
外掛執行邏輯和宿主隔離,確保框架的任何問題都不會影響宿主的正常執行。
入侵性極低
-
外掛開發等同於原生開發,四大元件無需繼承特定的基類;
-
精簡的外掛包,外掛可以依賴宿主中的程式碼和資源,也可以不依賴;
-
外掛的構建過程簡單,透過 Gradle 外掛來完成外掛的構建,整個過程對開發者透明。
如下是 VirtualAPK 和主流的外掛化框架之間的對比。
RePlugin (2017 年 7 月)
RePlugin是一套完整的、穩定的、適合全面使用的,佔坑類外掛化方案,由360手機衛士的RePlugin Team研發,也是業內首個提出”全面外掛化“(全面特性、全面相容、全面使用)的方案。
框架圖:
主要優勢有:
-
極其靈活:主程式無需升級(無需在Manifest中預埋元件),即可支援新增的四大元件,甚至全新的外掛
-
非常穩定:Hook 點僅有一處(ClassLoader),無任何 Binder Hook!如此可做到其崩潰率僅為“萬分之一”,並完美相容市面上近乎所有的 Android ROM。
-
特性豐富:支援近乎所有在“單品”開發時的特性。包括靜態 Receiver、 Task-Affinity 坑位、自定義 Theme、行程坑位、AppCompat、DataBinding等。
-
易於整合:無論外掛還是主程式,只需“數行”就能完成接入。
-
管理成熟:擁有成熟穩定的“外掛管理方案”,支援外掛安裝、升級、解除安裝、版本管理,甚至包括行程通訊、協議版本、安全校驗等。
數億支撐:有 360 手機衛士龐大的數億使用者做支撐,三年多的殘酷驗證,確保App用到的方案是最穩定、最適合使用的。
實戰
主要是測試各個框架之間上手的容易度如何,並做不同對比,這邊寫了兩個 Demo 例子,一個是基於 Small 框架,一個基於 VirtualAPK 框架,從中能看出不同。
Small 實踐
要取用官方最新的版本,不然在宿主和外掛合併build.gradle 的時候會出現一個 BUG,這是個坑位,註意行走。其次在模組命名上要遵循一定的規則,比如業務模組用 app. ,公共庫模組用 lib. ,相當於包名 .app.,.lib. 。每次在外掛中新增一個 activity 元件,都需要在宿主中配置路由,然後在重新編譯外掛一遍,不然直接執行的話,在宿主中是找到新新增的 activity 元件,會報該元件沒在系統 manifest 中,所以每次新增或修改建議外掛都重新編譯一遍。官方里說了,對於 Service 支援不太友好,就沒去實踐了。
VirtualAPK 實踐
有個坑需要註意的是構建環境,官方說明是要以下版本環境,Gradle 2.14.1 和 com.android.tools.build 2.1.3, 之前編譯的是用最新的Gradle版本,導致一直有問題,至於是否有其他問題,可以看官方檔案。
具體程式碼
Small Demo :https://github.com/cr330326/MySmall
VirtualAPK Demo :https://github.com/cr330326/MyVirtualAPKDemo
小結
正如開頭所說,要實現外掛化的框架,無非就是解決那典型的三個問題:外掛程式碼如何載入、外掛中的元件生命週期如何管理、外掛資源和宿主資源衝突怎麼辦。每個框架針對這三個問題,都有不同的解決方案,同時呢,根據時間順序,後出來的框架往往都會吸收已經出的框架精髓,進而修複那些比較有里程碑意義框架的不足。但這些框架的核心思想都是用到了代理樣式,有的在錶面層進行代理,有的則在系統應用層進行代理,透過代理達到替換和瞞天過海,最終讓 Android 系統誤以為呼叫外掛功能和呼叫原生開發的功能是一樣的,進而達到外掛化和原生相容程式設計的目的。
●編號347,輸入編號直達本文
●輸入m獲取到文章目錄
Java程式設計
更多推薦《18個技術類公眾微信》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。