【http://blog.cnbang.net/tech/2915/】

简介

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 语句:

1
[UIView alloc];

程序对这句话进行词法语法解析后,遍历语法树,会按顺序回调这几个方法:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 语句,加入这个全局字符串,最终拼接成 JS 程序。这样看起来是最简单的方法,但是实际上这样处理会很复杂,有三个问题:

  1. OC 代码解析和 JS 代码生成逻辑混在一起,程序复杂。
  2. 嵌套语法难以处理。例如 [[UIView alloc] init]是一个嵌套语法,方法调用的调用者是另一个方法调用,这种解析难以处理。
  3. 解析过程中需要很多变量记录上下文。例如碰到 UIView 这个 token,是出现在方法调用中,还是出现在变量声明中,所做的处理是不一样的,需要知道当前处于什么上下文。

于是考虑设计一个中间数据结构,解决这三个问题。这个数据结构就是 JPContext 以及它的子类们,对于不同的语法块会有对应不同的 JPContext 子类,例如对应方法调用的 JPMsgContext,方法定义的 JPMethodContext 等。

来看看这个数据结构是怎样解决这三个问题的。

1.拆分

JSContext 最基本的用途就是拆分 Objective-C 代码的解析和 JS 代码的生成,不让这两个逻辑混合在一起,在解析 Objective-C 时生成一个个相连的 JSContext,最后从第一个 JSContext 开始遍历整个链调用 JSContext 的 parse() 函数生成 JS 代码,举个例子:

1
2
3
4
5
6
7
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 程序了:

1
2
3
4
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 对象结构如图:

蓝色是这个对象或属性里包含的 OC 语句。JPMsgContext 有 receiver 和 selector 两个属性,receiver 可以是另一个 JPMsgContext 对象,也可以是字符串,selector 保存调用方法名和参数。这里外层 JPMsgContext 的 receiver 属性值就是 JPMsgContext 对象,因为它的调用者是另一个方法调用,而里面这个 JPMsgContext 对象 receiver 是字符串 UIView。就这样实现了嵌套调用的封装。

每个 JPContext 子类对象都有自己的封装规则,这里只以 JPMsgContext 为例,其他的请看代码。

3.上下文

解析过程中的上下文问题,还是以这份代码为例:

1
2
3
4
5
6
7
self.data = @{};
[[UIView alloc] initWithFrame:CGRectZero];   //1
JPBlock blk = ^(id data, NSError *err) {
    [self handleData:data];  //2
    callback(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 就行了,确定了这个方案,整个思路清晰多了,不用去处理一百多个回调,只需要处理好有限的几个就行。具体实现上就是用 JPCommonContext 表示原样输出的字符串,解析过程中找出未处理的语句,生成 JPCommonContext 加入到当前 JPContext 链中就可以了。

虽然这是很简单的方式,但像 JSPatch 的正则替换一样是核心点,也是 JSPatch Convertor 可以快速完成最重要的点。

总结

整个 JSPatch Convertor 原理就介绍到这里,总结起来就是:

  1. antlr 生成解析程序
  2. 遍历语法树,用 JPContext 解决代码耦合,嵌套语法,上下文的问题。
  3. 简化处理流程,只处理有限几个回调,其他原样输出。

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

JSPatch Convertor 实现原理详解 1相关推荐

  1. JSPatch Convertor 实现原理详解

    简介 JSPatch Convertor 可以自动把 Objective-C 代码转为 JSPatch 脚本. JSPatch 是以方法为单位进行代码替换的,若 OC 上某个方法里有一行出了bug,就 ...

  2. JSPatch实现原理详解:让JS调用/替换任意OC方法

    JSPatch实现原理详解:让JS调用/替换任意OC方法 2015-07-10 09:05 编辑: suiling 分类:iOS开发 来源:bang JSPatch以小巧的体积做到了让JS调用/替换任 ...

  3. mysql udf提权原理_udf提权原理详解

    0x00-前言 这个udf提权复现搞了三天,终于搞出来了.网上的教程对于初学者不太友好,以至于我一直迷迷糊糊的,走了不少弯路.下面就来总结一下我的理解. 想要知道udf提权是怎么回事,首先要先知道ud ...

  4. CRF(条件随机场)与Viterbi(维特比)算法原理详解

    摘自:https://mp.weixin.qq.com/s/GXbFxlExDtjtQe-OPwfokA https://www.cnblogs.com/zhibei/p/9391014.html C ...

  5. LVS原理详解(3种工作方式8种调度算法)--老男孩

    一.LVS原理详解(4种工作方式8种调度算法) 集群简介 集群就是一组独立的计算机,协同工作,对外提供服务.对客户端来说像是一台服务器提供服务. LVS在企业架构中的位置: 以上的架构只是众多企业里面 ...

  6. jQuery中getJSON跨域原理详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp28 jQuery中getJSON跨域原理详解 前几天我再开发一个叫 河蟹工 ...

  7. nginx配置文件及工作原理详解

    nginx配置文件及工作原理详解 1 nginx配置文件的结构 2 nginx工作原理 1 nginx配置文件的结构 1)以下是nginx配置文件默认的主要内容: #user nobody; #配置用 ...

  8. EMD算法之Hilbert-Huang Transform原理详解和案例分析

    目录 Hilbert-Huang Transform 希尔伯特-黄变换 Section I 人物简介 Section II Hilbert-Huang的应用领域 Section III Hilbert ...

  9. 图像质量损失函数SSIM Loss的原理详解和代码具体实现

    本文转自微信公众号SIGAI 文章PDF见: http://www.tensorinfinity.com/paper_164.html http://www.360doc.com/content/19 ...

最新文章

  1. 4K P60 444 相关的事
  2. S - Extended Traffic LightOJ - 1074
  3. Martin Davis最新访谈:机器学习是一个收敛的过程,背后理论并不高深
  4. mysql 整形转换_mysql IP地址整形转换
  5. [译] 我们是如何高效实现一致性哈希的
  6. java实现bloom filter_Java BloomFilter.add方法代码示例
  7. python中如何打印阶梯_字符阶梯(python)
  8. eclipse快捷键_Eclipse快捷键
  9. 一分钟了解自动化测试
  10. opencv中滚动条操作
  11. VB 共享软件防破解设计技术初探(二)
  12. 怎么完全卸载赛门铁克_symantec卸载方法
  13. 怎么用计算机计算行列式,行列式计算器怎么使用,行列式计算器安装使用教程...
  14. Python 爬取所有51VOA网站的Learn a words文本及mp3音频
  15. 你弄懂了Spark的Shuffle实现方式吗?手把手带你解析Spark的Sort Shuffle和Tungsten-Sort Shuffle
  16. SuperMap知识总结
  17. Linux MTD子系统学习(二)
  18. 大学JavaWeb课程设计——图书管理系统(应付作业及毕设)
  19. react 断网提示
  20. HTML5文件夹隐藏了怎么打开,如何打开隐藏文件夹,详细教您打开隐藏文件夹的方法...

热门文章

  1. P7368 [USACO05NOV]Asteroids G(k o¨ nig)
  2. html旋转木马切换效果,超酷jQuery 3D旋转木马效果轮播图插件 -HTML5功能
  3. LCT (Link-cut-tree)
  4. matlab电动机星三角启动仿真,电机星三角启动注意事项图文讲解
  5. git add 撤销git commit 撤销
  6. 金蝶打印时选择使用套打秒退
  7. 天猫tmall入驻规则
  8. 大数据之hadoop单机版虚拟机VirtualBox安装教程
  9. 数字化转型—‘前言’
  10. PyCharm下载主题以及个性化设置(详细)