阿裡巴巴對android熱修複技術已經進行了長達多年的探索。
最開始,是手淘基于xposed進行了改進,産生了針對android dalvik虛拟機運作時的java method hook技術,dexposed。但這個方案由于對底層dalvik結構過于依賴,最終無法繼續相容android5.0以後art虛拟機,是以作罷。
後來支付寶提出了新的熱修複方案andfix。andfix同樣是一種底層結構替換的方案,也達到了運作時生效即時修複的效果,并且重要的是,做到了dalvik和art環境的全版本相容。阿裡百川結合手淘在實際工程中使用andfix的經驗,對相關業務邏輯解耦後,推出了阿裡百川hotfix方案,并得到了良好的反響。
此時的百川hotfix已經是一個很不錯的産品了,對于基本的代碼修複需求都可以解決,安全性和易用性都做的比較好。然而,它所依賴的基石,andfix本身,是有局限性的。且不說其底層固定結構的替換方案穩定性不好,其使用範圍也存在着諸多限制,雖然可以通過改造代碼繞過限制來達到相同的修複目的,但這種方式既不優雅也不友善。而更大的問題是,andfix隻提供了代碼層面的修複,對于資源和so的修複都還未能實作。
再看一下同期的其他熱修複方案,此時的熱修複技術可謂是百花齊放,微信的tinker、qq空間的nuwa、餓了麼的amigo、美團的robust等等,各個熱修複方案争相釋出,都聲稱自己可以做到全方位全功能的熱修複。不過他們各自有自身的局限性,或者不夠穩定,或者更新檔過大,或者效率低下,或者使用起來過于繁瑣,大部分技術上看起來似乎可行,但實際體驗并不好。而在我們看來,有很多技術細節能夠做得更加完美。
sophix的橫空出世,将會打破各家熱修複技術紛争的局面。我們可以滿懷信心地說,在android熱修複的三大領域:代碼修複、資源修複、so修複方面,以及方案的安全性和易用性方面,sophix都做到了業界領先。
sophix的核心設計理念,就是非侵入性。
我們的打包過程不會侵入到apk的build流程中。我們所需要的,隻有已經生成完畢的新舊apk,而至于apk是如何生成的——是android studio打包出來的、還是eclipse打包出來的、或者是自定義的打包流程,我們一律不關心。在生成更新檔的過程中間既不會改變任何打包元件,也不插入任何aop代碼,我們極力做到了——不添加任何超出開發者預期的代碼,以避免多餘的熱修複代碼給開發者帶來困擾。
在sophix中,唯一需要的就是初始化和請求更新檔兩行代碼,甚至連入口application類我們都不做任何修改,這樣就給了開發者最大的透明度和自由度。我們甚至重新開發了打包工具,使得更新檔工具操作圖形界面化,這種所見即所得的更新檔生成方式也是阿裡熱修複獨家的。是以,sophix的接入成本也是目前市面上所有方案裡最低的。
這種非侵入式熱更新理念,是我們在設計過程中從使用者使用角度進行了深入思考而提煉出的核心思想。
這裡的使用者,指的自然是廣大的開發者。對于開發者而言,熱修複應該是一個與業務無關的sdk元件,在整個開發過程中感覺不到它的存在。最理想的情況,就是開發者拿過來兩個apk,一個是已經安裝在手機上的apk,另一個是将要釋出出去的apk。我們直接通過工具,就可以根據這兩個apk生成更新檔,然後把這個更新檔下發給已經安裝的舊app上,就可以直接加載,使舊app重生為新的app。而這個加載了更新檔包新app,在功能和使用上,将會和直接安裝新apk别無二緻。
至于sophix這個名字,是來源于sophic(明智的)+ fix,一個更明智的熱修複方案。
下面的這張表格,從幾個熱修複最重要的次元,把sophix和另外兩個主要商業化熱修複方案進行了比較。
方案對比
sophix
tinker
amigo
dex修複
同時支援即時生效和冷啟動修複
冷啟動修複
資源更新
差量包,不用合成
差量包,需要合成
全量包,不用合成
so庫更新
插樁實作,開發透明
替換接口,開發不透明
性能損耗
低,僅冷啟動情況下有些損耗
高,有合成操作
低,全量替換
四大元件
不能新增
能新增
生成更新檔
直接選擇已經編好的新舊包在本地生成
編譯新包時設定基線包
上傳完整新包到服務端
更新檔大小
小
大
接入成本
傻瓜式接入
複雜
一般
android版本
全部支援
安全機制
加密傳輸及簽名校驗
服務端支援
支援服務端控制
可以看到,sophix在各個名額上全面占優。而其中唯一支援不完善的地方就是四大元件,四大元件可以修改代碼,但是無法做到新增。這是因為如果要新增四大元件,必須在androidmanifest裡面預先插入代理元件,并且盡可能聲明所有權限,而這麼做就會給原先的app添加很多臃腫的代碼,對app運作流程的侵入性很強,是以,本着對開發者透明與代碼極簡的原則,我們沒有做這種多餘的處理。
直接看表格的話,其中有些技術細節可能還看不太明朗,那麼接下來,我将從各個角度,深度解讀sophix的技術優勢以及它與同類技術的差别。
sophix的誕生,起初是對原先的阿裡百川hotfix 1.x版本進行更新衍進。
原先百川hotfix服務端的整套請求控制流程,以及安全檢查這部分,是與熱修複功能相對分離的,是以我們依舊保留了這部分的邏輯。
而原本的熱修複方案,主要限制在于andfix本身,我們最開始也是從突破原先修複限制入手,希望能夠基于原先的andfix代碼做一些必要的改進。然而最終發現,andfix自身限制幾乎是無法繞過的,在運作時對原有類的結構是已經固化在記憶體中的,它的一些動态屬性和很難進行擴充。并且由于android系統的碎片化,廠商的虛拟機底層結構都不是确定的,是以直接基于原先機制進行擴充的風險很大。
是以我們繞開了具體的技術實作細節,直接從修複的原理入手,對原先的代碼修複技術進行深挖和改良。
回顧為期九個多月的探索與開發,這其中無處不展現着我們對易用性和優雅性的極緻追求,在技術先進性與易用性上我們達到了完美的平衡。是以,當我們再回頭看目前市面上的其他熱修複技術,真的有一種“曾經滄海難為水”的感覺。
代碼修複有兩大主要方案,一種是阿裡系的底層替換方案,另一種是騰訊系的類加載方案。
這兩類方案各有優劣:
底層替換方案限制頗多,但時效性最好,加載輕快,立即見效。
類加載方案時效性差,需要重新冷啟動才能見效,但修複範圍廣,限制少。
底層替換方案是在已經加載了的類中直接替換掉原有方法,是在原來類的基礎上進行修改的。因而無法實作對與原有類進行方法和字段的增減,因為這樣将破壞原有類的結構。
一旦更新檔類中出現了方法的增加和減少,就會導緻這個類以及整個dex的方法數的變化。方法數的變化伴随着方法索引的變化,這樣在通路方法時就無法正常地索引到正确的方法了。如果字段發生了增加和減少,和方法變化的情況一樣,所有字段的索引都會發生變化。并且更嚴重的問題是,如果在程式運作中間某個類突然增加了一個字段,那麼對于原先已經産生的這個類的執行個體,它們還是原來的結構,這是無法改變的。而新方法使用到這些老的執行個體對象時,通路新增字段就會産生不可預期的結果。
這是這類方案的固有限制,而底層替換方案最為人诟病的地方,在于底層替換的不穩定性。
傳統的底層替換方式,不論是dexposed、andfix或者其他安全界的hook方案,都是直接依賴修改虛拟機方法實體的具體字段。例如,改dalvik方法的jni函數指針、改類或方法的通路權限等等。這樣就帶來一個很嚴重的問題,由于android是開源的,各個手機廠商都可以對代碼進行改造,而andfix裡artmethod的結構是根據公開的android源碼中的結構寫死的。如果某個廠商對這個artmethod結構體進行了修改,就和原先開源代碼裡的結構不一緻,那麼在這個修改過了的裝置上,通用性的替換機制就會出問題。這便是不穩定的根源。
而我們也對代碼的底層替換原理重新進行了深入思考,從克服其限制和相容性入手,以一種更加優雅的替換思路,實作了即時生效的代碼熱修複。我們實作的是一種無視底層具體結構的替換方式,
也就是把原先這樣的逐一替換
變成了這樣的整體替換
這麼一來,我們不僅解決了相容性問題,并且由于忽略了底層artmethod結構的差異,對于所有的android版本都不再需要區分,代碼量大大減少。即使以後的android版本不斷修改artmethod的成員,隻要保證artmethod數組仍是以線性結構排列,就能直接适用于将來的android 8.0、9.0等新版本,無需再針對新的系統版本進行适配了。事實也證明确實如此,當我們拿到google剛發不久的android o(8.0)開發者預覽版的系統時,hotfix demo直接就能順利地加載更新檔跑起來了,我們并沒有做任何适配工作,魯棒性極好。
類加載方案的原理是在app重新啟動後讓classloader去加載新的類。因為在app運作到一半的時候,所有需要發生變更的類已經被加載過了,在android上是無法對一個類進行解除安裝的。如果不重新開機,原來的類還在虛拟機中,就無法加載新類。是以,隻有在下次重新開機的時候,在還沒走到業務邏輯之前搶先加載更新檔中的新類,這樣後續通路這個類時,就會resolve為新類。進而達到熱修複的目的。
再來看看騰訊系三大類加載方案的實作原理。qq空間方案會侵入打包流程,并且為了hack添加一些無用的資訊,實作起來很不優雅。而qfix的方案,需要擷取底層虛拟機的函數,不夠穩定可靠,并且有個比較大的問題是無法新增public函數。
微信的tinker方案是完整的全量dex加載,并且可謂是将更新檔合成做到了極緻,然而我們發現,精密的武器并非适用于所有戰場。tinker的合成方案,是從dex的方法和指令次元進行全量合成,整個過程都是自己研發的。雖然可以很大地節省空間,但由于對dex内容的比較粒度過細,實作較為複雜,性能消耗比較嚴重。實際上,dex的大小占整個apk的比例是比較低的,一個app裡面的dex檔案大小并不是主要部分,而占空間大的主要還是資源檔案。是以,tinker方案的時空代價轉換的成本效益不高。
其實,dex比較的最佳粒度,應該是在類的次元。它既不像方法和指令次元那樣的細微,也不像bsbiff比較那般的粗糙。在類的次元,可以達到時間和空間平衡的最佳效果。基于這個準則,我們另辟蹊徑,實作了一種完全不同的全量dex替換方案。
從圖中可以看到,我們重新編排了包中dex的順序。這樣,在虛拟機查找類的時候,會優先找到classes.dex中的類,然後才是classes2.dex、classes3.dex,也可以看做是dex檔案級别的類插樁方案。這個方式十分巧妙,它對舊包與更新檔包中classes.dex的順序進行了打破與重組,最終使得系統可以自然地識别到這個順序,以實作類覆寫的目的。這将會大大減少合成更新檔的開銷。
既然底層替換方案和類加載方案各有其優點,把他們聯合起來不是最好的選擇嗎?sophix的代碼修複體系正是同時涵蓋了這兩種方案。兩種方案的結合,可以實作優勢互補,完全兼顧的作用,可以靈活地根據實際情況自動切換。
這兩種方案我們都進行了重大的改進,并且從更新檔生成到應用的各個環節都進行了研究,使得二者能很好地整合在一起。在更新檔生成階段,更新檔工具會根據實際代碼變動情況進行自動選擇,針對小修改,在底層替換方案限制範圍内的,就直接采用底層替換修複嗎,這樣可以做到代碼修複即時生效。而對于代碼修改超出底層替換限制的,會使用類加載替換,這樣雖然及時性沒那麼好,但總歸可以達到熱修複的目的。
另外,運作時階段,sophix還會再判斷所運作的機型是否支援熱修複,這樣即使更新檔支援熱修複,但由于機型底層虛拟機構造不支援,還是會走類加載修複,進而達到最好的相容性。
目前市面上的很多資源熱修複方案基本上都是參考了instant run的實作。實際上,instant run的推出正是推動這次熱修複浪潮的主因,各家熱修複方案,在代碼、資源等方面的實作,很大程度上地參考了instant run的代碼,而資源修複方案正是被拿來用到最多的地方。
簡要說來,instant run中的資源熱修複分為兩步:
構造一個新的assetmanager,并通過反射調用addassetpath,把這個完整的新資源包加入到assetmanager中。這樣就得到了一個含有所有新資源的assetmanager。
找到所有之前引用到原有assetmanager的地方,通過反射,把引用處替換為assetmanager。
我們發現,其實大量代碼都是在處理相容性問題和找到所有assetmanager的引用處,真正的替換的邏輯其實很簡單。
我們的方案沒有直接使用instant run的技術,而是另辟蹊徑,構造了一個package id為0x66的資源包,這個包裡隻包含改變了的資源項,然後直接在原有assetmanager中addassetpath這個包就可以了。由于更新檔包的package id為0x66,不與目前已經加載的0x7f沖突,是以直接加入到已有的assetmanager中就可以直接使用了。更新檔包裡面的資源,隻包含原有包裡面沒有而新的包裡面有的新增資源,以及原有内容發生了改變的資源。并且,我們采用了更加優雅的替換方式,直接在原有的assetmanager對象上進行析構和重構,這樣所有原先對assetmanager對象的引用是沒有發生改變的,是以就不需要像instant run那樣進行繁瑣的修改了。
可以說,我們的資源修複方案,優越性超過了google官方的instant run方案。整個資源替換的方案優勢在于:
不修改assetmanager的引用處,替換更快更完全。(對比instanat run以及所有copycat的實作)
不必下發完整包,更新檔包中隻包含有變動的資源。(對比instanat run、amigo等方式的實作)
不需要在運作時合成完整包。不占用運作時計算和記憶體資源。(對比tinker的實作)
是以,我們不要被所謂的“官方實作”束縛住手腳,其實instant run的開發團隊和android framework的開發團隊并不是同一個團隊,他們對于android系統機制的了解未必十分深入。隻要你認真研讀系統代碼,實作一個比官方更好的方案絕非難事。是以我想說的是,要想實作技術方案的突破,首先就需要破除所謂“權威”的觀念。
so庫的修複本質上是對native方法的修複和替換。
我們知道jni程式設計中,native方法可以通過動态注冊和靜态注冊兩種方式進行。動态注冊的native方法必須實作<code>jni_onload</code>方法,同時實作一個<code>jninativemethod[]</code>數組,靜态注冊的native方法必須是<code>java+類完整路徑+方法名</code>的格式。
動态注冊的native方法映射通過加載so庫過程中調用jni_onload方法調用完成,靜态注冊的native方法映射是在該native方法第一次執行的時候才完成映射,當然前提是該so庫已經load過。
我們采用的是類似類修複反射注入方式。把更新檔so庫的路徑插入到nativelibrarydirectories數組的最前面,就能夠達到加載so庫的時候是更新檔so庫,而不是原來so庫的目錄,進而達到修複的目的。
采用這種方案,完全由sophix在啟動期間反射注入patch中的so庫。對開發者依然是透明的。不用像某些其他方案需要手動替換系統的system.load來實作替換目的。
熱修複是一個與業務完全無關的子產品,開發者如果要自己實作一套可靠的熱修複架構,将花費大量時間和精力。雖然市面上已經有很多開源的熱修複實作,然而其中的很多坑,往往要踩過才知道,等你把這些坑一一踩過之後,可能大量的使用者已經對你失去信心。是以,依靠一個穩定可靠、而且簡單實用的商業版本,反而能使各方面的成本降到最低。并且,熱修複并不是簡單的用戶端sdk,它還包含了安全機制和服務端的控制邏輯,這整條鍊路也不是短時間内可以快速完成的。
還是那句老話,專業是事交給專業的人去做。開發者應該把更多時間精力放到自己的核心業務之中。
sophix提供了一套更加完美的用戶端服務端一體的熱更新方案。做到了圖形界面一鍵打包、加密傳輸、簽名校驗和服務端控制釋出與灰階功能,讓你用最少的時間實作最強大可靠的全方位熱更新。并且在代碼修複、資源修複、so庫修複方面,都做到了業界最佳。
很多人會把熱修複技術跟其他國内廠商的“黑科技”混為一談。有人說,你們國内開發者就是瞎搞,就不能給我們android使用者一個更加純淨的環境嗎?
這裡我需要澄清一下。熱修複技術不同于其他國内的android“黑科技”。就比如,國内android程序保活,是讓app持續駐留在背景避免被系統殺死,這既耗費手機電量又占記憶體,浪費了很多手機資源。再比如,app自行定制的推送服務,無節操地對使用者進行資訊轟炸。還有更過分的全家桶,一個app同時拉起一票app,并且長期占着記憶體,使得手機卡頓不堪。總歸,這些技術都是為了app廠商的利益而損害手機使用者的實際體驗。
而熱修複技術是完全不同的,它達到的是一個手機使用者和開發者雙赢的目的。不僅廠商可以快速疊代更新app,使得功能能最快上線。并且由于熱更新過程是毫無感覺的,手機使用者也減少了繁瑣的更新步驟,節省了大量等待更新的時間。這實際上是改善了android的生态環境。隻是這其中最重要的,是要保證熱修複功能的穩定性。而sophix的穩定性,是經過了無數開發者檢驗的,并且還有手淘多年深厚的技術沉澱作為保障。
前段時間,蘋果封殺了ios的熱修複功能,很多人就是以不看好熱修複技術了。這裡我想說的是,蘋果的政策并不能證明他有多先進,相反,作為獨裁者,蘋果做過很多不得人心的事,就比如前段時間封殺微信的文章打賞功能。熱修複功能被禁止,會使得很多app不得不靠直接發版進行更新,這樣一旦新版本出了問題,整個更新疊代過程變得十分漫長。并且一些試驗性功能無法進行灰階,這就使得一個重要功能的更新将直接全量發版,如果功能不夠穩定,波及範圍就變得非常廣。而且,使用者需要重新下載下傳整個app,不僅流程漫長,原本不到1mb的更新檔就能解決的事,現在不得不下載下傳幾十mb的完整包才能更新。
再看回android的情況,android熱修複和ios是有極大不同的。主要有兩個方面:
谷歌和蘋果在中國的地位不同
android和ios的開放性不同
谷歌在中國沒有像蘋果那樣的控制力,即使它想要封殺也不可能,國内是有各個安卓應用市場的,沒有統一的app安裝管道。另外,android是開源的,各個廠商都可以做定制,想統一各家的安裝管道幾乎是不可能的。
我們對于未來是很樂觀的,android的熱修複領域不僅不會受到封殺,反而還有很大的發展空間。我們正在嘗試支援各大加強廠商,目前阿裡聚安全修複已經支援了sophix,熱修複結合安全加強,将會使得app的穩定性和安全性更加堅不可摧。甚至後續還可以與系統廠商合作,對系統app乃至系統元件進行修複,這樣就可以避免頻繁ota更新。
是以,熱修複所能發揮是價值将是十分巨大的。熱修複還可以與其他領域進行碰撞,引發無限的可能性。在這裡,我們歡迎所有應用廠商以及rom廠商與我們合作,共同使得android的生态更加完善。