javascript
JSPatch Convertor 实现原理详解
簡介
JSPatch Convertor 可以自動把 Objective-C 代碼轉(zhuǎn)為 JSPatch 腳本。
JSPatch 是以方法為單位進(jìn)行代碼替換的,若 OC 上某個方法里有一行出了bug,就需要把這個方法用 JS 重寫一遍才能進(jìn)行替換,這就需要很多人工把 Objective-C 代碼翻譯成 JS 的過程,而這種代碼轉(zhuǎn)換的過程遵循著固定的模式,應(yīng)該是可以做到自動完成的,于是想嘗試實現(xiàn)這樣的代碼自動轉(zhuǎn)換工具,從 Objective-C 自動轉(zhuǎn)為 JSPatch 腳本。
方案 / Antlr
做這樣的代碼轉(zhuǎn)換,最簡單的實現(xiàn)方式是什么?最初考慮是否能用正則表達(dá)式搞定,如果可以那是最簡單的,后來發(fā)現(xiàn)像 方法聲明 / get property / NSArray / NSString 等這些是可以用正則處理的,但需要匹配括號的像 block / 方法調(diào)用 /set property 這些難以用正則處理,于是只能轉(zhuǎn)向其他途徑。
Antlr
接下來的思路是對 Objective-C 進(jìn)行詞法語法解析,再遍歷語法樹生成對應(yīng)的 JS 代碼。Objective-C 詞法語法解析 clang 可以做到 ,但后來發(fā)現(xiàn)了 antlr 這個神器,以及為 antlr 定制的幾乎所有語言的語法描述文件,更符合我的需求。antlr 可以根據(jù)語法描述文件生成對應(yīng)的詞法語法解析程序,生成的程序可以是 Java / Python / C# / JavaScript 這四種之一。
也就是說,我們拿 ObjC.g4 這個語法文件,就可以通過 antlr 生成 Objective-C 語法解析程序,程序語言可以在上述四種語言中任挑,我挑選的是 JavaScript,生成的程序可以在這里 看到。官方文檔有生成的流程和使用方法,可以自己試下。
于是我們得到了一個 Objective-C 語法解析器,這個解析器可以針對輸入的 Objective-C 代碼生成 AST 抽象語法樹,并對這個語法樹進(jìn)行遍歷,遍歷過程的所有回調(diào)方法可以在這里看到,我們要做的就是處理這些回調(diào),轉(zhuǎn)為 JS 代碼。
遍歷過程
先來看看遍歷語法樹的過程是怎樣的,舉個簡單例子,我們輸入這樣一句 Objective-C 語句:
[UIView alloc];程序?qū)@句話進(jìn)行詞法語法解析后,遍歷語法樹,會按順序回調(diào)這幾個方法:
JPObjCListener.prototype.enterMessage_expression = function(ctx) {//檢測當(dāng)前進(jìn)入方法調(diào)用語法,ctx是整個方法調(diào)用語法樹,包含了receiver/selector等信息,也就是匹配了[UIView alloc];這整個語句。 };JPObjCListener.prototype.enterReceiver = function(ctx) {//檢測方法調(diào)用者,這里 ctx 包含了 UIView 這個 token };JPObjCListener.prototype.exitReceiver = function(ctx) {//方法調(diào)用者 token 結(jié)束,ctx 還是 UIView 這個token };JPObjCListener.prototype.enterMessage_selector = function(ctx) {//檢測方法名 selector,ctx 包含了 alloc 這個token,若有多個參數(shù)或參數(shù)值,都會保存在 ctx 里 };JPObjCListener.prototype.exitMessage_selector = function(ctx) {//selector token 結(jié)束,ctx同上。 };JPObjCListener.prototype.exitMessage_expression = function(ctx) {//方法調(diào)用結(jié)束 };每個回調(diào)的 ctx 都包含了各種信息,包括這個當(dāng)前解析字符串起始/終止位置,包含的子 ctx 等,具體可以在控制臺打出 ctx 觀察。整個解析過程就是按順序遇到什么類型的 token 就回調(diào)什么。
解析 / JPContext鏈
接下來就是要考慮怎樣處理這些回調(diào)后生成 JS 代碼,最容易想到的就是在一開始定義一個全局空字符串,在解析過程中直接生成 JS 語言字符串,加入這個全局字符串,這樣看起來是最簡單的方法,但是實際上這樣處理會很復(fù)雜,有三個問題:
解析和轉(zhuǎn)換代碼邏輯混在一起,程序復(fù)雜。
嵌套語法難以處理。例如 [[UIView alloc] init]; 是一個嵌套語法,方法調(diào)用的調(diào)用者是另一個方法調(diào)用,這種順序解析難以處理。
解析過程中會需要很多變量去處理狀態(tài)的問題。例如碰到 UIView 這個 token,是出現(xiàn)在方法調(diào)用中,還是出現(xiàn)在變量聲明中,所做的處理是不一樣的,需要知道當(dāng)前處于什么狀態(tài)。
于是考慮設(shè)計一個中間數(shù)據(jù)結(jié)構(gòu),可以解決這三個問題。這個數(shù)據(jù)結(jié)構(gòu)就是 JPContext 以及它的子類們,對于不同的語法塊會有對應(yīng)不同的 JPContext 子類,例如對應(yīng)方法調(diào)用的 JPMsgContext,方法定義的 JPMethodContext 等。
來看看這個數(shù)據(jù)結(jié)構(gòu)是怎樣解決這三個問題的
1.拆分
JSContext 最基本的用途就是拆分 Objective-C 代碼的解析和 JS 代碼的生成,不讓這兩個邏輯混合在一起,在解析 Objective-C 時生成一個個相連的 JSContext,最終從第一個 JSContext 開始遍歷整個鏈調(diào)用 JSContext 的 parse() 函數(shù)生成 JS 代碼,舉個例子:
self.data = @{}; [[UIView alloc] initWithFrame:CGRectZero]; JPBlock blk = ^(id data, NSError *err) {[self handleData:data];callback(data, err); } NSString *str = @“”;這段 OC 代碼最終解析成以下 JPContext 鏈:
解析的方法是設(shè)一個全局變量 currContext 保存當(dāng)前解析鏈上最后一個對象,每次解析到新內(nèi)容,生成下一個 JPContext 對象時,就把 currContext.next 設(shè)為這個新的 JPContext 對象,同時 currContext 也替換為這個新的 JPContext 對象,這樣循環(huán)直到代碼結(jié)束,就生成了一條 JPContext 鏈,從第一個 JPContext 開始遍歷整個鏈調(diào)用 parse() 函數(shù)就可以組合成最終的 JS 程序了:
var script = ''; while (ctx = ctx.next) {script += ctx.parse(); }不同的 JPContext 子類有不同的 parse() 實現(xiàn)去生成相應(yīng)的 JS 代碼,具體可以看代碼。
2.封裝語句
上面舉的例子中,[[UIView alloc] initWithFrame:CGRectZero]; 實際上是一個嵌套調(diào)用的語法,-initWithFrame: 的調(diào)用者是 [UIView alloc],是另一個方法調(diào)用語句,但最終在 JPContext 鏈上看到的只有一個 JPMsgContext 對象,這個對象把方法調(diào)用里的細(xì)節(jié)都封裝了,無論這個方法調(diào)用里有多少層嵌套,或者參數(shù)有多復(fù)雜,對外的表現(xiàn)都是只有一個 JPMsgContext 對象,實現(xiàn)了把語句封裝,降低復(fù)雜度的目的。
每個 JPContext 子類都有自己封裝的規(guī)則, 對于 JPMsgContext 來說,解析上述語句生成的 JPMsgContext 對象結(jié)構(gòu)如圖:
藍(lán)色是這個對象或?qū)傩岳锇恼Z句。JPMsgContext 有 receiver 和 selector 兩個屬性,receiver 可以是另一個 JPMsgContext 對象,也可以是字符串,selector保存調(diào)用方法名和參數(shù)。這里外層 JPMsgContext 的 receiver 屬性值就是 JPMsgContext 對象,因為它的調(diào)用者是另一個方法調(diào)用,而里面這個 JPMsgContext 對象 receiver 是字符串 ‘UIView’。就這樣實現(xiàn)了嵌套調(diào)用的封裝。
每個 JPContext 子類對象都有自己的封裝規(guī)則,這里只以 JPMsgContext 為例,其他的就看代碼吧。
3.狀態(tài)
解析過程中的狀態(tài)問題,還是以這份代碼為例:
self.data = @{}; [[UIView alloc] initWithFrame:CGRectZero]; //1 JPBlock blk = ^(id data, NSError *err) {[self handleData:data]; //2callback(data, err); } NSString *str = @“”;這份代碼出現(xiàn)了兩次方法調(diào)用(標(biāo)注1、2),其中一個是在 block 塊里,在解析這兩個方法調(diào)用時都會進(jìn)入同一個回調(diào),但對應(yīng)的是兩種狀態(tài),一種是這個語句處于全局,另一種是這個語句屬于 block 塊,解析過程中怎樣處理這兩種情況?
解決方法是稍微擴展一下第一點說到的 currContext 概念,不把它當(dāng) JPContext 鏈上的最后一個元素,而是作為游標(biāo),表示當(dāng)前處于哪個 JPContext 上。說得太抽象,舉例說明,細(xì)化一下這份代碼最終的 JPContext 鏈,展開 block 塊的解析,是這樣的:
解析到 block 時,會生成 JPBlockContext,但 currContext 不指向這個 JPBlockContext,而是指向它的一個屬性 JPBlockContentContext,在 block 塊結(jié)束時,currContext 重新指向 JPBlockContext。
這樣解析①和②這兩個方法調(diào)用語句時,程序做的事情都是一樣的,讓 currContext.next 指向生成的新的 JPMsgContext,只不過①的 currContext 是 JPAssignment,②的 currContext 是 JPBlockContentContext,相當(dāng)于靠 currContext 這個游標(biāo)保存上下文信息,程序處理時無需關(guān)心。
簡化
解決這三個問題后,還有第四個問題:Objective-C 語法特性太多。粗略計算有100多個語法特性回調(diào),把這些回調(diào)全部處理一遍得耗多大精力和時間?有沒有更簡單的辦法?
仔細(xì)想想,Objective-C 跟 JS 語法上很多是一樣的,我們主要需要處理的就是 方法調(diào)用/方法定義/block 這有限的幾種,其他的都不需要轉(zhuǎn)換,像 賦值/運算/循環(huán) 這些代碼都是一樣的,而像 struct / 指針等可以暫時不支持,只需要覆蓋日常使用80%以上的情況就可以了。
于是想到只處理 方法調(diào)用/方法定義/block 等有限幾個回調(diào),其他的原樣輸出到 JS 就行了,確定了這個方法,整個思路清晰多了,不用去處理一百多個回調(diào),只需要處理好有限的幾個就行, 雖然這是很簡單的方式,但像 JSPatch 的正則替換一樣是核心點,也是 JSPatch Convertor 可以快速完成最重要的點。
總結(jié)
整個 JSPatch Convertor 原理就介紹到這里,總結(jié)起來就是:
antlr 生成解析程序
處理回調(diào),用 JPContext 中間數(shù)據(jù)結(jié)構(gòu)解決代碼耦合,嵌套語法,狀態(tài)位的問題。
簡化處理流程,只處理有限幾個回調(diào),其他原樣輸出。
更多細(xì)節(jié)就要看代碼了,歡迎一起完善 JSPatch Convertor。
總結(jié)
以上是生活随笔為你收集整理的JSPatch Convertor 实现原理详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用纯css来实现一个优惠券
- 下一篇: spring boot 源码分析(七)