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
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看下它的汇编:
(更多…)
JSPatchX 是 JSPatch 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 动态性太强大,导致在这么封闭的情况下民间还是可以做出各种插件,其核心开发方式就是:
- dump 出 Xcode 所有头文件,知道 Xcode 里有哪些类和接口。
- 通过头文件方法名猜测方法的作用,swizzle 这些方法,插入自己的代码实现插件逻辑。
- 通过 NSNotificationCenter 监听各种事件的发生。
更详细的开发教程网上有不少文章,有兴趣的自行搜索吧。
起步
对于实现 JS 代码补全这个功能来说,主要分三步:
- 在编辑 JS 文件时开启代码补全功能。
- 找到用户输入代码时的回调,按 Xcode 要求组装代码补全对象数组返回。
- 根据已输入文字对补全对象数组进行过滤
第一步是通过替换 DVTTextCompletionDataSource
类里的 -strategies 方法,在源文件是 JS 时生成一个 IDEIndexCompletionStrategy
对象返回,就可以针对 JS 文件走代码补全逻辑了。
第二步是替换 IDEIndexCompletionStrategy
的 - completionItemsForDocumentLocation:context:highlyLikelyCompletionItems:areDefinitive:
方法,这个方法会在用户输入时被调用,在这里组装好应该出现的补全对象(IDEIndexCompletionItem
) 列表返回,Xcode 就会自动应用返回的 items 对输入进行补全。
第三步是在 DVTTextCompletionSession
的 -_setFilteringPrefix:forceFilter:
方法,针对第二步返回的 item 对象根据输入进行过滤。
要让自动补全插件程序跑通,只需实现上述三步。显然核心在第二步如何组装合适的补全对象 completionItem。代码里我们新增了一个 IDEIndexCompletionItem
的子类 JPCompletionItem
去表示,下面统一把这个补全对象称为 completionItem。接下来的问题就是怎样组装这些 completionItem。
实现
先看看我们需要哪些自动补全,概括起来有几种:
- 可能会被调用到的 OC 方法名
- JS 上新增的方法名,以及出现的类名
- JSPatch 自身的一些关键字接口,如 defineClass, require 等
- 当前 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 文件,这里的解析比较简单,没有用词法语法解析器,而是直接通过正则匹配取出需要的内容,这里通过正则提取了:
- require() 里的 className
- defineClass() 里的 className
- defineClass() 里所有的方法名
- 文件里所有 keyword
同样每个 JS 文件都会生成一个 JPJSFile 对象,包含了上述提取的元素,并生成和保存了方法名和keyword对应的 completionItems列表。
组装和缓存
解决了单个 OC / JS 文件的解析,接着就是决定解析哪些文件,以什么样的形式缓存和组装返回给XCode。
JPObjcIndex
先看看 OC 的解析,JPObjcIndex 负责 OC 头文件的解析,JS 可能调用到的 OC 代码只存在于两个地方,一是系统framework,二是项目里的代码,对这些 OC 头文件要以什么样的方式解析呢?这里有两个选择:
- 只解析与当前编辑的 JS 文件相关的 OC 头文件
- 一次性把所有文件都解析好,再进行筛选
若要用方案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 做了以下这些事:
- 解析所有 JS 文件,生成一个个对应的 JPJSFile 对象,缓存起来。
- 取出每个 JPJSFile 里解析的
require()
以及defineClass()
的 className,去 JPObjcIndex 取这些 Class 对应的 completionItems。 - 取出每个 JPJSFile 解析好的 method completionItems。
- 取出每个 JPJSFile 解析好的 keyword completionItems。
- 本地 keyword.plist 定义了 JSPatch 常用的一些自动补全关键字,例如 defineClass, CGRect 等,在这里取出这些数据并生成 completionItems。
- 把 2-5 步里的 completionItems 分成两种类型,keyword 类型和 method 类型,缓存起来并返回给 XCode。
- 当有 JS 文件保存时,重新对这个文件生成 JPJSFile 对象,并重做 2-6 步。
第6步分出来的两种类型应用于两种场景,method 类型会在 JS 输入 .
要进行方法调用时出现,这个类型里所有的 completionItem 都是方法,包括 OC 头文件定义的方法以及 JS 里解析的方法。keyword 类型则是其他的像类名/语句关键字等这些非方法,在平常输入中出现。
在没有 JS 文件保存时,用户编辑 JS 代码每一次输入走到补全逻辑时,JPJSIndex 都是直接返回内存里已组装好的 completionItems 列表,没有其他操作,提高操作性能。第7步虽然在有文件保存时重新做了 2-6 步对数据进行重新组装,但这个过程不涉及文件解析,只需要取内存里解析好的数据进行组装,并且文件保存不会那么频繁,所以性能上没有太大问题。
整个流程就是这样,实际上很简单,总结起来就是解析所有 OC 头文件,解析所有 JS 文件,组装并缓存好 completionItem 返回。
不足
做这个项目的想法是先用最简单的方式快速做出来,满足80%的需求,导致会有一些不足,例如
- 没有做 JS 语法解析,没有做细致的筛选规则,粗暴地全部提示。
- 没有补全 include 的其他 JS 文件里的全局变量。
- defineClass 里写定义方法时,若要覆盖 OC 原有方法,没有方法名补全(因为方法名只有在
.
后才有补全) - 没有加上除了 Foundation 和 UIKit 以外的 framework。
欢迎一起改进 JSPatchX,完善这些不足~
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 寻找这些方法调用。
JSPatch 在社区的推动下不断在优化改善,这篇文章总结下这几个月以来 JSPatch 的一些新特性,以及它们的实现原理。包括脱离锁的 performSelectorInOC 接口,支持可变参数方法调用,给新增方法指定类型的 defineProtocol 接口,支持重写 dealloc 方法,以及两个扩展 JPCleaner 和 JPLoader。
performSelectorInOC
JavaScript 语言是单线程的,在 OC 使用 JavaScriptCore 引擎执行 JS 代码时,会对 JS 代码块加锁,保证同个 JSContext 下的 JS 代码都是顺序执行。所以调用 JSPatch 替换的方法,以及在 JSPatch 里调用 OC 方法,都会在这个锁里执行,这导致三个问题:
- JSPatch替换的方法无法并行执行,如果如果主线程和子线程同时运行了 JSPatch 替换的方法,这些方法的执行都会顺序排队,主线程会等待子线程的方法执行完后再执行,如果子线程方法耗时长,主线程会等很久,卡住主线程。
- 某种情况下,JavaScriptCore 的锁与 OC 代码上的锁混合时,会产生死锁。
- UIWebView 的初始化会与 JavaScriptCore 冲突。若在 JavaScriptCore 的锁里(第一次)初始化 UIWebView 会导致 webview 无法解析页面。
简介
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,生成的程序可以在 [这里] 看到。官方文档有生成的流程和使用方法,可以自己试下。
(更多…)
注:本文较早撰写,随着 JSPatch 的改进,有些内容已与最新代码对不上,建议转看重新整理后的JSPatch实现原理详解。
距离上次写的<JSPatch实现原理详解>有一个月的时间,在这段时间里 JSPatch 在不断地完善和改进,代码已经有很多变化,有一些修改值得写一下,作为上一篇的补充。
Special Struct
先说下 _objc_msgForward,在上一篇提到为了让替换的方法走 forwardInvocation,把它指向一个不存在的 IMP: class_getMethodImplementation(cls, @selector(__JPNONImplementSelector)),实际上这样实现是多余的,若 class_getMethodImplementation 找不到 class / selector 对应的 IMP,会返回 _objc_msgForward 这个 IMP,所以更直接的方式是把要替换的方法都指向 _objc_msgForward,省去查找方法的时间。
接着出现另一个问题,如果替换方法的返回值是某些 struct,使用 _objc_msgForward(或者之前的 @selector(__JPNONImplementSelector))会 crash。几经辗转,找到了解决方法:对于某些架构某些 struct,必须使用 _objc_msgForward_stret 代替 _objc_msgForward。为什么要用 _objc_msgForward_stret 呢,找到一篇说明 objc_msgSend_stret 和 objc_msgSend 区别的文章),说得比较清楚,原理是一样的,是C的一些底层机制的原因,简单复述一下:
大多数CPU在执行C函数时会把前几个参数放进寄存器里,对 obj_msgSend 来说前两个参数固定是 self / _cmd,它们会放在寄存器上,在最后执行完后返回值也会保存在寄存器上,取这个寄存器的值就是返回值:
-(int) method:(id)arg; r3 = self r4 = _cmd, @selector(method:) r5 = arg (on exit) r3 = returned int
普通的返回值(int/pointer)很小,放在寄存器上没问题,但有些 struct 是很大的,寄存器放不下,所以要用另一种方式,在一开始申请一段内存,把指针保存在寄存器上,返回值往这个指针指向的内存写数据,所以寄存器要腾出一个位置放这个指针,self / _cmd 在寄存器的位置就变了:
-(struct st) method:(id)arg; r3 = &struct_var (in caller's stack frame) r4 = self r5 = _cmd, @selector(method:) r6 = arg (on exit) return value written into struct_var
objc_msgSend 不知道 self / _cmd 的位置变了,所以要用另一个方法 objc_msgSend_stret 代替。原理大概就是这样。
上面说某些架构某些 struct 有问题,那具体是哪些呢?iOS 架构中非 arm64 的都有这问题,而怎样的 struct 需要走上述流程用 xxx_stret 代替原方法则没有明确的规则,OC 也没有提供接口,只有在一个奇葩的接口上透露了这个天机,于是有这样一个神奇的判断:
if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound)
在 NSMethodSignature 的 debugDescription 上打出了是否 special struct,只能通过这字符串判断。所以最终的处理是,在非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否则走 _objc_msgForward。