Android動態加載技術 簡單易懂的介紹方式
基本信息
-
Author:kaedea
</li> -
GitHub:android-dynamical-loading
</li> </ul>我們很早開始就在Android項目中采用了動態加載技術,主要目的是為了達到讓用戶不用重新安裝APK就能升級應用的功能,這樣一來不但可以大大提高應用新版本的覆蓋率,也減少了服務器對舊版本接口兼容的壓力,同時如果也可以快速修復一些線上的BUG。
這種技術并不是常規的Android開發方式,早期并沒有完善的解決方案。從“不明覺厲”到穩定投入生產,一直以來我總想對此編寫一些文檔,這也是這篇日志的由來,沒想到前前后后竟然拖沓著編輯了一年多,所以日志里有的地方思路可能有點銜接得不是很好,日后我會慢慢修正。
技術背景
通過服務器配置一些參數,Android APP獲取這些參數再做出相應的邏輯,這是常有的事情。
比如現在大部分APP都有一個啟動頁面,如果到了一些重要的節日,APP的服務器會配置一些與時節相關的圖片,APP啟動時候再把原有的啟動圖換成這些新的圖片,這樣就能提高用戶的體驗了。
再則,早期個人開發者在安卓市場上發布應用的時候,如果應用里包含有廣告,那么有可能會審核不通過。那么就通過在服務器配置一個開關,審核應用的時候先把開關關閉,這樣應用就不會顯示廣告了;安卓市場審核通過后,再把服務器的廣告開關給打開,以這樣的手段規避市場的審核。
道高一尺魔高一丈。安卓市場開始掃描APK里面的Manifest甚至dex文件,查看開發者的APK包里是否有廣告的代碼,如果有就有可能審核不通過。
通過服務器怕配置開關參數的方法行不通了,開發者們開始想,“既然這樣,能不能先不要在APK寫廣告的代碼,在用戶運行APP的時候,再從服務器下載廣告的代碼,運行,再現實廣告呢?”。答案是肯定的,這就是動態加載:
在程序運行的時候,加載一些程序自身原本不存在的可執行文件并運行這些文件里的代碼邏輯。
</blockquote>看起來就像是應用從服務器下載了一些代碼,然后再執行這些代碼!
傳統PC軟件中的動態加載技術
動態加載技術在PC軟件領域廣泛使用,比如輸入法的截圖功能。剛剛安裝好的輸入法軟件可能沒有截圖功能,當你第一次使用的時候,輸入法會先從服務器下載并安裝截圖軟件,然后再執行截圖功能。
此外,許多的PC軟件的安裝目錄里面都有大量的DLL文件(Dynamic Link Library),PC軟件則是通過調用這些DLL里面的代碼執行特定的功能的,這就是一種動態加載技術。
熟悉Java的同學應該比較清楚,Java的可執行文件是Jar,運行在虛擬機上JVM上,虛擬機通過ClassLoader加載Jar文件并執行里面的代碼。所以Java程序也可以通過動態調用Jar文件達到動態加載的目的。
Android應用的動態加載技術
Android應用類似于Java程序,只不過虛擬機換成了Dalvik/ART,而Jar換成了Dex。在Android APP運行的時候,我們是不是也可以通過下載新的應用,或者通過調用外部的Dex文件來實現動態加載呢?
然而在Android上實現起來可沒那么容易,如果下載一個新的APK下來,不安裝這個APK的話可不能運行。如果讓用戶手動安裝完這個APK再啟動,那可不像是動態加載,純粹就是用戶安裝了一個新的應用,然后再啟動這個新的應用。
動態調用外部的Dex文件則是完全沒有問題的。在APK文件中往往有一個或者多個Dex文件,我們寫的每一句代碼都會被編譯到這些文件里面,Android應用運行的時候就是通過執行這些Dex文件完成應用的功能的。雖然一個APK一旦構建出來,我們是無法更換里面的Dex文件的,但是我們可以通過加載外部的Dex文件來實現動態加載,這個外部文件可以放在外部存儲,或者從網絡下載。
動態加載的定義
開始正題之前,在這里可以先給動態加載技術做一個簡單的定義。真正的動態加載應該是
-
應用在運行的時候通過加載一些本地不存在的可執行文件實現一些特定的功能
</li> -
這些可執行文件是可以替換的
</li> -
更換靜態資源(比如換啟動圖、換主題、或者用服務器參數開關控制廣告的隱藏現實等)不屬于動態加載
</li> -
Android中動態加載的核心思想是動態調用外部的Dex文件,極端的情況下,Android APK自身帶有的Dex文件只是一個程序的入口(或者說空殼),所有的功能都通過從服務器下載最新的Dex文件完成
</li> </ol>Android動態加載的類型
Android項目中,動態加載按技術實現上的區別大致可以分為兩種:
-
動態加載.so庫
</li> -
動態加載.dex/jar/apk(現在動態加載普遍說的是這種)
</li> </ol>其一,Android中NDK中其實就使用了動態加載,動態加載.so庫并通過JNI調用其封裝好的方法。后者一般是由C++編譯而成,運行在Native層,效率會比執行在虛擬機的Java代碼高很多,所以Android中經常通過動態加載.so庫來完成一些對性能比較有需求的工作(比如T9搜索、或者Bitmap的解碼、圖片高斯模糊處理等)。此外,由于.so庫是由C++編譯而來的,只能被反編譯成匯編代碼,相比Smali更難被破解,因此.so庫也可以被用于安全領域。需要特別說明的是,一般情況下我們是把.so庫一并打包在APK內部的,但是.so庫其實也是可以從外部存儲文件加載的。
其二,“基于ClassLoader的動態加載.dex/jar/apk”就是我們上面提到的“在Android中動態加載由Java代碼編譯而來的Dex包并執行其中的代碼邏輯”,這是常規Android開發比較少用到的一種技術,目前網絡上大多文章說到的動態加載指的就是這種(后面我們談到“動態加載”如果沒有特別指定,均默認是這種)。
Android項目中,所有Java代碼都會被編譯成Dex包,Android應用運行時,就是通過執行Dex包里的業務代碼邏輯來工作的。使用動態加載技術可以在Android應用運行時加載外部的Dex包,而通過網絡下載新的Dex包并替換原有的Dex包就可以達到不安裝新APK文件就升級應用(改變代碼邏輯)的目的。同時,使用動態加載技術,一般來說會使得Android開發工作變得更加復雜,這中開發方式不是官方推薦的,不是目前主流的Android開發方式,Github和StackOverflow上面外國的開發者也對此不是很感興趣,外國相關的教程更是少得可憐,目前只有在大天朝才有比較深入的研究和應用,特別是一些SDK組件項目和BAT家族的項目上,Github上的相關開源項目基本是國人在維護,偶爾有幾個外國人請求更新英文文檔。
Android動態加載的大致過程
無論上面的哪種動態加載,其實基本原理都是在程序運行時加載一些外部的可執行的文件,然后調用這些文件的某個方法執行業務邏輯。需要說明的是,因為文件是可執行的(so庫或者dex包,也就是一種動態鏈接庫),出于安全問題,Android并不允許直接加載手機外部存儲這類noexec(不可執行)存儲路徑上的可執行文件。
對于這些外部的可執行文件,在Android應用中調用它們前,都要先把他們拷貝到data/packagename/內部儲存文件路徑,確保庫不會被第三方應用惡意修改或攔截,然后再將他們加載到當前的運行環境并調用需要的方法執行相應的邏輯,從而實現動態調用。
動態加載的大致過程就是:
-
把可執行文件(.so/dex/jar/apk)拷貝到應用APP內部存儲
</li> -
加載可執行文件
</li> -
調用具體的方法執行業務邏輯
</li> </ol> </blockquote>以下分別對這兩種動態加載的實現方式做比較深入的介紹。
動態加載.so庫
動態加載.so庫應該就是Android最早期的動態加載了,不過.so庫不僅可以存放在APK文件內部,還可以存放在外部存儲。Android開發中,更換.so庫的情形并不多,但是可以通過把.so庫挪動到APK外部,減少APK的體積,畢竟許多.so文件的體積可是非常大的。
詳細的應用方式請參考后續日志 Android動態加載補充 加載SD卡的SO庫
動態加載.dex/jar/apk文件
我們經常講到的那種Android動態加載技術就是這種,后面我們談到“動態加載”如果沒有特別指定,均默認是這。為了方便區分概念,討論之前先要闡述一些術語。
主APK和插件APK
-
主APK:主項目APK、宿主APK(Host APK),也就是我們希望采用動態加載技術的主項目;
</li> -
插件APK:Plugin,從主項目分離開來,我們能通過動態加載加載到主項目里面來的模塊,一個主APK可以同時加載多個插件APK;
</li> </ul>基礎知識:ClassLoader和Dex
動態加載.dex/jar/apk文件的基礎是類加載器ClassLoader,它的包路徑是java.lang,由此可見其重要性,虛擬機就是通過類加載器加載期需要用的Class,這是Java程序運行的基礎。關于類加載器ClassLoader的工作機制,請參考 Android動態加載基礎 ClassLoader的工作機制
現在網上有多種基于ClassLoader的Android動態加載的開源項目,大部分核心思想都殊途同歸,按照復雜程度以及具體實現的框架,大致可以分為以下三種模式。
簡單的動態加載模式
Android在運行時使用ClassLoader動態加載外部的Dex文件非常簡單,不用覆蓋安裝新的APK,就可以更改APP的代碼邏輯。但是 Android卻很難使用插件APK里的res資源,這意味著無法使用新的XML布局等資源,同時由于無法更改本地的Manifest清單文件,所以無法啟動新的Activity等組件。
不過可以先把要用到的全部res資源都放到主APK里面,同時把所有需要的Activity先全部寫進Manifest里,只通過動態加載更新代碼,不更新res資源,如果需要改動UI界面,可以通過使用純Java代碼創建布局的方式繞開XML布局,也可以使用Fragment代替 Activity。
某種程度上,簡單的動態加載功能已經能滿足部分業務需求了,特別是一些早期的Android項目,那時候Android的技術還不是很成熟,而且早期的Android設備更是有大量的兼容性問題(做過Android1.6兼容的同學可能深有體會),只有這種簡單的加載方式才能穩定運行。這種模式的框架比較適用一些UI變化比較少的項目,比如游戲SDK,基本就只有登陸、注冊界面,而且基本不會變動,更新的往往只有代碼邏輯。
詳細的應用方式請參考后續日志 Android動態加載入門 簡單加載模式
使用代理Activity模式
從這個階段開始就稍微有點“黑科技”的味道了,比如我們可以通過動態加載,讓現在的Android應用啟動一些“新”的Activity,甚至不用安裝就啟動一個“新”的APK(原來的APK叫“主APK”,新的APK稱為“插件APK”)。主APK需要先注冊一個空殼的Activity用于代理執行插件APK的Activity的生命周期。
主要有以下特點
-
主APK可以啟動未安裝的插件APK;
</li> -
插件APK也可以作為一個普通APK安裝并且啟動;
</li> -
插件APK可以調用主APK里的一些功能;
</li> -
主APK和插件APK都要接入一套指定的接口才能實現以上功能;
</li> </ol>詳細的應用方式請參考后續日志 Android動態加載進階 代理Activity模式
動態創建Activity模式
天了嚕,到了這個階段就真的是“黑科技”的領域了,可以試想“從網絡下載一個Flappy Bird的APK,不用安裝就直接運行游戲”,或者“同時運行兩個甚至多個微信”。
這個階段有以下特點
-
主APK可以啟動一個未安裝的插件APK;
</li> -
插件APK可以是任意第三方APK,無需接入指定的接口;
</li> </ol>詳細的應用方式請參考后續日志 Android動態加載黑科技 動態創建Activity模式
為什么我們要使用動態加載技術
說實話,我也不知道,產品要求的!(警察蜀黍就是他,他只問我能不能實現,并木有問我實現起來難不難…)
Android開發中,最先使用動態加載技術的應該是SDK項目吧。現在網上有一大堆Android SDK項目,比如Google的Goole Play Service,向開發者提供支付、地圖等功能,又比如一些Android游戲市場的SDK,用于向游戲開發者提供賬號和支付功能。和普通Android 應用一樣,這些SDK項目也是要升級的,比如現在別人的Android應用里使用了我們的SDK1.0版本,然后發布到安卓市場上去。現在我們發現 SDK1.0有一些緊急的BUG,所以升級了一個SDK1.1版本,沒辦法,只能讓人家重新接入1.1版本再發布到市場。萬一我們有SDK1.2、1.3 等版本呢,本來讓人家每個版本都重新接入也無可厚非,不過產品可關心體驗啊,他就會問咯,“雖然我不懂技術,但是我想知道有沒有辦法,能讓人家只接入一次我們的SDK,以后我們發布新的SDK版本的時候他們的項目也能跟著自動升級?”,答曰,“有,使用動態加載的技術能辦到,只不過(開發工作量會劇增…)”,“那就用吧,我們要把產品的體驗做到極致”。
好吧,我并沒有黑產品的意思,現在團隊的產品也不錯,不過與上面類似的對話確實發生在13年我的項目里。這里提出來只是為了強調一下Android項目中采用動態加載技術的作用以及由此帶來的代價。
作用與代價
凡事都有兩面性,特別是這種非官方支持的非常規開發方式,在采用前一定要權衡清楚其作用與代價。如果決定了要采用動態加載技術,個人推薦可以現在實際項目的一些比較獨立的模塊使用這種框架,把遇到的一些問題解決之后,再慢慢引進到項目的核心模塊;如果遇到了一些無法跨越的問題,要有能夠迅速投入生產的替代方案。
作用
-
規避APK覆蓋安裝的升級過程,提高用戶體驗,順便能規避一些安卓市場的限制;
</li> -
動態修復應用的一些緊急BUG,做好最后一道保障;
</li> -
當應用體積太龐大的時候,可以把一些模塊通過動態加載以插件的形式分割出去,這樣可以減少主項目的體積,提高項目的編譯速度,也能讓主項目和插件項目并行開發;
</li> -
插件模塊可以用懶加載的方式在需要的時候才初始化,從而提高應用的啟動速度;
</li> -
從項目管理上來看,分割插件模塊的方式從項目級別做到了代碼分離,大大降低模塊之間的耦合度,如果出現BUG也容易定位問題;
</li> -
在Android應用上推廣其他應用的時候,可以使用動態加載技術讓用戶優先體驗新應用的功能,而不用下載并安裝全新的APK;
</li> -
減少主項目DEX的方法數,65535問題徹底成為歷史(雖然現在在Android Studio中很容易開啟MultiDex,這個問題也不難解決);
</li> </ol>代價
-
開發方式可能變得比較詭異、繁瑣,與常規開發方式不同;
</li> -
隨著動態加載框架復雜程度的加深,項目的構建過程也變得復雜,有可能要主項目和插件項目分別構建,再整合到一起;
</li> -
由于插件項目是獨立開發的,當主項目加載插件運行時,插件的運行環境已經完全不同,代碼邏輯容易出現BUG,而且在主項目中調試插件十分繁瑣;
</li> -
非常規的開發方式,有些框架使用反射強行調用了部分Android系統Framework層的代碼,部分Android ROM可能已經改動了這些代碼,所以有存在兼容性問題的風險,特別是在一些古老Android設備和部分三星的手機上;
</li> -
采用動態加載的插件在使用系統資源(特別是主題)時經常有一些兼容性問題,特別是部分三星的手機;
</li> </ol>其他動態修改代碼的技術
上面說到的都是基于ClassLoader的動態加載技術(除了加載SO庫外),使用ClassLoader的一個特點就是,如果程序不重新啟動,加載過一次的類就無法重新加載。因此,如果使用ClassLoader來動態升級APP或者動態修復BUG,都需要重新啟動APP才能生效。
除了使用ClassLoader外,還可以使用jni hook的方式修改程序的執行代碼。前者是在虛擬機上操作的,而后者做的已經是Native層級的工作了,直接修改應用運行時的內存地址,所以使用jni hook的方式時,不用重新應用就能生效。
目前采用jni hook方案的項目中比較熱門的有阿里的dexposed和AndFix,有興趣的同學可以參考 各大熱補丁方案分析和比較。
動態加載開源項目
dynamic-load-apk
原文 http://segmentfault.com/a/1190000004062866
android-pluginmgr
Direct-Load-apk
360 DroidPlugin
攜程網 DynamicAPK
Nuwa
-
-
-
-
-
-
-
-