美團熱更新方案 ASM 實踐
作者: 李楠,環信 Android 工程師,本文為 CSDN 首發,版權歸作者所有,未經允許,請勿轉載。
責編: 唐小引,技術之路,共同進步。歡迎技術投稿、給文章糾錯,請傳送郵件至[email protected]。
從《Android熱更新方案 Robust》一文可知,美團熱更新使用的是 Instant Run 的方案。本文將著重於分享美團熱更新方案中沒講到的部分,包含以下幾個方面:
- 作為雲服務提供廠商,需要提供給客戶 SDK,SDK 釋出後同樣要考慮 Bug 修復問題。此處將介紹作為 SDK 釋出者的熱更新方案選型,即為什麼使用美團方案&Instant Run 方案。
- 美團方案實現的大致結構;
- ASM 插樁的過程,位元組碼導讀,以及遇到的各種坑。
方案選擇
我們公司提供即時通訊服務,同時需要提供給使用者方便整合的即時通訊 SDK,每次 SDK 釋出的同時也面臨 SDK 釋出後緊急 Bug 的修復問題。現在市面上的熱更新方案普遍不適用 SDK 提供方使用。以阿里的 AndFix 和微信的 Tinker 為例,都是直接修改併合成新的 Apk。這樣做對於普通 App 沒有問題,但是對於 SDK 提供方是不可以的,SDK 釋出者不能夠直接修改 Apk,這個事情只能由 App 開發者來進行。
Tinker 方案如圖:
女媧(Nuwa)方案,由大眾點評 Jason Ross 實現並開源,其在 classLoader
patch
類所在的 dex
, 插入到 dex
佇列前面,這樣在 classloader
按照類名字載入的時候會優先載入 patch
類。女媧方案如圖:
Nuwa 方案有一個條件約束,就是每個類都要插樁,插入一個類的引用,並且這個被引用類需要打包到單獨的 dex
檔案中,這樣保證每個類都沒有被打上 CLASS_ISPREVERIFIED
標誌。
作為 SDK 提供者,只能提供 jar 包給使用者,無法約束使用者的 dex
生成過程,所以 Nuwa 方案無法直接應用。女媧方案是開源的,而且其中提供了 ASM 插樁示例,對於後面應用美團方案有很好參考意義。
美團& Instant Run 方案
美團方案也就是 Instant Run 的方案基本思路就是在每個函式都插樁,如果一個類存在 Bug,需要修復,就將插樁點類的 changeRedirect
欄位從 null
值變成 patch
類。基本原理在美團方案中有講述,但是美團文中沒有講最重要的一個問題,就是如何在每一個函式前面插樁,下面會詳細講一下。Patch 應用部分,這裡忽略,因為是 Java 程式碼,大家可以反編譯 Instant Run.jar,看一下大致思路,基本都能寫出來。
插樁
插樁的動作就是在每一個函式前面都插入 PatchProxy.isSupport...PatchProxy.accessDisPatch
這一系列程式碼(參看美團方案)。插樁工作直接修改 class
檔案,因為這樣不影響正常程式碼邏輯,只有最後打包釋出的時候才進行插樁。
插樁最常用的是 asm.jar。接下來的部分需要使用者先了解 asm.jar 的大致使用流程。瞭解這個過程最好是找個例項實踐一下,光看介紹文章是看不懂的。
ASM 有兩種方式解析 class
檔案,一種是 core API, “provides an event based representation of classes”,類似解析 XML 的 SAX 的事件觸發方式、遍歷類以及類的欄位,類中的方法,在遍歷的過程中會依次觸發相應的函式,比如遍歷類函式時,觸發 visitMethod(name, signature...)
,使用者可以在這個方法中修改函式實現。
另外一種是 tree API, “provides an object based representation”,類似解析 XML 中的 DOM 樹方式。本文中,這裡使用了 core API 方式。asm.jar 有對應的 manual asm4-guide.pdf,需要仔細研讀,瞭解其用法。
使用 asm.jar 把 java class 反編譯為位元組碼
反編譯為位元組碼對應的命令是
java -classpath "asm-all.jar" org.jetbrains.org.objectweb.asm.util.ASMifier State.class
這個地方有一個坑,官方版本 asm.jar 在執行 ASMifier 命令的時候總是報錯,後來在 Android Stuidio 的目錄下面找一個 asm-all.jar 替換再執行就不出問題了。但是用 asm.jar 插樁的過程,仍然使用官方提供的 asm.jar。
插入前程式碼:
class State {
long getIndex(int val) {
return 100;
}
}
ASMifier 反編譯後位元組碼如下:
mv = cw.visitMethod(0, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
插樁後代碼:
long getIndex(int a) {
if ($patch != null) {
if (PatchProxy.isSupport(new Object[0], this, $patch, false)) {
return ((Long) com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false)).longValue();
}
}
return 100;
}
ASMifier 反編譯後代碼如下:
mv = cw.visitMethod(ACC_PUBLIC, "getIndex", "(I)J", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
Label l0 = new Label();
mv.visitJumpInsn(IFNULL, l0);
mv.visitInsn(ICONST_0);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "isSupport", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Z", false);
mv.visitJumpInsn(IFEQ, l0);
mv.visitIntInsn(BIPUSH, 1);
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object");
mv.visitInsn(DUP);
mv.visitIntInsn(BIPUSH, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
mv.visitInsn(AASTORE);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;");
mv.visitInsn(ICONST_0);
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
mv.visitInsn(LRETURN);
mv.visitLabel(l0);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitLdcInsn(new Long(100L));
mv.visitInsn(LRETURN);
mv.visitMaxs(4, 2);
mv.visitEnd();
對於插樁程式來說,需要做的就是把差異部分插樁到程式碼中。
需要將全部入參傳遞給 patch
方法,插入的程式碼因此會根據入參進行調整,同時也要處理返回值.
可以觀察上面程式碼,上面的例子顯示了一個 int
型入參 a,裝箱變成 Integer
,放在一個 Object[]
陣列中,先後呼叫 isSupport
和 accessDispatch
,傳遞給 patch
類的對應方法,patch
返回型別是 Long
,然後呼叫 longValue
,拆箱變成 long
型別。
對於普通的 Java 物件,因為均派生自 Object,所以物件的引用直接放在陣列中;對於 primitive
型別(包括 int, long, float….)的處理,需要先呼叫 Integer
, Boolean
, Float
等 Java 物件的建構函式,將 primitive
型別裝箱後作為 object 物件放在陣列中。
如果原來函式返回結果的是 primitive
型別,需要插樁程式碼將其轉化為 primitive
型別。還要處理陣列型別,和 void
型別。Java 的 primitive
型別在 Java Virtual Machine Specification 中有定義。
這個插入過程有兩個關鍵問題,一個是函式 signature 的解析,另外一個是適配這個引數變化插入程式碼。下面詳細解釋下:
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
這個函式是 asm.jar 訪問類函式時觸發的事件,desc 變數對應 Java jni 中的 signature
,比如這裡是'(I)J'
, 需要解析並轉換成 primitive
型別、類、陣列、void。這部分程式碼參考了 Android 底層的原始碼 libcore/luni/src/main/java/libcore/reflect
,和 Sun Java 的 SignatureParser.java
,都有反映了這個遍歷過程。
理解 Java 位元組碼,需要理解 JVM 中的棧的結構。JVM 是一個基於棧的架構。方法執行的時候(包括 main
方法),在棧上會分配一個新的幀,這個棧幀包含一組區域性變數。這組區域性變數包含了方法執行過程中用到的所有變數,包括 this
引用,所有的方法引數,以及其它區域性定義的變數。對於類方法(也就是 static
方法)來說,方法引數是從第 0 個位置開始的,而對於例項方法來說,第 0 個位置上的變數是 this 指標。(引自:Java 位元組碼淺析)
分析中間部分位元組碼實現:
com.hyphenate.patch.PatchProxy.accessDispatch(new Object[] {a}, this, $patch, false))
mv.visitIntInsn(BIPUSH, 1); # 數字 1 入棧,對應 new Object[1]陣列長度 1。 棧:[1]
mv.visitTypeInsn(ANEWARRAY, "java/lang/Object"); # ANEWARRY:count(1) → arrayref, 棧:[arr_ref]
mv.visitInsn(DUP); # 棧:[arr_ref, arr_ref]
mv.visitIntInsn(BIPUSH, 0); # 棧:[arr_ref, arr_ref, 0]
mv.visitVarInsn(ILOAD, 1); # 區域性變數位置 1 的內容入棧, 棧:[arr_ref, arr_ref, 0, a]
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); # 呼叫 Integer.valueOf, INVOKESTATIC: [arg1, arg2, ...] → result, 棧:[arr_ref, arr_ref, 0, integerObjectOf_a]
mv.visitInsn(AASTORE); # store a reference into array: arrayref, index, value →, 棧:[arr_ref]
mv.visitVarInsn(ALOAD, 0); # this 入棧,棧:[arr_ref, this]
mv.visitFieldInsn(GETSTATIC, "com/hyphenate/State", "$patch", "Lcom/hyphenate/patch/PatchReDirection;"); #$patch 入棧,棧:[arr_ref, this, $patch]
mv.visitInsn(ICONST_0); #false 入棧, # 棧:[arr_ref, this, $patch, false]
mv.visitMethodInsn(INVOKESTATIC, "com/hyphenate/patch/PatchProxy", "accessDispatch", "([Ljava/lang/Object;Ljava/lang/Object;Lcom/hyphenate/patch/PatchReDirection;Z)Ljava/lang/Object;", false); # 呼叫 accessDispatch, 棧包含返回結果,棧:[longObject]
熟悉上面的位元組碼以及對應的棧幀變化,也就掌握了插樁過程。
坑
ClassVisitor.visitMethod()
中 access
如果是 ACC_SYNTHETIC
或者 ACC_BRIDGE
,插樁後無法正常執行。ACC_SYNTHETIC
表示函式由 JAVAC 自動生成的,enum
型別就會產生這種型別的方法,不需要插樁,直接略過。因為觀察到模版類也會產生 ACC_SYNTHETIC
,所以插樁過程跳過了模版類。
ClassVisitor.visit()
函式對應遍歷到類觸發的事件,access
如果是 ACC_INTERFACE
或者 ACC_ENUM
,無需插樁。簡單說就是介面和 enum
不涉及方法修改,無需插樁。
靜態方法的實現和普通類成員函式略有出入,對於彙編程式來說,本地棧的第一個位置,如果是普通方法,會儲存 this
引用,static
方法沒有 this
,這裡稍微調整一下就可以實現的。
不定引數由於要求連續輸入的引數型別相同,被編譯器直接變成了陣列,所以對本程式沒有造成影響。
大小
插樁因為對每個函式都插樁,反編譯後看實際上增加了大量程式碼,甚至可以說插入的程式碼比原有的程式碼還要多。但是實際上最終生成的 jar 包增長了大概 20% 多一點,並沒有想的那麼多,在可接受範圍內。因為 class
所佔的空間不止是程式碼部分,還包括類描述、欄位描述、方法描述、const-pool
等,程式碼段只佔其中的不到一半。可以參考 The class File Format。
討論
前面程式碼插樁的部分和美團熱更文章中保持一致,實際上還有些細節還可以調整。isSupport
這個函式的引數可以調整如下:
if (PatchProxy.isSupport(“getIndex”, "(I)J", false)) {
這樣能減小插樁部分程式碼,而且可以區分名字相同的函式。
PatchProxy.isSupport
最後一個引數表示是普通類函式還是 static
函式,這個是方便 Java 應用 patch 的時候處理。
瞭解最新移動開發、VR/AR 乾貨技術分享,請關注 mobilehub 微信公眾號(ID: mobilehub)。
相關推薦
美團熱更新方案 ASM 實踐
作者: 李楠,環信 Android 工程師,本文為 CSDN 首發,版權歸作者所有,未經允許,請勿轉載。 責編: 唐小引,技術之路,共同進步。歡迎技術投稿、給文章糾錯,請傳送郵件至[email protected]。 從《Android熱更新方案
Unity3D熱更新方案網摘總結
xiang 分配 3.5 速度慢 for 小夥伴 source 為什麽 software 參考:http://blog.csdn.net/guofeng526/article/details/52662994 http://blog.csdn.net/u010019717/
手遊熱更新方案--Unity3D下的CsToLua技術
con 我們 如何 研發 效率 並且 dll文件 play 表示 WeTest 導讀 CsToLua工具將客戶端 C#源碼自動轉換為Lua,實現熱更新,本文以麻將項目為例介紹客戶端技術細節。 麻將項目架構 其中ChinaMahjong-CSLua為C#工
美團接口自動化測試實踐
接口測試 生成 [] mage except 一個數據庫 分享 同一行 解釋 一、概述 1.1 接口自動化概述 眾所周知,接口自動化測試有著如下特點: 低投入,高產出。 比較容易實現自動化。 和UI自動化測試相比更加穩定。 如何做好一個接口自動化測試項目呢?
強化學習在美團“猜你喜歡”的實踐
1 概述 “猜你喜歡”是美團流量最大的推薦展位,位於首頁最下方,產品形態為資訊流,承擔了幫助使用者完成意圖轉化、發現興趣、並向美團點評各個業務方導流的責任。經過多年迭代,目前“猜你喜歡”基線策略的排序模型是業界領先的流式更新的Wide&Deep模型[1]。考慮Point-Wise模型缺少對候選集It
前端黑科技:美團網頁首幀優化實踐
前言 自JavaScript誕生以來,前端技術發展非常迅速。移動端白屏優化是前端介面體驗的一個重要優化方向,Web 前端誕生了 SSR 、CSR、預渲染等技術。在美團支付的前端技術體系裡,通過預渲染提升網頁首幀優化,從而優化了白屏問題,提升使用者體驗,並形成了最佳實踐。 在前端渲染領域,主要有以下幾種方式
美團Android資源混淆保護實踐
前言 Android應用中的APK安全性一直遭人詬病,市面上充斥著各種被破解或者漢化的應用,破解者可以非常簡單的通過破解工具就能對一個APK進行反編譯、破解、漢化等等,這樣就可以修改原有程式碼的邏輯、新增新程式碼、新增或修改資源、或者更有甚者植入病毒等等,從而破壞原有A
騰訊開源手遊熱更新方案Xlua嚐鮮(三)——C#訪問Lua
C#訪問Lua 這裡指的是C#主動發起對Lua資料結構的訪問。 一、獲取一個全域性基本資料型別 訪問LuaEnv.Global就可以了,上面有個模版Get方法,可指定返回的型別。 luaenv.Global.Get<int>("a"); luaenv.Globa
Android熱更新方案Robust
美團•大眾點評是中國最大的O2O交易平臺,目前已擁有近6億使用者,合作各類商戶達432萬,訂單峰值突破1150萬單。美團App是平臺主要的入口之一,O2O交易場景的複雜性決定了App穩定性要達到近乎苛刻的要求。使用者到店消費買優惠券時死活下不了單,定外賣一個明顯可用的紅包怎麼
Unity3D 熱更新方案(集合各位專家的彙總)
一、什麼是熱更新? 熱更新,是對hot update或者hot fix的翻譯,計算機術語,表示在不停機的前提下對系統進行更改(摘抄一下): “hot就是熱,機器執行會發燙,hot就是不停機的意思。 熱更新,是個很形象的詞,機器燙的時候更新,開著更新。 比如Windows不重啟的前提下安裝補丁 比
領域驅動設計(DDD)在美團點評業務系統的實踐
點選上方藍字訂閱,不錯過下一篇好文章本文轉自美團點評技術公眾號:meituantech前言至少3
移動端熱更新方案(iOS+Android)
一 、熱更新(熱修復)產品背景 這裡談到的熱更新都是指APP(不包含網頁)。APP按大類別可以粗略分為 應用 和 遊戲。 APP的開發週期是極其快速的,在實際開發流程中,我們總會有一些需求迫使我們短時間內快速上線,比如需求流程出錯,程式設計師主觀導致的一
來看看大資料的實戰魅力:美團大資料平臺架構實踐
今天給大家介紹的內容主要包括以下四個部分首先是介紹一下美團大資料平臺的架構,然後回顧一下歷史,看整個平臺演進的時間演進線,每一步是怎麼做的,以及一些挑戰和應對策略,最後總結一下,聊一聊我對平臺化的看法。 美團大資料平臺架構實踐 給大家介紹的內容主要包括以下四個部分首先是介紹一下美團大資料平
Lego:美團點評介面自動化測試實踐
概述 介面自動化概述 眾所周知,介面自動化測試有著如下特點: 低投入,高產出。 比較容易實現自動化。 和UI自動化測試相比更加穩定。 如何做好一個介面自動化測試專案呢? 我認為,一個“好
騰訊開源手遊熱更新方案Xlua嚐鮮(四)——Lua呼叫C#
new C#物件 你在C#這樣new一個物件: var newGameObj = new UnityEngine.GameObject(); 對應到Lua是這樣: local newGameObj =CS.UnityEngine.GameObject() 基本類似,除了:
skynet熱更新方案
普通程式lua的熱更新: lua的熱更新一般是比較方便的,比如下面一個模組module.lua local module = {} function module:func() print("module:func()") end 通常實現更
iOS 熱更新方案
以下是iOS app熱更新的幾種方案, 由於蘋果在2017年3月左右更新了開發者協議, 禁止需要線上稽核的應用進行熱更新, 所以請大家慎用 (企業版不需要提交稽核當然是可以使用的) 一、JSPatch 熱更新時,從伺服器拉去js指令碼。理論上可以修改和新建所有的模
Unity3D 熱更新方案
的應用下,是有些語義錯誤的,但是作為大家都熟知的一項技術,我們姑且這麼叫它,相信很長時間內,大家依然還會這麼叫,甚至有人叫它“暖更新”。 一、什麼是熱更新? 熱更新,是對hot update或者hot fix的翻譯,計算機術語,表示在不停機的前提下對系統進行更改(摘抄一下): “hot就是熱,機器執行會
輕量級低風險 iOS 熱更新方案
點選上方“iOS開發”,選擇“置頂公眾號”關鍵時刻,第一時間送達!我們都知道蘋果對 Hotfix
引入 Tinker 熱更新方案遇到的問題
1、每次 Run 都會生成一個 bakApk 問題: 引入 Tinker 熱更新方案後,每次 Run 都會生成一個 bakApk,如果每天要除錯很多地方,那麼 build/bakApk 下,會生成N個對應的 bakApk 資料夾,最終會使整個專案檔案