Tinker:全量替換眯亦,無(wú)須插樁
傳統(tǒng)的熱修復(fù)需要插樁實(shí)現(xiàn),插樁的原因和操作:
原因:
- 通過(guò)將補(bǔ)丁dex文件插入到類加載器的dexElement列表最前面般码,完成熱修復(fù)
- 調(diào)用bug類的時(shí)候就會(huì)先搜索到補(bǔ)丁dex里的類妻率,從而fix bug
- bug類和它引用的類都在一個(gè)dex中,這個(gè)類就被打上了CLASS_ISPREVERIFIED標(biāo)識(shí)板祝,如果這個(gè)類調(diào)用了插在dexElement隊(duì)列前面的補(bǔ)丁dex文件中的同名方法宫静,就會(huì)報(bào)錯(cuò),所以需要阻止bug類打上該標(biāo)識(shí)
- 通過(guò)“插樁”的方法避免需要被fix的class打上CLASS_ISPREVERIFIED標(biāo)識(shí)
如何插樁:
使可能會(huì)產(chǎn)生bug的class引用另外一個(gè)dex中的class券时,從而避免該class被打上CLASS_ISPREVERIFIED標(biāo)識(shí)(Groovy語(yǔ)言字節(jié)碼織入)孤里。
Tinker使用的是apk全量替換的方法,使用差量包補(bǔ)丁包和原來(lái)的apk合成新的apk橘洞,使用全新apk捌袜,從而不會(huì)出現(xiàn)引用其他dex的class的情況,避免了插樁炸枣。
Part 1:怎樣生成差量補(bǔ)丁
APK Diff包括:DexDiff虏等,ResDiff,ManifestDiff和SoDiff
1. ManifestDiff
用來(lái)檢測(cè)新的Manifest是否發(fā)送過(guò)改變适肠,Tinker不支持Manifest修改霍衫。原因應(yīng)該是apk在執(zhí)行install操作的時(shí)候會(huì)向系統(tǒng)注冊(cè)Manifest信息,tinker熱修復(fù)不會(huì)經(jīng)歷這個(gè)過(guò)程侯养。
2. ResDiff/SoDiff
BsDiff算法敦跌,求新版本和舊版本的二進(jìn)制差異。原理來(lái)自這篇博士論文Naive Differences of Executable Code
基本步驟:
a.對(duì)old文件中所有子字符串形成一個(gè)字典沸毁;
b.對(duì)比old文件和new文件峰髓,產(chǎn)生diffstring和extra string傻寂;
c.將diffstring 和extra string 以及相應(yīng)的控制字用zip壓縮成一個(gè)patch包。
優(yōu)點(diǎn):通用性強(qiáng)携兵,適用于所有文件的補(bǔ)丁包生成
缺點(diǎn):沒(méi)有針對(duì)特定的格式做優(yōu)化疾掰,導(dǎo)致補(bǔ)丁包可能過(guò)大
3. DexDiff
對(duì)Dex文件進(jìn)行差量包生成,傳統(tǒng)的方法有兩種徐紧,除了上面提到的Bsdiff算法静檬,還有一種反編譯方法,通過(guò)對(duì)dex文件反編譯后的class進(jìn)行比較并级,以確定哪些class發(fā)生了變化的方式拂檩,并對(duì)發(fā)生變化的class文件進(jìn)行補(bǔ)丁操作。
微信團(tuán)隊(duì)為了使得差異包最小化嘲碧,充分利用了Dex的結(jié)構(gòu)稻励,開(kāi)發(fā)了專門應(yīng)用于Dex文件的差量包生成算法DexDiff算法,跟Bsdiff相比愈涩,喪失了通用性望抽,但是效率更高。
DexClassLoader文件結(jié)構(gòu)如下履婉,分為Header煤篙,Table,Data三部分毁腿。
Header:
dex文件頭部辑奈,記錄整個(gè)dex文件的相關(guān)屬性,如都包含哪些部分(如String已烤,F(xiàn)ield鸠窗,Method,Class等)草戈,每部分的大小和偏移量塌鸯。
Table:
存放每種類型數(shù)據(jù)的地址列表侍瑟,如在String Table中唐片,連續(xù)存放若干個(gè)String的地址,根據(jù)每個(gè)地址涨颜,可以在Data段找到改地址存儲(chǔ)的字符串费韭。
Data:
存放具體的數(shù)據(jù),由Table段不同類型的地址進(jìn)行索引庭瑰。
以String數(shù)據(jù)為例星持,首先讀取Dex Header部分String IDs offset和String IDs Size的內(nèi)容,如0X70和0X14弹灭,代表String數(shù)據(jù)在Table段的偏移量是0X70督暂,共20個(gè)揪垄。在Table段讀取這20個(gè)數(shù)據(jù),每個(gè)數(shù)據(jù)4個(gè)字節(jié)逻翁,根據(jù)這4個(gè)字節(jié)代表的地址饥努,去DataSection找這個(gè)地址存儲(chǔ)的內(nèi)容,解析成String數(shù)據(jù)八回。
接下來(lái)計(jì)算新舊Dex的String數(shù)據(jù)的Diff數(shù)據(jù)酷愧。采用最小序列生成算法,生成由舊String列表生成新列表的操作缠诅,用刪除溶浴,添加,修改三種操作表示管引。
算法描述如下士败,摘抄自這篇帖子:
首先我們需要將新舊內(nèi)容排序,這需要針對(duì)排序的數(shù)組進(jìn)行操作
新舊兩個(gè)指針褥伴,在內(nèi)容一樣的時(shí)候 old拱烁、new 指針同時(shí)加1,在 old 內(nèi)容小于 new >內(nèi)容的時(shí)候 old 指針加1噩翠,標(biāo)記當(dāng)前 old 項(xiàng)為刪除
在 old 內(nèi)容大于 new 內(nèi)容 new 指針加1戏自, 標(biāo)記當(dāng)前 new 項(xiàng)為新增
------old-----
11 foo2
12 foo5
13 hello dodola
14 hello dodola1------new-----
11 foo3
12 foo5
13 hello dodola1
14 hello dodola3對(duì)比的old cursor 和 new cursor 指針的改變以及操作判定,判定過(guò)程如下
old_11 new_11 cmp <0 del
old_12 new_11 cmp >0 add
old_12 new_12 cmp =0 no
old_13 new_13 cmp <0 del
old_14 new_13 cmp =0 no
break;進(jìn)入下一步過(guò)程
可以確定的是刪除的內(nèi)容肯定是從 old 中的 index 進(jìn)行刪除的 添加的內(nèi)容肯定是從 new 中的 index 中來(lái)的伤锚,按照這個(gè)邏輯我們可以整理如下內(nèi)容擅笔。old_11 del
new_11 add
old_13 del
new_14 add到這一步我們需要找出替換的內(nèi)容,很明顯替換的內(nèi)容就是從old中del的并且在 >new 中 add 的并且 index 相同的i tem屯援,所以這就簡(jiǎn)單了
old_11 replace
old_13 del
new_14 add這樣就生成了兩個(gè)Dex的String部分的變化猛们。
Part 2:怎么加載新的apk
- Dex加載:將合成的新的dex加入到PathClassLoader的dexElements列表中
PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements",
makePathElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
- Res加載
訪問(wèn)應(yīng)用程序資源的函數(shù)有兩個(gè):getResources和getAssets。getResources返回Resources對(duì)象狞洋,Resources對(duì)象通過(guò)資源的ID訪問(wèn)編譯后的資源弯淘。getAssets返回AssetManager對(duì)象,AssetManager對(duì)象通過(guò)資源的文件名訪問(wèn)編譯后或未經(jīng)編譯的資源文件吉懊。實(shí)際上庐橙,Resources訪問(wèn)資源是先通過(guò)資源ID獲取文件名,然后通過(guò)AssetManager根據(jù)文件名訪問(wèn)資源文件借嗽。
為了使這兩個(gè)方法加載新的資源文件态鳖,執(zhí)行以下操作:
a. 新建一個(gè)AssetManager對(duì)象newAssetManager,通過(guò)反射調(diào)用其addAssetPath方法恶导,傳入新生成的apk文件路徑
b. 新建Resources對(duì)象浆竭,通過(guò)反射設(shè)置其mAssets屬性的值為newAssetManager
通過(guò)這種方式實(shí)現(xiàn)新的資源文件的加載。
addAssetPathMethod.invoke(newAssetManager, externalResourceFile)
assetsFiled.set(resources, newAssetManager);
參考:
https://github.com/WeMobileDev/article/blob/master/%E5%BE%AE%E4%BF%A1Android%E7%83%AD%E8%A1%A5%E4%B8%81%E5%AE%9E%E8%B7%B5%E6%BC%94%E8%BF%9B%E4%B9%8B%E8%B7%AF.md
https://www.zybuluo.com/dodola/note/554061
http://www.reibang.com/p/f7f0a712ddfe
http://blog.csdn.net/add_ada/article/details/51232889