iOS开发基础18-深入理解 Objective-C Runtime 机制

Objective-C Runtime 深度解析:数据结构、消息发送与动态特性

Objective-C 是基于 C 语言、加入面向对象特性和消息传递机制的动态语言。Runtime 是其核心,负责方法调用、动态类型检查、方法解析与转发等。本文基于 objc4 开源代码,全面解析 Runtime 的数据结构、消息流程和高级功能。


一、核心数据结构

1. SEL — 方法选择器

SEL 是表示方法选择器的类型,本质是映射到方法名的唯一字符串指针:

typedef struct objc_selector *SEL;

获取方式:

SEL sel1 = @selector(methodName);          // 编译器生成
SEL sel2 = sel_registerName("methodName"); // Runtime 函数注册

转换为字符串:

NSString *name = NSStringFromSelector(sel);

同一进程中,相同方法名的 SEL 是同一个指针(唯一且不可变),因此比较方法名应使用 sel1 == sel2 而非字符串比较。

2. id — 通用对象指针

id 是通用类型指针,可表示任意 Objective-C 对象:

typedef struct objc_object *id;

objc_object 结构体的核心是 isa 指针:

struct objc_object {
    Class isa;  // 指向对象所属的类
};

通过 isa 指针可以找到类对象,进而获得方法列表、实例变量列表等信息。

arm64 下的 nonpointer isa:在 64 位架构中,isa 不再是纯指针,而是一个 64 位共用体(union),仅部分位存储类指针,其余位存储引用计数、弱引用标志、关联对象标志、析构状态等信息,以节省内存。通过 isa & ISA_MASK 可获取真实的类指针。

3. Class — 类对象

Class 是指向 objc_class 结构体的指针,类本质上也是对象:

typedef struct objc_class *Class;

struct objc_class {
    Class isa;                // 指向 Meta-class
    Class superclass;         // 指向父类
    cache_t cache;            // 方法缓存(哈希表)
    class_data_bits_t bits;   // 指向类数据(方法列表、属性列表、协议列表等)
};

Meta-class(元类)体系

Objective-C 中类方法和实例方法分开存储,类方法存储在 Meta-class 中:

实例对象 ──isa──▶ 类对象 ──isa──▶ Meta-class ──isa──▶ 根类 Meta-class ──isa──▶ 自己(闭环)
                      │
                   superclass
                      │
                   父类对象
  • 实例的 isa → 类对象(存储实例方法)
  • 类对象的 isa → Meta-class(存储类方法)
  • Meta-class 的 isa → 根类(NSObject)的 Meta-class
  • 根类 Meta-class 的 isa → 自己,形成闭环
  • 类对象和 Meta-class 都有 superclass 指针,分别指向父类和父类的 Meta-class

4. Method — 方法结构

typedef struct objc_method *Method;

struct objc_method {
    SEL method_name;     // 方法选择器
    char *method_types;  // 方法类型编码(参数和返回值类型)
    IMP method_imp;      // 指向方法实现的函数指针
};

IMP — 方法实现指针

typedef void (*IMP)(id, SEL, ...);

IMP 直接指向方法实现的 C 函数,调用时可绕过消息发送流程,直接执行函数,性能更高。获取方式:

IMP imp = [self methodForSelector:@selector(methodName)];

5. Ivar — 实例变量

typedef struct objc_ivar *Ivar;

struct objc_ivar {
    char *ivar_name;    // 变量名
    char *ivar_type;    // 变量类型编码
    int ivar_offset;    // 变量在对象内存中的偏移量
};

通过 ivar_offset 可以直接从对象内存地址偏移读取实例变量,这也是 KVC 能直接访问实例变量的底层基础。

6. Cache — 方法缓存

cache 用于缓存已查找过的方法,避免每次调用都遍历方法列表,显著提升性能:

struct cache_t {
    bucket_t *buckets;   // 哈希桶数组
    mask_t mask;         // 掩码(容量 - 1,用于哈希取模)
    mask_t occupied;     // 已占用数量
};

查找方法时,先在 cache 中通过 SEL 哈希查找,命中则直接调用;未命中则遍历 methodLists,找到后缓存到 cache 中供下次使用。

7. objc_msgSend — 消息发送核心

id objc_msgSend(id self, SEL op, ...);

编译器将 [receiver message] 转换为 objc_msgSend(receiver, @selector(message))。该函数用汇编实现以追求极致性能,流程如下:

  1. 通过 receiver->isa 获取类对象。
  2. 在类的 cache 中查找方法(哈希查找)。
  3. 缓存未命中则在类的 methodLists 中查找。
  4. 类中未找到则沿 superclass 链向父类查找。
  5. 找到后将方法缓存到 cache,并跳转到其 IMP 执行。
  6. 整个继承链都未找到则进入消息转发流程。

二、消息发送与消息转发

1. self 与 super

self 是消息的实际接收者,super 是编译器标识符,告诉编译器从父类开始查找方法,但消息接收者仍是 self

@implementation Son : Father
- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"%@", NSStringFromClass([self class]));   // Son
        NSLog(@"%@", NSStringFromClass([super class])); // Son
    }
    return self;
}
@end

两者都打印 Son,因为 class 方法在 NSObject 中实现,返回的是消息接收者的实际类,而 super 调用的接收者仍然是 self(Son 实例)。

super 的本质是 objc_msgSendSuper,它从父类的方法列表开始查找,但 self 参数不变。

2. 隐藏参数 self 和 _cmd

每个 Objective-C 方法实现都有两个隐式参数:

- (void)exampleMethod {
    NSLog(@"self: %@", self);          // 消息接收者
    NSLog(@"_cmd: %@", NSStringFromSelector(_cmd)); // 当前方法的 SEL
}

这两个参数由 Runtime 在调用方法时自动传入,使得方法实现可以动态访问调用者和方法自身。

3. 完整消息转发流程

当对象收到未实现的消息时,Runtime 提供三次"补救"机会,均失败后才崩溃:

对象收到未实现的消息
        │
        ▼
① +resolveInstanceMethod:(类方法)
   动态添加方法实现 → 返回 YES,重新发送消息
   返回 NO → 进入下一步
        │
        ▼
② -forwardingTargetForSelector:
   返回非 nil 对象 → 消息转发到该对象
   返回 nil → 进入下一步
        │
        ▼
③ -methodSignatureForSelector:
   返回非 nil → 创建 NSInvocation,调用 -forwardInvocation:
   返回 nil → 调用 -doesNotRecognizeSelector:(崩溃)

① Method Resolution(方法解析)

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(testMethod)) {
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
        return YES; // 返回 YES 后 Runtime 会重新发送消息
    }
    return [super resolveInstanceMethod:sel];
}

// 动态添加的 C 函数实现
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"动态方法实现");
}

类方法解析使用 + resolveClassMethod:

② Fast Forwarding(快速转发)

将消息转发给另一个对象,轻量高效:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(testMethod)) {
        return self.alternateTarget; // 转发到备用对象
    }
    return [super forwardingTargetForSelector:aSelector];
}

③ Normal Forwarding(完整转发)

创建 NSInvocation 封装完整的消息(目标、选择器、参数),可修改参数后转发:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(testMethod)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    if ([self.alternateTarget respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:self.alternateTarget];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

三、Runtime 高级功能

1. Associated Objects(关联对象)

为已有的类(尤其是 Category)动态添加属性,无需修改类的原始结构:

// API
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);

内存管理策略:

策略 对应属性修饰符
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
OBJC_ASSOCIATION_RETAIN strong(原子)
OBJC_ASSOCIATION_COPY copy(原子)

使用示例(Category 添加属性):

// UIViewController+Toast.h
@interface UIViewController (Toast)
@property (nonatomic, copy) NSString *toastMessage;
@end

// UIViewController+Toast.m
#import <objc/runtime.h>

static const void *kToastMessageKey = &kToastMessageKey;

@implementation UIViewController (Toast)

- (void)setToastMessage:(NSString *)toastMessage {
    objc_setAssociatedObject(self, kToastMessageKey, toastMessage, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)toastMessage {
    return objc_getAssociatedObject(self, kToastMessageKey);
}

@end

关联对象存储在全局的 AssociationsManager 哈希表中,不占用对象本身的内存。对象 dealloc 时 Runtime 自动调用 objc_removeAssociatedObjects 清理。

2. Method Swizzling(方法交换)

在运行时交换两个方法的实现(IMP),用于修改已有方法行为、添加日志、统计埋点等:

#import <objc/runtime.h>

@implementation UIViewController (Logging)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method original = class_getInstanceMethod(self, @selector(viewDidAppear:));
        Method swizzled = class_getInstanceMethod(self, @selector(log_viewDidAppear:));
        method_exchangeImplementations(original, swizzled);
    });
}

- (void)log_viewDidAppear:(BOOL)animated {
    // 由于方法已交换,这里调用 log_viewDidAppear: 实际执行的是原始 viewDidAppear:
    [self log_viewDidAppear:animated];
    NSLog(@"页面显示: %@", NSStringFromClass([self class]));
}

@end

关键注意事项:

  • + load 中执行(类被加载到 Runtime 时自动调用,早于任何实例创建)。
  • 使用 dispatch_once 保证线程安全且只执行一次。
  • 交换后,在自定义方法中调用"自己的方法名"实际执行的是原始实现(因为 SEL 和 IMP 的对应关系已交换)。
  • 注意类簇(如 NSStringNSArray)的 Swizzling 可能影响多个内部类。

3. 面向切面编程(AOP)

基于 Method Swizzling 实现横切关注点(日志、性能监控、埋点),无需修改业务代码:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method original = class_getInstanceMethod(self, @selector(targetMethod));
        Method swizzled = class_getInstanceMethod(self, @selector(aop_targetMethod));
        method_exchangeImplementations(original, swizzled);
    });
}

- (void)aop_targetMethod {
    // 前置处理:记录开始时间
    CFTimeInterval startTime = CACurrentMediaTime();

    // 调用原始方法(方法已交换,调用 aop_targetMethod 实际执行原始实现)
    [self aop_targetMethod];

    // 后置处理:计算耗时
    NSLog(@"方法耗时: %.4fms", (CACurrentMediaTime() - startTime) * 1000);
}

业界优秀的 AOP 库:Aspects,基于 Runtime 消息转发实现,API 简洁,支持实例和类方法的 Hook。


四、Swift 与 Runtime

纯 Swift 类使用静态派发(直接调用或 vtable),不支持 Runtime 动态特性。继承自 NSObject 的 Swift 类可以使用 Runtime:

class Person: NSObject {
    @objc dynamic func sayHello() { // dynamic 关键字强制使用消息发送
        print("Hello")
    }
}

let p = Person()
p.perform(Selector(("sayHello"))) // Runtime 消息发送
特性 纯 Swift 类 继承 NSObject 的 Swift 类 Objective-C 类
消息发送 否(静态派发) 是(@objc dynamic
Method Swizzling
Associated Objects
KVO 需手动实现 @objc dynamic 属性支持 支持

五、总结

  • 数据结构isa 指针是 Runtime 的核心,通过它可以找到类对象、方法列表、实例变量等。arm64 下 isa 是优化过的 nonpointer 共用体。
  • 消息发送objc_msgSend 用汇编实现,通过 isa → cache → methodLists → 父类 的流程查找方法,找到后缓存并执行。
  • 消息转发:未找到方法时依次经过 resolveInstanceMethod:forwardingTargetForSelector:methodSignatureForSelector: + forwardInvocation:,均失败后调用 doesNotRecognizeSelector: 崩溃。
  • 动态特性:Associated Objects 为 Category 添加属性,Method Swizzling 交换方法实现,两者是 Runtime 最常用的高级功能。
  • Swift 中:继承 NSObject 并标记 @objc dynamic 才能使用 Runtime 动态特性,纯 Swift 类使用静态派发。

理解 Runtime 机制是排查崩溃(unrecognized selector)、实现 AOP、Hook 系统方法、优化性能的基础。


posted @ 2015-07-22 13:50  Mr.陳  阅读(432)  评论(0)    收藏  举报