聊聊熱更新

想寫這篇文章挺久了憎夷,今天我們來聊一聊熱更新粱玲。

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ì)碳蛋。幾乎所有熱更的流程基本都符合下面這張流程圖的邏輯(ReactNativeWeex等跨平臺方案今天不在我們的討論范圍內(nèi),他們確實有具備熱更的能力省咨,但我認為他們不夠trick肃弟。時至今日,此類的跨平臺框架基本已經(jīng)被apple認可零蓉,因為他們已經(jīng)是非常成熟的框架笤受,風險也相對可控)

執(zhí)行流程

接下來,我會拿兩個有代表性的方案敌蜂,進行一些技術(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)
  },
})
  1. 這里為什么對UIViewController要用require松申,是因為在下面的handleBtn這個方法中使用了這個類云芦,JSPatch.js中的require方法在當前全局中保存了UIViewContrller這個對象,防止在進行UIViewController.alloc().init()方法的鏈式調(diào)用中不會出現(xiàn)UIViewContrller is not defined的報錯贸桶。

  2. JSPatch.jsdefineClass = 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)用。

  3. 當然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的成本高多了,用JSJSContext進行動態(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)于的源碼閱讀和解釋的文章缺很少,所以今天我想特別來說說狸窘。

img

原理部分墩朦,我照搬了作者的原圖。接下來具體來說說技術(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非常像,但是又有一些不一樣昭灵》婉桑可以看到伐谈,該腳本目的是改掉原始ViewControllersequentialStatementExample方法。

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ā)展儿捧,方案五花八門,但以上講到的兩種方案是最具代表性的挑宠,只要將熱更做到安全可控菲盾,我相信熱更還是有未來的,篇幅有限各淀,先到這里懒鉴。你知道的越多,你不知道的越多揪阿。

0x06.參考

bang590/JSPatch

YPLiang19/Mango

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疗我,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子南捂,更是在濱河造成了極大的恐慌吴裤,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件溺健,死亡現(xiàn)場離奇詭異麦牺,居然都是意外死亡,警方通過查閱死者的電腦和手機鞭缭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門剖膳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人岭辣,你說我怎么就攤上這事吱晒。” “怎么了沦童?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵仑濒,是天一觀的道長。 經(jīng)常有香客問我偷遗,道長墩瞳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任氏豌,我火速辦了婚禮喉酌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己泪电,他們只是感情好般妙,可當我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著歪架,像睡著了一般股冗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上和蚪,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天止状,我揣著相機與錄音,去河邊找鬼攒霹。 笑死怯疤,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的催束。 我是一名探鬼主播集峦,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抠刺!你這毒婦竟也來了塔淤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤速妖,失蹤者是張志新(化名)和其女友劉穎高蜂,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罕容,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡备恤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了锦秒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片露泊。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖旅择,靈堂內(nèi)的尸體忽然破棺而出惭笑,到底是詐尸還是另有隱情,我是刑警寧澤生真,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布脖咐,位于F島的核電站,受9級特大地震影響汇歹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜偿凭,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一产弹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦痰哨、人聲如沸胶果。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽早抠。三九已至,卻和暖如春撬讽,著一層夾襖步出監(jiān)牢的瞬間蕊连,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工游昼, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留甘苍,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓烘豌,卻偏偏與公主長得像载庭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子廊佩,可洞房花燭夜當晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容