【腾讯Bugly干货分享】JSPatch 成长之路

本文来自于腾讯bugly开发者社区,非经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/579ef...前端

Dev Club 是一个交流移动开发技术,结交朋友,扩展人脉的社群,成员都是通过审核的移动开发工程师。每周都会举行嘉宾分享,话题讨论等活动。vue

本期,咱们邀请了腾讯WXG iOS开发工程师——bang 陈振焯,为你们分享《JSPatch成长之路》react

如何加入 Dev Club?git

移动端开发经验 >= 2 年,微信扫描下方群管理微信二维码,备注姓名-公司(或产品) 申请加入。github

外部群二维码


分享内容简介:
JSPatch 是 iOS 上的动态更新框架,只须要引入小小的引擎文件,就能够用 JS 调用和替换任意 OC 方法。目前被广泛用于实时修复 bug,已有超过2500个 APP 接入,本次分享介绍 JSPatch 发展过程当中遇到的问题和解决思路。
(此内容已在 GMTC 线下分享过,本次从新整理为线上分享)数据库

内容大致框架:json

  1. 起步:介绍 JSPatch 的诞生和当时碰到的难题后端

  2. 发展:介绍 JSPatch 如何补全周边功能变得更好用缓存

  3. 下一步:介绍 JSPatch 下一步的计划安全

分享人介绍:

bang 陈振焯 广州研发部 目前负责开发微信读书iOS端,博客 http://blog.cnbang.net


下面是本期分享内容整理


你们好,我是 bang,目前在广州研发部作微信读书 iOS 端,今天分享的主题是《JSPatch 成长之路》。

我在去年5月发布了 JSPatch (https://github.com/bang590/JS... 这个开源库,如今普遍应用于 iOS 的热修复,今天分享一下 JSPatch 过去一年以来的成长。

分享共分为三个部分:

  1. 起步 —— 介绍JSPatch的诞生和当时碰到的难题

  2. 发展 —— 介绍JSPatch如何补全周边功能变得愈来愈好用

  3. 下一步 —— 介绍JSPatch下一步的计划

1、起步

先说下起步阶段。当时碰到的一个问题是:APP 线上 bug 修复周期长,成本高,版本发布出去后,发现一个 bug,要修复这个 bug 就必须得另外发一个版本,也就是要经历:测试——打包——发布——审核——用户下载,这一系列过程,成本很是高,最后还很难让全部用户都升级上来。

当时业界已有一个解决方案,叫 waxPatch,它是在APP里嵌入 lua 引擎,而后经过 OC 的 runtime 接口在 lua 里调用和替换 OC 方法,这样就能够下发 lua 脚本替换原生代码,动态修复 bug。

waxPatch: https://github.com/mmin18/Wax...

这是个不错的方案,但当时的 waxPatch 存在不少缺陷:

首先是 wax 已经多年不维护了,致使不支持一些 block/64 位等新特性,此外当时 wax 还有文档不足,测试不足,线程不安全,难以调试等坑。

因而开始探求更好的解决方案。很容易想到若是用 JavaScript 作这个事情的话,相对 lua 原生就有一些优点:

  1. iOS 里已内置 JavascriptCore 引擎,无需再另外嵌入。

  2. JS 在终端应用普遍,不少混合开发内嵌 H5 页面就是用 JS。

  3. 符合苹果审核规则,苹果在文档里说明不能够下载可执行的代码,由 JavascriptCore 执行的除外

那么有没有人试过这样作呢?用 Javascript 调用和替换 OC 方法,固然是有的。

  • 在当时有一个开源库 JavascriptBridge (https://github.com/kishikawak..., 它能够用 JS 调用 OC 接口。
不过它用的是 JavascriptCore 原生的接口作的,须要事先在 OC 里定义好要调用的接口,没有事先定义的不能调,这致使它的实现很臃肿,由于要在 OC 定义大量的方法。此外它也不能替换 OC 方法,实用性很低。

  • 当时国外还有一个热补丁服务叫 rollout (http://rollout.io)。 它是一个服务平台,底层也是用 JS 调用和替换 OC 方法去实时修复 bug,不过它不是开源的,只能在这个平台上用,另外它的 JS 写法是比较复杂的,看看这个例子就知道,这致使它不得不在平台上作一些便捷的功能,把一些经常使用的操做封装起来,减小使用者写代码。

总的来讲,当时并无一个更好的方案,因而想本身造个。

当时指望作到的效果是这样的:

我在 JS 写 UIView.allOC(), 而后传给 JavascriptCore 执行,JavascriptCore 把我要调用的信息,这里类名是 UIView,类方法名是 alloc 传递给 OC,OC runtime 就能够找到这个类和方法进行调用。这是最基本的一个语句调用。

实际上当时实现这个最基本的调用就遇到一个槛,在 JS 里这条语句根本没法执行:

要让这条语句在 JS 环境中能够执行,在 JS 的语言规则下,UIView 必须是一个对象,alloc 必须这个对象的一个方法。

也就是说要像这样定义后才能够执行:

UIView 必须是一个对象这点没问题,在调用前定义就能够,但 UIView 的方法必须在调用前定义就很糟糕,这意味着若是你想调用任意 UIView 的方法,你就须要提早把全部 UIView 的方法都找出来,每个方法都要预先定义好。

也就是说在使用UIView以前,须要先去 OC 把UIView全部方法找出来,而后构建UIView对象,每一个方法都在这个对象里生成对应的函数,而后你才能够调用UIView的任意方法。

JSPatch 在开发时就尝试过这种方案,后来发现这些对象的方法太多了,仅 NSObject 基类的实例方法就有830个,类方法有118个,这致使在JS生成的对应的对象占用内存极高,NSObject就占了1.3M,UIView占2M。这根本不可用。

对此我还进行了一些优化尝试,例如去除掉里面的下划线开头的私有方法,在 JS 构造继承链共用基类方法。但这些优化都没多少效果,占用内存依旧很高。当时就以为不太可能实现。

实际上当时我陷入了一个思惟定势,作终端久了,思惟停留在 iOS 的 OC 世界,写代码必须遵照语言的规则,上述的困难也是在遵照 JS 语言规则这个前提下碰到的。

若是有方法不遵照语言规则呢?实际上在 JS 界,有个很经常使用的伎俩,就是预编译:

也就是咱们写的脚本不直接拿给 JS 引擎执行,而是进行一些转换后才执行,在现代框架这个用法很常见, react/vue 都用了,甚至还有像 coffieScript 这样把 JS 彻底换成另外一种语言的作法。

想到这一点,刚才的问题就很好解决了。

只须要把全部函数调用在执行前都替换一遍,变成去调用一个固定的 __c 函数, 这个 __c 函数模拟了 ruby/lua 等语言的元方法机制,对方法调用进行转发。

仍是以调用 UIViewalloc 方法为例,这个语句在执行以前会被替换为调 UIView 的 __c 方法,把 "alloc" 做为字符串参数穿进去,在 __c 方法里判断调用者 UIView 是否是 OC 对象。若是是,就把类名和传进来的方法名传到 OC 层进行调用,若是不是,就调用回 JS 这个对象的方法。

这样作简洁高效地解决了前面的问题,不须要去 OC 遍历每一个类的方法,不须要存储这些方法,就能够调用任意 OC 方法,只须要给 JS 基类定义一个 __c 方法就能够了。正则替换后不管调用 OC 的什么方法,都不会有语法错误,由于都变成调用这个 __c 方法,在这个 __c 方法里作处理去 OC 层调用相应的方法就好了。

使用这种方案后内存的占用降低了 99%,甚至更多,也使 JSPatch 的使用成为可能,这是 JSPatch 最核心的一点。

解决这个核心问题后,后面就是细化功能了,JSPatch 发布之后一直在完善,包括最基本的调用和替换OC方法,还有支持64位,支持 block,支持包括 c指针/Class/结构体等类型,支持 c函数的调用等,这里面的细节原理我以为看文章会比较清晰,相关文章均可以在 github wiki (https://github.com/bang590/JS... 上找到,这里就很少说了。

2、发展

接下来讲说 JSPatch 是怎样进行进一步发展的。

在完善 JSPatch 的同时,我也在想,如何把 hotfix 作得更好。

主要有两个思路:

  1. 下降使用门槛

  2. 提升使用效率

咱们一个点一个点来看对这两个问题是怎么作的。

首先 JSPatch 在易用性上一直坚持着一个理念,就是 keep it simple and tiny,用中文说就是保持精巧,保持好用。JSPatch 从开源到如今一年多,增长和完善了不少功能,但它的使用方式和接口都没有变过,一直以来都是只有三个文件,拖入项目直接可使用,也会很谨慎地新增接口,不会影响到旧接口的使用,不会出现同一份代码在旧版本能用,在新版本不能用的状况,在易用性上下降使用门槛。

另外一个问题是安全问题

JSPatch 能够调用和替换任意 OC 方法,权限很大,若是在传输过程当中被第三方攻击,替换了下发的代码去执行,会对用户和 APP 自己形成很大伤害。若是每一个接入 JSPatch 的人都要考虑这个安全问题,接入门槛就会很高,也可能会由于考虑不周全致使 APP 处于危险状态。

对此当时详细考虑了安全策略。对这种状况:

  • 最简单的方案是直接对脚本加密,后台使用固定密钥加密脚本后下发,客户端使用一样的密钥解密。这种方案的优势是简单,缺点是这个密钥必须存在客户端,黑客很容易破解拿到这个密钥,而后经过传输过程第三方攻击,下发一样用这个密钥加密的恶意代码,就没有安全可言了。

  • 第二个方案是让脚本经过 https 传输,这个方案的优势是安全性高,只要客户端对证书进行过足够的验证,就能很好地保证安全性。缺点是这个方案门槛高,部署繁琐,须要购买证书,对一些中小 APP 来讲可能难以接受,而且若是用户手机信任了一些恶意证书,也仍是存在被攻击的危险。

  • 第三个方案是使用RSA签名验证。
    整个流程是这样:

第一步服务端计算脚本文件的MD5值,用存在服务端的私钥对这个MD5值进行加密,而后把这个MD5值和脚本一块儿打包下发给客户端。客户端拿到脚本和加密后的MD5值,用存在客户端的公钥进行解密,拿到服务端计算的MD5值,本地再计算一遍脚本文件的MD5值,对比这两个值是否一致,若一致则表示传输过程当中没有被篡改。

若是第三方要截获请求下发恶意脚本,第三方必须用私钥加密这个恶意脚本的MD5值一块儿下发,才能经过验证执行,只要服务端不被攻破,第三方就没有私钥,也就没法进行篡改。

能够看到这第三个方案门槛低,通用性高,部署简单,安全性也高,对服务端和客户端都没有什么特殊要求。

我把这一套安全方案作成一个组件,叫 JPLoader,也开源在 JSPatch 项目上(https://github.com/bang590/JS..., 须要部署 JSPatch 的同窗能够直接使用这套组件,解决安全性问题,客户端的工做就完成了,只剩下后端的工做。

前面把安全性问题解决了,只剩下后端的工做,但搭建后台对使用者来讲也是挺麻烦的事,特别是做为 iOS 开发者,在中小公司本身搭后台麻烦,在大公司要后台帮你搭也不容易,这又会致使使用 JSPatch 的门槛仍是很高。

因而在想这部分工做能不能也帮使用者省了呢?

对此我搭建了 JSPatch 平台 (http://JSPatch.com), 让使用 JSPatch 的人不须要搭建后台,直接经过平台下发补丁代码。

这个平台几个月前已经开放注册,如今全部人均可以使用。

在搭建这个平台时,碰到一个问题值得分享一下,就是如何支持高并发?

因为 JSPatch 的补丁特性,补丁须要及时推送给用户,也就是说至少须要在每次启动时向服务端请求询问 APP 是否有新的补丁,有的话下发执行。这里询问的请求量是很高的,单个 APP 能够控制,但平台要面对多个 APP,累计起来的请求数量会很是多,并发会很高,怎样支撑这样的高并发?

正常来讲这样一个系统总体设计大体是这样的:

平台用户把脚本放到平台服务端,服务端的数据库保存着脚本的各类信息和内容,APP 客户端向平台发起请求询问是否有新脚本,平台服务端接收到请求后经过 CGI 处理请求参数,根据 APPkey 等参数从数据库拿出这个 APP的信息,而后组装数据告诉APP客户端有没有新脚本。

这里的询问请求至少时 APP 每次启动都要发一次请求,才能保证脚本的更新能尽快下发。请求量大时,这里从数据库取出数据很容易成为整个系统的瓶颈,CGI 处理请求参数和组装数据也要耗很多资源。

对此我改用了另外一种方式:

平台用户上传脚本到平台服务器时,服务端除了把 APP 信息存在 DB 外,同时会另外上传一份 JSON 静态文件到静态云服务器,JSON 里保存了当前补丁的版本,而这个静态资源的文件名是由 APPkey/APP 版本号组成的。

例如这里脚本补丁版本号是10,这个JSON静态文件的内容就是 {v:10}。能够想象静态文件的访问路径就是:

http://JSPatch.com/{APPkey}/{APP_version}.json

而后 APP 客户端再也不向平台服务端发请求,而是向这个静态资源服务器发请求,根据 APPKey 和 APP 版本直接请求到这个 JSON 文件,里面带的版本号信息就能够告诉 APP 脚本是否有更新。

整个流程就变成了:

APP 向静态服务器询问是否有新补丁,静态服务器直接返回预先设置好的 JSON,就结束了。

这样 APP 永远不会跟平台服务器打交道,只需跟静态资源服务器打交道,静态资源的高并发处理起来就简单得多,成本也低不少,如今有不少静态资源云存储,直接接入就能够了,以这些云存储的能力,支持多高的并发都没有问题,用户量多大的 APP 接入均可以支撑到。就是这样 JSPatch 平台解决了高并发问题,能够投入使用。

接下来在开发效率上,有一个问题是转换代码效率低。

咱们用 JSPatch 修复 bug 时时以方法为单位进行替换的,若原方法有上百行,你的需求只是修改其中一两行代码,你也要把这上百行代码人工翻译成 JS 才行。对此我开发了JSPatch Convertor 这个工具,能够自动把 OC 代码转为 JSPatch 代码,提高开发效率。

这个工具也开源在 github 上(https://github.com/bang590/JS..., 支持了大部分语法特性,但目前还作不到支持全部特性,像私有变量/静态变量/宏这些还不支持,因此转换后须要人工修改,但仍是很大地提升了使用 JSPatch 的效率。

总结下来,在下降使用门槛上,JSPatch 保证了易用性,封装了安全方案,提供了 JSPatch 平台让使用者能够直接接入,另外还有完善的文档和解析文章保证使用无障碍。提升使用效率上,作了 JSPatch Convertor 自动转换代码,也内置了一些扩展方便直接调用一些经常使用的 C函数。

通过不断发展,JSPatch 能够说是 iOS hotfix 的最佳解决方案。

目前大部分应用都已经接入使用,据不彻底统计至少有 2500 个 APP 接入,通过了的大用户量的考验。

3、下一步

接下来讲说下一步的计划,JSPatch 在 hotfix 上已经作得不错,目前下一步打算推进使用 JSPatch 开发功能模块。

JSPatch 作这个事情跟 React Nativeweex 这类方案比起来,会有一些优点:

  • 首先 React Native 和 weex 都是从前端出发扩展到终端,是前端方案的延伸,他们的体系对于前端来讲更熟悉,对于终端来讲,意味着要从新学习前端的一套知识,学习成本较高,而 JSPatch 是从终端出发,编码体系也差很少是直译 OC,学习成本较低。

  • 第二点是 ReactNative 和 Weex 是比较大型的框架,环境配置都很复杂,也会增大很多安装包的大小,若是说只想扩展实现一两个小功能,接入这么大型的框架不合适。而 JSPatch 前面也说了,属于微型框架,只有三个文件,也无需环境配置。

  • 第三点是 ReactNative 和 Weex 的组件都是要一个个封装好,难以复用现有的 OC 组件,而且他们都是大型框架,在未成熟阶段框架自己实现上的坑会不少,而 JSPatch 能够直接复用全部 OC 现有组件,而且只是薄薄的转接层,坑会较少。

但 JSPatch 要用于开发功能,有两个问题:

  1. 开发效率较低

  2. 运行效率较低。

在开发效率上,我作了两件事去提升,第一个是 JSPatchX 代码补全插件 (https://github.com/bang590/JS...

写 JSPatch 代码时并不像 OC 那样有代码补全,在调用 OC 长长的方法时效率会很低,并且用 JSPatch 写功能时,不像 hotfix 那样有对应的 OC 代码,也没法使用前面说的 JSPatchConvertor 进行转换。因而作了 JSPatchX 去弥补这个缺陷,能够在 XCode 自动提示补全 JSPatch 代码。

另外一个是 Playground 即时刷新范例 (https://github.com/bang590/JS...

能够实时预览 JSPatch 脚本执行的结果,无需像原生代码那样每一次修改都要 build 重启才能看到效果,这也是脚本语言的优点。使用者能够仿照这个 playground 的实现,在开发功能时在本身的页面实现这样的即时刷新,这样必定程度上提升了开发效率。

接下来看看运行效率。

JSPatch 写功能时运行效率低,因而着手进行优化,第一步是肯定瓶颈,发现运行速度最慢的在于在 JS 调用 JS 上定义的新方法。

例如这里新定义了一个dribbbleView类,里面有个新方法renderItem,在 JS 里调用这个新方法时,速度很慢。

分析下这个调用过程:

主要问题在于这个新定义的方法与 OC 挂钩,这一次普通的调用,须要在 JS 和 OC 之间不断来回通讯,不断进行参数转换,通过这9个步骤后才能成功调用。

对此我经过一些手段作了优化,把这样的方法直接放在 JS 环境上,在 JS 调用这个方法时无需再与 OC 通讯,整个调用流程就变成了只有两步:

通过这个优化后,这样的方法调用性能最高提升 700 倍,这才使 JSPatch 写功能变成一件靠谱的事。

除此以外还作了一些其余优化,包括提高新增 property 性能,提供跟定义 OC 类同样的纯 JS 类定义接口,自动转换参数类型等,具体优化细节能够在这篇文章(http://blog.cnbang.net/tech/3... 上看到。

我用 JSPatch 写了个 Dribbble 客户端 demo (https://github.com/bang590/JS...  在 iPhone5C 上测试过,滑动性能没有问题。

最后,能够从这个脑图看出 JSPatch 的现状,周边设施仍在继续建设中。

我今天的分享就到这里,谢谢。

问答环节:

Q1: JSPatch 的底层原理跟 ReactNative 是否是差很少呢?有受到其启发么?

JSPatch 的原理跟 ReactNative 是彻底不同的,JSPatch 是 OC 方法调用和替换的一层转接,ReactNative 并不会去调用和替换 OC 方法,它有本身的一套通讯规则。

Q2: 自己基于OC runtime 对 Swift 的项目如何支持?

Swift 相关问题在 wiki 里有提到:


  1. 只支持调用继承自 NSObject 的 Swift 类

  2. 继承自 NSObject 的 Swift 类,其继承自父类的方法和属性能够在 JS 调用,其余自定义方法和属性一样须要加 dynamic 关键字才行。

  3. 若方法的参数/属性类型为 Swift 特有(如 Character / Tuple),则此方法和属性没法经过 JS 调用。

  4. Swift 项目在 JSPatch 新增类与 OC 无异,能够正常使用。

Swift 的原生类目前没找到替换的方法,动态调用却是能够实现。

Q3: JSPatch 运行一次就会把JS转换为 OC 缓存起来?那咱们能够利用它去作一些重复调用的事情?甚至用来开发?它的效率和原生相近吧?

会缓存一些 methodSignature,但仍是得经过反射 (className->class->imp) 去找到要调用的方法,效率会比原生低。但通常程序的瓶颈不会在语言这里。

Q4: 对于 JSPatch 资源更新服务平台仍是表示一些担心,若是被别人攻破了,岂不是不少 APP 都受牵连了?

JSPatch 平台就算平台被人黑了,也没法对平台上的 APP 下发恶意代码。只要使用者用了自定义的 RSA 密钥就能够了,只有使用者有私钥,每次发布脚本都要使用这个私钥,平台不会保存它,详情可见:http://JSPatch.com/DOCs/rsa

Q5: 如今 iOS 加快了审核速度,好像如今是24小时内审核上线。那如今 JSPatch 前景还会好么?

审核只是一个环节,测试/打包/发布/用户下载,这些其余环节仍是不可少,而且最大的问题仍是是用户下载更新不可控。

Q6: Swift 属于静态编译类型,是否是能够利用相似 c函数替换的方法呢?像 fishhook 这样的工具

fishhook 须要编译时肯定要替换的函数指针,并不能在运行时替换任意 c函数

Q7: 我看网上的一些介绍说 JSPatch 对小的 bug 修复好点,大的仍是提交新的版本,可是我看您介绍使用静态资源服务器管理.应该不存在数据量大,并发的问题.这个您怎么看?

他指的大的 bug 应该是要写不少代码才能修复的 bug 吧?这点应该跟 JSPatch 开发效率问题有关,对于大量的代码他不想原生 OC 写一套修复,再用 JSPatch 写一套,跟数据量和并发应该没什么关系。

Q8: 为什么 JSPatch 上面,QQmail 没有接入?有什么顾虑吗?

由于QQ邮箱在 JSPatch 出现以前已接入 lua,刚出现时 JSPatch 还不是很成熟,团队当时想同时使用两种方案做对比,时间久了也没有再切换过来了。

Q9: 有没有可能进一步提高 JSPatch convertor 的能力。最终发到直接打开 Xcode 项目,寻找依赖,经过语法语义分析等,将 OC 转换为 JS

能够作到的,不过这事要投入很大精力,以前有搞过一个 demo,直接用 OC 写 Patch,而后在执行前转换成 JSPatch 代码,有一个开源库 JSTalk (https://github.com/ccgus/JStalk) 有基本的 OC->JS 的转换,但要作到好用还有不少工做。

Q10: 请问若是个人 APP 引入了 JSPatch, 可是产生 crash 的代码并非经过 JS 写的, 而是原生的 OC 代码, 那么 JSPatch 能够经过下发 JS 脚本修复这种 crash 吗, 若是能够的话, 原理是怎样的?

能够,原理就是把致使 crash 出现的方法替换掉,OC 调用那个方法时转成调用 JSPatch 里写的替换的方法,就不会 crash 了

Q11: 有没有意识到 JSPatch 的性能瓶颈最终都取决于 JavascriptCore 的性能?因此低端机永远是低性能,有没有想过借鉴 JSX 作点事情呢?

JavascriptCore 的性能并无问题,性能瓶颈不是 JavascriptCore,目前来看瓶颈会是 OC 与 JS 通讯时大对象的参数转换,但这是能够避免的

Q12: JSPatch 效率怎么样啊

效率能够试试上文说的 dribbbleDemo

Q13: 调试 JSPatch 时能不能打断点,如何定位到 JS 的 crash 堆栈

能够断点,文档有写:https://github.com/bang590/JS...

若是你们对本次分享还有问题,请按如下格式在DEV社区(dev.qq.com)发问答帖

发帖格式:【bang@DEV Club 你问我答】 “问题”

最后,欢迎你们关注 JSPatch 公众号:JSPatchDev,会即时推送 JSPatch 最新信息以及相关技术文章:

更多精彩内容欢迎关注bugly的微信公众帐号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!

相关文章
相关标签/搜索