核心概念
本质 :runtime是 oc 的一个运行时库(libobjc.A,dylib),它为 oc 添加了 面向对象的能力 以及 运行时的动态特性。
- 面向对象的能力 :
rutime用 C 语言实现了类、对象、封装、继承、多态等面向对象的核心概念。 - 运行时的动态特性 :
runtime可以让很多操作可以在 运行时 确定,如 动态方法调用、动态类型检查、动态方法添加、动态属性访问、动态类创建消息转发机制等。- 动态语言 的优劣势
- 优势 :
- 灵活性:可以在运行时修改程序的行为
- 可扩展性:可以动态添加功能
- 反射能力:可以在运行时检查和操作对象
- 代价 :
- 性能开销:运行时查找比编译时确定慢
- 类型安全:可编译时无法检查所有错误
- 调试困难:动态行为难以追踪
- 优势 :
- 动态语言 的优劣势
Runtime数据结构
首先我们要清晰,所有的 OC 对象(包括 NSObject 自身)在内存中,本质上都是一个 objc_object 结构体实例。然后了解对象体系之间的关系(见下文)。
swift
/// 核心数据结构
///
/// 1. 任何实例(`id` 就是 objc_object * 类型)
struct objc_object {
Class _Nonnull isa; // isa 指针,指向对象的类
};
/// 2. 类对象 / 元类对象
struct objc_class {
Class _Nonnull isa; // 继承自 objc_object
Class _Nullable super_class;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 类的具体信息 -> class_rw_t
};
struct class_rw_t {
method_array_t methods; // -> objc_method[](元类对象存储类方法、类对象存储实例方法)
property_array_t properties; // -> objc_property[]
ivar_array_t ivars; // -> objc_ivar[]
const class_ro_t *ro; // 编译期只读信息
};
/// 3. 方法描述
struct objc_method {
SEL name; // 方法名(方法选择子)
const char *types; // 方法类型编码
IMP imp; // 方法实现(函数指针)
};
/// 4. 属性描述
struct objc_property {
const char * _Nonnull name; // 属性名
const char * _Nullable attributes; // 属性特性
};
/// 5. 实例变量
struct objc_ivar {
char * _Nullable name; // 变量名
char * _Nullable type; // 变量类型
int offset; // 变量偏移量
uint32_t alignment_raw; // 对齐信息
uint32_t size; // 变量大小
};
1. isa 指针
含义 :isa指针是 OC 对象中,指向其类对象和元类对象的指针。当对象接收到消息时,系统通过isa定位类对象,从而查找方法实现。 作用 :决定对象的类型、以及方法查找起点。比如实例对象的isa决定它属于哪个类,类方法的查找从元类对象开始。
优化 (64-bit) :在 64 位系统中,isa被设计为了叫做「位域 (bitfield)」的结构。除了存放类指针外,还内嵌了一些状态位(引用计数、弱引用、析构标志等),这样做可以将小对象的数据直接编码进指针本身,节省了小对象的内存与访问开销。
arduino
union isa_t {
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; // 是否启用优化
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // 存真正的类指针
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
对象模型
对象、类、元类关系: 
拿Person类举个例子:
objectivec
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)changeNameWith:(NSString *)newName;
+ (instancetype)mockPerson;
@end
Person *p1 = [Person new];
实际的对象体系链路:
scss
实例对象:object_object (p1)
_____
| isa | ---> 类对象:object_class (Person.class)
_____ ____________
| isa | -------------
c_object (NSObject.class) <- | super_class | | methods[] (-changeNameWith:)
| bits | ------------------> { properties[] (name)
____________ | ivars[] (_name)
▼
元类对象:object_class (Person.meta)
____________
| isa | ---------
objc_class (NSObject.meta) <- | super_class | | methods[] (+alloc、+mockPerson)
| bits | ------------------>{ properties[] (无)
____________ | ivars[] (无)
▼
根元类对象:objc_class (NSObject.meta)
____________
| isa | ---> 指向自己
objc_class (NSObject.class) <- | super_class |
| bits |
____________
消息传递机制
1. 消息发送机制
当代码写:
ini
[p sayHello];
编译器会转化成:
less
objc_msgSend(p, @selector(sayHello));
objc_msgSend 查找流程:
- 检查 实例对象 是否为
nil - 从 实例对象 的
isa指针找到 类对象 - 在 类对象 的
cache_t中查找方法 - 没命中缓存,则在 类对象 的方法列表中查找
- 没找到,沿着继承链向上查找
- 如果都未找到,进入消息转发流程
缓存机制(cache_t):哈希查找 SEL
arduino
struct cache_t {
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 哈希表
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 掩码
uint16_t _flags; // 标志位
uint16_t _occupied; // 已占用数量
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
// 哈希查找算法
bucket_t *find(SEL s, id receiver);
void insert(SEL sel, IMP imp, id receiver);
};
2. 消息转发机制
-
阶段一:动态方法解析 a. 运行时动态添加 实例方法 实现:
+ (BOOL)resolveInstanceMethod:(SEL)selb. 在运行时动态添加 类方法 实现:+ (BOOL)resolveClassMethod:(SEL)sel「成功返回 YES;失败返回 NO,继续快速转发」 -
阶段二:快速转发 a. 返回一个能处理该消息的对象,Runtime 会将消息转发给该对象:
- (id)forwardingTargetForSelector:(SEL)aSelector「成功返回对象;失败返回nil,继续完整转发」 -
阶段三:完整转发 a. 返回方法签名:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelectorb. 执行实际的消息转发逻辑:- (void)forwardInvocation:(NSInvocation *)anInvocation「失败抛出异常」 -
抛出异常 a.
- (void)doesNotRecognizeSelector:(SEL)aSelector
快速转发 vs 完整转发
| 快速转发 | 完整转发 | |
|---|---|---|
| 调用时机 | 动态方法解析失败 | 快速转发失败 |
| 返回值 | 目标对象 | void |
| 调用机制 | 重新调用 objc_msgSend(newTarget, sel) | 手动通过 NSInvocation 调用 |
| 性能 | 🚀 快 | 🐢慢 |
| 使用场景 | 简单转发给另一个对象 | 复杂消息路由(动态修改调用) |
| 常见用途 | 代理、代理转发 | 消息分发中心、AOP |
NSInvocation 介绍
完整转发最后会用到的类,NSInvocation是什么?
- 封装方法调用的对象
- 存储目标对象、选择器、参数、返回值
- 可以延迟执行或多次执行
Runtime API 与动态能力
1. 方法交换(Method Swizzing)
本质 :修改方法列表中 SEL 和 IMP 的对应关系 注意事项:
- 在 +load 方法中进行,确保类加载时执行。
- 原因:+load 在类加载到内存时调用,早于任何消息发送,避免竞态。
- 使用
dispatch_once确保只执行一次。- 原因:避免重复交换,导致方法实现被换回原样,保证线程安全。
- 考虑继承关系,避免重复交换。
- 原因:子类继承父类方法,如果父子交换同一个方法,会导致重复交换,交换失败。
- 方法签名要匹配。
- 原因:方法签名不匹配,会导致参数传递错误、返回值错误、内存问题等。
- 在交换的方法中调用原方法
- 原因:保持原有功能,避免功能丢失;保证调用链完整;避免破坏其他依赖。
scss
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleMethod:@selector(viewDidLoad)
withMethod:@selector(swizzled_viewDidLoad)];
});
}
+ (void)swizzleMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector {
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 尝试添加原方法,如果已存在则返回 NO
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_viewDidLoad {
// 调用原始实现(此时已经交换,所以调用自己就是调用原方法)
[self swizzled_viewDidLoad];
// 添加自定义逻辑
NSLog(@"viewDidLoad called for %@", NSStringFromClass([self class]));
}
@end
2. 动态特性应用
2.1. 动态添加方法
less
void dynamicRun(id self, SEL _cmd) {
NSLog(@"动态添加了 run 方法 ✅");
}
@implementation Dog
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(run)) {
class_addMethod(self, sel, (IMP)dynamicRun, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
// 调用
Dog *d = [Dog new];
[d run]; // 输出:动态添加了 run 方法 ✅
📍应用: • 动态桥接(比如 JS ↔ OC 交互) • 模块化架构下的延迟加载方法 • 动态生成代理类 / Mock 类 • 序列化或模型自动解析(根据字段动态添加访问方法)
2.2. 关联对象
less
objc_setAssociatedObject(self, @selector(name), @"齐生", OBJC_ASSOCIATION_COPY_NONATOMIC);
NSString *name = objc_getAssociatedObject(self, @selector(name));
📍应用: • 在 Category 中「模拟添加属性」 • 附加元数据(缓存、tag、状态) • 替代继承实现扩展
2.3. 动态创建类
less
Class MyClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);
class_addMethod(MyClass, @selector(sayHi), (IMP)sayHiIMP, "v@:");
objc_registerClassPair(MyClass);
id obj = [[MyClass alloc] init];
[obj performSelector:@selector(sayHi)];
📍应用:
- Runtime 自动生成 model
- Mock 测试
- 动态代理
- 含义:在运行时把消息转发给另一个对象来处理。
3. Category 解析
arduino
// Category 的数据结构
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; // 属性列表
struct property_list_t *_classProperties; // 类属性列表
};
// Category 加载过程
1. 编译时:Category 被编译成 category_t 结构体
2. 运行时:通过 Runtime 加载到类对象中
3. 合并:Category 的方法、属性、协议合并到类的 rw 数据中
多个 Category 的加载顺序?
- 编译顺序决定加载顺序
- 后编译的 Category 方法会"覆盖"先编译的
- 实际上是插入到方法列表前面,优先被查找到
+load 方法的调用顺序?
- 先调用类的
+load方法(按继承关系,父类优先) - 再调用
Category的+load方法(按编译顺序) - 每个
+load方法只会被调用一次
+initialize 方法的调用顺序?
- 类第一次接收消息时调用
- 按继承关系调用,父类优先
- Category 的
+initialize会"覆盖"类的+initialize
Category vs Extension
| Category | Extension | |
|---|---|---|
| 定义位置 | 任意文件 | 必须和类的声明在同一个编译单元(.m) |
| 时机 | 运行时加载 | 编译器期处理 |
| 性能 | 不如 Extension |
好,因为在编译期合并,不需要runtime注册 |
| 是否能添加成员变量 | ❌ 不行 | ✅ 可以 |
| 是否能重写原方法 | ✅ 可以,但容易引发冲突 | ✅ 可以 |
| 应用场景 | 添加方法、拆分类实现、mock系统类 | 声明私有属性 |
Category 可以添加实例变量吗?
- 不行🙅,因为类结构(
class_ro_t)已经在编译时确定,runtime动态加载Category时无法修改对象的内存布局。
Category中可以添加属性吗?
- 可以✅,可以声明,并通过关联对象来实现。
4. KVO 实现原理
4.1. KVO 的 isa-swizzling 机制
实现原理:
- 动态创建子类
- 修改对象的 isa 指针,指向新子类
- 重写子类的 setter,添加通知逻辑
- 重写 class 方法,返回原始类
csharp
/// KVO 子类的方法实现伪代码
- (void)setProperty:(id)value {
[self willChangeValueForKey:@"property"];
[super setProperty:value]; // 调用原始实现
[self didChangeValueForKey:@"property"];
}
- (Class)class {
return [OriginalClass class]; // 隐藏子类的存在
}
- (void)dealloc {
// 清理 KVO 相关资源
[super dealloc];
}
相似名词 KVC
定义 :通过 字符串键名(key) 访问对象属性的机制。
功能:
- 直接访问属性,不经过 setter/getter。
- 可以通过
keyPath访问嵌套对象。 - 支持字典与模型的动态映射(JSON -> Model)。
流程 :当调用 [person setValue:@"齐生" forKey:@"name"];
- 查找 setter 方法
- 如果没有 setter,查看有无成员变量
_name、_isName、name、isName。 - 如果没有相似成员变量,调用
- (void)setValue:forUndefinedKey:抛异常或自定义处理。
注意⚠️:
- KVC 会绕过封装,直接访问 ivar,违反封装性
- 可用于"动态赋值"、"私有属性访问"等
应用场景
- JSON 转 Model(属性遍历、KVC)
- 方法耗时统计(方法交换)
- 简单的 AOP (面向切面编程) 框架(NSInvocation、关联对象、Around Advice)
关联知识点 AOP (面向切面编程)
含义:一种编程思想,能够在「不改动原有代码逻辑」的前提下,在指定的"切面点"上插入额外逻辑(如埋点、日志、监控、权限校验等)。
| 核心概念 | 含义 |
|---|---|
| 切点(Pointcut) | 想"切入"的位置,比如方法调用前/后 |
| 通知(Advice) | 在切点执行的额外逻辑(before、after、around) |
| 切面(Aspect) | 切点 + 通知 的组合 |
| 织入(Weaving) | 把这些逻辑动态插入代码执行流程的过程 |
iOS 实现 AOP 的方法
- 方法交换:在交换方法中实现新的逻辑
- 消息转发:利用
forwardInvocation:或resolveInstanceMethod:在运行时拦截消息,再"转发"到自己的处理逻辑。 - 三方库:Aspects