Android動態加載技術 簡單易懂的介紹方式

Last Edit: 2016-2-10java

基本信息


咱們很早開始就在Android項目中採用了動態加載技術,主要目的是爲了達到讓用戶不用從新安裝APK就能升級應用的功能(特別是 SDK項目),這樣一來不但能夠大大提升應用新版本的覆蓋率,也減小了服務器對舊版本接口兼容的壓力,同時若是也能夠快速修復一些線上的BUG。github

這種技術並非常規的Android開發方式,早期並無完善的解決方案。從「不明覺厲」到穩定投入生產,一直以來我總想對此編寫一些文檔,這也是這篇日誌的由來,沒想到前先後後居然拖沓着編輯了一年多,因此日誌裏有的地方思路可能有點銜接得不是很好,若是有修正建議請直接回復。segmentfault

技術背景

經過服務器配置一些參數,Android APP獲取這些參數再作出相應的邏輯,這是常有的事情。安全

好比如今大部分APP都有一個啓動頁面,若是到了一些重要的節日,APP的服務器會配置一些與時節相關的圖片,APP啓動時候再把原有的啓動圖換成這些新的圖片,這樣就能提升用戶的體驗了。服務器

再則,早期我的開發者在安卓市場上發佈應用的時候,若是應用裏包含有廣告,那麼有可能會審覈不經過。那麼就經過在服務器配置一個開關,審覈應用的時候先把開關關閉,這樣應用就不會顯示廣告了;安卓市場審覈經過後,再把服務器的廣告開關給打開,以這樣的手段規避市場的審覈。微信

道高一尺魔高一丈。安卓市場開始掃描APK裏面的Manifest甚至dex文件,查看開發者的APK包裏是否有廣告的代碼,若是有就有可能審覈不經過。網絡

經過服務器怕配置開關參數的方法行不通了,開發者們開始想,「既然這樣,能不能先不要在APK寫廣告的代碼,在用戶運行APP的時候,再從服務器下載廣告的代碼,運行,再現實廣告呢?」。答案是確定的,這就是動態加載:app

在程序運行的時候,加載一些程序自身本來不存在的可執行文件並運行這些文件裏的代碼邏輯。

看起來就像是應用從服務器下載了一些代碼,而後再執行這些代碼!

傳統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文件來實現動態加載,這個外部文件能夠放在外部存儲,或者從網絡下載。

動態加載的定義

開始正題以前,在這裏能夠先給動態加載技術作一個簡單的定義。真正的動態加載應該是

  1. 應用在運行的時候經過加載一些本地不存在的可執行文件實現一些特定的功能;

  2. 這些可執行文件是能夠替換的;

  3. 更換靜態資源(好比換啓動圖、換主題、或者用服務器參數開關控制廣告的隱藏現實等)不屬於 動態加載;

  4. Android中動態加載的核心思想是動態調用外部的 dex文件,極端的狀況下,Android APK自身帶有的Dex文件只是一個程序的入口(或者說空殼),全部的功能都經過從服務器下載最新的Dex文件完成;

Android動態加載的類型

Android項目中,動態加載技術按照加載的可執行文件的不一樣大體能夠分爲兩種:

  1. 動態加載so庫

  2. 動態加載dex/jar/apk文件(如今動態加載廣泛說的是這種);

其一,Android中NDK中其實就使用了動態加載,動態加載.so庫並經過JNI調用其封裝好的方法。後者通常是由C/C++編譯而成,運行在Native層,效率會比執行在虛擬機層的Java代碼高不少,因此Android中常常經過動態加載.so庫來完成一些對性能比較有需求的工做(好比T9搜索、或者Bitmap的解碼、圖片高斯模糊處理等)。此外,因爲so庫是由C/C++編譯而來的,只能被反編譯成彙編代碼,相比中dex文件反編譯獲得的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開發方式,GithubStackOverflow 上面外國的開發者也對此不是很感興趣,外國相關的教程更是少得可憐,目前只有在大天朝纔有比較深刻的研究和應用,特別是一些SDK組件項目和 BAT家族 的項目上,Github上的相關開源項目基本是國人在維護,偶爾有幾個外國人請求更新英文文檔。

Android動態加載的大體過程

不管上面的哪一種動態加載,其實基本原理都是在程序運行時加載一些外部的可執行的文件,而後調用這些文件的某個方法執行業務邏輯。須要說明的是,由於文件是可執行的(so庫或者dex包,也就是一種動態連接庫),出於安全問題,Android並不容許直接加載手機外部存儲這類noexec(不可執行)存儲路徑上的可執行文件。

對於這些外部的可執行文件,在Android應用中調用它們前,都要先把他們拷貝到data/packagename/內部儲存文件路徑,確保庫不會被第三方應用惡意修改或攔截,而後再將他們加載到當前的運行環境並調用須要的方法執行相應的邏輯,從而實現動態調用。

動態加載的大體過程就是:

  1. 把可執行文件(.so/dex/jar/apk)拷貝到應用APP內部存儲;

  2. 加載可執行文件;

  3. 調用具體的方法執行業務邏輯;


如下分別對這兩種動態加載的實現方式作比較深刻的介紹。

動態加載 so庫

動態加載so庫應該就是Android最先期的動態加載了,不過so庫不只能夠存放在APK文件內部,還能夠存放在外部存儲。Android開發中,更換so庫的情形並很少,可是能夠經過把so庫挪動到APK外部,減小APK的體積,畢竟許多so庫文件的體積但是很是大的。

詳細的應用方式請參考後續日誌 Android動態加載補充 加載SD卡的SO庫

動態加載 dex/jar/apk文件

咱們常常講到的那種Android動態加載技術就是這種,後面咱們談到「動態加載」若是沒有特別指定,均默認是這個。

基礎知識:類加載器ClassLoader和dex文件

動態加載dex/jar/apk文件的基礎是類加載器ClassLoader,它的包路徑是java.lang,因而可知其重要性,虛擬機就是經過類加載器加載其須要用的Class,這是Java程序運行的基礎。

關於類加載器ClassLoader的工做機制,請參考 Android動態加載基礎 ClassLoader的工做機制

如今網上有多種基於ClassLoader的Android動態加載的開源項目,大部分核心思想都異曲同工,按照複雜程度以及具體實現的框架,大體能夠分爲如下三種形式,或者說模式 [1]

簡單的動態加載模式

理解ClassLoader的工做機制後,咱們知道了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[2]須要先註冊一個空殼的Activity用於代理執行插件APK的Activity的生命週期。

主要有如下特色:

  1. 宿主APK能夠啓動未安裝的插件APK;

  2. 插件APK也能夠做爲一個普通APK安裝而且啓動;

  3. 插件APK能夠調用宿主APK裏的一些功能;

  4. 宿主APK和插件APK都要接入一套指定的接口框架才能實現以上功能;

同時也主要有一下幾點限制:

  1. 須要在Manifest註冊的功能都沒法在插件實現,好比應用權限、LaunchMode、靜態廣播等;

  2. 宿主一個代理用的Activity難以知足插件一些特殊的Activity的需求,插件Activity的開發受限於代理Activity;

  3. 宿主項目和插件項目的開發都要接入共同的框架,大多時候,插件須要依附宿主才能運行,沒法獨立運行;

詳細的應用方式請參考後續日誌 Android動態加載進階 代理Activity模式

代理Activity模式的核心在於「使用宿主的一個代理Activity爲插件全部的Activity提供組件工做須要的環境」,隨着代理模式的逐漸成熟,如今還出現了「使用Hack手段給插件的Activity注入環境」的模式,這裏暫時不展開,之後會繼續分析。

咱們目前有投入到生產中的開發方式只有簡單模式和代理模式,在設計的前期遇到很多兼容性的問題,不過好在Android 4.0之後的機型上就比較少了。

動態建立Activity模式

天了嚕,到了這個階段就真的是「黑科技」的領域了,從而使其能夠正常運行。能夠試想「從網絡下載一個Flappy Bird的APK,不用安裝就直接運行遊戲」,或者「同時運行兩個甚至多個微信」。

動態建立Activity模式的核心是「運行時字節碼操做」,如今宿主註冊一個不存在的Activity,啓動插件的某個Activity時都把想要啓動的Activity替換成前面註冊的Activity,從而是後者能正常啓動。

這個模式有如下特色:

  1. 主APK能夠啓動一個未安裝的插件APK;

  2. 插件APK能夠是任意第三方APK,無需接入指定的接口,理所固然也能夠獨立運行;

詳細的應用方式請參考後續日誌 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.二、1.3等版本呢,原本讓人家每一個版本都從新接入也無可厚非,不過產品可關心體驗啊,他就會問咯,「雖然我不懂技術,可是我想知道有沒有辦法,能讓人家只接入一次咱們的SDK,之後咱們發佈新的SDK版本的時候他們的項目也能跟着自動升級?」,答曰,「有,使用動態加載的技術能辦到,只不過(開發工做量會劇增…)」,「那就用吧,咱們要把產品的體驗作到極致」。

好吧,我並無黑產品的意思,如今團隊的產品也不錯,不過與上面相似的對話確實發生在我之前的項目裏。這裏提出來只是爲了強調一下Android項目中採用動態加載技術的 做用 以及由此帶來的 代價

做用與代價

凡事都有兩面性,特別是這種 非官方支持很是規 開發方式,在採用前必定要權衡清楚其做用與代價。若是決定了要採用動態加載技術,我的推薦能夠如今實際項目的一些比較獨立的模塊使用這種框架,把遇到的一些問題解決以後,再慢慢引進到項目的核心模塊;若是遇到了一些沒法跨越的問題,要有可以迅速投入生產的替代方案。

做用

  1. 規避APK覆蓋安裝的升級過程,提升用戶體驗,順便能 規避 一些安卓市場的限制;

  2. 動態修復應用的一些 緊急BUG,作好最後一道保障;

  3. 當應用體積太龐大的時候,能夠把一些模塊經過動態加載以插件的形式分割出去,這樣能夠減小主項目的體積,提升項目的編譯速度,也能讓主項目和插件項目並行開發;

  4. 插件模塊能夠用懶加載的方式在須要的時候才初始化,從而 提升應用的啓動速度

  5. 從項目管理上來看,分割插件模塊的方式作到了 項目級別的代碼分離,大大下降模塊之間的耦合度,同一個項目可以分割出不一樣模塊在多個開發團隊之間 並行開發,若是出現BUG也容易定位問題;

  6. 在Android應用上 推廣 其餘應用的時候,可使用動態加載技術讓用戶優先體驗新應用的功能,而不用下載並安裝全新的APK;

  7. 減小主項目DEX的方法數,65535問題 完全成爲歷史(雖然如今在Android Studio中很容易開啓MultiDex,這個問題也不難解決);

代價

  1. 開發方式可能變得比較詭異、繁瑣,與常規開發方式不一樣;

  2. 隨着動態加載框架複雜程度的加深,項目的構建過程也變得複雜,有可能要主項目和插件項目分別構建,再整合到一塊兒;

  3. 因爲插件項目是獨立開發的,當主項目加載插件運行時,插件的運行環境已經徹底不一樣,代碼邏輯容易出現BUG,並且在主項目中調試插件十分繁瑣;

  4. 很是規的開發方式,有些框架使用反射強行調用了部分Android系統Framework層的代碼,部分Android ROM可能已經改動了這些代碼,因此有存在兼容性問題的風險,特別是在一些古老Android設備和部分三星的手機上;

  5. 採用動態加載的插件在使用系統資源(特別是Theme)時常常有一些兼容性問題,特別是部分三星的手機;

其餘動態修改代碼的技術

上面說到的都是基於ClassLoader的動態加載技術(除了加載SO庫外),使用ClassLoader的一個特色就是,若是程序不從新啓動,加載過一次的類就沒法從新加載。所以,若是使用ClassLoader來動態升級APP或者動態修復BUG,都須要從新啓動APP才能生效。

除了使用ClassLoader外,還可使用jni hook的方式修改程序的執行代碼。前者是在虛擬機上操做的,然後者作的已是Native層級的工做了,直接修改應用運行時的內存地址,因此使用jni hook的方式時,不用從新應用就能生效。

目前採用jni hook方案的項目中比較熱門的有阿里的dexposed和AndFix,有興趣的同窗能夠參考 各大熱補丁方案分析和比較

動態加載開源項目

腳註

[1] 其實也說不上什麼模式,這不過這些動態加載的開發方式都有本身明顯的特徵,因此姑且用「形式或者模式」來稱呼好了。

[2] 爲了方便區分概念,闡述一些術語:宿主:Host,主項目APK、主APK,也就是咱們但願採用動態加載技術的主項目;插件:Plugin,能夠是dex、jar或者apk文件,從主項目分離開來,咱們能經過動態加載加載到主項目裏面來的模塊,一個主APK能夠同時加載多個插件APK;

相關文章
相關標籤/搜索