在处理高频、大量的 Objective-C 方法调用时,单纯猜测 objc_msgSend 是瓶颈往往是不准确的。我们需要通过定量测量、缓存命中率分析以及汇编级追踪来确定瓶颈所在。
以下是针对消息发送性能分析的专业步骤:
1. 使用 Time Profiler 定位热点
Xcode 里的 Instruments - Time Profiler 是第一步。
-
如何分析:
- 启动 Time Profiler 并运行你的性能敏感路径(如列表快速滚动或大数据处理)。
- 查看 Call Tree,寻找耗时占比高的函数。
- 如果你发现
objc_msgSend或lookUpImpOrForward频繁出现在顶层,这说明查找成本已经很高了。 - 关注 CPU 占用:如果某个方法自身的逻辑很简单(比如只是返回一个变量),但其调用耗时在采样中占比很大,说明发送消息的开销已经盖过了逻辑本身的开销。
2. 测量缓存命中率
objc_msgSend 的性能高度依赖 cache_t。如果发生缓存抖动(Cache Thrashing) ,性能会断崖式下跌。
-
分析手段:
- Runtime 监控:虽然生产环境难以直接获取,但在开发调试阶段,你可以利用底层工具查看类缓存的大小和填充度。
- 埋点分析:通过
_objc_registerMethodSignature相关的钩子或 DTrace 脚本,观察lookUpImpOrForward(慢速路径入口)的调用频率。 - 逻辑推断:如果一个对象拥有成百上千个方法,且在一个短周期内被随机调用了大量不同的 Selector,缓存会频繁扩容、清空,导致性能损耗。
3. 统计消息发送次数 (LogObjcCalls)
Objective-C 运行时提供了一个隐藏的调试功能,可以记录所有的消息发送。
-
开启方式:
在代码中调用外部函数
instrumnetObjcMessageSends(YES)或在 LLDB 中执行:Bash
expr (void)instrumentObjcMessageSends(YES) -
分析结果:
日志会记录在
/private/tmp/msgSends-xxxx中。通过分析这个文件,你可以看到:- 哪些 Selector 被调用最频繁?
- 哪些 Class 是消息发送的大户?
- 是否存在不必要的重复调用(如在循环内反复请求同一个属性的 Getter)?
4. 汇编级别的代价估算
对于极端的性能优化(如音频处理或物理引擎),你需要对比 Static Call 与 Dynamic Call 的比例。
-
指令分析:
使用汇编视图查看你的关键路径:
- 静态调用:只有一条
bl指令(分支跳转)。 - 消息发送:包含
mov参数、adrp加载 Selector、最后bl _objc_msgSend。
- 静态调用:只有一条
-
结论:如果你的代码段中
objc_msgSend密集成堆,且 CPU 的循环计数器显示这些调用占据了大部分周期,那么消息发送就是明确的瓶颈。
5. 常见的瓶颈诱因与特征
| 瓶颈特征 | 潜在原因 | 表现 |
|---|---|---|
| 慢速路径耗时高 | 继承链过深 | 频繁在 lookUpImpOrForward 中递归查找父类。 |
| 转发成本高 | 滥用 NSInvocation | forwardInvocation: 占用大量 CPU,伴随大量内存分配。 |
| 属性访问开销 | 滥用 atomic | 每次消息发送内部都伴随着 os_unfair_lock 的加锁动作。 |
| 缓存频繁失效 | 动态添加/交换方法 | 调用 method_exchangeImplementations 会导致该类的缓存被清空。 |
💡 优化实验建议
如果你怀疑 objc_msgSend 是元凶,可以尝试进行 A/B 测试:
将一段高频调用的 Objective-C 方法替换为 C 函数 或 Swift 静态方法(去掉 @objc)。如果重构后该路径的耗时下降了 30%~50% ,则证实了消息发送确实是瓶颈。