一、消息发送的完整路径
在Objective-C中,方法的调用并非简单的函数跳转,而是一条经过精心设计的消息传递链路。理解这条链路,是掌握Runtime、解决疑难Bug、以及设计高效Hook机制的前提。
1.1 消息发送的汇编级追踪
// objc_msgSend的C签名(其实质是一个汇编函数)
// id objc_msgSend(id self, SEL op, ...);
// 消息发送的完整调用栈(从OC到IMP的路径)
//
// objc_msgSend(receiver, @selector(foo))
// ├── [0] Check nil receiver(快速路径:nil直接返回)
// ├── [1] CacheLookup(查找方法缓存)
// │ └── 命中 → 直接跳转执行
// ├── [2] _objc_msgSend_uncached(缓存未命中)
// │ └── _class_lookupMethodAndLoadCache3
// │ ├── lookupMethodForClass(从类的方法列表遍历)
// │ └── 找到 → 写入缓存 → 跳转执行
// │ └── 未找到 → 进入消息转发流程
// └── [3] Method dispatch → IMP
// 为什么objc_msgSend要用汇编实现?
// ① 方法查找需要:self寄存器 + 方法选择器寄存器
// ② 多返回值约定:C函数无法同时返回两个值(IMP + 实现地址)
// ③ 行内缓存写入:避免函数调用开销(内联展开)
// ④ SIMD/浮点协议:不影响浮点寄存器状态
// 汇编实现(arm64简化版):
// ENTRY _objc_msgSend
// ldp x0, x1, [x0] // x0=receiver, x1=SEL
// and x1, x1, #ISA_MASK // x1=isa
// LLookupImpCyclic:
// sub x2, x1, #_CACHE // x2=cache
// ldp x3, x4, [x2, #CACHE_MASK] // x3=buckets, x4=occupied
// LSearchBucket:
// cmp x3, x1 // 对比SEL
// b.eq LFound // 找到则跳转执行
// sub x3, x3, #16 // 移动到下一个bucket
// sub x4, x4, #1 // 计数器-1
// cbnz x4, LSearchBucket // 不为0继续搜索
// b LMiss // 缓存miss
二、方法缓存机制与缓存污染
2.1 cache_t 的数据结构
// objc-cache.h 核心结构
struct cache_t {
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<uint16_t> _flags;
uint16_t _occupied;
};
explicit_atomic<uintptr_t> _maybeMask;
};
};
// _bucketsAndMaybeMask 的位布局(arm64):
// 高位存buckets指针,低位存maybeMask(哈希掩码)
// buckets数组:每个bucket = { IMP, SEL }
// _occupied:当前已填充的bucket数量
// 缓存查找算法(开放寻址 + 线性探测)
static inline mask_t cache_t::mask()
{
return _maybeMask.load(memory_order_relaxed);
}
static inline bucket_t * cache_t::buckets()
{
uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
return (bucket_t *)(addr & ~mask());
}
// 哈希选择器:
// static inline uint32_t hash(SEL sel, mask_t mask) {
// uintptr_t s = (uintptr_t)sel;
// return ((s >> 2) ^ (s >> 0)) & mask;
// }
// 缓存未命中时的填充(写入IMP和SEL)
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
// ① 扩容条件:_occupied >= capacity * 3/4
// ② 插入:直接覆盖已有slot(不考虑冲突)
// ③ 标记:_occupied++
}
// ⚠️ 缓存污染的典型场景
// 1. 大量不同SEL写入 → 缓存频繁失效
// 2. 类簇(Class Cluster):[NSArray array] 返回 __NSArrayI vs __NSArrayM
// 两者各自维护独立缓存,消息传递路径不同
// 3. dealloc中向已释放对象发消息 → EXC_BAD_ACCESS
三、消息转发的完整四步流程
3.1 转发流程图
// 消息转发的四个阶段(按顺序依次尝试)
//
// ┌─────────────────────────────────────────────┐
// │ Step 1: +resolveInstanceMethod │
// │ (+resolveClassMethod for class m) │
// │ 用途:动态添加方法实现 │
// │ 返回:YES表示已处理,NO则进入Step 2 │
// └─────────────────────────────────────────────┘
// ↓
// ┌─────────────────────────────────────────────┐
// │ Step 2: -forwardingTargetForSelector │
// │ 用途:更换消息接收者(快速转发) │
// │ 返回:nil则进入Step 3 │
// └─────────────────────────────────────────────┘
// ↓
// ┌─────────────────────────────────────────────┐
// │ Step 3: -methodSignatureForSelector │
// │ +instanceMethods, +classMethods │
// │ 用途:获取方法签名(为Step 4准备) │
// │ 返回:nil则抛出异常 │
// └─────────────────────────────────────────────┘
// ↓
// ┌─────────────────────────────────────────────┐
// │ Step 4: -forwardInvocation: │
// │ NSInvocation包装的消息 │
// │ 用途:完全控制消息处理逻辑 │
// └─────────────────────────────────────────────┘
3.2 各阶段实战代码
// 示例:实现一个"空响应"对象(类似null object pattern)
@interface NullHandler : NSObject
@end
@implementation NullHandler
// Step 1:动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 方式一:直接添加IMP(处理getter/setter)
if (sel == @selector(getValue)) {
class_addMethod([self class], sel,
imp_implementationWithBlock(^(id self) {
return @(-1); // 返回默认值
}),
"@@:"
);
return YES;
}
return [super resolveInstanceMethod:sel];
}
// Step 2:快速转发(更换接收者)
- (id)forwardingTargetForSelector:(SEL)aSelector {
// 将消息转发给另一个对象
// 适用场景:委托模式、跨线程消息转发
if ([self.target respondsToSelector:aSelector]) {
return self.target; // nil表示不需要转发
}
return nil; // 进入Step 3
}
// Step 3:获取方法签名(必须精确)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
// 如果目标是NSObject,签名查询可以简单处理
NSMethodSignature *sig = [super methodSignatureForSelector:sel];
if (!sig) {
// 手动构造签名:返回类型@,参数类型v@:
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return sig;
}
// Step 4:完整转发(最灵活,控制力最强)
- (void)forwardInvocation:(NSInvocation *)invocation {
// 方案一:转发给其他对象(可多个)
if ([self.target respondsToSelector:invocation.selector]) {
[invocation invokeWithTarget:self.target];
}
// 方案二:日志记录未处理消息(调试利器)
NSLog(@"[NullHandler] 未处理消息: %@ %@",
NSStringFromSelector(invocation.selector),
invocation.target);
// 方案三:静默吞掉(完全不处理)
// 不调用invoke,不会崩溃
}
@end
四、消息转发的性能代价与优化策略
// 性能对比:直接调用 vs 缓存命中 vs 完整转发
// 测试基准:MacBook Pro M1,百万次调用平均值
//
// 方法调用类型 耗时(纳秒/次)
// ─────────────────────────────────────
// 缓存命中 (inline cache) ~1-2ns
// 完整消息转发 (4步) ~800-2000ns ⚠️
// objc_msgSendSlowMiss ~200-400ns
//
// 结论:完整转发代价是直接调用的 400-2000 倍
// ⚡ 优化策略一:缓存已转发的方法签名
static NSMutableDictionary *forwardedSelectorsCache;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
@synchronized (forwardedSelectorsCache) {
if (forwardedSelectorsCache[@selector]) {
return forwardedSelectorsCache[@selector];
}
}
NSMethodSignature *sig = [super methodSignatureForSelector:sel];
@synchronized (forwardedSelectorsCache) {
forwardedSelectorsCache[@selector] = sig ?: [NSNull null];
}
return sig;
}
// ⚡ 优化策略二:避免在forwardInvocation中创建大对象
// ❌ 差:每次转发都创建NSData
- (void)forwardInvocation:(NSInvocation *)inv {
self.largeData = [[NSData alloc] initWithContentsOfFile:path]; // 每次都分配
}
// ✅ 好:懒加载 + 复用
- (NSData *)lazyLargeData {
if (!_largeData) _largeData = [[NSData alloc] initWithContentsOfFile:path];
return _largeData;
}
// ⚡ 优化策略三:批量转发(装饰器模式)
// 适用于:多个对象需要相同转发逻辑
@interface ForwardingDecorator : NSObject
@property (nonatomic, strong) NSObject *target;
@property (nonatomic, strong) NSArray *handledSelectors;
- (instancetype)initWithTarget:(NSObject *)target
handledSelectors:(NSArray *)sels;
@end
@implementation ForwardingDecorator
- (BOOL)respondsToSelector:(SEL)aSelector {
return [self.handledSelectors containsObject:NSStringFromSelector(aSelector)]
|| [super respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.handledSelectors containsObject:NSStringFromSelector(aSelector)]) {
return self.target;
}
return nil;
}
@end
五、消息转发的安全与逆向应用
// 🔐 场景一:防止被Hook(应用加固)
// 原理:Method Swizzling利用消息转发机制拦截调用
// 对策:直接替换IMP,绕过消息转发
// ❌ 易被Hook的方式(走消息转发)
- (void)secureOperation {
// 这里可以被swizzling拦截
}
// ✅ 难被Hook的方式(直接IMP)
// 通过clang directive强制内联或直接调用C函数
__attribute__((noinline))
void secure_operation_impl() {
// 直接机器码执行,不经过objc_msgSend
}
// 🔍 场景二:逆向工程中的消息转发利用
// 利用forwardInvocation实现Method Swizzling
@interface MethodSwizzler : NSObject
+ (void)swizzleMethod:(SEL)originalSel
withMethod:(SEL)swizzledSel
inClass:(Class)cls;
@end
@implementation MethodSwizzler
+ (void)swizzleMethod:(SEL)originalSel
withMethod:(SEL)swizzledSel
inClass:(Class)cls {
Method original = class_getInstanceMethod(cls, originalSel);
Method swizzled = class_getInstanceMethod(cls, swizzledSel);
// addMethod:即使原方法不存在也添加成功(返回YES)
// 这样可以给category添加IMP
BOOL didAdd = class_addMethod(cls, originalSel,
method_getImplementation(swizzled),
method_getTypeEncoding(swizzled));
if (didAdd) {
class_replaceMethod(cls, swizzledSel,
method_getImplementation(original),
method_getTypeEncoding(original));
} else {
// 原方法存在,直接交换
method_exchangeImplementations(original, swizzled);
}
}
@end