Android動態載入技術 簡單易懂的介紹方式
基本資訊
Author: kaedea
GitHub: android-dynamical-loading
我們很早開始就在Android專案中採用了動態載入技術,主要目的是為了達到讓使用者不用重新安裝APK就能升級應用的功能(特別是 SDK專案),這樣一來不但可以大大提高應用新版本的覆蓋率,也減少了伺服器對舊版本介面相容的壓力,同時如果也可以快速修復一些線上的BUG。
這種技術並不是常規的Android開發方式,早期並沒有完善的解決方案。從“不明覺厲”到穩定投入生產,一直以來我總想對此編寫一些文件,這也是這篇日誌的由來,沒想到前前後後竟然拖沓著編輯了一年多,所以日誌裡有的地方思路可能有點銜接得不是很好,如果有修正建議請直接回復。
技術背景
通過伺服器配置一些引數,Android APP獲取這些引數再做出相應的邏輯,這是常有的事情。
比如現在大部分APP都有一個啟動頁面,如果到了一些重要的節日,APP的伺服器會配置一些與時節相關的圖片,APP啟動時候再把原有的啟動圖換成這些新的圖片,這樣就能提高使用者的體驗了。
再則,早期個人開發者在安卓市場上釋出應用的時候,如果應用裡包含有廣告,那麼有可能會稽核不通過。那麼就通過在伺服器配置一個開關,稽核應用的時候先把開關關閉,這樣應用就不會顯示廣告了;安卓市場稽核通過後,再把伺服器的廣告開關給開啟,以這樣的手段規避市場的稽核。
道高一尺魔高一丈 。安卓市場開始掃描APK裡面的Manifest甚至dex檔案,檢視開發者的APK包裡是否有廣告的程式碼,如果有就有可能稽核不通過。
通過伺服器怕配置開關引數的方法行不通了,開發者們開始想,“既然這樣,能不能先不要在APK寫廣告的程式碼,在使用者執行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開發方式, Github 和 StackOverflow 上面外國的開發者也對此不是很感興趣,外國相關的教程更是少得可憐,目前只有在大天朝才有比較深入的研究和應用,特別是一些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.宿主專案和外掛專案的開發都要接入共同的框架,大多時候,外掛需要依附宿主才能執行,無法獨立執行;
代理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.2、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;
參考資料
Android進階


需要這些資料的大夥關注+點贊+加群:185873940 免費獲取!
群內還有許多免費的關於高階安卓學習資料,包括高階UI、效能優化、架構師課程、 NDK、混合式開發:ReactNative+Weex等多個Android技術知識的架構視訊資料,還有職業生涯規劃及面試指導。