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。