1. Runtime 架构详解与内部工作机制
OC 作为一门动态语言,其强大的灵活性主要源于其底层运行时系统(Runtime)。Runtime 系统实质上是一套 C 和汇编语言编写的 API,它作为 OC 与底层系统之间的桥梁,使得 OC 拥有了动态特性。
OC Runtime 系统的核心架构由以下几个关键组件构成:
- Class 结构体:用于表示类的基本单元
- Object 结构体:表示对象实例
- Method 结构体:封装方法信息
- IMP 函数指针:指向方法的实际实现
- SEL 选择器:方法的唯一标识符
在 iOS 开发中,我们日常编写的 OC 代码最终都会转换为 Runtime 的 C 函数调用。这一转换过程由编译器和运行时系统共同完成。

现在,让我们来看一下 Runtime 系统中最核心的几个数据结构的定义:
objective-c
// objc 对象结构
struct objc_object {
Class isa; // 指向类的指针
};
// 类结构
struct objc_class {
Class isa; // 元类指针
Class super_class; // 父类指针
const char *name; // 类名
long version; // 版本信息
long info; // 类信息
long instance_size; // 实例大小
struct objc_ivar_list *ivars; // 实例变量列表
struct objc_method_list **methodLists; // 方法列表
struct objc_cache *cache; // 方法缓存
struct objc_protocol_list *protocols; // 协议列表
};
// 方法结构
struct objc_method {
SEL method_name; // 方法选择器
char *method_types; // 方法类型编码
IMP method_imp; // 方法实现函数指针
};
Runtime 系统的核心工作原理围绕着这些结构体展开,通过它们实现了 OC 语言的动态特性,包括动态类型、动态绑定以及动态加载等功能。
2. 消息传递机制
2.1 objc_msgSend实现原理
OC 中的方法调用本质上是消息传递过程。当我们编写如下代码时:
objective-c
[object method:argument];
编译器会将其转换为:
objective-c
objc_msgSend(object, @selector(method:), argument);
objc_msgSend
函数是 Runtime 系统的核心,它的基本实现原理如下:
- 获取对象的
isa
指针,找到对应的类 - 在类的方法缓存中查找方法
- 如果缓存中找不到,则在方法列表中查找
- 如果当前类中找不到,则沿着继承链向上查找
- 如果继承链上都没有找到,则进入消息转发流程
- 找到方法后,执行对应的 IMP 函数指针
objc_msgSend
主要使用汇编语言实现,以保证最高的性能。其伪代码逻辑大致如下:
objective-c
id objc_msgSend(id self, SEL _cmd, ...) {
if (self == nil) return nil;
// 查找实现
IMP imp = lookUpImpOrForward(self, _cmd);
// 执行方法实现,传递所有参数
return imp(self, _cmd, ...);
}
2.2 方法查找流程分析
当 objc_msgSend
函数开始查找方法实现时,会按照以下顺序执行查找:
- 缓存查找:首先在对象所属类的方法缓存(cache)中查找,缓存使用哈希表实现,查找速度非常快。
- 当前类查找:如果缓存中没有找到,则在类的方法列表(methodLists)中查找。这个过程相对较慢,因为需要遍历方法列表。
- 父类链查找:如果在当前类中没有找到方法,则沿着继承链向上查找父类的方法,同样遵循缓存优先的原则。
- 动态解析 :如果继承链上都没有找到对应方法,则会触发动态方法解析流程,调用
+resolveInstanceMethod:
或+resolveClassMethod:
方法。 - 消息转发:如果动态解析未能解决问题,则进入完整的消息转发流程。
方法查找的源码实现位于 objc-runtime-new.mm
文件中的 lookUpImpOrForward
函数:
objective-c
IMP lookUpImpOrForward(id obj, SEL sel, Class cls, int behavior) {
// 缓存查找
if (cache_getImp(cls, sel, &obj)) {
return (IMP)imp;
}
// 类方法列表查找
method_t *m = getMethodNoSuper_nolock(cls, sel);
if (m) return m->imp;
// 父类查找
for (Class curClass = cls->superclass; curClass; curClass = curClass->superclass) {
// 检查父类缓存
if (cache_getImp(curClass, sel, &obj)) {
return (IMP)imp;
}
// 检查父类方法列表
method_t *m = getMethodNoSuper_nolock(curClass, sel);
if (m) return m->imp;
}
// 方法未找到,进入转发流程
return _objc_msgForward;
}
2.3 消息转发完整流程
当 Runtime 系统在对象的继承链上都未能找到对应的方法实现时,会启动消息转发机制,这是 OC 实现动态特性的重要一环。完整的消息转发流程分为三个阶段:

第一阶段:动态方法解析
首先,Runtime 系统会调用 +resolveInstanceMethod:
或 +resolveClassMethod:
方法(取决于是实例方法还是类方法),给对象所属类一次动态添加方法实现的机会。
objective-c
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 动态添加的方法实现
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"动态添加的方法实现");
}
如果方法解析成功返回 YES,那么消息发送过程会重新开始,再次查找方法实现。
第二阶段:快速转发
如果动态方法解析未能解决问题,则进入快速转发阶段。Runtime 系统会调用 -forwardingTargetForSelector:
方法,询问对象是否有其他对象可以处理这个消息。
objective-c
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(someMethod)) {
// 返回能够处理该消息的对象
return alternativeObject;
}
return [super forwardingTargetForSelector:aSelector];
}
这一阶段的性能相对较高,因为 Runtime 系统会直接将消息重定向到目标对象,而不会创建完整的消息转发机制。
第三阶段:标准转发
如果前两个阶段都未能解决问题,则进入标准转发阶段。这一阶段会进行更完整的消息转发处理,但性能开销较大。
首先,Runtime 系统会调用 -methodSignatureForSelector:
方法获取方法签名:
objective-c
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(someMethod)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
然后,根据方法签名创建 NSInvocation
对象,并调用 -forwardInvocation:
方法:
objective-c
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
if ([alternativeObject respondsToSelector:selector]) {
[anInvocation invokeWithTarget:alternativeObject];
} else {
[super forwardInvocation:anInvocation];
}
}
如果所有转发流程都失败了,则会调用 -doesNotRecognizeSelector:
方法,通常会导致应用崩溃并抛出异常。
2.4 方法缓存优化机制
为了提高方法调用的性能,OC Runtime 系统在类结构中实现了方法缓存机制。每个类都有一个 cache_t
类型的 cache
字段,用于存储曾经调用过的方法。
objective-c
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
};
struct bucket_t {
cache_key_t _key; // SEL
IMP _imp; // 实现函数指针
};
方法缓存使用了哈希表实现,查找效率为 O(1),大大提升了频繁调用方法的性能。缓存机制的基本原理是:
- 空间换时间:使用额外的内存空间来存储已经查找过的方法,避免重复的方法查找过程。
- 哈希算法:通过 SEL 选择器计算哈希值,快速定位到对应的桶(bucket)。
- 动态扩容:当缓存中的元素数量达到阈值时,会触发扩容操作,重新分配更大的内存空间。
- 局部性原理:利用程序的局部性原理,即同一个方法可能会被连续多次调用,或者某些方法会被频繁调用。
当类接收到新消息时,会先在缓存中查找,如果找到则直接使用缓存的实现;如果未找到,则会在方法列表中查找,并将结果存入缓存,以便下次快速访问。
3. Runtime API实现
3.1 动态方法解析
动态方法解析是 OC Runtime 系统提供的一种在运行时添加方法实现的机制。通过实现 +resolveInstanceMethod:
或 +resolveClassMethod:
方法,可以在对象接收到未实现的消息时动态添加方法实现。
核心 API 是 class_addMethod
,它允许我们在运行时向类添加方法:
objective-c
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
参数说明:
cls
:要添加方法的类name
:方法的选择器(SEL)imp
:方法的实现函数指针(IMP)types
:方法的类型编码,描述参数和返回值类型
实际应用示例:
objective-c
@implementation MyClass
// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self,
sel,
(IMP)dynamicMethodImplementation,
"v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
// C 函数作为方法实现
void dynamicMethodImplementation(id self, SEL _cmd) {
NSLog(@"这是动态添加的方法实现");
}
这种机制常用于实现 @dynamic
属性的存取方法、模拟多重继承以及实现消息转发等高级功能。
3.2 方法替换与交换技术
OC Runtime 系统提供了方法替换和交换的 API,这些技术常用于实现方法拦截、AOP(面向切面编程)以及为已有方法添加功能等场景。
3.2.1 方法替换(Method Swizzling)
最常用的方法交换 API 是 method_exchangeImplementations
:
objective-c
void method_exchangeImplementations(Method m1, Method m2);
这个函数会交换两个方法的实现,使得调用一个方法时会执行另一个方法的实现,反之亦然。
使用示例:
objective-c
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 获取原始方法和自定义方法
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzled_viewDidAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 先尝试添加方法实现
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 如果添加成功,说明原方法不存在,则替换新增方法的实现
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 如果添加失败,说明原方法已存在,则交换两个方法的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
// 自定义方法实现
- (void)swizzled_viewDidAppear:(BOOL)animated {
// 调用原始实现
[self swizzled_viewDidAppear:animated];
// 添加自定义行为
NSLog(@"视图控制器 %@ 已显示", self);
}
@end
方法替换的常见应用场景:
- 为系统方法添加额外功能(如日志记录、性能监控)
- 修复系统库的 bug
- 实现全局的 AOP 功能
- 在不修改原始类的情况下扩展功能
3.2.2 方法替换的实现原理
method_exchangeImplementations
内部实现相对简单,它交换两个 Method 结构体中的 IMP 指针:
objective-c
void method_exchangeImplementations(Method m1, Method m2) {
if (!m1 || !m2) return;
rwlock_writer_t lock(runtimeLock);
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
}
需要注意的是,方法交换操作应当在 +load
方法中进行,因为这个方法在类被加载时调用,可以确保在应用启动后、任何其他代码执行前完成方法交换。
3.3 分类与扩展底层区别
OC 中的分类(Category)和扩展(Extension)是两种常用的类扩展机制,虽然在语法上相似,但在 Runtime 底层实现上有着本质区别。
3.3.1 分类(Category)
分类在底层由 category_t
结构体表示:
objective-c
struct category_t {
const char *name; // 分类名
classref_t cls; // 分类所属的类
struct method_list_t *instanceMethods; // 实例方法列表
struct method_list_t *classMethods; // 类方法列表
struct protocol_list_t *protocols; // 协议列表
struct property_list_t *instanceProperties; // 属性列表
// Fields below this point are available with MAC_OS_X_VERSION_10_7 or later
struct property_list_t *_classProperties; // 类属性列表
};
分类的关键特点:
- 生命周期:分类在运行时动态加载,编译后会生成独立的目标文件(.o 文件)。
- 加载过程 :在 Runtime 初始化过程中,系统会调用
_objc_init
函数,进而调用map_images
和_read_images
函数,将分类中的方法、属性、协议等附加到原始类中。 - 方法冲突:如果分类中的方法与原类方法同名,分类方法会"覆盖"原类方法。这里的"覆盖"是通过方法列表的合并顺序实现的,分类的方法被放在原类方法列表的前面,导致在查找时优先找到分类方法。
- 多个分类:如果多个分类中存在同名方法,后编译的分类会"覆盖"先编译的分类(与编译顺序有关,可在 Build Phases -> Compile Sources 中查看)。
- 限制:分类不能添加实例变量,因为这会改变对象的内存布局,影响已创建的对象。
3.3.2 扩展(Extension)
扩展在底层没有单独的数据结构表示,它本质上是一种特殊的匿名分类,但只在编译期生效。
扩展的关键特点:
- 生命周期:扩展在编译期就与原类合并,不会生成独立的目标文件。
- 合并过程:编译器会将扩展中的内容直接合并到原类的定义中,就像这些内容本来就写在原类中一样。
- 实例变量:扩展可以添加实例变量,因为它在编译期就确定了对象的内存布局。
- 访问控制:扩展可以为原类添加私有方法和属性,这些方法和属性在类的实现文件中可见,但对外不可见。
- 限制:扩展只能为有源码的类添加功能,不能对系统的类或第三方库的类使用扩展。
3.3.3 底层实现区别总结
- 存在形式 :分类由
category_t
结构体表示,而扩展没有单独的结构体。 - 加载时机:分类在运行时动态加载,扩展在编译期就与原类合并。
- 实例变量:分类不能添加实例变量,扩展可以添加实例变量。
- 方法覆盖:分类可以覆盖原类方法,多个分类之间按编译顺序覆盖;扩展的方法和原类方法等价,不存在覆盖关系。
- 源码要求:分类可以为任何类添加功能,扩展只能为有源码的类添加功能。
4. Runtime 操作的线程安全策略
OC Runtime 系统在设计中考虑了多线程环境,但并非所有 Runtime 操作都是线程安全的。了解 Runtime 的线程安全策略对于开发多线程应用至关重要。
4.1 Runtime 系统中的锁机制
Runtime 系统主要通过以下几种锁机制保证线程安全:
- 全局 Runtime 锁(RuntimeLock):保护全局数据结构,如类列表、方法缓存等。
- 自旋锁(spinlock):用于保护方法缓存等轻量级操作。
- 读写锁(rwlock):允许多读单写,用于优化某些常见的读取操作。
- OSSpinLock/os_unfair_lock :用于保护对象的
isa
指针等关键字段。
4.2 线程安全的 Runtime 操作
以下 Runtime 操作在内部实现了线程安全保障:
- 类注册与初始化 :
objc_registerClassPair
、objc_getClass
等函数。 - 消息发送 :
objc_msgSend
及其变体是线程安全的。 - 方法查找 :
class_getInstanceMethod
、class_getClassMethod
等。 - 属性访问 :
class_getProperty
、protocol_getProperty
等。 - 对象分配与释放 :
object_getClass
、object_setClass
等。
4.3 非线程安全的 Runtime 操作
以下 Runtime 操作需要开发者自行保证线程安全:
- 动态添加方法 :
class_addMethod
应避免在多线程环境中同时向同一个类添加方法。 - 方法交换 :
method_exchangeImplementations
不应在多线程环境中并发执行。 - 关联对象操作 :
objc_setAssociatedObject
和objc_getAssociatedObject
在并发访问同一个对象的同一个键时可能不安全。 - 动态创建类 :
objc_allocateClassPair
不应在多线程环境中并发创建具有相同名称的类。 - Category 加载:不应在运行时动态加载 Category,这可能导致未定义行为。
4.4 线程安全案例
在 +load 方法中执行方法交换 :+load
方法在主线程的应用启动时调用,是执行方法交换的安全时机。
objective-c
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 执行方法交换
Method originalMethod = class_getInstanceMethod(self, @selector(originalMethod));
Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzledMethod));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
使用 dispatch_once 确保初始化代码只执行一次:
objective-c
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 执行一次性初始化代码
// 如添加方法、创建动态类等
});
}
为动态类操作添加线程同步:
objective-c
static OSSpinLock lock = OS_SPINLOCK_INIT;
// 线程安全地添加方法
OSSpinLockLock(&lock);
class_addMethod(cls, selector, implementation, types);
OSSpinLockUnlock(&lock);
避免运行时修改系统类:修改系统类的行为可能导致与系统框架的并发冲突,应谨慎使用。
优先使用高级 API:尽可能使用 OC 的高级 API,它们通常已实现内部线程安全。
总结
OC Runtime 系统是这门语言动态特性的基石,Runtime 系统的核心价值在于它赋予了静态编译语言以动态特性,使得 OC 能够支持反射、动态加载、消息转发等高级功能。这些特性为 iOS 和 macOS 开发提供了强大的工具,也为框架开发者提供了极大的灵活性。
然而,与强大的能力相伴的往往是复杂性和潜在风险。过度依赖 Runtime 系统的动态特性可能导致代码难以理解和维护,甚至带来性能和安全隐患。在实际开发中,我们应当在灵活性和可维护性之间寻找适当的平衡点,合理使用 Runtime 系统提供的能力。