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