JSPatch Convertor 实现原理详解

简介

JSPatch Convertor 能够自动把 Objective-C 代码转为 JSPatch 脚本。git

JSPatch 是以方法为单位进行代码替换的,若 OC 上某个方法里有一行出了bug,就须要把这个方法用 JS 重写一遍才能进行替换,这就须要不少人工把 Objective-C 代码翻译成 JS 的过程,而这种代码转换的过程遵循着固定的模式,应该是能够作到自动完成的,因而想尝试实现这样的代码自动转换工具,从 Objective-C 自动转为 JSPatch 脚本。github

方案 / Antlr

作这样的代码转换,最简单的实现方式是什么?最初考虑是否能用正则表达式搞定,若是能够那是最简单的,后来发现像 方法声明 / get property / NSArray / NSString 等这些是能够用正则处理的,但须要匹配括号的像 block / 方法调用 /set property 这些难以用正则处理,因而只能转向其余途径。正则表达式

Antlr

接下来的思路是对 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

解析 / JPContext链

接下来就是要考虑怎样处理这些回调后生成 JS 代码,最容易想到的就是在一开始定义一个全局空字符串,在解析过程当中直接生成 JS 语言字符串,加入这个全局字符串,这样看起来是最简单的方法,可是实际上这样处理会很复杂,有三个问题:prototype

  1. 解析和转换代码逻辑混在一块儿,程序复杂。

  2. 嵌套语法难以处理。例如 [[UIView alloc] init]; 是一个嵌套语法,方法调用的调用者是另外一个方法调用,这种顺序解析难以处理。

  3. 解析过程当中会须要不少变量去处理状态的问题。例如碰到 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];  //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 原理就介绍到这里,总结起来就是:

  1. antlr 生成解析程序

  2. 处理回调,用 JPContext 中间数据结构解决代码耦合,嵌套语法,状态位的问题。

  3. 简化处理流程,只处理有限几个回调,其余原样输出。

更多细节就要看代码了,欢迎一块儿完善 JSPatch Convertor

相关文章
相关标签/搜索