聊聊JSPatch的动态性原理

JSPatch简介

JSPatch 是一个开源项目(Github链接),只需要在项目里引入极小的引擎文件,就可以使用 JavaScript 调用任何 Objective-C 的原生接口,替换任意 Objective-C 原生方法。目前主要用于下发 JS 脚本替换原生 Objective-C 代码,实时修复线上 bug。已超过 3500 个 App 在使用,成为 App 标配功能。

基础原理

1、Objective-C 方面
Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:代码时更具灵活性,我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。这种特性意味着需要一个运行时系统来执行编译的代码,它让所有的工作可以正常的运行。这个运行时系统即 Objc Runtime。Objc Runtime 其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。
要理解 Runtime 库,首先要了解 Objective-C 类与对象基础数据结构。类是由 Class 类型来表示的,它实际上是一个指向 objc_class 结构体的指针,定义可在 objc/runtime.h 中看到:

objc.jpg

在这个定义中,这里只关注3个字段

1、ivars :存放属性链表,记录类实例的所有属性定义。
2、methodLists :存放法树链表,记录类实例的所有方法实现指针。
3、cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据 isa 指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是 methodLists 中遍历一遍,性能势必很差。这时 cache 就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到 cache 列表中,下次调用的时候 Runtime 就会优先去 cache 中查找,如果 cache 没有命中,才去 methodLists 中查找方法。这样大大提高了调用的效率。

同时 objc/runtime.h 中还提供了大量的 API 来操作类与对象。类的操作方法大部分是以 class 为前缀的,而对象的操作方法大部分是以 objc 或 object_ 为前缀。这里我们只关注方法操作函数,如下:

class.jpg

1、class_addMethod:如果本类中包含一个同名的实现,则函数会返回 NO。如果要修改已存在实现,可以使用 method_setImplementation。
2、class_replaceMethod:该函数的行为可以分为两种:如果类中不存在 name 指定的方法,则类似于 class_addMethod 函数一样会添加方法;如果类中已存在 name 指定的方法,则类似于 method_setImplementation 一样替代原方法的实现。
3、method_setImplementation:重置方法实现。
4、method_exchangeImplementations:交换方法实现。

在 Objective-C 中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是 SEL 的名字。每个类都有一个方法列表 methodLists ,存放着 SEL 的名字和 IMP 方法实现的映射关系如图示。IMP 类似函数指针,指向具体的 Method 实现。

method-list.jpg

利用 Runtime 可以实现在运行时偷换 SEL 对应的方法实现 Method 或重置 IMP 方法实现,达到 hook 的目的。

method-swizzing.jpg

以上也就是大名鼎鼎的黑魔法原理(Method Swizzling),常见的用法

code.jpg

当然 Object-C 还支持动态创建对象,动态添加方法,动态添加属性(Object-C 中当类注册完成后无法动态添加属性,但可以用关联方法来模拟属性功能,属性的本质是 get 和 set 方法)

alloc-class.jpg

2、JavaScriptCore 方面

前端开发的同学应该知道,浏览器核心模块主要是渲染引擎和 JavaScript 引擎两部分组成。前者用于处理页面布局,渲染及 DOM 结构等,后者用于 JavaScript 的解析、执行及 DOM 交互等。JavaScriptCore 是一种 JavaScript 引擎,主要为 webkit 提供脚本处理能力(其主要以 safari 浏览器为代表)。除此之外,还有著名的 Jscript(IE), SpiderMonkey(firefox)和V8(chrome)。它提供了以下主要功能:
1、Objective-C –> JavaScript (即在 Objective-C 语言环境里执行 JavaScript 代码段、方法,创建 JavaScript 变量及变量操作等等)执行 JavaScript 代码的方法:首先引入 JavaScriptCore.h,然后通过 JSContext 创建 JS 运行环境,再通过 evaluateScript 来执行结果

oc-javascript.jpg

需要注意 Objective-C 和 JS 数据类型之间的转换表:

convert-type.jpg

2、JavaScript –> Objective-C(即在 JavaScript 语言环境里调用 Objective-C 公开给 JavaScript 的方法)。有 JSExport 协议和 Block 两种方式

javascript-oc.jpg

3、内存管理和线程封装(主要是需要注意引用和线程使用冲突)

当 JS 对象引用到 Object-C 对象(继承了 JSExport 协议),而 Object-C 对象又引用到 JS 对象 时就会发生循环用(很少见的场景,即使真存在,也可以通过架构设计的方式来避免)

memory-leak.jpg

这个时候就需要使用到 JSManagerValue 包装一下

memory-leak2.jpg

实际代码如下:

jscode.jpg

至于线程冲突就涉及到 JSVirtualMachine 的理解:其实每一个 JSVirtualMachine 都管理着一个 JavaScript 虚拟机(JSContext 的载体),它运行在 Object-C 中的一个独立线程队列,相同的 JSVirtualMachine 共用同一个线程队列,不同的 JSVirtualMachine 当然也就处于不同的线程队列,它们之间无法进行数据通讯,只能通过 Object-C 来做通讯中转,所以会出现线程冲突问题。理解了这个关键点,解决冲突问题就容易了。
通常初始化 JSContext 环境都会加载在一个 JSVirtualMachine 虚拟机,即使不指定 JSVirtualMachine 对象,也会默认加载一个,如下图示:

JSVirtualMachine.jpg

3、Object-C 和 JavaScript 之间的桥接
JSPatch 中两者之间交互依托前面的数据类型转换表,使用最简单的字符串传递方式交互信息达到动态化的目的。一句话总结:JS 传递字符串给 OC,OC 通过 Runtime 接口调用和替换 OC 方法,这是最基础的原理。如下图示:

jspatch.jpg

详细的打怪涨经验方式,还是去参考 bang 神的文档,芝麻开门:
https://github.com/bang590/JSPatch/wiki/JSPatch-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3

服务端原理

JSPatch 需要使用者有一个后台可以下发和管理脚本,并且需要处理传输安全等部署工作,JSPatch 平台帮你做了这些事,提供了脚本后台托管,版本管理,保证传输安全等功能,让你无需搭建一个后台,无需关心部署操作。但还是需要了解一些服务端原理的。下载 JS 脚本只是简单的 get 请求,这里要研究的是其中传输安全,灰度下发和回滚机制。

1、安全机制

这里就直接引用 bang 神的原文,如下图示:

transport-safe.jpg

1、服务端计算出脚本文件的 MD5 值,作为这个文件的数字签名。
2、服务端通过私钥加密第 1 步算出的 MD5 值,得到一个加密后的 MD5 值。
3、把脚本文件和加密后的 MD5 值一起下发给客户端。
4、客户端拿到加密后的 MD5 值,通过保存在客户端的公钥解密。
5、客户端计算脚本文件的 MD5 值。
6、对比第 4/5 步的两个 MD5 值(分别是客户端和服务端计算出来的 MD5 值),若相等则通过校验。

只要通过校验,就能确保脚本在传输的过程中没有被篡改,因为第三方若要篡改脚本文件,必须计算出新的脚本文件 MD5 并用私钥加密,客户端公钥才能解密出这个 MD5 值,而在服务端未泄露的情况下第三方是拿不到私钥的。
JSPatch 平台是用 PHP 实现的,这里笔者用 Node.js 仿照流程来模拟基础实现原理。

node-jspatch.jpg

运行效果如下:

node-response.jpg

这里为了看效果并没有对 data 进行加密,实际生产环境中使用时,脚本需要进行版本管理,提交 PR-Review,通过以后才能通过服务获取,而脚本内容也是需要进行 RSA 加密的,安全第一嘛。再配合上苹果的 ATS 要求所有APP域名都支持HTTPS传输(此要求 delay 了),至此已经实现了安全传输机制。

2、灰度下发机制

灰度下发涉及到数据上传,分析等,甚至有的公司都已经做到了大数据挖掘的程度,JSPatch 平台支持按用户数量、按条件(常被用来做新功能发布或线上调试)灰度下发,其中按条件还支持后台动态配置条件,功能很强大。详细的可以参考 http://www.jspatch.com/Docs/rule

3、回滚机制

这部分 JSPatch 平台并没有详细说明,但目前实践中大部分都是简单粗暴地重传:即已下发脚本出 bug 了,就再出一个 fixed patch,重新下发。但这种方式对于日活千万上亿的 APP,是不能容忍的。这里可以提供一种使用基于 git 版本开源项目管理的方式:每一次脚本下发都提交 PR-Review,Review 通过以后 merger 再下发,一旦出错直接 git revert。Native 端缓存最新版本和上一个版本的 patch 补丁,共两份,当检测到回滚发生(服务端下发的版本标识小于 Native 端版本)时,把上一个版本的 patch 标识为最新,出错的 patch 标识为历史版本,完成回滚操作。

后记

使用 JSPatch 已有半年多时间了,从中收获到很多,也踩过不少坑,比如:无法替换 main 函数之前执行的类方法 +(void)load; +(void)initialize; 等,无法调用被 hook 住的源方法,但都一一趟过了,总的来说还是一个非常强大商业化工具。有感兴趣的小伙伴还是强烈推荐多多阅读 bang 神的 Wiki,传送门:https://github.com/bang590/JSPatch/wiki

希望本文能对准备接入或者学习 JSPatch 的开发人员有所帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容