Android 外掛化和熱修復知識梳理

weixin_34402408發表於2017-11-28

概述

在Android開發中,外掛化和熱修復的話題越來越多的被大家提及,同時隨著技術的迭代,各種框架的發展更新,外掛化和熱修復的框架似乎已經日趨成熟,許多開發者也把這兩項技術運用到實際開發協作和正式的產品當中。因此,我們勢必需要了解一下這兩門技術。

外掛化和熱修復

首先需要明確的一點,外掛化和熱修復不是同一個概念,雖然站在技術實現的角度來說,他們都是從系統載入器的角度出發,無論是採用hook方式,亦或是代理方式或者是其他底層實現,都是通過“欺騙”Android 系統的方式來讓宿主正常的載入和執行外掛(補丁)中的內容;但是二者的出發點是不同的。外掛化顧名思義,更多是想把需要實現的模組或功能當做一個獨立的提取出來,減少宿主的規模,當需要使用到相應的功能時再去載入相應的模組。熱修復則往往是從修復bug的角度出發,強調的是在不需要二次安裝應用的前提下修復已知的bug。

為了方便敘述,做以下稱謂約定:

宿主: 就是當前執行的APP
外掛: 相對於外掛化技術來說,就是要載入執行的apk類檔案
補丁: 相對於熱修復技術來說,就是要載入執行的.patch,.dex,*.apk等一系列包含dex修復內容的檔案。

以下提到內容中的宿主和外掛(補丁),均是上述含義,不再贅述。

1115031-70a55fecac4da43b.png
Android外掛化技術的典型應用

上圖就是對Android外掛化和熱修復之間關係的體現。據我所知,在某些開發團隊中,會把熱修復的技術,作為在APP端部署日常活動的功能來用。雖然,實際效果來看是沒有問題的,但長期使用還是值得商榷的。

早期很多應用的動態換膚功能,就是參考了Android 外掛化的技術,最早的新浪微博夜間模式就是通過下載一個夜間模式的apk檔案完成,當時做為開發者的自己,感覺很高階。關於動態載入的應用,其實有很多可以擴充套件的思路,比如特定節日的促銷活動,逃避稽核機制的動態廣告載入都是Android外掛化技術可以考慮的實現,更多內容可以參考Android動態載入技術 簡單易懂的介紹方式

下面就從外掛化技術的發展源頭,逐步敘述一下二者的發展歷程及現狀,瞭解一下時至今日,熱修復框架的發展到了各種地步,總體梳理一下熱修復的原理,對現有的框架有一個瞭解。

外掛化

發展歷程及現狀

關於外掛化技術的起源可以追溯到5年前

  • 2012年的 AndroidDynamicLoader ,他的原理是動態載入不同的Fragment實現UI替換,可以說是開山鼻祖了,但是這種方案可擴充套件性不強。

  • 再到後來出現了23Code,他可以直接下載一個自定義控制元件的demo,並且執行起來。

  • 2014年一個里程碑式的年份,任玉剛(俗稱主席)釋出了dynamic-load-apk,也叫做DL。在這個框架裡提供了兩個很重要的思路:

    • 如何管理外掛內Activity的生命週期: 使用 DLProxyActivity 採用靜態代理的方式去呼叫外掛中Activity的生命週期方法。
    • 如何載入外掛內的資原始檔:通過反射呼叫AssetManager 中到的addAssetPath方法就可以將特定路徑的資源載入到系統記憶體中使用。

    以上兩點,可以說是非常有意義的,尤其是第二點關於外掛資源的記載,是後期出現的許多框架的參考思路。這個框架也有一些侷限,不支援外掛內Service、BroadcastReceiver等需要註冊才能使用的元件,同時外掛apk也需要按照其開發規範來實現,總體來說還是有一定的成本,但無論怎樣都是一個很有價值的框架。(話說這個框架貌似已經不再維護了,最近一次關於程式碼的更新都是2年前了,o(╥﹏╥)o)。

  • 2015年 DroidPlugin
    DroidPlugin 是Andy Zhang在Android系統上實現了一種新的 外掛機制 :它可以在無需安裝、修改的情況下執行APK檔案,此機制對改進大型APP的架構,實現多團隊協作開發具有一定的好處。 這段話是DroidPlugin在Github README 文件中的介紹。這款來自360的外掛化框架.

  • 2015年 DynamicAPK 這個就……,貌似因為License的原因已經完全不更新了。

  • 2017 RePlugin 這是360 開源的外掛化框架,按照他自己的說法,相較於其他框架,他對系統的hook只有一處,那就是ClassLoader,這樣從理論來說,貌似會有更好的穩定性。

  • 2017年 atlas這個是阿里今年剛剛開源的外掛化開發框架,可以說是非常強大;具體原理參考詳解 Atlas 框架原理;還沒有用過。

  • Small 最後再說一下Small,個人感覺Small 所提供了一種比外掛化更高層次的概念,元件化;把一個完整的APP看成是由許多可以複用模組元件組成(這個有點像React Native的開發理念);開發起來像是搭積木的感覺。有興趣的可以去Small官網瞭解一下。

熱修復

相較於外掛化,熱修復技術的使用更加的頻繁,因為這項技術切實關切到我們實際開發的產品,能夠更快速更便捷的修復線上bug,才能帶來更好的使用者體驗。因此下面就結合熱修復的原理了解一下熱修復的使用及發展現狀。

以下所有分析源自熱修復相關文章,這裡只是把結論整理了出來。具體分析就不再拾人牙慧了,對實現細節有興趣的同學可以檢視相應的連結。

類載入原理

說起熱修復就不得不提類的載入機制,和常規的JVM類似,在Android中類的載入也是通過ClassLoader來完成,具體來說就是PathClassLoader 和 DexClassLoader 這兩個Android專用的類載入器,這兩個類的區別如下:

  • PathClassLoader:只能載入已經安裝到Android系統中的apk檔案(/data/app目錄),是Android預設使用的類載入器。
  • DexClassLoader:可以載入任意目錄下的dex/jar/apk/zip檔案,也就是我們一開始提到的補丁。

這兩個類都是繼承自BaseDexClassLoader,我們可以看一下BaseDexClassLoader的建構函式。

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

這個建構函式只做了一件事,就是通過傳遞進來的相關引數,初始化了一個DexPathList物件。DexPathList的建構函式,就是將引數中傳遞進來的程式檔案(就是補丁檔案)封裝成Element物件,並將這些物件新增到一個Element的陣列集合dexElements中去

ClassLoaer 的載入機制是一種特別聰明的方式,雙親委託機制,在這種機制下,一個Class只會被載入一次。

對於ClassLoader載入機制及雙親委託機制的分析可以參考Android解析ClassLoader(一)Java中的ClassLoader

這裡需要明白的一點是對於一個ClassLoader(類載入器)來說,將一個具體的類(class)載入到記憶體中其實是由虛擬機器完成的,對於開發者來說,我們關注的重點應該是如何去找到這個需要載入的類

假設我們現在要去查詢一個名為name的class,那麼DexClassLoader將通過以下步驟實現:

  • 在DexClassLoader的findClass 方法中通過一個DexPathList物件findClass()方法來獲取class
  • 在DexPathList的findClass 方法中,對之前構造好dexElements陣列集合進行遍歷,一旦找到類名與name相同的類時,就直接返回這個class,找不到則返回null。

總的來說,通過DexClassLoader查詢一個類,最終就是就是在一個陣列中查詢特定值的操作。

綜合以上所有的觀點,我們很容易想到一種非常簡單粗暴的熱修復方案。假設現在程式碼中的某一個類或者是某幾個類有bug,那麼我們可以在修復完bug之後,可以將這些個類打包成一個補丁檔案,然後通過這個補丁檔案封裝出一個Element物件,並且將這個Element物件插到原有dexElements陣列的最前端,這樣當DexClassLoader去載入類時,優先會從我們插入的這個Element中找到相應的類,雖然那個有bug的類還存在於陣列中後面的Element中,但由於雙親載入機制的特點,這個有bug的類已經沒有機會被載入了,這樣一個bug就在沒有重新安裝應用的情況下修復了。

有了上面的思路,其實我們就可以自己動手去實現一個簡單的熱修復框架了。這裡推薦一篇
熱修復——深入淺出原理與實現,文中作者深入分析了熱修復原理,並基於以上原理實現了一個基礎的熱修復框架,實現過程分析的非常細緻深入,非常適合做為熱修復入門原理的瞭解。

QQ 空間超級補丁方案

看完上面的原理,是不是覺得熱修復很簡單,沒什麼可研究的呢?其實不然,Java是一門物件導向的語言,我們使用的類會有繼承關係,會相互依賴引用。同時Android虛擬機器和常規的JVM 不同,載入的並不是.class而是dex(準確的來說是經過優化的odex),在這樣一個過程中,勢必會有一些新的問題值得我們去關注。這個問題就是的CLASS_ISPREVERIFIED,什麼意思呢。

  • 在apk安裝的時候系統會將dex檔案優化成odex檔案,在優化的過程中會涉及一個預校驗的過程
  • 如果一個類的static方法,private方法,override方法以及建構函式中引用了其他類,而且這些類都屬於同一個dex檔案,此時該類就會被打上CLASS_ISPREVERIFIED
  • 如果在執行時被打上CLASS_ISPREVERIFIED的類引用了其他dex的類,就會報錯
  • 正常的分包方案會保證相關類被打入同一個dex檔案
  • 想要使得patch可以被正常載入,就必須保證類不會被打上CLASS_ISPREVERIFIED標記。而要實現這個目的就必須要在分完包後的class中植入對其他dex檔案中類的引用

以上內容摘自Android熱修復技術——QQ空間補丁方案解析(2)

要在已經編譯完成後的類中植入對其他類的引用,就需要操作位元組碼,慣用的方案是插樁。常見的工具有javaassist,asm等

QQ 空間補丁方案就是使用javaassist 插樁的方式解決了CLASS_ISPREVERIFIED的難題。

Tinker

QQ空間超級補丁,“超級補丁”很多情況下意味著補丁檔案很大,而將這樣一個大資料夾載入在記憶體中構建一個Element物件,插入到陣列最前端是需要耗費時間的,無疑會印象應用啟動的速度。因此Tinker 提出了另外一種思路。

1115031-695bdc33e66eb074.png
image

圖片源自https://github.com/Tencent/tinker

Tinker的思路是這樣的,通過修復好的class.dex 和原有的class.dex比較差生差量包補丁檔案patch.dex,在手機上這個patch.dex又會和原有的class.dex 合併生成新的檔案fix_class.dex,用這個新的fix_class.dex 整體替換原有的dexPathList的中的內容,可以說是從根本上把bug給幹掉了。

Tinker 提供的思路可以說是非常新奇,也非常值得我們去學習。上圖中過程看似簡單,但其實具體實現起來還真的不簡單。你有想過兩個.dex 是如何比較得出差異化檔案patch.dex 的嗎?有興趣的同學可以看看鴻翔的這篇分析Android 熱修復 Tinker 原始碼分析之DexDiff / DexPatch

當然,需要注意的是,patch.dex和原先的class.dex 合併的時候需要新的程式去完成,同時考慮的現在大多數應用的規模,multidex已經是很常見的事情了,因此多個dex 之間的合併策略及成功率,都是在使用Tinker時需要考慮的問題。

關於Tinker 更多細節可以參考 微信Android熱補丁實踐演進之路

Tinker 提供的文件及example非常完善,對於有興趣接入的開發者可以說是非常友好了,但總體來說接入過程還是有些複雜,對整個專案的侵入還是較強,Tinker是個人唯一使用過的熱修復的框架,總體來說還是不錯的,通過接入到實際應用中,對gradle也有了新的認識,對gradle有興趣的同學,其實可以看看tinker的gradle接入方式

HotFix

以上提到的兩種方式,雖然策略有所不同,但總的來說都是從上層ClassLoader的角度出發,由於ClassLoader的特點,如果想要新的補丁檔案再次生效,無論你是插樁還是提前合併,都需要重新啟動應用來載入新的DexPathList。這樣就無法在使用者神不知鬼不覺的情況下把bug修復了,HotFix在這方面就有絕對的優勢了。

HotFix(即AndFix),是在AndFix 的基礎之上提供了補丁安全服務及版本管理等相關內容,方便廣大的開發人員使用。

AndFix 提供了一種執行時在Native修改Filed指標的方式,實現方法的替換,達到即時生效無需重啟,對應用無效能消耗的目的。

1115031-2f4eec2c3f925834.png
AndFix 原理

更多細可以參考https://github.com/alibaba/AndFix,Native層不怎麼理解,就不強行裝逼了o(╯□╰)o。

由於他是Native層操作,因此如果我們在Java層中新增欄位,或者是修改類的方法,他是無能為力的。同時由於Android在國內變成了安卓,各大手機廠商定製了自己的ROM,所以很多底層實現的差異,導致AndFix的相容性並不是很好。

Sophix

阿里推出業界首個非侵入式熱修復方案Sophix,顛覆移動端傳統發版更新流程!

這是我第一次瞭解到Sophix時看到的文章標題原文連結;對於技術類的文章來說,敢於使用顛覆這兩個字,要麼是標題黨;要麼就是真的很有貨。

Sophix 可以說是博採眾長,前面提到的Tinker及AndFix 都在某一方面存在缺陷。因此Sophix 便取長補短,採用全量替換的思路,從一種更高的層次實現了熱修復。這貌似也是事物發展的一貫規律,後來的新生事物總結前人的經驗教訓,吸收好的思想,變得更好。

關於Sophix 的原理看了很多篇文章,感覺這篇乾貨滿滿,Android熱修復方案介紹分析的不錯,有興趣的可以看一下。

總的來說,Sophix應該是現有最成熟的熱修復方案了。

其他及總結

當然就熱修復的實現,各個大廠還有各自的實現,比如餓了嗎的Amigo,美團的Robust,實現及優缺點各有差異,但總的來說就是兩大類

  • ClassLoader 載入方案
  • Native層替換方案

或者是參考Android Studio Instant Run 的思路實現程式碼整體的增量更新。但這樣勢必會帶來效能的影響。

綜上所述,其實對於熱修復很難有一種十分完美的解決方案。在Android開發中,四大元件使用前需要在AndroidManifest中提前宣告,而如果需要使用熱修復的方式,無論是提前佔坑亦或是動態修改,都會帶來很強的侵入性(因此,Sophix是不支援四大元件修復的,這也是其非侵入性設計理念無法避免的事情了,不知道以後會不會有新的辦法)。再者Android碎片化的問題,對熱修復方案的適配也是一個考驗。通過檢視幾大以開源在Github上的熱修復方案,在issue中可以看到提到最多的問題還是相容性。

因此,面對實際的開發,選擇使用或者說選擇哪種方案,必須符合實際的應用的場景,一句話,沒有最好的,只有合適的。


好了,外掛化和熱修復知識就梳理到這裡了。

相關內容

Android動態載入技術 簡單易懂的介紹方式
Android 外掛化的 過去 現在 未來
ZeusPlugin: 掌閱APP外掛補丁
Android外掛化:從入門到放棄
Android 全面外掛化 RePlugin 流程與原始碼解析
《全面外掛化——RePlugin的使命》
詳解 Atlas 框架原理
熱修復——深入淺出原理與實現
Android解析ClassLoader(一)Java中的ClassLoader
Android熱修復技術——QQ空間補丁方案解析(2)
Android熱修復技術——QQ空間補丁方案解析(3)
微信Android熱補丁實踐演進之路
Android熱修復技術總結
乾貨滿滿,Android熱修復方案介紹

相關文章