OCRunner 第零篇:从零教你写一个 iOS 热修复框架

Stella981
• 阅读 1555

作者: SilverFruity, https://github.com/SilverFruity

为什么要热修复

在软件开发过程中,很难避免 BUG 的存在,尤其是对于一些达到一定规模的 App 因为协作模式错综复杂,就很容易带着问题上线。

一旦问题上线之后,问题就麻烦了,不仅需要重新打包、测试,而且还需要重新提交审核,而这种修复问题的方式往往是低效且漫长的。

因此,在开发一个 App 的过程中,稳定性的就变成了一个难题,唯一的原因就是不希望带着问题上线导致用户对 App 失去信任。

热修复就可以很好的解决这类棘手的问题,因此带着好奇之心,研究了一下热修复在 iOS 端的可行性,实现 了一个较为完备的热修复框架,我把它叫做 OCRunner。

我也会在未来几个月,把我做 OCRunner 的一些经验总结成博文,在「老司机技术周报」的公众号上与大 家分享。

已有的开源方案

采用 JavaScriptCore 的方案

*JSPatch*[1]通过下发 JavaScript 脚本,使用系统提供的  JavaScriptCore 执行 JavaScript 脚本,通过 JavaScript 代码解析代码中的类信息,动态调用相应的 Objective-C 函数,然后配合 libffi 修改 Runtime,最终实现热更新,奠定了 Objective-C 的热修复基础。

*TTPatch*[2]和 JSPatch 一样使用 JavaScriptCore 执行 JavaScript 脚本,但支持实时预览,这个是我超级喜欢的功能。

自实现脚本解释器的方案

*OCEval*[3]下发 Objective-C 源码解释执行,但支持的语法有限。作者lilidan[4]自己实现了 Objective-C 的词法分析器和语法分析器。其中的FunctionSearch的实现[5]也帮助了我许多。DynamicOC[6]将 OCEvel 的词法解析器和语法解析器使用 lex&yacc 实现。

*Mango*[7]作者设计了 Mango 的脚本语言(和 Objective-C 极为相似),使用 lex&yacc 生成语法树,然后再将 Mango 脚本的语法树解释执行。这也就是 OCRunner 的起源。

OCRunner[8]是我在 Mango 的基础之上优化后的一个方案。和 Mango 相同的是,采用lex&yacc生成语法树后解释执行、JSPatch 的 Runtime 的思想。但这一次,我们可以书写 Objective-C,然后直接动态执行它。同时也支持了许多特性:结构体、枚举、函数指针等。

其他库不足的问题:

  1. 支持不够完善:结构体、系统函数、枚举等。

  2. 语法问题:一些语法上的小问题,有时候无异于回炉重造,也是需要时间成本的。有时候一些奇怪的语法问题,或许都要花你一个下午的时间。

  3. 传输加解密:  JSPatch 等均采用 RSA (非对称加密)对整个脚本进行加密,个人认为着实耗费性能了一些。

为什么要写 OCRunner

这次可以慢慢的讨论这个问题了~

19 年 4 月的时候,刚好在趋势榜上看见了Mango[9]项目,发现它是使用自定义的、和 Objective-C 语法非常相似的脚本语言(相当于自己创造一名语言吧,balabala),当时早已对编译相关的心驰神往,奈何一直没有一个合适的机会去学习,再加上当时的公司有热更新的需求,就想着试一试,看能不能写一个将 Objective-C 代码转换为 Mango 脚本的转换器(也想过能不能借此自己完成一个热更新库)。

19 年 4 月 晚上 10 点正式开启了每天回家爆肝 lex&yacc 的升级打怪之路,各种各样的问题炸得我在锅里反复横跳(解决办法: 「  lex&yacc.pdf 」抱着啃),但最严重的的莫过于指针相关的,参考了好几个基于 lex&yacc 的开源编译器,才得以找到折中的解决办法。中间也发生了许多事情,从 19 年 7 月后,我休息了4个月。

20 年 3 月的时候,想着 oc2mango 做了那么久,得把翻译器做出来了才能给自己一个交代,肝了一段时间,正确的使用 yyless 后,问题相较之前已经少了很多。这个时候 oc2mango 翻译器也能正常使用了,虽然仍有一些小问题。这个时候翻译器的小目标也就完成了✌️。

当翻译器完成以后,我的野心变大了,我想试试我自己能不能完成一个像 Mango 一样的热更新库(内心戏:lex&yacc 就是从它那儿学来的,应该不难吧?😂)。结果确实是年少无知,各种各样的坑。当完成了和 Mango 一样的功能时,这个时候我就已经很想获得 github 的✨✨了,跑去提了一个老司机的 issue,额,被打回来了,也是应该的,当时确实有很多问题(天真的以为,arm64 下,直接将参数放在相应的寄存器上,然后调用函数就行了 👏)。后面去认真看了 arm64 程序调用标准和 libffi 的代码后,自然而然的也就实现了自定义 arm64 ABI(其实哭着看了一个月)。后续也实现了结构体和系统函数指针的调用以及 Json 补丁和二进制补丁。

直到发布OCRunner:完全体的iOS热修复方案[10] 后,我才每天开开心心的看着 star 突突的涨。

其实打从一开始,我就没想过我会把 OCRunner 写出来,最开始的时候离我太遥远了,我也只能给自己一步一步的定一个小目标。起初的时候,github 星星算不上我的动力,oc2mango 和 OCRunner 极少的星星也是去群里贴小广告来的。更多的动力,其实是来源于自己完成了一个又一个的目标后的成就感,别人不能满足自己,就自己满足自己吧😂(啤酒炸鸡走起 hhhh)。保持一个乐观的心态,向前冲~

整体架构的启发

在说OCRunner之前,我们先聊聊Clang和LLVM(让我多打几个字😂)。

通常我们在Xcode上运行 Objective-C 代码时,我们的编译器前端使用的是Clang,后端使用的是LLVM。首先 Clang 它通过词法、语法分析后,生成 抽象语法树 ,再由 抽象语法树 生成 LLVM IR 交给 LLVM 进行代码优化,最后将优化后的 LLVM IR 生成指定平台的机器代码,如图:

OCRunner 第零篇:从零教你写一个 iOS 热修复框架

图片出自: 简述 LLVM 与 Clang 及其关系

在写 OCRunner 之前,我就在想,能不能用相似的架构去完成 OCRunner ?

Clang替换为自己撸的渣渣编译器,LLVM 替换为自己完成的解释执行器,LLVM IR(中间代码)替换为 抽象语法树 或者是其他。代码优化,不存在的,远着呢,哈哈。

但是,目前首先需要做的是,OCRunner能够正常跑起来😂。

OCRunner 是一个将 Objective-C 代码作为输入的语法树解释执行器

其主要过程为: Objective-C -> 抽象语法树 -> 解释执行语法树。

没有了解过这方面的人,光是看着抽象语法树这个鬼就已经头大了,何况是解释执行语法树(作者也是过来人之一🍻)。但是相信大家看完整个文章以后,对抽象语法树的认识会印象深刻的。

  • Objective-C -> 抽象语法树:  由 oc2mangoLib 来完成。

  • 抽象语法树 -> 解释执行语法树: 由 OCRunner 来完成。

起初的 OCRunner 项目中是包含了 oc2mangoLib 库的,为什么呢?

  1. 作者当时对 Mango 中的很多东西也一知半解,需要用当前已有的方式去实现,等它的基础功能完善以后,才有时间去尝试我自己的东西。

  2. 省事,单元测试中,一个单元测试既测试了语法树生成,也测试了运行结果。

  3. 当时我也没想好,究竟应该以什么样的形式或者格式来作为中间者,它应该是一种固定的数据格式亦或是一种数据协议。

既然抽象语法树已经能够满足解释运行的需求了,为什么还得要个中间数据协议呢?

还记得Xcode中的Bitcode吗?它的作用就是以 LLVM IR 替代目标程序,比如我们上传 iTune Store Connect 的时候,其实上传的是我们的LLVM IR,苹果在自己的服务器上使用 LLVM 将我们的LLVM IR转换为相应目标程序。很多时候,我就在想,LLVM IR 和我们在面对对象中使用的 Interface 相同。咳咳,说个原因,跑偏了这么多😂,回到正题。

  1. 词法分析、语法分析产生的相关崩溃,应该由编译前端来负责,不应该出现在解释执行的过程中,比如:语法报错等等。

  2. 不采用共同的数据格式,那就只能采用传输源码的方式,亦或是目前JSPatch或者Mango对源码进行RSA加密的方式。1. 不加密:补丁源码泄漏,安全风险太高, 2. RSA加密的方式:性能拉垮,破解得到源码不难,3. 不论加密或者不加密,源码增加后,数据量大小的增量更大。

  3. 针对编译器前端的优化,不必每次都更新 OCRunner 解释器。

  4. 各自更清晰的职责划分。

  5. 作者太菜,对写出的 oc2mangoLib 没什么自信😂。

以采用 Json 补丁为中间数据格式为例,此时我们的整个流程如下:

Objective-C -> 抽象语法树 -> Json补丁 -> 抽象语法树 -> 解释执行语法树

  1. Objective-C -> 抽象语法树: oc2mangoLib

  2. 抽象语法树 -> Json补丁: ORPatchFile

  3. Json补丁 -> 抽象语法树: ORPatchFile

  4. 抽象语法树 -> 解释执行语法树: OCRunner

OCRunner

最后再(啰嗦)介绍一下 OCRunner:

首先 PatchGenerator[11] 使用 oc2mangoLib 将 Objective-C 源码生成语法树,随后使用 ORPatchFile 将语法树序列化为补丁文件,最后 OCRunner 使用 OCPatchFile 将补丁反序列化为语法树后解释执行语法树。

优点:

  • 前后端分离:PatchGenerator 生成补丁,OCRunner 解释执行补丁。

  • 补丁文件大小:相对于传输脚本文件,二进制补丁的大小拥有更好的表现。

  • 完善的 Objective-C 语法支持:结构体,Protocol,枚举,C 函数声明即链接系统函数,多参数调用等。

  • 可选的自定义 Arm64 ABI :基于 Arm64 过程调用规约和 iOS TypeEncode 完成。

性能:

设备: iPhone SE 2,iOS 14.3

在求斐波那契数列第25项的测试中:

  • JSPatch: 执行时间,平均为 0.169 s。内存占用一直稳定在 12 MB 左右。

  • OCRunner: 执行时间,平均为 1.05 s。内存占用,峰值为 60 MB 左右,其他稳定在 12 MB 左右。

  • Mango: 执行时间,平均时间为 2.38 s。内存占用,持续走高,最高的时候大约为 350 MB。

结论:

目前递归方法调用的速度,大约为JSPatch的1/5倍,为MangoFix的2.5倍左右。

问题:

关于递归方法调用时的内存占用,目前存在占用过大的问题。求斐波那契数列数列第30项的时候,OCRunner内存峰值占用大概在600MB。

Hook Objective-C 方法

  • JSPatch ,通过将类的目标方法替换为 objc_msgForward,同时将 forwardInvocation: 方法的 IMP 替换为 JPForwardInvocation 函数,当目标方法被调用时触发消息转发,在 JPForwardInvocation 函数中获取 NSInvocation 的各个参数值,再使用 JavaScriptCore 调用相应的函数,再将得到的结果设置到 NSInvocation 的返回值。

  • OCRunner(MangoFix),通过直接将类的目标方法的 IMP 直接替换为使用 libffi 注册的 methodIMP 函数,通过 class 和 SEL 从 OCRunner 的方法注册表中获取到 ORMethodImplementation ,将它解释执行,获取到结果后,再写入 ret 指针中。

项目整体图

最后放一张项目的整体架构图,方便大家理解

OCRunner 第零篇:从零教你写一个 iOS 热修复框架

oc2mango简介

One More Thing

这是一个系列文章,感兴趣的可以关注一下「老司机技术周报」公众号,后续文章都会在公众号中发布

关注我们

我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。

OCRunner 第零篇:从零教你写一个 iOS 热修复框架

关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。

后续规划

OCRunner 第一篇:实现一个简单版 OCRunner

在正文开始前,我们先看看一个语法树解释执行.gif,OCRunner的核心解释执行部分也是如此。

OCRunner 第零篇:从零教你写一个 iOS 热修复框架

本图出自: 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩[12]

如果你让我仅仅只靠文字就能给你解释清楚 OCRunner 是怎么运行起来的,你们可能是在为难我小蒋,肚子里墨水真的没那么多啊,太难了😂。所以在这一小节,我给大家准备了一个简单版 OCRunner,希望大家能从这个例子中,真正知道它是如何运行的。

希望你们看见项目中的 SingleEngine 类不要笑(哈哈哈),我只是想表达 单缸发动机 的意思 - _ -,我心爱的小摩托就是单缸拖拉机。

OCRunner 第二篇:二进制补丁文件的实现

以上,作者正在快马加鞭打磨中.....

参考资料

[1]

JSPatch: https://github.com/bang590/JSPatch

[2]

TTPatch: https://github.com/yangyangFeng/TTPatch

[3]

OCEval: https://github.com/lilidan/OCEval

[4]

lilidan: https://github.com/lilidan

[5]

FunctionSearch的实现: https://github.com/lilidan/OCEval/blob/master/OCEval/helper/FuntionSearch.c

[6]

DynamicOC: https://github.com/letqingbin/DynamicOC

[7]

Mango: https://github.com/YPLiang19/Mango

[8]

OCRunner: https://github.com/SilverFruity/OCRunner

[9]

Mango: https://github.com/YPLiang19/Mango

[10]

OCRunner:完全体的iOS热修复方案: https://silverfruity.github.io/2020/09/04/OCRunner/

[11]

PatchGenerator: https://github.com/SilverFruity/oc2mango

[12]

本图出自: 虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩: https://www.iteye.com/blog/rednaxelafx-492667

本文分享自微信公众号 - 老司机技术周报(LSJCoding)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
3年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
9个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这