想寫這篇文章挺久了憎夷,今天我們來聊一聊熱更新粱玲。
0x01.前言
國內(nèi)開發(fā)者對于熱更新不可謂不熱衷躬柬,前仆后繼地發(fā)明一個又一個新思路,我自己也是對熱更新特別感興趣抽减,尤其對iOS來說允青,這更是一片敏感的灰色地帶,自從jspatch
被蘋果警告之后卵沉,各家公司的熱更需求依然沒有減弱颠锉,因此這個話題變得相當微妙,各家公司都有一些自己的方案史汗,并且不再開源琼掠,因為開源意味著會像jspatch
一樣變成把子,業(yè)界最著名的DynamicCocoa就是一個非常典型的例子停撞,時至今日瓷蛙,再去打開這個倉庫的issue
依然會讓你捧腹大笑。
0x02.演進過程
其實一路走來我對熱更新演進過程的總結(jié)就是兩個字 —— 內(nèi)卷
戈毒,雖然這兩個戲謔的字不是那么合適艰猬,但事實就是如此,熱更的方案變得更加多元化埋市,防審核能力也變得更強冠桃。開發(fā)大佬們也已經(jīng)不再局限于現(xiàn)成的腳本語言,轉(zhuǎn)而自己去開發(fā)新的腳本語言恐疲。
眾所周知一個apple應(yīng)用是靜態(tài)編譯成二進制文件的(AOT)腊满,在線上去修改此類文件是不現(xiàn)實的,因此就需要用到腳本語言(JIT)+ 動態(tài)化語言的能力培己,這就是熱更的本質(zhì)碳蛋。幾乎所有熱更的流程基本都符合下面這張流程圖的邏輯(ReactNative
和Weex
等跨平臺方案今天不在我們的討論范圍內(nèi),他們確實有具備熱更的能力省咨,但我認為他們不夠trick
肃弟。時至今日,此類的跨平臺框架基本已經(jīng)被apple認可零蓉,因為他們已經(jīng)是非常成熟的框架笤受,風險也相對可控)
接下來,我會拿兩個有代表性的方案敌蜂,進行一些技術(shù)細節(jié)的闡述箩兽。
0x03. JSPatch
第一個例子拿JSPatch
我覺得是毫無疑問的,國內(nèi)目前最有影響力的框架我依然覺得是JSPatch
章喉,作者也一直是我的偶像汗贫,blog的文章我也經(jīng)常拜讀身坐。作者將JS
這種現(xiàn)成的語言作為熱更腳本,再加上平臺提供的JScontext
能夠快速地進行與native的通信落包。站在巨人的肩膀上決定了JSPatch
從開始就是一個輕量高性能的熱更框架部蛇。下面來簡單說說JSPatch
的執(zhí)行邏輯和原理。
0x031.JSContext
JSContext
咐蝇, 一個JS
執(zhí)行環(huán)境的對象涯鲁,在一個JSContext
對象創(chuàng)建時,會在其中內(nèi)置一個JSVirtualMachine(JS虛擬機)
,你可以非常方便快速地在native執(zhí)行一段JS或者進行兩種語言的互通有序,在這之中你不需要依賴任何WebView(就像node.js一樣提供一個node環(huán)境)抹腿。就像下面這樣:
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var name = 'wbq'; var age = '18'; function addFunc(value1, value2){ return value1 + value2};"];
JSValue *value = [context[@"addFunc"] callWithArguments:@[@12, @323]];
NSLog(@"%@", value.toString);
每個JSContext
對象默認是隔離的,也就是他們都是單獨的環(huán)境笔呀,彼此之前不能傳遞信息幢踏,當然你也可以通過:- (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;
來創(chuàng)建多個JSContext
對象來共享一個虛擬機髓需,這里不展開许师。
0x032.引擎啟動
引擎啟動之后,框架會通過上述的JSContext
執(zhí)行一個叫作JSPatch.js的文件僚匆,這個文件是框架的重點之一微渠,我把他歸結(jié)為兩個功能:1.收集并解析熱更腳本的數(shù)據(jù)。2.將解析完的數(shù)據(jù)交還給Native處理咧擂。
0x033.解析腳本/方法替換
拉取腳本我覺得沒什么好說的逞盆,下面講講腳本解析,挑demo中最簡單的一個例子來講
require('UIViewController');
defineClass('JPViewController', {
handleBtn: function(sender) {
console.log(this);
var tableViewCtrl = UIViewController.alloc().init()
self.navigationController().pushViewController_animated(tableViewCtrl, YES)
},
})
這里為什么對
UIViewController
要用require
松申,是因為在下面的handleBtn
這個方法中使用了這個類云芦,JSPatch.js中的require
方法在當前全局中保存了UIViewContrller
這個對象,防止在進行UIViewController.alloc().init()
方法的鏈式調(diào)用中不會出現(xiàn)UIViewContrller is not defined
的報錯贸桶。-
JSPatch.js的
defineClass = function(declaration, properties, instMethods, clsMethods){...}
方法傳入的是類的聲明舅逸,屬性、實例方法皇筛、類方法琉历。而例子中第二個參數(shù)傳入是一個實例方法的map,是因為作者在其中發(fā)現(xiàn)第二個參數(shù)不是數(shù)組的情況下水醋,就會把第二個參數(shù)當作實例方法往前提旗笔。之后會把解析完的腳本數(shù)據(jù)通過native早就準備好的方法_OC_defineClass
,進行處理:context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) { return defineClass(classDeclaration, instanceMethods, classMethods); };
其實OC的
defineClass
方法內(nèi)部的細節(jié)很多拄踪,當然本質(zhì)就是進行方法替換蝇恶,作者先將將方法轉(zhuǎn)發(fā)的方法- (void)forwardInvocation:(NSInvocation *)anInvocation
進行了替換,再將熱更腳本收集的方法全部掛到替換的方法轉(zhuǎn)發(fā)方法
上(第一次會有點饒惶桐,可以多看幾遍源碼)撮弧,NSInvocation
對象包含所有的方法入?yún)⒑头椒ê灻拿停浅_m合在這里進行方法的調(diào)用。 當然
UIViewController.alloc().init()
類似的語句是做不到直接調(diào)用的想虎,因為js中壓根沒有這些方法卦尊,作者博客也有講到,所以另辟蹊徑舌厨。在這一步岂却,JSPatch.js會通過正則
對熱更腳本進行一輪膠水代碼的添加,在解析階段裙椭,上述這句話會變成UIViewController.__c("alloc")().__c("init")()
躏哩,這樣通過一個__c
的統(tǒng)一的膠水函數(shù),就能非常方便地把諸如alloc
揉燃、init
方法交還給native
進行調(diào)用了扫尺。
以上就是大致的執(zhí)行流程了,當前其中還有很多復雜的細節(jié)炊汤,比如返回值的處理正驻,上面只是講到無返回值類型最簡單的一種。各種參數(shù)類型的處理抢腐,各種語法的處理比如block姑曙,結(jié)構(gòu)體,用libffi處理C函數(shù)等等迈倍。
前段時間伤靠,在重讀源碼的過程中,逮住了一只野生的bang大佬啼染,問了幾個細節(jié)問題宴合,收到了解答也是非常的開心。
0x34 小結(jié)
總的來說JSPatch
是一個非常優(yōu)秀的熱更框架迹鹅,作者也是一步步精益求精卦洽,滿足了廣大開發(fā)者的各種訴求。思路和實現(xiàn)都非常值得閱讀和學習徒欣,目前github還在維護的熱更框架采用JS
作為腳本語言的基本都是學習了JSPatch
思路逐样,例如這個TTPatch(真沒看出啥大區(qū)別...)。當然此類的解決方案雖然還是有過審的可能打肝,但是大家漸漸地開始不太敢用了脂新,畢竟被下架付出的成本遠遠比線上bug的成本高多了,用JS
和JSContext
進行動態(tài)化太容易被機審掃到了粗梭。
0x04.Mango
第二個為什么選這個框架呢争便,因為第一次看到這個框架的時候,還是比較眼前一亮的断医,這個框架就是我所說的自制語言和自制編譯器/解釋器的代表型框架滞乙。具體可以看作者原理與使用介紹MangoFix:iOS熱修復另辟蹊徑奏纪,總的來說,開發(fā)者可以使用一種以.mg
為后綴的腳本文件斩启,文件內(nèi)容類似OC的語法序调,所以可以很快上手,之后同樣對腳本進行解析兔簇,不同于JSPatch
的方法轉(zhuǎn)發(fā)发绢,Mango是直接通過libffi
進行了方法替換。在研究框架的過程中垄琐,我發(fā)現(xiàn)如此優(yōu)秀的輪子边酒,網(wǎng)上關(guān)于的源碼閱讀和解釋的文章缺很少,所以今天我想特別來說說狸窘。
原理部分墩朦,我照搬了作者的原圖。接下來具體來說說技術(shù)的實現(xiàn)點翻擒。
0x041.Lex&&Yacc
作者在腳本的解析中用到了該工具氓涣,Lex&&Yacc
可以讓你輕易的解析復雜的語言,當你需要讀取一個配置文件時韭寸,或者你需要編寫一個你自己使用的語言的編譯器時春哨,你不用手工寫解析器,他可以直接幫你做到你想要的事恩伺。lex
負責詞法的分析,將腳本切割成一個個token
椰拒,yacc
負責解析語法分析晶渠,將token
按照你寫的規(guī)則生成抽象語法樹。注意 Lex 和 Yacc 都是基于正則表達燃观,后面講到褒脯。
0x042.BNF
什么是BNF?總的來說BNF是一個描述語法規(guī)則的語言
缆毁。具體可以我上一篇編譯原理(1)簡單地介紹了一下BNF番川,不是啥新東西,Yacc
同樣可以通過BNF提取語法規(guī)則脊框。
0x043.腳本解析
掌握了以上幾個知識點颁督,去理解原理也就變得不那么難了,還是找一個最簡單的例子來聊聊浇雹。
class ViewController:UIViewController {
- (void)sequentialStatementExample{
NSString *text = @"1";
self.resultView.text = text;
}
}
上面是一個mg文件沉御,可以看到和OC非常像,但是又有一些不一樣昭灵》婉桑可以看到伐谈,該腳本目的是改掉原始ViewController
的sequentialStatementExample
方法。
1.Lex
部分:首先通過正則把關(guān)鍵字匹配出來试疙,比如class
诵棵、ViewController
、:
祝旷、void
非春、@
、"1"
一個個切出來缓屠,就是上述的token
<INITIAL>"void" {return VOID; } //void關(guān)鍵字
<INITIAL>"class" {return CLASS; } //class關(guān)鍵字
<INITIAL>[A-Za-z_$][A-Za-z_$0-9]* {
NSString *identifier = mf_create_identifier(yytext);
yylval.identifier = (__bridge_retained void *)identifier;
return IDENTIFIER;
} //標識符 例如ViewController奇昙、UIViewController
<INITIAL>":" {return COLON; } //分號
<INITIAL>\" {
mf_open_string_literal_buf();
BEGIN STRING_LITERAL_STATE;
} //第一次匹配到'"'開始進入字符串收集狀態(tài)
<STRING_LITERAL_STATE>. {
mf_append_string_literal(yytext[0]);
} //匹配換行符之外的任意字符,字符串進行拼接
<STRING_LITERAL_STATE>\" {
MFExpression *expression = mf_create_expression(MF_STRING_EXPRESSION);
expression.cstringValue = mf_end_string_literal();
yylval.expression = (__bridge_retained void *)expression;
BEGIN INITIAL;
return STRING_LITERAL;
} //在字符串狀態(tài)匹配到'"',意味字符串匹配結(jié)束敌完,繼續(xù)進入首字符匹配模式
....
yylval
其中保存著相關(guān)的信息储耐,這個信息就是在詞法分析文件中(lex)進行設(shè)置的,而在語法分析文件中(yacc)就直接采用了
yytext
是指向所匹配的字符串的指針(以 NULL 結(jié)尾)滨溉,yytext[0]
就是當前首字符什湘,而 yyleng
是這個字符串的長度。
2.Yacc
部分剛開始啃確實會有一點難理解晦攒,先來個簡單的闽撤,如何匹配一個實例方法
instance_method_definition: annotation_if SUB LP type_specifier RP method_name block_statement
{
MFExpression *annotaionIfConditionExpr = (__bridge_transfer MFExpression *)$1;
MFTypeSpecifier *returnTypeSpecifier = (__bridge_transfer MFTypeSpecifier *)$4;
NSArray *items = (__bridge_transfer NSArray *)$6;
MFBlockBody *block = (__bridge_transfer MFBlockBody *)$7;
MFMethodDefinition *methodDefinition = mf_create_method_definition(annotaionIfConditionExpr, NO, returnTypeSpecifier, items, block);
$$ = (__bridge_retained void *)methodDefinition;
}
;
annotation_if 可以是empty,SUB = '-'脯颜, LP = '{' 哟旗,type_specifier = '返回類型' ,RP = ‘}’ 栋操,method_name = '函數(shù)名' 闸餐,block_statement = ‘函數(shù)體’ ,這樣就可以通過instance_method_definition
匹配出一個實例方法了矾芙。但真正能把上述這個簡單的腳本匹配出來需要還經(jīng)過層層匹配舍沙,我稍微總結(jié)了一下:
member_definition_list : member_definition | member_definition_list member_definition;
member_definition : property_definition | method_definition
method_definition : instance_method_definition | class_method_definition;
instance_method_definition : annotation_if "-" "(" type_specifier ")" method_name block_statement
type_specifier : void | BOOL | Class | id | ...
block_statement : "{" statement_list "}"
statement_list : statement | statement_list statement
statement : declaration_statement | if_statement | switch_statement | for_statement | foreach_statement | while_statement | do_while_statement | break_statement | continue_statement | return_statement | expression_statement
expression_statement : expression ";"
expression : assign_expression
assign_expression : ternary_operator_expression | primary_expression assignment_operator ternary_operator_expression
ternary_operator_expression : logic_or_expression | logic_or_expression "?" ternary_operator_expression ":" ternary_operator_expression |
logic_or_expression "?" ":" ternary_operator_expression
logic_or_expression : logic_and_expression | logic_or_expression "||" logic_and_expression
logic_and_expression : equality_expression | logic_and_expression "&&" equality_expression
equality_expression : relational_expression | equality_expression "==" relational_expression | equality_expression "!=" relational_expression
relational_expression: additive_expression | relational_expression "<"| "<=" | ">" | ">=" additive_expression
additive_expression : multiplication_expression | additive_expression "+" multiplication_expression | additive_expression "-" multiplication_expression
multiplication_expression : unary_expression | multiplication_expression "*" unary_expression
unary_expression : postfix_expression | "!" unary_expression | "-" unary_expression
postfix_expression : primary_expression | primary_expression "++" | primary_expression "--"
primary_expression : IDENTIFIER | "&" IDENTIFIER | primary_expression "." IDENTIFIER | primary_expression "." key_work_identifier | primary_expression "." selector "()" | primary_expression "." selector "(" expression_list ")"
| primary_expression "(" ")" | ...
就一個簡單的實例方法需要經(jīng)過這么多規(guī)則的篩選,不得不佩服作者細膩的邏輯能力剔宪,其實這之中也有非常多的細節(jié)拂铡,你要做很多二義性的判斷,各種特殊情況的處理葱绒,我覺得這個工作量絕對是不小的感帅。
話說回來光匹配是不夠的,在匹配過程中哈街,還需要生成AST(抽象語法樹)留瞳。比如$1、$2
這類就是獲取匹配出來的yylval.expression
骚秦,數(shù)字代表順序代表匹配的token順序她倘,例如上述MFTypeSpecifier *returnTypeSpecifier = (__bridge_transfer MFTypeSpecifier *)$4;
就是把第四個token璧微,方法的返回類型,用returnTypeSpecifier
接收硬梁。又比如$$
就代表把收集的語義封裝的對象繼續(xù)向上傳遞前硫。
PS:我自己有個疑問,yacc在層層向上匹配的過程中荧止,如果發(fā)現(xiàn)自己匹配錯了會怎么樣呢屹电?是回溯嗎?跃巡,還是在匹配過程中危号,他已經(jīng)已經(jīng)知道自己要走哪條路了?
解析完之后素邪,會得到一個個的AST的對象保存在內(nèi)存中外莲,比如方法對象就是保存在NSMutableDictionary當中。之后通過libffi進行方法替換兔朦。
0x044.方法執(zhí)行
方法執(zhí)行的邏輯也挺復雜偷线,不斷地計算AST的節(jié)點。各個語法樹節(jié)點對象的類都需要具備類型(賦值沽甥、if語句声邦、for循環(huán)、switch等等條件都會進到不一樣的執(zhí)行邏輯當中)和執(zhí)行的表達式摆舟。eval方法將計算與以該節(jié)點為根的子樹對應(yīng)的語句亥曹、表達式及子表達式,并返回執(zhí)行結(jié)果盏檐。一直執(zhí)行歇式,執(zhí)行到根節(jié)點為止。作者在這一塊可以說實現(xiàn)了一整個解釋器/虛擬機胡野。
0x045.小結(jié)
其實這個框架讓我佩服的點是他完成了自制語言整個編譯+解釋的過程,雖不知道作者是何許人也痕鳍,但是大佬的底層功力不可謂不深厚硫豆。尤其是編譯原理,搞得相當?shù)拿靼琢簦疫@種入門級小白只能頂禮膜拜熊响。業(yè)界也有在大佬基礎(chǔ)上進行更進一步探索的比如:OCRunner,和該作者簡單的聊了一下诗赌,作者本身只想做一個將OC語言翻譯成mg文件的工具汗茄,最后干脆自己直接實現(xiàn)了從OC到AST的過程,我認為這個方向也很不錯铭若,畢竟開發(fā)者上手一門新的腳本語言洪碳,還是需要學習成本的递览。但是作者也表示,用Lex&&Yacc
來作為OC的翻譯工具還是有很多坑瞳腌,比如識別*
——NSString *a 和 a * b
具有二義性的需要特殊處理绞铃, 也處理不了頭文件的展開。作者最后說如果有機會可能會直接上clang嫂侍,我:.......................
0x05.總結(jié)
熱更經(jīng)過這么多年的發(fā)展儿捧,方案五花八門,但以上講到的兩種方案是最具代表性的挑宠,只要將熱更做到安全可控菲盾,我相信熱更還是有未來的,篇幅有限各淀,先到這里懒鉴。你知道的越多,你不知道的越多揪阿。