如何动态创建 block – JPBlock 扩展原理详解

2017-1-23 评论(14) 分类:技术文章

简介

JSPatch 最近新增了扩展 JPBlock,解决之前 block 使用上的一些限制:

  1. block 参数个数最多支持6个。
  2. block 参数类型不能是 double / struct / union 类型。
  3. 不支持 JS 封装的 block 传到 OC 再传回 JS 去调用。

接入 JPBlock 后,就可以:

  1. block 参数支持任意多个。
  2. block 参数类型可以是除 struct / union 以外的任意类型,包括 double。
  3. 支持 JS 封装的 block 传到 OC 再传回 JS 去调用。

这篇文章说说这里的实现原理。

(更多…)

iOS 动态化的故事

2016-12-21 评论(15) 分类:技术文章

今天聊聊 iOS 动态化的故事。

问题

在开发模式上,web 的方式是比较先进的,有各种优点,包括跨平台/UI开发效率高,最重要的是可以时刻保证用户看到的程序是最新的,没有版本概念,整个系统时刻保持在掌握之中,而客户端开发模式相对 web 开发是一种倒退,客户端做不到这样的动态化,无法随时更新,目前一个客户端程序要更新成本是很高的,需要发布版本,也无法保证所有人都能更新到这个版本,这是最大的弱点,也是非常大的一块需求。

原因

为什么会有这种倒退,最主要原因是:苹果引领的体验优先规则。

在 iPhone 出现之前大家并不太在意一个软件的动画体验,一个 web 应用是很少有动画的,点一个按钮,一整块内容直接刷新,再点个链接整个页面变白刷新,PC上网页滚动都是一格格滚动的,而不是现在手机上那种顺滑流畅的滚动,PC客户端软件也一样,大家都觉得没什么问题,用得挺好,但苹果改变了这种情况,iPhone 刚推出时页面间切换的动画,60fps 的丝滑滚动,点击的即时响应,微软的人都惊呼是黑魔法,让人用了就上瘾,再也回不去,而 web 的方式还不足以像原生客户端那样支持这样的流畅性,做不到好的体验,无法被人接受,开发上优势再多也无法干过客户端,参考 facebook 初期用 web 技术构建 app 的惨状,没办法,服了苹果,大家只能按照苹果的方式干,做原生客户端。

当然这里还有手机环境网络不稳定,流量费贵的原因,但这些都可以在技术上通过缓存解决,最主要还是体验问题。

那发展到今天,这个体验问题解决了没有呢?没有,即使发展到今天手机性能已上天,但 web 做出来的东西体验仍然跟客户端有差别,大家也已经习惯了 APP 的方式,也被流畅的 APP 惯坏。现在 APP 里也有不少功能是 web实现,但大家都知道这是牺牲了一些性能体验去换取开发和发布效率,只是一种权衡。

解决方案

那这个问题怎么解决呢,现在业界有两种方案。

1.优化 web 性能

既然用 web 方式开发的劣势只是性能体验跟不上,那就优化性能吧。web 性能瓶颈在哪里?在那个有悠久历史的 webkit 引擎,它有各种历史问题,要改进它并不容易。我们要的是 web 的开发和发布方式,不需要 web 的全部,那能不能重新实现一个渲染引擎呢,这个引擎可以针对原生客户端优化,不需要兼容繁杂的 web 标准,不跟 web 那些历史问题扯上关系,于是就有了 React NativeWeex 这种方案,web 的方式开发,原生的方式渲染,拥有 web 优秀的开发和发布方式,又有不错的性能体验,看起来很完美,很有前途的方案。

一个方案能不能推广开让大家都使用,主要看成本和收益。目前 React Native 和 Weex 等这些方案的接入成本是很高的,一是它们本身就是大型框架,学习成本高,后期维护成本以及团队学习成本同样高。二是它们还不够成熟,还在继续填坑中,使用的过程可能要一起去填坑。收益上也不够理想,就目前状况它们能代替的是那些本来用 H5 实现的模块,换成这种方式实现后性能体验会更好,但也不能保证像原生那样好,很多场景需要深入框架进行优化。整个 APP 都使用这种方案构建还不靠谱,部分使用又无法使整个 APP 保持动态化,总体上来说收益也没有达到有绝对优势的程度,成本高收益低,推广起来会比较困难,还需期待其继续发展。

2.原生动态化

另一种是方案是,我可以放弃用 web 的开发方式,放弃 web 跨平台/UI开发简单的优越性,我想继续用原生的方式开发这个APP,但又希望这个 APP 随时可以更新,让程序时刻在自己掌握中,出了问题可以快速修复,还想要可以随时更新版本快速迭代,可以不?完全可以,用动态库就行了。

动态库

技术上要在 iOS 上做到原生动态化比 Android 更容易,iOS 开发语言 Objective-C 天生动态,运行时都能随意替换方法,运行时加载动态库又是项很老的技术,只要我把增量的代码和资源打包到一个 framework 里,动态下发运行时加载,修 bug,加功能都不在话下,性能完全无损,这件事就结束了。

但是呢。苹果把加载动态库的功能给封了,动态库必须跟随安装包一起签名才能被加载,无法通过别的途径签名后再下发。

为什么这么做呢,这又涉及到苹果另一个创举:审核模式(苹果的创举之多令人发指)。一个软件,要在一个平台上发布,需要先通过这个平台的人工审核,这个好像在苹果之前没见过有别人这样做过,windows 不用,mac 不用,各种 unix 不用,web 也不用,苹果为了对自己的平台有绝对控制权,搞了这样一个东西,审核模式就跟动态化冲突了,如果我一个 APP 可以不经过审核不断下发 framework 添加修改功能, 还需要苹果审核做什么?

因为这种限制,没法用最方便的方式进行动态更新了,整个 APP 发出去后就不受控制,有什么严重 bug,需要添加什么功能,都乖乖打个包提交给苹果审核,再等用户慢慢更新,对于急性子的中国人来说,这种事难以容忍。

绕道

苹果把动态库这扇门关了,我们可以绕个道从另一个门进,动态加功能可以缓缓,大家需求还不那么迫切,但修 bug 的需求就很急,很多公司 APP 的 crash 率是 KPI 来着,看着线上 crash 不断增多又毫无办法是很不爽的事,于是有了 waxPatchJSPatch 这种方案,曲线救国。

JSPatch 把 OC 手动翻译成 JS,在运行时通过 OC 的动态特性去调用和替换 OC 方法,实时修复 bug。修 bug 这个需求基本是满足了,虽然小绕了下道,但成本还是很低的,引擎本身也很小很轻量,接入对 APP 不会有任何负面影响,在关键时刻又可以帮大忙,成本低收益高,于是很容易推广开。

人欲望是无穷的,技术宅的折腾是无止境的,JSPatch 满足了修 bug 的需求,但还是无法满足动态化的全部需求,最大的缺点在于需要手写 JS,虽然已经有转换器辅助,但还没做到100%准确,用来修 bug 还好,用来添加功能的话学习成本和开发效率还不够。

于是有了滴滴的 DynamicCocoa 这种方案,绕了一个更大的道,从编译阶段入手,通过 clang 把 OC 代码编译成自己定制的 JS 格式,再动态下发去执行,做到原生开发,动态运行,主打动态添加功能,当然顺便把修 bug 也给支持了。手机 QQ 内部也有一个类似的方案,不过更进一步,他们通过 clang 把 OC 代码编译成自己定制的字节码动态下发,然后开发一个虚拟机去执行(惊呆了),同样实现了原生开发,动态运行,都是 NB 得很的方案。只要底层处理做得足够好,也是个成本低收益高的方案,不过目前都还没开源,还没能看到实际效果和 NB 的源码,挺期待。

审核

这种方案有没有什么问题呢,问题在对于苹果审核比较尴尬。这种方案做到极致后(所有OC/C语法都支持),实际上是绕道实现了动态加载 framework 的全部功能,开发体验还是一致的,如果苹果同意这种方案,那相当于允许加载动态库,那还不如直接把门开了,让大家直接用动态库去做这个事情,用动态库还能在签名时禁止使用私有 API,用这种编译成脚本/字节码下发的方案可就禁止不了了。

这跟 JSPatch 还不太一样,JSPatch 虽然我也想推广动态添加功能的用法,但因为开发体验问题大部分还是用于修 bug,苹果审核对 JSPatch 开始也是有一些拒绝案例,后来估计看到大家只是用它来修 bug 和 crash,提升 APP 的质量,就默许了。但 DynamicCocoa 和手Q的方案一开始目标和效果就是跟加载动态库对齐,大规模推广后苹果会怎么看就不知道了。

我觉得苹果现在的审核方式挺有问题的,有多少APP是审核时用一套,审核通过后又通过后台一些开关把不符合规则的一些功能开放出来?只要能连上网,就有N种方式修改 APP 里的功能,苹果完全拦不住。个人认为审核方式应该改为只在发布新 APP 时审核,发布后允许动态下发代码更新,版本更新也不需要重新审核,而是通过举报和抽查的方式去审核已上线的 APP,这样既能顾及开发效率,方便开发者快速迭代做出更好的 APP,也更能确保审核效果,只是实施起来没有现在简单粗暴。

最后

故事讲到这里已经差不多了,再多说一点,有个让我觉得很奇怪的问题,就是国外开发者只热衷于使用第一种方案解决问题,也就是使用web技术,用 React Native / NativeScript 的方式去做这个事情,对第二种方案很冷淡,包括 iOS Android 都是,原生热更新只在国内火,国外根本不感冒,国外有个 rollout 热更新平台也不温不火,为什么呢?是国外用户更守规则,或是用户对线上 bug 容忍度高,开发者对线上 bug 并不那么着急?还是 Android 被 Google Play 卡死断了这念想,iOS JSPatch 之类的方案推广不利?缺少国外一线开发者的支持,让系统原生支持动态化就比较困难了。

个人认为由系统支持动态化(允许加载动态库)在当前环境下是最好的,兼顾开发效率和 APP 体验,虽然不能跨平台,但也还能接受,可惜这个主动权掌握在苹果手上,开发者无能为力,才会出现这么多强行绕道突破的方案。web 的方式可能是未来,但目前适用范围有限,接下来怎么发展,拭目以待吧。

APP 缓存数据线程安全问题探讨

2016-11-22 评论(17) 分类:技术文章

问题

一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存从网络拉取的数据,下次读取可以直接从 Sqlite DB 读取。我们先忽略从网络请求数据这一环节,假设数据已经保存在 DB 里,那我们要做的事就是,ViewController 从 DB 取数据,再传给 view 渲染:
cache1

这是最简单的情况,随着程序变复杂,多个 ViewController 都要向 DB 取数据,ViewController本身也会因为数据变化重新去 DB 取数据,会有两个问题:

  • 数据每次有变动,ViewController 都要重新去DB读取,做 IO 操作。
  • 多个 ViewController 之间可能会共用数据,例如同一份数据,本来在 Controller1 已经从 DB 取出来了,在 Controller2 要使用得重新去 DB 读取,浪费 IO。

cache2
(更多…)

iOS 动态更新方案对比:JSPatch vs React Native

2016-8-9 评论(9) 分类:技术文章 Tags:

JSPatch 是 iOS 平台上的一个开源库,只需接入极小的三个引擎文件,即可以用 JS 调用和替换任意 OC 方法,也就是说可以在 APP 上线后通过下发 JS 脚本,实时修改任意 OC 方法的实现,达到修复 bug 或动态运营的目的。目前 JSPatch 被大规模应用于热修复(hotfix),已有超过 2500 个 APP 接入。

虽然 JSPatch 目前大部分只用于热修复,但因为 JSPatch 可以调用任意 OC 方法,实际上它也可以做热更新的工作,也就是动态为 APP 添加功能模块,并对这些功能模块进行实时更新,可以起到跟 React Native 一样的作用。我们从学习成本、接入成本、开发效率、热更新能力和性能体验这几个方面来对比一下使用 React Native 和 JSPatch 做热更新的差异。

学习成本

React Native 是从 web 前端开发框架 React 延伸出来的解决方案,主要解决的问题是web页面在移动端性能低的问题,React Native 让开发者可以像开发web页面那样用 React 的方式开发功能,同时框架会通过 JS 与 OC 的通信让界面使用原生组件渲染,让开发出来的功能拥有原生APP的性能和体验。

这里会有一个学习成本的问题,大部分 iOS 开发者并不熟悉web前端开发,当他们需要一个动态化的方案去开发一个功能模块时,若使用React Native,就意味着他需要学习web前端的一整套开发技能,学习成本很高,所以目前一些使用 React Native 的团队里,这部分功能的开发是由前端开发者负责,而不是终端开发者负责。

但前端开发者负责这部分功能也会有一些学习成本的问题,因为 React Native 还未达到十分成熟的程度,出了bug或有性能问题需要深入 React Native 客户端代码去排查和优化。也就是说 React-Native 是跨 web 前端开发和终端开发的技术,要求使用者同时有这两方面能力才能使用得当,这不可避免带来学习成本的提高。

而 JSPatch 是从终端开发出发的一种方案,JSPatch 写出来的代码风格与 OC 原生开发一致,使用者不需要有 web 前端的知识和经验,只需要有 iOS 开发经验,再加上一点 JS 语法的了解,就可以很好地使用,对终端开发来说学习成本很低。

可以看一下同样实现一个简单的界面,React Native 和 JSPatch 代码的对比:

//React Native
class HelloWorld extends Component {
  render() {
    return (
      <View style={styles.btnArea}>
        <View style={styles.btnWrapper}>
          <TouchableHighlight underlayColor="#ED5F37” onPress={this.login}
              activeOpacity={0.7}>
            <Text style={styles.btn}>登录</Text>
          </TouchableHighlight>
        </View>
      </View>
    );
  }
  login(){

  };
}

var styles = StyleSheet.create({
  btnArea: {
    justifyContent: 'center',
    marginLeft: 20,
    marginRight: 20,
    marginTop: 100,
    flexDirection: 'row',
  },
  btnWrapper: {
    backgroundColor: '#FC6E50',
    borderRadius: 5,
    flex: 1
  },
  btn: {
    paddingTop: 10,
    paddingBottom: 10,
    color: '#ffffff',
    textAlign: 'center',
  },
});
//JSPatch
require('UIColor, UIScreen, UIButton')
defineClass('HelloWord : UIView', {
    initWithFrame: function(frame) {
        if(self = super.initWithFrame(frame)){
            var screenWidth = UIScreen.mainScreen().bounds().width
            var loginBtn = UIButton.alloc().initWithFrame({x: 20, y: 50, width: screenWidth - 40, height: 30});
            loginBtn.setBackgroundColor(UIColor.greenColor())
            loginBtn.setTitle_forState("Login", 0)
            loginBtn.layer().setCornerRadius(5)
            loginBtn.addTarget_action_forControlEvents(self, 'handleBtn', 1<<6);
            self.addSubview(loginBtn);
        }
        return self;
    },
    handleBtn: function() {
    }
})

接入成本

接入成本上,React Native 是比较大的框架,据统计目前核心代码里 OC 和 JS 代码加起来有4w行,接入后安装包体积增大 1.8M 左右。而 JSPatch 是微型框架,只有 3 个文件 2k 行代码,接入后增大 100K 左右。另外 React Native 需要搭建一套开发环境,有很多依赖的库,环境的搭建被称为一个痛点。而 JSPatch 无需搭建环境,只需要拖入三个文件到工程中即可使用。

React Native 是大框架,维护起来成本也会增大,在性能调优和 bug 查找时,必须深入了解整个框架的原理和执行流程,此外 React Native 目前还未达到稳定状态,升级时踩坑不可避免。相对来说 JSPatch 接入后的维护成本会低一些,因为 JSPatch 只是作为很薄的一层转接口,没有太多规则和框架,也就没有太多坑,本身代码量小,需要深入了解去调试 bug 或性能调优时成本也低。

开发效率

在 UI 层上目前 HTML + CSS 的方式开发效率是比手写布局高的,React Native 也是用近似 HTML+CSS 去绘制 UI,这方面开发效率相对 JSPatch 会高一些,但 JSPatch 也可以借助 iOS 一些成熟的库去提高效率,例如使用 Massory,让 UI 的开发效率不会相差太多。逻辑层方面的开发效率双方是一样的。

此外 React Native 在开发效率上的另一个优势是支持跨平台,React Native 本意是复用逻辑层代码,UI 层根据不同平台写不同的代码,但 UI 层目前也可以通过 ReactMix 之类的工具做到跨平台,所以UI层和逻辑层代码都能得到一定程度的复用。而 JSPatch 目前只能用于 iOS 平台,没有跨平台能力。

实际上跨平台有它适用和不适用的场景,跨平台有它的代价,就是需要兼顾每个平台的特性,导致效果不佳。

跨平台典型的适用场景是电商活动页面,以展示为主,重开发效率轻交互体验,但不适用于功能性的模块。对 Android 来说目前热更新方案十分成熟,Android 十分自由,可以直接用原生开发后生成diff包下发运行,这种无论是开发效率和效果都是最好的。所以若是重体验的功能模块,Android使用原生的热更新方案,iOS 使用 JSPatch 开发,会更适合。

JSPatch 也做了一些事情尝试提高开发效率,例如做了 XCode 代码提示插件 JSPatchX,让用 JS 调用 OC 代码时会出现代码提示,另外跟 React Native 一样有开发时可以实时刷新界面查看修改效果的功能,目前仍在继续做一些措施和工具提高开发效率。

热更新能力

React Native 和 JSPatch 都能对用其开发出来的功能模块进行热更新,这也是这种方案最大的好处。不过 React Native 在热更新时无法使用事先没有做过桥接的原生组件,例如需要加一个发送短信功能,需要用到原生 MessageUI.framework 的接口,若没有在编译时加上提供给 JS 的接口,是无法调用到的。而 JSPatch 可以调用到任意已在项目里的组件,以及任意原生 framework 接口,不需要事先做桥接,在热更新的能力上,相对来说 JSPatch 的能力和自由度会更高一些。

性能体验

使用 React Native 和 JSPatch 性能上会比原生差点,但都能得到比纯 H5 页面或 hybrid 更好的性能和体验。

JSPatch 的性能问题主要在于 JS 和 OC 的通信,每次调用 OC 方法都要通过 OC runtime 接口,并进行参数转换。runtime 接口调用带来的耗时一般不会成为瓶颈,参数转换则需要注意避免在 JS 和 OC 之间传递大的数据集合对象。JSPatch 在性能方面也针对开发功能做了不少优化,尽力减少了 JS 和 OC 的通信,github 项目主页上有完整的小 App demo,目前来看并没有碰到太多性能问题。

React Native 的性能问题会复杂一些,因为框架本身的模块初始化/react组件初始化/JS渲染逻辑等会消耗不少时间和内存,这些地方若使用或优化不当都会对性能和体验造成影响。JS 和 OC 的通信也是一个耗性能的点,不过这点上 React Native 优化得比较好,没有成为主要消耗点。

在性能和体验问题上,两者有不同的性能消耗点,从最终效果来看两者差别不大。

总结

学习成本
接入成本
热更新能力
开发效率
性能体验
JSPatch
中,不跨平台
React Native
高,跨平台
高 

总的来说,JSPatch在学习成本,接入成本,热更新能力上占优,而 React Native 在开发效率和跨平台能力上占优,大家可以根据需求的不同选用不同的热更新方案。JSPatch 目前仍在不断发展中,后续会致力于提高开发效率,完善周边支持,欢迎参与这个开源项目的开发。

(本文发表于《程序员》2016年8月刊)

JSPatch平台: http://jspatch.com

如何动态调用 C 函数

2016-7-5 评论(15) 分类:技术文章 Tags:

JSPatch 支持了动态调用 C 函数,无需在编译前桥接每个要调用的 C 函数,只需要在 JS 里调用前声明下这个函数,就可以直接调用:

require('JPEngine').addExtensions(['JPCFunction'])
defineCFunction("malloc", "void *, size_t")
malloc(10)

我们一步步来看看怎样可以做到动态调用 C 函数。

函数地址

首先若要动态调用 C 函数,第一步就是需要通过传入一个函数名字符串找到这个函数地址,这里一个必要的前提条件就是 C 编译后的可执行文件里必须有原函数名的信息,才有可能做到通过函数名字符串找到函数地址。我们写个简单的程序来看看它编译后可执行文件的内容有没有这个信息:

//main.m
void test() {
}

int main() {
  return 0;
}

编译这个文件,并用otool看下它的汇编:
(更多…)

XCode 代码补全插件 – JSPatchX 原理解析

2016-4-26 评论(2) 分类:技术文章 Tags:

JSPatchXJSPatch Xcode 代码自动补全插件,目前在 github 开源,效果见图:

做完一个开源项目照例写篇文章说明下实现原理,主要目的是让想对这个项目做贡献改进的人可以通过文章更容易地了解这个项目的由来,思路,核心原理和流程,降低参与这个项目开发的门槛。

由来

JSPatch 脚本一个不爽的地方就是没有代码补全,而调用 OC 方法时方法名又死长,写起来很不方便。

对此之前做了 JSPatch Convertor,可以自动把 OC 代码转为 JSPatch 脚本,这个工具的使用场景是用 JSPatch 做 hotfix 时,需要重写原 OC 的整个方法,这时用工具把这个方法的 OC 代码直接转为 JS 再进行修改,可以很大地降低工作量,缓解了这个问题。但若要用 JSPatch 开发新功能模块,就不会有 OC 代码可以去转换,这时提高编码效率的唯一方式就是做代码补全插件。

在寻找实现方案的时候得知公司内一牛人 louis 已经实现了 lua 的 XCode 代码补全插件,沟通后还很慷慨地给了源码,省去了很多研究 XCode 代码补全机制的功夫,于是参考他的编码,并且直接用了他 OC 头文件解析的代码,开发了 JSPatchX。所以这个项目算是我与 louis 联合开发的,在此感谢 louis~

插件入门

XCode 有个很坑爹的地方,就是它并不官方支持插件开发,官方没有文档,XCode 也没有开源,但由于 XCode 是 Objective-C 写的,OC 动态性太强大,导致在这么封闭的情况下民间还是可以做出各种插件,其核心开发方式就是:

  1. dump 出 Xcode 所有头文件,知道 Xcode 里有哪些类和接口。
  2. 通过头文件方法名猜测方法的作用,swizzle 这些方法,插入自己的代码实现插件逻辑。
  3. 通过 NSNotificationCenter 监听各种事件的发生。

更详细的开发教程网上有不少文章,有兴趣的自行搜索吧。

起步

对于实现 JS 代码补全这个功能来说,主要分三步:

  1. 在编辑 JS 文件时开启代码补全功能。
  2. 找到用户输入代码时的回调,按 Xcode 要求组装代码补全对象数组返回。
  3. 根据已输入文字对补全对象数组进行过滤

第一步是通过替换 DVTTextCompletionDataSource 类里的 -strategies 方法,在源文件是 JS 时生成一个 IDEIndexCompletionStrategy 对象返回,就可以针对 JS 文件走代码补全逻辑了。

第二步是替换 IDEIndexCompletionStrategy- completionItemsForDocumentLocation:context:highlyLikelyCompletionItems:areDefinitive: 方法,这个方法会在用户输入时被调用,在这里组装好应该出现的补全对象(IDEIndexCompletionItem) 列表返回,Xcode 就会自动应用返回的 items 对输入进行补全。

第三步是在 DVTTextCompletionSession-_setFilteringPrefix:forceFilter: 方法,针对第二步返回的 item 对象根据输入进行过滤。

要让自动补全插件程序跑通,只需实现上述三步。显然核心在第二步如何组装合适的补全对象 completionItem。代码里我们新增了一个 IDEIndexCompletionItem 的子类 JPCompletionItem 去表示,下面统一把这个补全对象称为 completionItem。接下来的问题就是怎样组装这些 completionItem。

实现

先看看我们需要哪些自动补全,概括起来有几种:

  1. 可能会被调用到的 OC 方法名
  2. JS 上新增的方法名,以及出现的类名
  3. JSPatch 自身的一些关键字接口,如 defineClass, require 等
  4. 当前 JS 文件里出现过的关键字

前三点应该没有异议,第四点要解释一下,实际上若要做得精细,应该加上 JS 语言本身自带的 API 和关键字(var / Math 函数 / String 函数等),以及JS 当前作用域上的变量的补全,但这样做一是 API 太多,二是实现复杂,所以用 “当前 JS 文件里出现过的关键字” 代替这两点,只要文件里出现过的单词就会有补全提示,也就是说一些关键字和变量第一次输入时没有提示,但在同个文件第二次输入就有补全提示了,sublime 默认就是这样的补全规则,实际使用效果很好,所以选择用这种简单的方式满足需求。

具体实现上,分三步走,一是解析 OC 头文件,二是解析 JS 文件,三是对解析后的数据进行缓存和组装 completionItem。

解析 OC 头文件

JPObjcFile 负责解析 OC 头文件,因为这里可以认为外部可以调用的 OC 接口都在头文件里,所以只需要解析头文件,这样处理比较简单,解析效率也很高。louis写了个 OC 头文件解析器,把头文件里的 class / protocol / import 解析出来,最终每个头文件都会解析成对应的 JPObjcFile 对象,这个对象保存着文件里 class / protocol 对应的方法的 completionItems,可以按需求直接输出。

解析 JS 文件

JPJSFile 负责解析 JS 文件,这里的解析比较简单,没有用词法语法解析器,而是直接通过正则匹配取出需要的内容,这里通过正则提取了:

  1. require() 里的 className
  2. defineClass() 里的 className
  3. defineClass() 里所有的方法名
  4. 文件里所有 keyword

同样每个 JS 文件都会生成一个 JPJSFile 对象,包含了上述提取的元素,并生成和保存了方法名和keyword对应的 completionItems列表。

组装和缓存

解决了单个 OC / JS 文件的解析,接着就是决定解析哪些文件,以什么样的形式缓存和组装返回给XCode。

JPObjcIndex

先看看 OC 的解析,JPObjcIndex 负责 OC 头文件的解析,JS 可能调用到的 OC 代码只存在于两个地方,一是系统framework,二是项目里的代码,对这些 OC 头文件要以什么样的方式解析呢?这里有两个选择:

  1. 只解析与当前编辑的 JS 文件相关的 OC 头文件
  2. 一次性把所有文件都解析好,再进行筛选

若要用方案1,只解析与当前编辑的 JS 文件相关的文件,则需要知道 JS 文件引用到了哪些 OC 文件,需要像 OC 代码那样有 #import 其他文件的规则,而 JSPatch 的规则是调用 OC 代码时不需要 import OC 文件,只需要通过 require(‘className') 接口引入类,所以线索只有 require() 里的类名,而在还没解析时是不清楚类名和 OC 文件的对应关系的,无法知道当前 JS 文件依赖了哪些 OC 头文件,所以这里只能选择一次性把所有 OC 头文件都解析好。

JPObjcIndex 默认扫描了 Foundation 和 UIKit 这两个 framework 里的所有头文件,以及当前项目里的所有 OC 头文件,在 JPObjcIndex 里以 className 为 key 进行缓存,对外提供通过 className 去取这些类相应 completionItem 的接口。

JPJSIndex

对于 JS 文件,为了简单起见,同样采用了一次性解析全部文件的方式。JPJSIndex 做了以下这些事:

  1. 解析所有 JS 文件,生成一个个对应的 JPJSFile 对象,缓存起来。
  2. 取出每个 JPJSFile 里解析的 require() 以及 defineClass() 的 className,去 JPObjcIndex 取这些 Class 对应的 completionItems。
  3. 取出每个 JPJSFile 解析好的 method completionItems。
  4. 取出每个 JPJSFile 解析好的 keyword completionItems。
  5. 本地 keyword.plist 定义了 JSPatch 常用的一些自动补全关键字,例如 defineClass, CGRect 等,在这里取出这些数据并生成 completionItems。
  6. 把 2-5 步里的 completionItems 分成两种类型,keyword 类型和 method 类型,缓存起来并返回给 XCode。
  7. 当有 JS 文件保存时,重新对这个文件生成 JPJSFile 对象,并重做 2-6 步。

第6步分出来的两种类型应用于两种场景,method 类型会在 JS 输入 . 要进行方法调用时出现,这个类型里所有的 completionItem 都是方法,包括 OC 头文件定义的方法以及 JS 里解析的方法。keyword 类型则是其他的像类名/语句关键字等这些非方法,在平常输入中出现。

在没有 JS 文件保存时,用户编辑 JS 代码每一次输入走到补全逻辑时,JPJSIndex 都是直接返回内存里已组装好的 completionItems 列表,没有其他操作,提高操作性能。第7步虽然在有文件保存时重新做了 2-6 步对数据进行重新组装,但这个过程不涉及文件解析,只需要取内存里解析好的数据进行组装,并且文件保存不会那么频繁,所以性能上没有太大问题。

整个流程就是这样,实际上很简单,总结起来就是解析所有 OC 头文件,解析所有 JS 文件,组装并缓存好 completionItem 返回。

不足

做这个项目的想法是先用最简单的方式快速做出来,满足80%的需求,导致会有一些不足,例如

  1. 没有做 JS 语法解析,没有做细致的筛选规则,粗暴地全部提示。
  2. 没有补全 include 的其他 JS 文件里的全局变量。
  3. defineClass 里写定义方法时,若要覆盖 OC 原有方法,没有方法名补全(因为方法名只有在 . 后才有补全)
  4. 没有加上除了 Foundation 和 UIKit 以外的 framework。

欢迎一起改进 JSPatchX,完善这些不足~

JSPatch更新:完善开发功能模块的能力

2016-4-6 评论(8) 分类:技术文章 Tags:

JSPatch 开源以来大部分被用于 hotfix,替换原生方法修复线上bug,但实际上 JSPatch 一直拥有动态添加功能模块的能力,因为 JSPatch 可以创建和调用任意 OC 类和方法,完全可以用 JSPatch 写功能模块,然后动态下发加载。只是之前在性能和开发体验上有些问题,还没有太多这方面的应用。这次 JSPatch 做了较大的更新,扫除这些问题,让用纯 JS 写功能模块变得实用。这里有个用 JS 写的 Dribbble 客户端 Demo,可以体验下效果。

来看看这次更新做了什么。

性能优化

通过工具可以看到使用 JSPatch 写功能模块时,耗时较多的点在于 JS 和 OC 的通信,以及通信过程中参数的转换,于是在这块寻找优化点。写功能时需要新增很多类和方法,例如:

defineClass('JPDribbbleView:UIView', {
  renderItem: function(item) {
    ...
  },
})

defineClass('JPDribbbleViewController:UIViewController', {
  render: function(){
    var view = JPDribbbleView.alloc().init();
    view.renderItem(item);
  }
});

上面两个都是新增的类,两个方法也是新增的,按之前的流程,这里的定义会传入 OC,在 OC 生成这两个类,并在这个类上添加这里定义的方法,调用时进入 OC 寻找这些方法调用。

(更多…)

iOS 组件化方案探索

2016-3-18 评论(80) 分类:技术文章 Tags:

看了 Limboy(文章1 文章2) 和 Casa (文章) 对 iOS 组件化方案的讨论,写篇文章梳理下思路。

首先我觉得”组件”在这里不太合适,因为按我理解组件是指比较小的功能块,这些组件不需要多少组件间通信,没什么依赖,也就不需要做什么其他处理,面向对象就能搞定。而这里提到的是较大粒度的业务功能,我们习惯称为”模块”。为了方便表述,下面模块和组件代表同一个意思,都是指较大粒度的业务模块。

一个 APP 有多个模块,模块之间会通信,互相调用,例如微信读书有 书籍详情 想法列表 阅读器 发现卡片 等等模块,这些模块会互相调用,例如 书籍详情要调起阅读器和想法列表,阅读器要调起想法列表和书籍详情,等等,一般我们是怎样调用呢,以阅读器为例,会这样写:

(更多…)

JSPatch 近期新特性解析

2016-3-14 评论(8) 分类:技术文章 Tags:

JSPatch 在社区的推动下不断在优化改善,这篇文章总结下这几个月以来 JSPatch 的一些新特性,以及它们的实现原理。包括脱离锁的 performSelectorInOC 接口,支持可变参数方法调用,给新增方法指定类型的 defineProtocol 接口,支持重写 dealloc 方法,以及两个扩展 JPCleaner 和 JPLoader。

performSelectorInOC

JavaScript 语言是单线程的,在 OC 使用 JavaScriptCore 引擎执行 JS 代码时,会对 JS 代码块加锁,保证同个 JSContext 下的 JS 代码都是顺序执行。所以调用 JSPatch 替换的方法,以及在 JSPatch 里调用 OC 方法,都会在这个锁里执行,这导致三个问题:

  1. JSPatch替换的方法无法并行执行,如果如果主线程和子线程同时运行了 JSPatch 替换的方法,这些方法的执行都会顺序排队,主线程会等待子线程的方法执行完后再执行,如果子线程方法耗时长,主线程会等很久,卡住主线程。
  2. 某种情况下,JavaScriptCore 的锁与 OC 代码上的锁混合时,会产生死锁。
  3. UIWebView 的初始化会与 JavaScriptCore 冲突。若在 JavaScriptCore 的锁里(第一次)初始化 UIWebView 会导致 webview 无法解析页面。

(更多…)

JSPatch Convertor 实现原理详解

2015-10-13 评论(9) 分类:技术文章 Tags:

简介

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,生成的程序可以在 [这里] 看到。官方文档有生成的流程和使用方法,可以自己试下。
(更多…)