APK動態載入框架(DL)解析
轉載請註明出處:http://blog.csdn.net/singwhatiwanna/article/details/39937639 (來自singwhatiwanna的csdn部落格)
前言
好久沒有釋出新的文章,這次打算髮表一下我這幾個月的一個核心研究成果:APK動態載入框架(DL)。這段時間我致力於github的開源貢獻,開源了2個比較有用且有意義的專案,一個是PinnedHeaderExpandableListView,另一個是APK動態載入框架。具體可以參見我的github:https://github.com/singwhatiwanna
本次要介紹的是APK動態載入框架(DL),這個專案除了我以外,還有兩個共同開發者:田嘯(時之沙),宋思宇。
為了更好地理解本文,你需要首先閱讀Android apk動態載入機制的研究這一系列文章,分別為:
Android apk動態載入機制的研究(二):資源載入和activity生命週期管理
另外,這個開源專案我起了個名字,叫做DL。本文中的DL均指APK動態載入框架。
專案地址
https://github.com/singwhatiwanna/dynamic-load-apk,歡迎star和fork。
執行效果圖:
意義
這裡說說這個開源專案的意義。首先要說的是動態載入技術(或者說外掛化)在技術驅動型的公司中扮演這相當重要的角色,當專案越來越龐大的時候,需要通過外掛化來減輕應用的記憶體和cpu佔用,還可以實現熱插拔,即在不釋出新版本的情況下更新某些模組。
我幾個月前開始進行這項技術的研究,當時查詢了很多資料,沒有找到很好的開源。目前淘寶、微信等都有成熟的動態載入框架,包括apkplug,但是它們都是不開源的。還有github上有一個開源專案AndroidDynamicLoader,其思想是通過Fragment 以及 schema的方式實習的,這是一種可行的技術方案,但是還有限制太多,這意味這你的activity必須通過Fragment去實現,這在activity跳轉和靈活性上有一定的不便,在實際的使用中會有一些很奇怪的bug不好解決,總之,這還是一種不是特別完備的動態載入技術。然後,我發現,目前針對動態載入這一塊成熟的開源基本還是空白的,不管是國內還是國外。而在公司內部,動態載入作為一項核心技術,也不可能是初級開發人員所能夠接觸到的,於是,我決定做一個成熟點的開源,期待能填補這一塊空白。
DL功能介紹
DL支援很多特性,而這些特性使得外掛的開發過程變得透明、高效。
1. plugin無需安裝即可由宿主調起。
2. 支援用R訪問plugin資源3. plugin支援Activity和FragmentActivity(未來還將支援其他元件)
4. 基本無反射呼叫
5. 外掛安裝後仍可獨立執行從而便於除錯
6. 支援3種plugin對host的呼叫模式:(1)無呼叫(但仍然可以用反射呼叫)。
(2)部分呼叫,host可公開部分介面供plugin呼叫。 這前兩種模式適用於plugin開發者無法獲得host程式碼的情況。
(3)完全呼叫,plugin可以完全呼叫host內容。這種模式適用於plugin開發者能獲得host程式碼的情況。
7. 只需引入DL的一個jar包即可高效開發外掛,DL的工作過程對開發者完全透明
8. 支援android2.x版本
架構解析
如果大家閱讀過本文頭部提到的兩篇文章,那麼對DL的架構應該有大致的瞭解,本文就不再從頭開始介紹了,而是從如下變更的幾方面進行解析,這些優化使得DL的功能和之前比起來更加強大更加易用使用易於擴充套件。
1. DL對activity生命週期管理的改進
2. DL對類載入器的支援(DLClassLoader)
3. DL對宿主(host)和外掛(plugin)通訊的支援
4. DL對外掛獨立執行的支援
5. DL對activity隨意跳轉的支援(DLIntent)
6. DL對外掛管理的支援(DLPluginManager)
其中5和6屬於加強功能,目前正在dev分支上進行開發(本文暫不介紹),其他功能均在穩定版分支master上。
DL對activity生命週期管理的改進
大家知道,DL最開始的時候採用反射去管理activity的生命週期,這樣存在一些不便,比如反射程式碼寫起來複雜,並且過多使用反射有一定的效能開銷。針對這個問題,我們採用了介面機制,將activity的大部分生命週期方法提取出來作為一個介面(DLPlugin),然後通過代理activity(DLProxyActivity)去呼叫外掛activity實現的生命週期方法,這樣就完成了外掛activity的生命週期管理,並且沒有采用反射,當我們想增加一個新的生命週期方法的時候,只需要在介面中宣告一下同時在代理activity中實現一下即可,下面看一下程式碼:
介面DLPlugin
- public interface DLPlugin {
- public void onStart();
- public void onRestart();
- public void onActivityResult(int requestCode, int resultCode, Intent data);
- public void onResume();
- public void onPause();
- public void onStop();
- public void onDestroy();
- public void onCreate(Bundle savedInstanceState);
- public void setProxy(Activity proxyActivity, String dexPath);
- public void onSaveInstanceState(Bundle outState);
- public void onNewIntent(Intent intent);
- public void onRestoreInstanceState(Bundle savedInstanceState);
- public boolean onTouchEvent(MotionEvent event);
- public boolean onKeyUp(int keyCode, KeyEvent event);
- public void onWindowAttributesChanged(LayoutParams params);
- public void onWindowFocusChanged(boolean hasFocus);
- public void onBackPressed();
- }
- ...
- @Override
- protected void onStart() {
- mRemoteActivity.onStart();
- super.onStart();
- }
- @Override
- protected void onRestart() {
- mRemoteActivity.onRestart();
- super.onRestart();
- }
- @Override
- protected void onResume() {
- mRemoteActivity.onResume();
- super.onResume();
- }
- @Override
- protected void onPause() {
- mRemoteActivity.onPause();
- super.onPause();
- }
- @Override
- protected void onStop() {
- mRemoteActivity.onStop();
- super.onStop();
- }
- ...
DL對類載入器的支援
為了更好地對多外掛進行支援,我們提供了一個DLClassoader類,專門去管理各個外掛的DexClassoader,這樣,同一個外掛就可以採用同一個ClassLoader去載入類從而避免多個classloader載入同一個類時所引發的型別轉換錯誤。
- public class DLClassLoader extends DexClassLoader {
- private static final String TAG = "DLClassLoader";
- private static final HashMap<String, DLClassLoader> mPluginClassLoaders = new HashMap<String, DLClassLoader>();
- protected DLClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
- super(dexPath, optimizedDirectory, libraryPath, parent);
- }
- /**
- * return a available classloader which belongs to different apk
- */
- public static DLClassLoader getClassLoader(String dexPath, Context context, ClassLoader parentLoader) {
- DLClassLoader dLClassLoader = mPluginClassLoaders.get(dexPath);
- if (dLClassLoader != null)
- return dLClassLoader;
- File dexOutputDir = context.getDir("dex", Context.MODE_PRIVATE);
- final String dexOutputPath = dexOutputDir.getAbsolutePath();
- dLClassLoader = new DLClassLoader(dexPath, dexOutputPath, null, parentLoader);
- mPluginClassLoaders.put(dexPath, dLClassLoader);
- return dLClassLoader;
- }
- }
DL對宿主(host)和外掛(plugin)通訊的支援
這一點很重要,因為往往宿主需要和外掛進行各種通訊,因此DL對宿主和外掛的通訊做了很好的支援,目前總共有3中模式,如下圖所示:
下面分別介紹上述三種模式,針對上述三種模式,我們分別提供了3組例子,其中:
1. depend_on_host:外掛完全依賴宿主的模式,適合於能夠能到宿主的原始碼的情況
其中host指宿主工程,plugin指外掛工程
2. depend_on_interface:外掛部分依賴宿主的模式,或者說外掛依賴宿主提供的介面,適合能夠拿到宿主的介面的情況
其中host指宿主工程,plugin指外掛工程,common指介面工程
3. main:外掛不依賴宿主的模式,這是DL推薦的模式
其中host指宿主工程,plugin指外掛工程
模式1:這是DL推薦的模式,對應的工程目錄為main。在這種模式下,宿主和外掛不需要通訊,兩者是獨立開發的,宿主引用DL的jar包(dl-lib.jar),外掛也需要引用DL的jar包,但是不能放入到外掛工程的libs目錄下面,換句話說,就是外掛編譯的時候依賴jar包但是打包成apk的時候不要把jar包打進去,這是因為,dl-lib.jar已經在宿主工程中存在了,如果外掛中也有這個jar包,就會發生類連結錯誤,原因很簡單,記憶體中有兩份一樣的類,重複了。至於support-v4也是同樣的道理。對於eclipse很簡單,只需要在外掛工程中建立一個目錄,比如external-jars,然後把dl-lib.jar和support-v4.jar放進去,同時在.classpath中追加如下兩句即可:
<classpathentry kind="lib" path="external-jars/dl-lib.jar"/>
<classpathentry kind="lib" path="external-jars/android-support-v4.jar"/>
這樣,編譯的時候就能夠正常進行,但是打包的時候,就不會把上面兩個jar包打入到外掛apk中。
至於ant環境和gradle,解決辦法不一樣,具體方法後面再補上,但是思想都是一樣的,即:外掛apk中不要打入上述2個jar包。
模式2:外掛部分依賴宿主的模式,或者說外掛依賴宿主提供的介面,適合能夠拿到宿主的介面的情況。在這種模式下,宿主放出一些介面並實現一些介面,然後給外掛呼叫,這樣外掛就可以訪問宿主的一些服務等
模式3:外掛完全依賴宿主的模式,適合於能夠能到宿主的原始碼的情況。這種模式一般多用在公司內部,外掛可以訪問宿主的所有程式碼,但是,這樣外掛和宿主的耦合比較高,宿主一動,外掛就必須動,比較麻煩
具體採用哪種方式,需要結合實際情況來選擇,一般來說,如果是宿主和外掛不是同一個公司開發,建議選擇模式1和模式2;如果宿主和外掛都在同一個公司開發,那麼選擇哪個都可以。從DL的實現出發,我們推薦採用模式1,真的需要通訊的話採用模式2,儘量不要採用模式3.
DL對外掛獨立執行的支援
為了便於除錯,採用DL所開發的外掛都可以獨立執行,當然,這要分情況來說:
對於模式1,如果外掛想獨立執行,只需要把external-jars下的jar包拷貝一份到外掛的libs目錄下即可
對於模式2,只需要提供一個宿主介面的預設實現即可
對於模式3,只需要apk打包時把所引用的宿主程式碼打包進去即可,具體方式可以參看sample/depend_on_host目錄。
在開發過程中,應該先開啟外掛的獨立執行功能以便於除錯,等功能開發完畢後再將其外掛化。
DLIntent和DLPluginManager
這兩項都屬於加強功能,目前正在dev分支進行code review,大家感興趣可以去dev分支上檢視,等驗證通過即merge到穩定版master分支。
DLIntent:通過DLIntent來完成activity的無約束調起
DLPluginManager:對宿主的所有外掛提供綜合管理功能。
開發規範
目前DL已經達到了第一個穩定版,經過大量機型的驗證,目前得出的結論是DL是可靠的(相容到android2.x),可以用在實際的應用開發中。但是,我們知道,動態載入是一個技術壁壘,其很難達到一種完美的狀態,畢竟,讓一個apk不安裝跑起來,這是多麼不可思議的事情。因此,希望大家辯證地去看這個問題,下面列出我們目前還不支援的功能,或者說是一種開發規範吧,希望大家在開發過程中去遵守這個規範,這樣才能讓外掛穩定地跑起來。
DL 1.0開發規範:
1. 目前不支援service
2. 目前只支援動態註冊廣播
3. 目前支援Activity和FragmentActivity,這也是常用的activity
4. 目前不支援外掛中的assets
5. 呼叫Context的時候,請適當使用that,大部分常用api是不需要用that的,但是一些不常用api還是需要用that來訪問。that是apk中activity的基類BaseActivity系列中的一個成員,它在apk安裝執行的時候指向this,而在未安裝的時候指向宿主程式中的代理activity,由於that的動態分配特性,通過that去呼叫activity的成員方法,在apk安裝以後仍然可以正常執行。
6. 慎重使用this,因為this指向的是當前物件,即apk中的activity,但是由於activity已經不是常規意義上的activity,所以this是沒有意義的,但是,當this表示的不是Context物件的時候除外,比如this表示一個由activity實現的介面。
希望能夠給大家帶來一些幫助,希望大家多多支援!
本開源專案地址:https://github.com/singwhatiwanna/dynamic-load-apk,歡迎大家star和fork。
相關文章
- DLL動態庫動態載入
- 動態載入UserControl
- Apk_動態除錯方案APK除錯
- python動態載入(三)Python
- QLibrary 載入動態庫
- vue 動態載入元件Vue元件
- goloader - golang動態載入Golang
- Java動態載入類Java
- 指令碼的動態載入指令碼
- 使用dlopen載入動態庫
- ListView動態載入資料View
- Composer 自動載入原始碼解析原始碼
- echarts遷移圖動態載入Echarts
- OrchardCore 如何動態載入模組?
- 如何能看到框架檔案動態載入順序與執行情況?框架
- 微前端框架single-spa子應用載入解析前端框架
- 【Android APK】解析SD卡上的APK檔案AndroidAPKSD卡
- javascript如何動態載入js檔案JavaScriptJS
- vue如何動態載入本地圖片Vue地圖
- Android native層動態載入so庫Android
- 動態載入的一些坑
- 全面解析Pytorch框架下模型儲存,載入以及凍結PyTorch框架模型
- 啟動優化之動態庫延遲載入優化
- Drools與動態載入規則檔案
- SyntaxHighlighter 頁面動態js載入方式整理JS
- 載入動態連結庫——dlopen dlsym dlclose
- vue後臺管理之動態載入路由Vue路由
- Scrapy框架-通過scrapy_splash解析動態渲染的資料框架
- 效能優化 (八) APK 加固之動態替換 Application優化APKAPP
- Viper解析&載入配置
- 動態框架方法論框架
- vue 動態選單以及動態路由載入、重新整理採的坑Vue路由
- Protobuf 動態載入 .proto 檔案並操作 Message
- ElementUI級聯選擇器動態載入DemoUI
- Unity3D動態載入FBX檔案Unity3D
- JavaScript系列:動態建立iframe並載入頁面JavaScript
- jQuery 動態載入下拉框選項(Django)jQueryDjango
- 優雅的實現動態載入 css、jsCSSJS
- Jquery Datatables (2) 動態載入資料型別jQuery資料型別