自研 iOS 热更新机制——OCPack技术方案总结

2020-10-22  本文已影响0人  iOSWater

https://juejin.im/post/6884833291740905480?utm_source=gold_browser_extension

I. 方案简介

OCPack是一种 iOS 平台上 App 动态化技术方案,用户可以使用 Objective-C 语言编写待动态化的功能逻辑(生成.m文件),然后通过OCPack提供的工具链生成 patch 文件(.bin格式)。客户端则内置了一个基于 Native 环境的的虚拟栈机,它可以动态加载并执行存储在客户端的 patch 文件中的方法。Patch 文件可根据业务需要随时下载、更新并由虚拟机重新加载、运行。

此方案的主要优点:

II. 技术方案

OCPack复用 clang 前端来分析目标 Objective-C 代码的语法树,通过自定义 ASTFrontendAction 来遍历语法树,生成自定义指令集的汇编程序。在客户端,由自研的虚拟栈机来解释执行汇编程序中的二进制指令。

生成 patch 文件的基本数据流程是:

注:OCPack自定义的栈机汇编指令主要有67条,除基本指令以外,主要是依据语法树结点类型设计。

在运行时,客户端内置的虚拟栈机能够根据用户需求加载指定的 patch 文件,然后就可以执行其中任意方法了。

以下分模块来介绍主要技术点:

编译模块:

功能:Objective-C程序(.m) -> 语法树 -> 汇编程序(.s)

1. 独立的编译程序

主要使用 clang 的 libTooling 接口,实现了 AST FrontendAction,通过实现自定义的 ASTConsumer 递归遍历语法树,对不同的节点类型作相应处理,生成可执行的汇编指令程序。

:为了进一步提高开发效率,OCPack还实现了一个独立的 clang plugin,可以通过给工程添加一个 .xcconfig 文件(替换默认的 clang),实现在 Xcode 中显示相关的编译错误,并能一键生成 .bin文件,省去了获取编译选项和手工查看错误日志的步骤,简化了开发流程。

2. 栈机汇编指令集

为了连接包含有 Objective-C 代码逻辑的语法树和客户端运行的虚拟机,OCPack需要定义一套比较完整的汇编指令集。该指令集应该满足以下两个条件:

以下简要介绍几个比较典型的指令的设计方案:

2.1 push 和 pop 指令

栈机中最基本的部件是操作栈,用于存放正在进行中的操作数和操作结果。如:要计算 1 + 2,栈机需要执行类似以下指令:

    push instant 1
    push instant 2
    add
复制代码

先将两个操作数12依次 push 进操作栈,再执行 add 操作。add 操作负责先 pop 对应的操作数,经过加法计算后再将结果 push 进栈。以上指令执行完后,操作栈顶存放的就是操作结果3

但只有操作栈是不够的,程序逻辑的复杂性要求像局部变量、方法参数等数据拥有确定不变的内存地址,因此OCPack将局部变量、静态变量、常量、指针、立即数等分别对应一个段,每种类型的变量对应于所属段中的一个序号(index)。

在对语法树进行遍历的过程中,OCPack编译器会维护一个符号表,对每个变量声明(VarDecl)建立相应的符号表项,存放其段名和序号(index)。

对语法树中的变量引用(VarDeclRef 结点),OCPack编译器会找到其相应的 VarDecl 的符号表项,生成相应的 push、pop 指令。

push和pop指令的参数就是段名和序号(index):

2.2 prolog 指令

方法调用和传参这块的设计需要一些特别的考虑,主要需要满足几个要求:

为了满足这些需求,OCPack中设计了 prolog 这一指令:

2.3 ret 指令

此处有一次数据拷贝,拷贝大小即为返回值的大小。为尽量减少对调用者的影响,在编译期给 ret 方法增加 retSize 参数,以便在执行 ret 的时候就能完成数据拷贝,栈帧回退到调用者后,调用者可以预期返回值就在自己操作栈的栈顶,后续逻辑不受当前栈顶值是由方法调用返回还是自行 push 得到的影响,逻辑较清晰。

2.4 跳转指令

为了实现条件判断 if/else 和 for 循环等流程控制语法,OCPack指令集定义了jmpjmp_if指令,根据语法树中对应类型的节点具体情况,生成相应的跳转指令和跳转 label。这些文本跳转 label 会被存储在 .s 文件中,然后在下一阶段(Assembler 将 .s 转换为 .bin时)被替换成相应的偏移地址。

2.5 switch指令
1) switch跳转表

switch需要运行时决定跳到哪个 case label 对应的地址,只用jmp_if需要在 case列表代码尾部插入多条比较语句,而栈机又需要每次比较前都push相应的参数,实现比较繁琐而且性能较差,因此OCPack在指令集中增加了cmp_nresolve_labeljmp_tos指令。

2) continue和break的支持:

分别维护一个 break和 continue 的 label 栈,栈顶元素为当前 break 或 continue 调用时应该 jmp 到的目标 label,在目标表达式开始和结束时进行入栈和出栈操作。在遇到语法树上结点为 break 或 continue 时,取出当前栈顶的目标 label,生成jmp 目标label指令。

2.6 call指令
2.7 基本一元、二元运算符指令

注: 此指令只支持整型、浮点型等基本数据类型的运算,不支持自定义类型重载的运算符

2.8 左右值转换

注: 在实现初期,OCPack的 push 指令是直接将 seg 段 index 处变量的右值 push 进操作栈(即这种情况下忽略左右值转换的结点),但后来发现在类似赋值操作中的左值变量的情况下,AST 中没有左右值转换结点,如果对这些情况特殊处理,逻辑会变得较为复杂且难以保证覆盖完全,后来决定完全依照 AST 中结点的排布逻辑,将 push 操作的对象改成了对应变量的左值,牺牲部分性能换取程序的可靠性。

2.9 Objective-C 方法调用指令

注: 实现过程中,Objective-C 调用指令所需的输入数据的内存排布顺序也经历了一番修改。因为对于 Objective-C 方法来说,只有拿到 selector 才能知道具体有多少个参数,所以之前设计是参数表倒着放,即第一个参数放栈顶,第二个参数依次往下排。这样可以稳定地 pop 两次就得到 selector 的声明,然后再根据 selector 中指明的参数个数及大小 pop 所有的参数。但这种方法在参数大小大于 64 bit 的情况下(如 struct)就比较难处理了,因为要得到正确的 struct 数据,程序需要 pop 对应个数的64 bit,然后做拼接,烦琐而且容易出错。经权衡,还是在指令参数中增加了参数表长度(编译期得到),在调用 OBJC_MSG_CLASS/OBJC_MSG_INST 指令前,参数还是按顺序push(即第一个参数先 push,栈顶是最后一个参数),在指令的实现中,根据指令参数中提供的参数表长度,直接从 sp 算出第一个参数的起始位置,这样所有的参数都可以用指针访问,而不用关心其大小了。原先需要的多次 pop 指令,变成只需在指令退出前,将 sp 回退参数表长度即可。

汇编模块

功能:汇编程序(.s) -> 二进制补丁程序(.bin)

解析整个 .s 文本,将文本 token 转换为对应的二进制数据,主要包括:

转换完成后将各数据存入内存中相应的数据段,再将整个内存 dump 成一个二进制文件。

注: 二进程文件在运行时所需的大部分数据其排布都与 .bin 文件里的排布完全相同,这样能方便地使用内存映射来实现 .bin 文件的加载,从而可以减少私有内存的占用量。

加载模块

功能:二进制补丁程序(.bin)加载

在调用 load_image 时,虚拟机会先将 .bin 文件 mmap 到一段内存中,检测 magic number, bin version 及 arch 是否匹配。

然后按全局区的大小申请一段 shared anonymous mmap 内存。

然后分别加载各个数据段,建立必要的运行时内存数据,主要的数据段包括:

注:bin文件具体格式如下: [图片上传中...(image-1b9c27-1603349931409-1)]

执行模块

功能:二进制补丁程序(.bin)的执行

1) 虚拟机基本信息

注:VM 函数栈帧具体内存布局如下:

[图片上传中...(image-370312-1603349931409-0)]

2) Objective-C 调用虚拟机方法

3) 虚拟机调用 OC 方法(f1),f1 又调用到了虚拟机的方法(f2)

要支持此流程,需保证 f2 调用完成后虚拟机当前栈帧的 sp 与调用前完全一致,以保证 f1 的执行不受影响。

4) 虚拟机方法间互相调用

在调用OC方法时,会先检测对应的方法是否在导出函数表中,如果在,则走此流程。这也要求调用虚拟机方法时的参数表应该与直接调用 OC 方法是一致的,否则还需要重新拷贝参数做适配,降低虚拟机性能。

5) 多线程支持

6) 内存占用

7) 崩溃时的栈回溯

8) 崩溃符号解析

OCPack编译器生成或指定一个 GUID,后续生成的所有相关文件(包括.s、.sym、.bin以及运行时生成的崩溃 log)中都存有此 GUID。线上的崩溃日志发送回来后,崩溃解析服务器能够根据日志中的 GUID 查找到相应的符号文件进行符号解析。同时构建服务器数据库中存储了对应 GUID 的 bin 文件打包时所有依赖项的源信息(包括对应的 .s 文件、bin 代码对应的源代码版本、OCPack工具链的版本等),方便开发重现、定位相关问题。

9) Hook Objective-C 方法

性能优化

  1. 二进制程序的大小优化
  1. 性能优化

III. 未来计划

链接器

其他语法支持

性能优化

Debug工具

作者:zhangjiezhi_
链接:https://juejin.im/post/6884833291740905480
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上一篇下一篇

猜你喜欢

热点阅读