Android热更新方案Robust
美團(tuán)?大眾點(diǎn)評(píng)是中國(guó)最大的O2O交易平臺(tái),目前已擁有近6億用戶,合作各類商戶達(dá)432萬(wàn),訂單峰值突破1150萬(wàn)單。美團(tuán)App是平臺(tái)主要的入口之一,O2O交易場(chǎng)景的復(fù)雜性決定了App穩(wěn)定性要達(dá)到近乎苛刻的要求。用戶到店消費(fèi)買優(yōu)惠券時(shí)死活下不了單,定外賣一個(gè)明顯可用的紅包怎么點(diǎn)也選不中,上了一個(gè)新活動(dòng)用戶一點(diǎn)就Crash……過(guò)去發(fā)生過(guò)的這些畫面太美不敢想象。客戶端相對(duì)Web版最大的短板就是有發(fā)版的概念,對(duì)線上事故很難有即時(shí)生效的解決方式,每次發(fā)版都如臨深淵如履薄冰,畢竟就算再完善的開(kāi)發(fā)測(cè)試流程也無(wú)法保證不會(huì)將Bug帶到線上。
從去年開(kāi)始,Android平臺(tái)出現(xiàn)了一些優(yōu)秀的熱更新方案,主要可以分為兩類:一類是基于multidex的熱更新框架,包括Nuwa、Tinker等;另一類就是native hook方案,如阿里開(kāi)源的Andfix和Dexposed。這樣客戶端也有了實(shí)時(shí)修復(fù)線上問(wèn)題的可能。但經(jīng)過(guò)調(diào)研之后,我們發(fā)現(xiàn)上述方案或多或少都有一些問(wèn)題,基于native hook的方案:需要針對(duì)dalvik虛擬機(jī)和art虛擬機(jī)做適配,需要考慮指令集的兼容問(wèn)題,需要native代碼支持,兼容性上會(huì)有一定的影響;基于Multidex的方案,需要反射更改DexElements,改變Dex的加載順序,這使得patch需要在下次啟動(dòng)時(shí)才能生效,實(shí)時(shí)性就受到了影響,同時(shí)這種方案在android N [speed-profile]編譯模式下可能會(huì)有問(wèn)題,可以參考Android N混合編譯與對(duì)熱補(bǔ)丁影響解析。考慮到美團(tuán)Android用戶機(jī)型分布的碎片化,很難有一個(gè)方案能覆蓋所有機(jī)型。
去年底的Android Dev Summit上,Google高調(diào)發(fā)布了Android Studio 2.0,其中最重要的新特性Instant Run,實(shí)現(xiàn)了對(duì)代碼修改的實(shí)時(shí)生效(熱插拔)。我們?cè)诹私釯nstant Run原理之后,實(shí)現(xiàn)了一個(gè)兼容性更強(qiáng)的熱更新方案,這就是產(chǎn)品化的hotpatch框架--Robust。
原理
Robust插件對(duì)每個(gè)產(chǎn)品代碼的每個(gè)函數(shù)都在編譯打包階段自動(dòng)的插入了一段代碼,插入過(guò)程對(duì)業(yè)務(wù)開(kāi)發(fā)是完全透明。如State.java的getIndex函數(shù):
public long getIndex() {return 100;}被處理成如下的實(shí)現(xiàn):
public static ChangeQuickRedirect changeQuickRedirect;public long getIndex() {if(changeQuickRedirect != null) {//PatchProxy中封裝了獲取當(dāng)前className和methodName的邏輯,并在其內(nèi)部最終調(diào)用了changeQuickRedirect的對(duì)應(yīng)函數(shù)if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();}}return 100L;}可以看到Robust為每個(gè)class增加了個(gè)類型為ChangeQuickRedirect的靜態(tài)成員,而在每個(gè)方法前都插入了使用changeQuickRedirect相關(guān)的邏輯,當(dāng) changeQuickRedirect不為null時(shí),可能會(huì)執(zhí)行到accessDispatch從而替換掉之前老的邏輯,達(dá)到fix的目的。
如果需將getIndex函數(shù)的返回值改為return 106,那么對(duì)應(yīng)生成的patch,主要包含兩個(gè)class:PatchesInfoImpl.java和StatePatch.java。
PatchesInfoImpl.java:
StatePatch.java:
public class StatePatch implements ChangeQuickRedirect {@Overridepublic Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {String[] signature = methodSignature.split(":");if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> areturn 106;}return null;}@Overridepublic boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {String[] signature = methodSignature.split(":");if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> areturn true;}return false;} }客戶端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex后,用DexClassLoader加載patch.dex,反射拿到PatchesInfoImpl.java這個(gè)class。拿到后,創(chuàng)建這個(gè)class的一個(gè)對(duì)象。然后通過(guò)這個(gè)對(duì)象的getPatchedClassesInfo函數(shù),知道需要patch的class為com.meituan.sample.d(com.meituan.sample.State混淆后的名字),再反射得到當(dāng)前運(yùn)行環(huán)境中的com.meituan.sample.d class,將其中的changeQuickRedirect字段賦值為用patch.dex中的StatePatch.java這個(gè)class new出來(lái)的對(duì)象。這就是打patch的主要過(guò)程。通過(guò)原理分析,其實(shí)Robust只是在正常的使用DexClassLoader,所以可以說(shuō)這套框架是沒(méi)有兼容性問(wèn)題的。
大體流程如下:
插件的問(wèn)題
OK,到這里Robust原理就介紹完了。很簡(jiǎn)單是不是?而且sample這個(gè)例子中也驗(yàn)證成功了。難道一切這么順利?其實(shí)現(xiàn)實(shí)并不是這樣,我們將這套實(shí)現(xiàn)用到美團(tuán)的主App時(shí),問(wèn)題出現(xiàn)了:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536居然不能打出包來(lái)了!從原理上分析,除了引入的patch過(guò)程aar外,我們這套實(shí)現(xiàn)是不會(huì)增加別的方法的,而且引入的那個(gè)aar的方法才100個(gè)左右,怎么會(huì)造成美團(tuán)的mainDex超過(guò)65536呢?進(jìn)一步分析,我們一共處理7萬(wàn)多個(gè)函數(shù),導(dǎo)致最后方法數(shù)總共增加7661個(gè)。這是為什么呢?
看下patch前后的dex對(duì)比:
針對(duì)com.meituan.android.order.adapter.OrderCenterListAdapter.java分析一下,發(fā)現(xiàn)進(jìn)行hotpatch之后增加了如下6個(gè)方法:
public boolean isEditMode() {return isEditMode;} private int incrementDelCount() {return delCount.incrementAndGet();} private boolean isNeedDisplayRemainingTime(OrderData orderData) {return null != orderData.remindtime && getRemainingTimeMillis(orderData.remindtime) > 0;} private boolean isNeedDisplayUnclickableButton(OrderData orderData) {return null != orderData.remindtime && getRemainingTimeMillis(orderData.remindtime) <= 0;} private boolean isNeedDisplayExpiring(boolean expiring) {return expiring && isNeedDisplayExpiring;} private View getViewByTemplate(int template, View convertView, ViewGroup parent) {View view = null;switch (template) {case TEMPLATE_DEFALUT:default:view = mInflater.inflate(R.layout.order_center_list_item, null);}return view;}但是這些多出來(lái)的函數(shù)其實(shí)就在原來(lái)的產(chǎn)品代碼中,為什么沒(méi)有Robust的情況下不見(jiàn)了,而使用了插件后又出現(xiàn)在最終的class中了呢?只有一個(gè)可能,就是ProGuard的內(nèi)聯(lián)受到了影響。使用了Robust插件后,原來(lái)能被ProGuard內(nèi)聯(lián)的函數(shù)不能被內(nèi)聯(lián)了。看了下ProGuard的Optimizer.java的相關(guān)片段:
if (methodInliningUnique) {// Inline methods that are only invoked once.programClassPool.classesAccept(new AllMethodVisitor(new AllAttributeVisitor(new MethodInliner(configuration.microEdition,configuration.allowAccessModification,true,methodInliningUniqueCounter)))); } if (methodInliningShort) {// Inline short methods.programClassPool.classesAccept(new AllMethodVisitor(new AllAttributeVisitor(new MethodInliner(configuration.microEdition,configuration.allowAccessModification,false,methodInliningShortCounter)))); }通過(guò)注釋可以看出,如果只被調(diào)用一次或者足夠小的函數(shù),都可能被內(nèi)聯(lián)。深入分析代碼,我們發(fā)現(xiàn)確實(shí)如此,只被調(diào)用了一次的私有函數(shù)、只有一行函數(shù)體的函數(shù)(比如get、set函數(shù)等)都極可能內(nèi)聯(lián)。前面com.meituan.android.order.adapter.OrderCenterListAdapter.java多出的那6個(gè)函數(shù)也證明了這一點(diǎn)。知道原因了就能有解決問(wèn)題的思路。
其實(shí)仔細(xì)思考下,那些可能被內(nèi)聯(lián)的只有一行函數(shù)體的函數(shù),真的有被插件處理的必要嗎?別說(shuō)一行代碼的函數(shù)出問(wèn)題的可能性小,就算出問(wèn)題了也可以通過(guò)patch內(nèi)聯(lián)它的那個(gè)函數(shù)來(lái)解決問(wèn)題,或者patch這一行代碼調(diào)用的那個(gè)函數(shù)。只調(diào)用了一次的函數(shù)其實(shí)是一樣的。所以通過(guò)分析,這樣的函數(shù)其實(shí)是可以不被插件處理的。那么有了這個(gè)認(rèn)識(shí),我們對(duì)插件做了處理函數(shù)的判斷,跳過(guò)被ProGuard內(nèi)聯(lián)可能性比較大的函數(shù)。重新在團(tuán)購(gòu)試了一次,這次apk順利的打包出來(lái)了。通過(guò)對(duì)打出來(lái)apk中的dex做分析,發(fā)現(xiàn)優(yōu)化后的插件還是影響了內(nèi)聯(lián)效果,不過(guò)只導(dǎo)致方法數(shù)增加了不到1000個(gè),所以算是臨時(shí)簡(jiǎn)單的解決了這個(gè)問(wèn)題。
影響
原理上,Robust是為每個(gè)函數(shù)都插入了一段邏輯,為每個(gè)class插入了ChangeQuickRedirect的字段,所以最終肯定會(huì)增加apk的體積。以美團(tuán)主App為例,平均一個(gè)函數(shù)會(huì)比原來(lái)增加17.47個(gè)字節(jié),整個(gè)App中我們一共處理了6萬(wàn)多個(gè)函數(shù),導(dǎo)致包大小由原來(lái)的19.71M增加到了20.73M。有些class沒(méi)有必要添加ChangeQuickRedirect字段,以后可以通過(guò)將這些class過(guò)濾掉的方式來(lái)做優(yōu)化。
Robust在每個(gè)方法前都加上了額外的邏輯,那對(duì)性能上有什么影響呢?
從圖中可以看到,對(duì)一個(gè)只有內(nèi)存運(yùn)算的函數(shù),處理前后分別執(zhí)行10萬(wàn)次的時(shí)間增加了128ms。這是在華為4A上的測(cè)試結(jié)果。
對(duì)啟動(dòng)速度上的影響:
在同一個(gè)機(jī)器上的結(jié)果,處理前后的啟動(dòng)時(shí)間相差了5ms。
補(bǔ)丁的問(wèn)題
再來(lái)看看補(bǔ)丁本身。要制作出補(bǔ)丁,我們可能會(huì)面臨如下兩個(gè)問(wèn)題:
1. 如何解決混淆問(wèn)題? 2. 被補(bǔ)的函數(shù)中使用了super相關(guān)的調(diào)用怎么辦?其實(shí)混淆的問(wèn)題比較好處理。先針對(duì)混淆前的代碼生成patch.class,然后利用生成release包時(shí)對(duì)應(yīng)的mapping文件中的class的映射關(guān)系,對(duì)patch.class做字符串上的處理,讓它使用線上運(yùn)行環(huán)境中混淆的class。
被補(bǔ)的函數(shù)中使用了super相關(guān)的調(diào)用怎么辦?比如某個(gè)Activity的onCreate方法中需要調(diào)用super.onCreate,而現(xiàn)在這個(gè)bad.Class的badMethod就是這個(gè)Activity的onCreate方法,那么在patched.class的patchedMethod中如何通過(guò)這個(gè)Activity的對(duì)象,調(diào)用它父類的onCreate方法呢?通過(guò)分析Instant Run對(duì)這個(gè)問(wèn)題的處理,發(fā)現(xiàn)它是在每個(gè)class中都添加了一個(gè)代理函數(shù),專門來(lái)處理super的問(wèn)題的。為每個(gè)class都增加一個(gè)函數(shù)無(wú)疑會(huì)增加總的方法數(shù),這樣做肯定會(huì)遇到65536這個(gè)問(wèn)題。所以直接使用Instant Run的做法顯然是不可取的。
在Java中super是個(gè)關(guān)鍵字,也無(wú)法通過(guò)別的對(duì)象來(lái)訪問(wèn)到。看來(lái),想直接在patched.java代碼中通過(guò)Activity的對(duì)象調(diào)用到它父類的onCreate方法有點(diǎn)不太可能了。不過(guò)通過(guò)對(duì)class文件做分析,發(fā)現(xiàn)普通的函數(shù)調(diào)用是使用JVM指令集的invokevirtual指令,而super.onCreate的調(diào)用使用的是invokesuper指令。那是不是將class文件中這個(gè)調(diào)用的指令改為invokesuper就好了?看如下的例子:
產(chǎn)品代碼SuperClass.java:
產(chǎn)品代碼TestSuperClass.java:
public class TestSuperClass extends SuperClass{String subUuid;public void setSubUuid(String id) {subUuid = id;}@Overridepublic void thisIsSuper() {Log.d("TestSuperClass", "thisIsSuper no call");} }TestSuperPatch.java是DexClassLoader將要加載的代碼:
public class TestSuperPatch {public static void testSuperCall() {TestSuperClass testSuperClass = new TestSuperClass();String t = UUID.randomUUID().toString();Log.d("TestSuperPatch", "UUID " + t);testSuperClass.setUuid(t);testSuperClass.thisIsSuper();} }對(duì)TestSuperPatch.class的testSuperClass.thisIsSuper()調(diào)用做invokesuper的替換,并且將invokesuper的調(diào)用作用在testSuperClass這個(gè)對(duì)象上,然后加載運(yùn)行:
Caused by: java.lang.NoSuchMethodError: No super method thisIsSuper()V in class Lcom/meituan/sample/TestSuperClass; or its super classes (declaration of 'com.meituan.sample.TestSuperClass' appears in /data/app/com.meituan.robust.sample-3/base.apk)報(bào)錯(cuò)信息說(shuō)在TestSuperClass和TestSuperClass的父類中沒(méi)有找到thisIsSuper()V函數(shù)!但是實(shí)際上TestSuperClass和父類中是存在thisIsSuper()V函數(shù)的,而且通過(guò)apk反編譯看也確實(shí)存在的,那怎么就找不到呢?分析invokesuper指令的實(shí)現(xiàn),發(fā)現(xiàn)系統(tǒng)會(huì)在執(zhí)行指令所在的class的父類中去找需要調(diào)用的方法,所以要將TestSuperPatch跟TestSuperClass一樣作為SuperClass的子類。修改如下:
public class TestSuperPatch extends SuperClass {... }然后再做一次嘗試:
08-11 09:12:03.012 1787-1787/? D/TestSuperPatch: UUID c5216480-5c3a-4990-896d-58c3696170c5 08-11 09:12:03.012 1787-1787/? D/SuperClass: thisIsSuper c5216480-5c3a-4990-896d-58c3696170c5看一下testSuperCall的實(shí)現(xiàn),將UUID.randomUUID().toString()的結(jié)果,通過(guò)setUuid賦值給了testSuperClass這個(gè)對(duì)象的父類的uuid字段。從日志可以看出,對(duì)testSuperClass.thisIsSuper處理后,確實(shí)是調(diào)用到了testSuperClass這個(gè)對(duì)象的super的thisIsSuper函數(shù)。OK,super的問(wèn)題看來(lái)解決了,而且這種方式不會(huì)增加方法數(shù)。
上線后的效果
Robust 靠譜嗎?
嘗試修個(gè)線上的問(wèn)題,我們是在07.14下午17:00多的時(shí)候上線的補(bǔ)丁,我們可以看到接下來(lái)的幾天一直到07.17號(hào)將補(bǔ)丁下線,這個(gè)線上問(wèn)題得到了明顯的修復(fù),補(bǔ)丁下線后看到07.18號(hào)這個(gè)問(wèn)題又明顯上升了。直到07.18號(hào)下班前又重新上線補(bǔ)丁。
補(bǔ)丁的兼容性和成功率如何?通過(guò)以上的理論分析,可以看到這套實(shí)現(xiàn)基本沒(méi)有兼容性問(wèn)題,實(shí)際上線的數(shù)據(jù)如下:
先簡(jiǎn)單解釋下這幾個(gè)指標(biāo):
補(bǔ)丁列表拉取成功率=拉取補(bǔ)丁列表成功的用戶/嘗試?yán)⊙a(bǔ)丁列表的用戶
補(bǔ)丁下載成功率=下載補(bǔ)丁成功的用戶/補(bǔ)丁列表拉取成功的用戶
patch應(yīng)用成功率=patch成功的用戶/補(bǔ)丁下載成功的用戶
通過(guò)這個(gè)表能夠看出,我們的patch信息拉取的成功最低,平均97%多,這是因?yàn)閷?shí)際的網(wǎng)絡(luò)原因,而下載成功后的patch成功率是一直在99.8%以上。而且我們做的是無(wú)差別下發(fā),服務(wù)端沒(méi)有做任何針對(duì)機(jī)型版本的過(guò)濾,線上的結(jié)果再次證明了Robust的高兼容性。
總結(jié)
目前業(yè)界已有的Android App熱更新方案,包括Multidesk和native hook兩類,都存在一些兼容性問(wèn)題。為此我們借鑒Instant Run原理,實(shí)現(xiàn)了一個(gè)兼容性更強(qiáng)的熱更新方案--Robust。Robust除了高兼容性之外,還有實(shí)時(shí)生效的優(yōu)勢(shì)。so和資源的替換目前暫時(shí)未做實(shí)現(xiàn),但是從框架上來(lái)說(shuō)未來(lái)是完全有能力支持的。當(dāng)然,這套方案雖然對(duì)開(kāi)發(fā)者是透明的,但畢竟在編譯階段有插件侵入了產(chǎn)品代碼,對(duì)運(yùn)行效率、方法數(shù)、包體積還是產(chǎn)生了一些副作用。這也是我們下一步努力的方向。
參考文獻(xiàn)
- Instant Run, Android Tools Project Site, http://tools.android.com/tech-docs/instant-run.
- Oracle, The Java Virtual Machine Instruction Set, https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html.
- Oracle, ClassLoader, https://docs.oracle.com/javase/7/docs/api/java/lang/ClassLoader.html).
- ltshddx, https://github.com/ltshddx/jaop).
- w4lle, Android熱補(bǔ)丁之AndFix原理解析.
- shwenzhang, Android N混合編譯與對(duì)熱補(bǔ)丁影響解析.
https://tech.meituan.com/android_robust.html
總結(jié)
以上是生活随笔為你收集整理的Android热更新方案Robust的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: [Android]你不知道的Androi
- 下一篇: Android动态日志系统Holmes