SDK全局热更新方案(全网唯一)
大家好,我是拭心,這篇文章是一個好友 Divin 的投稿,介紹 SDK 熱更新的一種實現思路,希望對你有所啟發。
一、背景
App熱更新
目前市面上成熟的商業熱更新方案不少,有騰訊Bugly的Tinker封裝,有阿里云的Sophix,也有游戲垂直行業的卓盟樂變。這些成熟方案,都有一個適用范圍,即對App、對游戲整包進行熱更新。前兩者是和包名綁定在一起的,所以只適用于App熱更新;而卓盟樂變則專注于游戲行業,可支持多渠道包熱更新。其實最好的還是Sophix,可惜沒有開源,雖有公開原理,但是公開資料里也透露了探索與開發周期長達9個月。
在社區,比較流行的熱更新有Tinker、QZone、AndFix(HotFix)、Sophix、Robust、Dexposed、Nuwa、Amigo,同商業熱更新方案一樣,也是適用于App整包熱更新。在這些方案里,影響力最大的是微信的Tinker方案,13048個Star,擁有完善的文檔,整個框架注重高可用性,最重要的是官方持續維護,在2018年12月,merge7次。相比之下,其他有在Github上開源的框架,star數都是7000以下,上次更新時間都在1年前,甚至2年前。
SDK熱更新
SDK熱更新,這是一個極少被關注的問題,Google、百度上相關的文章一篇都沒有。我們首先進行思考,SDK熱更新同App熱更新有什么不同?,SDK熱更新要做什么?
SDK熱更新同App熱更新有什么不同?
?App熱更新,輸入的是一個基準包和一個新版包,輸出的是差分包(或補丁),將這個差分包(或補丁)下載到客戶端,客戶端加載后生效。?SDK熱更新,輸入的是一個基準SDK和一個新版SDK,輸出的也是差分包(或補丁),不同的是,SDK會被集成到不同的游戲包中,這個游戲包也會被分成各式各樣的渠道包,我們要將這個差分包(或補丁)下載到所有游戲、所有渠道包,并加載生效。
SDK熱更新要做什么?
1. 對SDK的代碼、資源進行標識,我們要進行熱更新的對象,就是這些代碼、資源。
比如,我們可以進行這樣標識:所有在com.divin.包名之下的java類,所有assets/divin/文件夾之下的Assets文件,所有以divin_開頭的Res文件,所有/res/values/文件,所有以divin_開頭的so文件。
2. 在熱更新的整個流程,對上述代碼、資源進行特別操作。
包括build(計算差分)、patch(合并差分)、load(加載差分)。
十分感謝微信Tinker的開源,對外開放了完整的熱更新過程,站在偉人肩上,下面的SDK熱更新,都是基于Tinker開源庫進行的修改。
熱更新重點
1. dex熱更新,即Java代碼熱更新。
阿里系(AndFix,Hotfix)走的底層替換方案,好處在于實時生效,騰訊系(Tinker)走的是類加載方案,好處在于高兼容性。阿里百川系(Sophix)就有點機智了,兩種方案都有使用,還進行了一定的升級,優先走底層替換方案,底層替換方案走不下去了就走類加載方案。
AndFix(HotFix)的底層替換方案已過時,Sophix的無視底層具體結構的底層替換方案較新。感興趣的同學可以深入了解下,追尋極致的代碼熱替換[1]。
Tinker的類加載方案,需要重啟應用后讓Classloader去加載新類。因為Android上無法對一個類進行卸載,不重啟,則無法加載新類。
2. 資源文件熱更新。
這里也是有兩個流派,一個流派是參考Instant Run通過addAssetPath加載新的資源包到AssetsManager,然后再替換Resource中的AssetsManager;一個流派是構造新的R文件資源地址以0x66開頭的資源包,再通過addAssetPath加載新的資源包到AssetsManager,因為新的R文件資源地址以0x66開頭,新的Java代碼里,也引用0x66開頭的資源,這樣就可以新舊資源不干擾且都能生效。
Tinker屬于第一個流派[2],Sophix屬于第二個流派[3]。
非常遺憾的是,在我們基于Tinker實現SDK資源更新(即指定資源更新)時,只知道第一個流派,并不知道第二個流派(那篇文章沒細讀,印象不深)。所以后文中所提到的SDK資源更新(指定資源更新),其實是自己摸索出來的,可以理解成流派二的拼多多版,實現了資源新增、更改,但暫未支持R文件直接引用。
3. so文件熱更新。
說到這里,是真感謝這世界上有數組這玩意。so文件的熱更新,也是把補丁so庫的路徑插入到nativeLibraryDirectories數組的最前面。
二、Tinker
開源
Tinker已開源,Tencent/tinker[4],同時有詳細的使用Wiki,Tinker使用Wiki[5]。
熱更新過程
Tinker的整個熱更新過程,可以理解成四個步驟。
1. Tinker集成
集成Tinker分兩大塊,一塊是Application改造,一塊是定制化功能。第一塊較為簡單,使用Annotation Processor在編譯時生成新Application;第二塊非常復雜。
2. build(計算差分)
build有兩種模式,一種是供Android Studio開發使用的Gradle模式,一種是使用Java實現的命令行模式。二者最底層,其實都是使用的tinker-patch-lib,一個用Java實現的核心庫。
3. patch(合并差分)
4. load(加載差分)
源碼結構
Tinker的源碼分為這么幾大塊:
1. tinker-sample-android
顧名思義,這是一個demo,龐大!龐大!龐大!從未見過一個第三方SDK,暴露了如此多的api,可以定制如此多的功能!難怪Sophix在其官方文檔中對熱更新方案做橫向對比時,把自己描述為“傻瓜式接入”,把Amigo描述為“一般”,卻把Tinker描述為“復雜”。其實微信官方也有描述,Tinker為了實現“高可用”的目標,在接入成本上做了妥協。熱補丁并不簡單,在使用之前請務必先仔細閱讀XXXX。總的來說,感謝騰訊baba。
demo里,示例了:
①如何控制熱更新的請求過濾、合并過程、加載過程、合并后的后續處理、升級熱更新模塊本身的代碼。
②如何改造Application。
③Gradle集成模式的42個參考配置。 42個參考配置!42個參考配置!42個參考配置!
這里讓大家放心的是,復雜的是Tinker的定制化開發,而不是給到cp的SDK。我們可以對外隱藏這些定制化開發的細節。
2. tinker-build
這是熱更新過程中build步驟的源碼,有三個子模塊,tinker-patch-lilb是核心代碼,tinker-patch-cli是命令行模式的源碼,tinker-patch-gradle-plugin是Gradle模式的源碼。
3. tinker-android
這是熱更新過程中patch和load步驟的源碼,隨Apk、游戲運行在客戶端。也有Application改造時用到的Annotation Processor庫的源碼。
4. tinker-commons
tinker-build所用到的基礎庫。
5. third-party
tinker-build所用到的第三方庫。
三、SDK熱更新實現
1. 指定代碼熱更新。
我們回顧熱更新的4個步驟,第二個步驟是build(計算差分),輸入的是一個基準包和一個新版包,輸出的是差分包(或補丁)。如果在這個核心算法的里,增加一項功能,只比對SDK的代碼,不比對游戲的代碼,是不是就可行了呢?
這種思路,有一點點站在業務層反推實現方案的嫌疑。但最后實踐檢驗,還真可以這樣。
我們回顧demo中的一項功能,升級熱更新模塊本身的代碼,那Tinker如何去實現這一個功能的呢?Tinker通過一個配置表來配置熱更新模塊本身的代碼。
<issue id="dex"><loader value="com.tencent.tinker.loader.*"/><loader value="tinker.sample.android.SampleApplication"/><loader value="tinker.sample.android.app.GameClass"/> </issue>這里的配置是支持Pattern的。
把游戲的代碼也當熱更新模塊本身的代碼配置,是否OK?
結果是不OK。能夠build,但是不能patch、load。
網上所有的博客,其實都有提到Tinker自研了一套dex diff、patch的算法,可以高效地比對出差分包,并在客戶端patch出目標dex包。難道是Tinker這一套算法不支持這樣地添加非熱更新模塊代碼?
這時候我們回過頭理解這一套dex diff、patch算法,也許你都還用不上深入理解,看到上面的幾行字,說不定就能發現玄妙。有興趣可以把視野停在此處思考一下。
?占?.?.?.?.?.?位?.?.?.?.?.?符
Tinker的dex diff、patch算法,說到底,就是一個可逆的過程,先計算兩個包的區別特征,再通過一個包以及區別特征,來推出另一個包。這套算法是從dex的方法和指令維度進行全量合成。
用簡單的公式來表示:
服務端diff: New.dex - base.dex = patch.file
客戶端patch: base.dex + patch.file = New.dex
在上面的嘗試中,客戶端patch所用到的base.dex,已經不是服務端diff所用到的base.dex了。前者是游戲包的dex,后者是SDK的dex。
擺在我們面前的選擇只有兩個,一個是理解并修改這套算法,另一個是,另辟蹊徑。但前者,顯然不是3、5天調研時間能完成的。
柳暗花明又一村~
調試源碼時,發現了這玩意:
@Override public void onAllPatchesEnd() throws Exception {if (!hasDexChanged) {Logger.d("No dexes were changed, nothing needs to be done next.");return;}if (config.mIsProtectedApp) {generateChangedClassesDexFile();} else {generatePatchInfoFile();}addTestDex(); }超想用抖音的BGM描述一下內心的心情,“這是什么造型,挺別致哦~”
在開發者配置isProtectedApp的true或false時,其實Tinker走了兩套不一樣的差分算法。false時,走Tinker自研的差分算法;true時,走常規的差分算法。
這套差分算法是基于Class類的,可以被客戶端patch、load的。
接著,就是對配置表loader配置的復刻了,這里思路比較清晰,增加一個isSDKMode配置,如果為true則走SDK模式,不去讀loader配置,而去讀loader配置的復刻字段sdkPackage,用來填寫需要更新的SDK代碼。我們SDK是com.divin.*。
<issue id="dex"><loader value="com.tencent.tinker.loader.*"/><loader value="tinker.sample.android.SampleApplication"/><loader value="tinker.sample.android.app.GameClass"/><isSDKMode value="true"/><sdkPackage value="com.divin.*"/> </issue>搞定!
2. 指定資源文件熱更新。
我們先說一下不同資源,在Apk包中的目錄結構。
解壓縮Apk包后,根目錄下有assets和res文件夾。如果你用這個Apk包的目錄結構和Android工程源碼的目錄結構做對比,assets中的內容是一一對應的,Apk包的res文件夾也能Android工程源碼的res中資源一一對應起來,但是會少了Android工程源碼的res/values文件夾下的文件。
這些res/values文件去哪兒了呢?
resources.arsc
所以,指定資源文件熱更新要分兩大塊,一塊是不能一一對應上的res/values文件,一塊是能一一對應上的assets文件和res文件。
不能一一對應上的res/values文件
重述一下,Android工程源碼中,不能一一對應上的res/values文件,到Apk文件目錄的resources.arsc文件中去了。
我們回顧Tinker更新步驟,第2步build,通過diff算法生成差分包,第3步patch,通過patch算法生成新的res資源包,第4步load,加載新的res資源包。
用SDK的resources.arsc生成差分包,再用游戲的舊resources.arsc計算新的resources.arsc?
這樣,又面臨我們做指定代碼熱更新時面臨的問題。擺在我們面前的選擇只有兩個,一個是理解并修改這套算法,另一個是,另辟蹊徑。
What?? 逼我們上梁山??
這里面臨兩個問題:
?我們無法計算出新的resources.arsc文件。?就算計算出來了也沒用,因為resources.arsc不僅有SDK的資源,還有游戲的資源。使用SDK的resources.arsc文件,必然會讓游戲因找不到資源而崩潰!
車到山前必有路,逐個擊破!
第一個問題。?其實Res資源也是有兩種算法,一種是Tinker自研的diff、patch算法,一種是不計算差分,完整下載,完整加載。具體到每一個資源,到底走哪種算法,其實是根據資源的大小做的判斷,默認是100kb以下的完整下載、完整加載,100kb以上的走自研的diff、patch算法。
那我們就強行走第二種算法,這里要做的事情有二件:
?控制差分的判斷邏輯,強行走第二種算法。?修改patch時的CSC、md5完整性判斷邏輯。(TODO:預研時,我是直接去掉了,實際業務中,需要增加新的完整性判斷邏輯)
第二個問題。我們細讀Tinker的資源load流程,它生效的原理是Instant Run那一套流派一。
流派一原理簡述如下:
?
先獲取默認的AssetManager,通過反射獲取其構造方法
?
通過AssertManager的addAssetPath函數,加入外部的資源路徑
?
將Resources的mAssets的字段設為前面的AssertManager
這一套,所實現的效果,就是用addAssetPath用新的Res資源包替換原來的Res資源包。慢著,add,Asset,Path,添加資源目錄,能不能添加多個呢?
看Android源碼找找希望吧。
/*** @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}* @hide*/@Deprecated@UnsupportedAppUsagepublic int addAssetPath(String path) {return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);}private int addAssetPathInternal(String path, boolean overlay, boolean appAsLib) {Preconditions.checkNotNull(path, "path");synchronized (this) {ensureOpenLocked();final int count = mApkAssets.length;// See if we already have it loaded.for (int i = 0; i < count; i++) {if (mApkAssets[i].getAssetPath().equals(path)) {return i + 1;}}final ApkAssets assets;try {if (overlay) {// TODO(b/70343104): This hardcoded path will be removed once// addAssetPathInternal is deleted.final String idmapPath = "/data/resource-cache/"+ path.substring(1).replace('/', '@')+ "@idmap";assets = ApkAssets.loadOverlayFromPath(idmapPath, false /*system*/);} else {assets = ApkAssets.loadFromPath(path, false /*system*/, appAsLib);}} catch (IOException e) {return 0;}mApkAssets = Arrays.copyOf(mApkAssets, count + 1);mApkAssets[count] = assets;nativeSetApkAssets(mObject, mApkAssets, true);invalidateCachesLocked(-1);return count + 1;}}BGM再來一次,“這是什么造型,挺別致哦~”
mApkAssets,偉大的數組!
獲取新的AssetsManager,先添加熱更新的新Res資源,再添加游戲原本的舊Res資源。這樣,會先去第一個Res中找資源,第一個Res中找不到再去第二個Res中找。
所以,這里是能實現對SDK資源的新增、修改,但是不能刪去資源,同時也不支持R文件直接引用,因為R文件的地址是常量,在Apk編譯時,這些常量會跟著引用R文件的業務Class走。如果想保持R文件的地址不變,可以修改APT編譯器,也能通過Apktool來做,當然還有上面提到的資源熱更新流派二。
能一一對應上的assets文件和res文件。
這里實現起來,其實和代碼熱更新有些相似。Tinker默認有這樣的配置表:
<issue id="resource"><!--what resource in apk are expected to deal with tinkerPatch--><!--it support * or ? pattern.--><!--you must include all your resources in apk here--><!--otherwise, they won't repack in the new apk resources--><pattern value="res/*"/><pattern value="assets/*"/><pattern value="resources.arsc"/><pattern value="AndroidManifest.xml"/><!--ignore add, delete or modify resource change--><!--Warning, we can only use for files no relative with resources.arsc, such as assets files--><!--it support * or ? pattern.--><!--Such as I want assets/meta.txt use the base.apk version whatever it is change ir not.--><ignoreChange value="assets/sample_meta.txt"/><!--ignore any warning caused by add, delete or modify changes on resources specified by this pattern.--><ignoreChangeWarning value="" /><!--default 100kb--><!--for modify resource, if it is larger than 'largeModSize'--><!--we would like to use bsdiff algorithm to reduce patch file size--><largeModSize value="10000000"/> </issue>增加一個isSDKMode配置,如果為true則走SDK模式,不去讀ignoreChange配置,而去讀ignoreChange配置的復刻字段sdkResPath,
<issue id="resource"><!--what resource in apk are expected to deal with tinkerPatch--><!--it support * or ? pattern.--><!--you must include all your resources in apk here--><!--otherwise, they won't repack in the new apk resources--><pattern value="res/*"/><pattern value="assets/*"/><pattern value="resources.arsc"/><pattern value="AndroidManifest.xml"/><!--ignore add, delete or modify resource change--><!--Warning, we can only use for files no relative with resources.arsc, such as assets files--><!--it support * or ? pattern.--><!--Such as I want assets/meta.txt use the base.apk version whatever it is change ir not.--><isSDKMode value="true"><sdkResPath value="assets/only_use_to_test_tinker_resource.txt"/><sdkResPath value="assets/divin/*"/><sdkResPath value="res/*/divin_*"/><sdkResPath value="resources.arsc"/><sdkResPath value="AndroidManifest.xml"/><ignoreChange value="assets/sample_meta.txt"/><!--ignore any warning caused by add, delete or modify changes on resources specified by this pattern.--><ignoreChangeWarning value="" /><!--default 100kb--><!--for modify resource, if it is larger than 'largeModSize'--><!--we would like to use bsdiff algorithm to reduce patch file size--><largeModSize value="10000000"/> </issue>至于差分算法,倒是沒有什么問題。不論是Tinker自研的diff、patch算法,還是完整下載、完整加載,都可行,畢竟要更新的文件都是SDK獨有的,游戲并沒有共用。當然啦,使用Tinker自研的diff、patch算法肯定是最好的,畢竟可以減小差分包大小。
3. 指定so文件熱更新。
TODO
四、效果
SDK熱更新范圍
?代碼: 所有在com.divin.包名之下的java類?assets: 所有assets/divin/文件夾之下的文件?普通Res: 所有以divin_開頭的文件?/res/values/: 所有文件, 但是只能實現增加/更改values,不能實現刪除values.?so庫: 以divin_開頭的so文件
SDK熱更新限制
?無法更新AndroidManifest?在部分三星android-21的機型上無法生效?資源替換不支持遠程View, 如應用icon.?不支持SDK直接R文件引用資源
集成配置
1. app.gradle
dependencies {// tinker-android-lib(本地module) 為必須依賴// anno為可選依賴,用于使用AnnotationProcessor生成Application//implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }implementation project(':tinker-android::tinker-android-lib')annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true } }2. 修改Application
參考SampleApplicationLike.java改造Application.
3. 更新
TinkerLogic.patch(Context context)References
[1]?追尋極致的代碼熱替換:?https://yq.aliyun.com/articles/74598?spm=a2c4e.11153940.blogcont103527.9.4838625aQYwZRa
[2]?第一個流派:?https://www.cnblogs.com/yyangblog/p/6252490.html
[3]?第二個流派:?https://yq.aliyun.com/articles/96378?spm=a2c4e.11153940.blogcont103527.11.4838625aQYwZRa
[4]?Tencent/tinker:?https://github.com/Tencent/tinker
[5]?Tinker使用Wiki:?https://github.com/Tencent/tinker/wiki
總結
以上是生活随笔為你收集整理的SDK全局热更新方案(全网唯一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 最全的SQL练习题(做完你就是高手)
- 下一篇: Tensorflow2.*教程之使用Te