JSPatch Convertor 能夠自動把 Objective-C 代碼轉爲 JSPatch 腳本。git
JSPatch 是以方法爲單位進行代碼替換的,若 OC 上某個方法裏有一行出了bug,就須要把這個方法用 JS 重寫一遍才能進行替換,這就須要不少人工把 Objective-C 代碼翻譯成 JS 的過程,而這種代碼轉換的過程遵循着固定的模式,應該是能夠作到自動完成的,因而想嘗試實現這樣的代碼自動轉換工具,從 Objective-C 自動轉爲 JSPatch 腳本。github
作這樣的代碼轉換,最簡單的實現方式是什麼?最初考慮是否能用正則表達式搞定,若是能夠那是最簡單的,後來發現像 方法聲明 / get property / NSArray / NSString 等這些是能夠用正則處理的,但須要匹配括號的像 block / 方法調用 /set property 這些難以用正則處理,因而只能轉向其餘途徑。正則表達式
接下來的思路是對 Objective-C 進行詞法語法解析,再遍歷語法樹生成對應的 JS 代碼。Objective-C 詞法語法解析 clang 能夠作到 ,但後來發現了 antlr 這個神器,以及爲 antlr 定製的幾乎全部語言的語法描述文件,更符合個人需求。antlr 能夠根據語法描述文件生成對應的詞法語法解析程序,生成的程序能夠是 Java / Python / C# / JavaScript 這四種之一。express
也就是說,咱們拿 ObjC.g4 這個語法文件,就能夠經過 antlr 生成 Objective-C 語法解析程序,程序語言能夠在上述四種語言中任挑,我挑選的是 JavaScript,生成的程序能夠在這裏 看到。官方文檔有生成的流程和使用方法,能夠本身試下。數據結構
因而咱們獲得了一個 Objective-C 語法解析器,這個解析器能夠針對輸入的 Objective-C 代碼生成 AST 抽象語法樹,並對這個語法樹進行遍歷,遍歷過程的全部回調方法能夠在這裏看到,咱們要作的就是處理這些回調,轉爲 JS 代碼。函數
先來看看遍歷語法樹的過程是怎樣的,舉個簡單例子,咱們輸入這樣一句 Objective-C 語句:工具
[UIView alloc];
程序對這句話進行詞法語法解析後,遍歷語法樹,會按順序回調這幾個方法:spa
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 就回調什麼。.net
接下來就是要考慮怎樣處理這些回調後生成 JS 代碼,最容易想到的就是在一開始定義一個全局空字符串,在解析過程當中直接生成 JS 語言字符串,加入這個全局字符串,這樣看起來是最簡單的方法,可是實際上這樣處理會很複雜,有三個問題:prototype
解析和轉換代碼邏輯混在一塊兒,程序複雜。
嵌套語法難以處理。例如 [[UIView alloc] init]; 是一個嵌套語法,方法調用的調用者是另外一個方法調用,這種順序解析難以處理。
解析過程當中會須要不少變量去處理狀態的問題。例如碰到 UIView
這個 token,是出如今方法調用中,仍是出如今變量聲明中,所作的處理是不同的,須要知道當前處於什麼狀態。
因而考慮設計一箇中間數據結構,能夠解決這三個問題。這個數據結構就是 JPContext 以及它的子類們,對於不一樣的語法塊會有對應不一樣的 JPContext 子類,例如對應方法調用的 JPMsgContext,方法定義的 JPMethodContext 等。
來看看這個數據結構是怎樣解決這三個問題的
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 代碼,具體能夠看代碼。
上面舉的例子中,[[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 爲例,其餘的就看代碼吧。
解析過程當中的狀態問題,仍是以這份代碼爲例:
self.data = @{}; [[UIView alloc] initWithFrame:CGRectZero]; //1 JPBlock blk = ^(id data, NSError *err) { [self handleData:data]; //2 callback(data, err); } NSString *str = @「」;
這份代碼出現了兩次方法調用(標註一、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。