iOS开发基础81-Runtime 的五大高频使用场景
Runtime 实战:iOS 开发中 Runtime 的五大高频使用场景
Runtime(运行时)是 Objective-C 的核心机制,前面我们已经专门讲过 Runtime 的底层原理(isa 指针、类结构、方法缓存、消息转发等)。本篇不再重复原理,而是聚焦实际开发中 Runtime 的高频使用场景,每个场景都给出完整可运行的代码,并解析其底层逻辑。最后还会补充 MJExtension 等主流框架的底层实现原理。
一、Runtime 简介(快速回顾)
一句话原理
Objective-C 是动态语言,方法调用的本质是运行时通过 objc_msgSend 发送消息,在运行时才决定调用哪个方法实现,而不是像 C 语言那样在编译期就确定。
// OC 写法
Person *p = [[Person alloc] init];
[p eat];
// 底层本质:发送消息
objc_msgSend(p, @selector(eat));
// 类方法同理
[Person eat];
objc_msgSend([Person class], @selector(eat));
使用
objc_msgSend需要导入#import <objc/message.h>,并在 Build Settings 中把Enable Strict Checking of objc_msgSend Calls设为NO(Xcode 新版本默认严格检查参数)。
二、场景一:Method Swizzling(方法交换 / 黑魔法)
一句话原理
通过 Runtime 交换两个方法的实现地址(IMP),使得调用原方法时实际执行的是自定义方法的代码,从而在不修改系统类源码的情况下,给系统方法"注入"额外逻辑。
使用场景
- 给系统方法添加额外功能(如监控
imageNamed:是否加载成功、统计页面停留时间)。 - 防崩溃(拦截数组越界、字典存 nil 等常见崩溃)。
- 埋点 AOP(无侵入式统计页面浏览、按钮点击)。
- 替换系统方法的默认行为。
完整实现代码
步骤一:给 UIImage 写一个分类
// UIImage+Image.h
#import <UIKit/UIKit.h>
@interface UIImage (Image)
// 自定义方法:加载图片并打印日志
+ (instancetype)ch_imageNamed:(NSString *)name;
@end
// UIImage+Image.m
#import "UIImage+Image.h"
#import <objc/runtime.h>
@implementation UIImage (Image)
// 分类加载到内存时调用(整个 App 生命周期只调用一次)
+ (void)load {
// 用 dispatch_once 保证方法交换只执行一次(线程安全)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 获取系统方法 imageNamed: 的 Method
Method imageNamed = class_getClassMethod(self, @selector(imageNamed:));
// 获取自定义方法 ch_imageNamed: 的 Method
Method ch_imageNamed = class_getClassMethod(self, @selector(ch_imageNamed:));
// 交换两个方法的实现地址
method_exchangeImplementations(imageNamed, ch_imageNamed);
});
}
// 自定义方法:加载图片 + 打印日志
+ (instancetype)ch_imageNamed:(NSString *)name {
// 注意:这里调用 ch_imageNamed:,因为方法已经交换了,
// 实际执行的是系统的 imageNamed: 实现
UIImage *image = [self ch_imageNamed:name];
if (image == nil) {
NSLog(@"[Runtime] 图片加载失败: %@", name);
}
return image;
}
@end
步骤二:正常使用系统方法
// 调用系统方法,实际会先执行 ch_imageNamed: 的逻辑
UIImage *image = [UIImage imageNamed:@"test"];
// 如果图片不存在,会打印:[Runtime] 图片加载失败: test
交换原理图解
交换前:
@selector(imageNamed:) → 系统实现 IMP_A
@selector(ch_imageNamed:) → 自定义实现 IMP_B
交换后(method_exchangeImplementations 交换了两个方法的 IMP):
@selector(imageNamed:) → 自定义实现 IMP_B(先打印日志,再调用 ch_imageNamed:)
@selector(ch_imageNamed:) → 系统实现 IMP_A(真正加载图片)
所以调用 [UIImage imageNamed:@"test"] 时:
→ 找到 @selector(imageNamed:) 对应的 IMP_B
→ 执行 IMP_B(打印日志)
→ IMP_B 内部调用 [self ch_imageNamed:name]
→ 找到 @selector(ch_imageNamed:) 对应的 IMP_A
→ 执行 IMP_A(系统真正加载图片)
关键注意事项
| 注意点 | 说明 |
|---|---|
| 在 +load 中交换 | +load 在类加载到内存时调用,时机最早,且整个生命周期只调用一次 |
| dispatch_once 保护 | 防止多线程环境下重复交换(虽然 +load 本身是线程安全的,但加 dispatch_once 是最佳实践) |
| 自定义方法加前缀 | 如 ch_imageNamed:,避免和系统方法或其他分类重名 |
| 不要在自定义方法里调用原方法名 | 会造成递归死循环,必须调用交换后的方法名(如 ch_imageNamed:) |
| 继承链问题 | 如果父类和子类都交换了同一个方法,需要注意交换的是哪个类的方法,用 class_getInstanceMethod 获取的是当前类或父类的方法 |
常见使用场景举例
场景 1:无侵入埋点(统计所有 UIViewController 的页面浏览)
@implementation UIViewController (Track)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(viewDidAppear:));
Method swizzled = class_getInstanceMethod(self, @selector(ch_viewDidAppear:));
method_exchangeImplementations(original, swizzled);
});
}
- (void)ch_viewDidAppear:(BOOL)animated {
[self ch_viewDidAppear:animated]; // 调用原方法
NSLog(@"[埋点] 页面浏览: %@", NSStringFromClass([self class]));
}
@end
场景 2:防数组越界崩溃
@implementation NSArray (Safe)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// NSArray 的真正类是 __NSArrayI(不可变数组),需要对它进行交换
Class arrayClass = NSClassFromString(@"__NSArrayI");
Method original = class_getInstanceMethod(arrayClass, @selector(objectAtIndex:));
Method swizzled = class_getInstanceMethod(arrayClass, @selector(ch_objectAtIndex:));
method_exchangeImplementations(original, swizzled);
});
}
- (id)ch_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
NSLog(@"[Runtime] 数组越界拦截: index=%lu, count=%lu", (unsigned long)index, (unsigned long)self.count);
return nil;
}
return [self ch_objectAtIndex:index];
}
@end
注意:NSArray/NSDictionary 是类簇(class cluster),真正的类名是
__NSArrayI、__NSArrayM、__NSDictionaryI等,交换方法时要对真正的类进行操作。
三、场景二:动态添加方法
一句话原理
当对象调用了一个未实现的方法时,Runtime 会给我们一次"补救"的机会——调用 resolveInstanceMethod:(或 resolveClassMethod:),我们可以在这个方法里动态地给类添加一个方法实现,让对象能正常响应这个调用,而不是崩溃。
使用场景
- 类方法非常多,加载类时为每个方法生成映射表耗费资源,可以用动态添加方法实现懒加载。
- 实现动态方法解析(如 JS 调用原生方法时,根据方法名动态添加实现)。
- 防止调用未实现方法导致的崩溃(兜底方案)。
- 面试高频考点:消息转发流程。
完整实现代码
// Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
// 只声明,不实现 eat 方法
- (void)eat;
@end
// Person.m
#import "Person.h"
#import <objc/runtime.h>
@implementation Person
// 当对象调用了未实现的实例方法时,Runtime 会调用这个方法
// 返回 YES 表示我们已经动态添加了方法,可以处理这个调用
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
// 动态添加 eat 方法
// 参数说明:
// 1. 给哪个类添加方法
// 2. 添加方法的 SEL(方法编号)
// 3. 方法的实现(函数指针,C 函数)
// 4. 方法的类型编码:"v@:" 表示
// v → 返回值 void
// @ → 第一个参数 self(id 类型)
// : → 第二个参数 _cmd(SEL 类型)
class_addMethod(self, sel, (IMP)eatImplementation, "v@:");
return YES;
}
// 其他方法交给父类处理
return [super resolveInstanceMethod:sel];
}
// 方法的 C 函数实现(必须是这种格式:前两个隐式参数 self 和 _cmd)
void eatImplementation(id self, SEL _cmd) {
NSLog(@"[Runtime] %@ 执行了动态添加的方法: %@", self, NSStringFromSelector(_cmd));
}
@end
// 使用
Person *p = [[Person alloc] init];
[p performSelector:@selector(eat)];
// 输出:[Runtime] <Person: 0x6000...> 执行了动态添加的方法: eat
方法类型编码说明
class_addMethod 的第四个参数是方法的类型编码字符串,格式为 "返回值类型参数1类型参数2类型...":
| 编码 | 类型 |
|---|---|
v |
void |
@ |
id(对象) |
: |
SEL(方法选择器) |
i |
int |
f |
float |
d |
double |
B |
BOOL |
* |
char *(C 字符串) |
# |
Class |
每个 OC 方法都有两个隐式参数:self(类型 @)和 _cmd(类型 :),所以无参数无返回值的方法类型编码是 "v@:"。
带参数的例子:
// - (int)add:(int)a b:(int)b;
// 类型编码:"i@:ii"(返回 int,self,_cmd,int a,int b)
消息转发完整流程(面试高频)
当对象调用一个未实现的方法时,Runtime 不会立即崩溃,而是按以下顺序给出三次"补救"机会:
1. resolveInstanceMethod:(动态方法解析)
→ 我们可以在这里用 class_addMethod 动态添加方法
→ 返回 YES 表示已处理,重新发送消息
2. forwardingTargetForSelector:(快速转发)
→ 返回一个能响应这个方法的备用对象
→ Runtime 把消息转发给这个备用对象
3. methodSignatureForSelector: + forwardInvocation:(完整转发)
→ methodSignatureForSelector: 返回方法签名
→ forwardInvocation: 把调用打包成 NSInvocation,我们可以自定义转发逻辑
如果以上三步都没有处理,最终调用 doesNotRecognizeSelector:,抛出异常崩溃
这是 Runtime 面试的超高频考点,必须记住这三步的顺序和作用。
四、场景三:关联对象(给分类添加属性)
一句话原理
分类(Category)默认不能添加成员变量(因为分类的内存布局在编译期就确定了,不能再增加 ivar),但可以通过 Runtime 的关联对象(Associated Objects)机制,给对象动态绑定一个键值对,从而实现"分类添加属性"的效果。
使用场景
- 给系统类的分类添加属性(如给 UIView 添加
tapBlock属性)。 - 给分类添加状态变量(如给 UIButton 添加
loading状态)。 - 在运行时动态给对象关联额外数据。
完整实现代码
// NSObject+Property.h
#import <Foundation/Foundation.h>
@interface NSObject (Property)
// 给 NSObject 分类添加一个 name 属性
@property (nonatomic, copy) NSString *name;
@end
// NSObject+Property.m
#import "NSObject+Property.h"
#import <objc/runtime.h>
@implementation NSObject (Property)
// 关联对象的 key(静态变量,保证唯一性)
static const void *kNameKey = &kNameKey;
// 也可以用 static char kNameKey; 或用 @selector(name) 作为 key
- (NSString *)name {
// 根据 key 获取关联的 value
return objc_getAssociatedObject(self, kNameKey);
}
- (void)setName:(NSString *)name {
// 给对象关联一个键值对
// 参数:
// 1. 给哪个对象关联
// 2. 关联的 key
// 3. 关联的 value
// 4. 关联策略(内存管理语义)
objc_setAssociatedObject(self, kNameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
// 使用
NSObject *obj = [[NSObject alloc] init];
obj.name = @"Runtime 关联对象";
NSLog(@"%@", obj.name); // 输出:Runtime 关联对象
关联策略(第五个参数)
| 策略 | 对应 @property 修饰符 | 说明 |
|---|---|---|
OBJC_ASSOCIATION_ASSIGN |
assign |
弱引用,不持有对象 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC |
strong, nonatomic |
强引用,非原子 |
OBJC_ASSOCIATION_COPY_NONATOMIC |
copy, nonatomic |
拷贝,非原子 |
OBJC_ASSOCIATION_RETAIN |
strong, atomic |
强引用,原子 |
OBJC_ASSOCIATION_COPY |
copy, atomic |
拷贝,原子 |
关联对象的 key 选择
常见的三种 key 写法:
// 方式一:静态变量地址(推荐,最安全)
static const void *kKey = &kKey;
// 方式二:静态 char
static char kKey;
// 方式三:用 @selector 作为 key(简洁,但要注意 selector 唯一性)
objc_getAssociatedObject(self, @selector(name));
实际开发示例:给 UIButton 添加点击回调 Block
// UIButton+Block.h
#import <UIKit/UIKit.h>
typedef void(^ButtonClickBlock)(UIButton *button);
@interface UIButton (Block)
@property (nonatomic, copy) ButtonClickBlock clickBlock;
@end
// UIButton+Block.m
#import "UIButton+Block.h"
#import <objc/runtime.h>
@implementation UIButton (Block)
static const void *kClickBlockKey = &kClickBlockKey;
- (ButtonClickBlock)clickBlock {
return objc_getAssociatedObject(self, kClickBlockKey);
}
- (void)setClickBlock:(ButtonClickBlock)clickBlock {
objc_setAssociatedObject(self, kClickBlockKey, clickBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
// 绑定点击事件
[self removeTarget:self action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:@selector(handleClick:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)handleClick:(UIButton *)button {
if (self.clickBlock) {
self.clickBlock(button);
}
}
@end
// 使用
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.clickBlock = ^(UIButton *button) {
NSLog(@"按钮被点击了");
};
五、场景四:字典转模型(MJExtension 底层原理)
一句话原理
利用 Runtime 遍历模型类的所有属性(class_copyPropertyList),根据属性名去字典中查找对应的 value,然后通过 KVC(setValue:forKey:)给模型属性赋值,从而实现自动的字典转模型。MJExtension、YYModel、MJExtension 等主流框架的底层都是这个思路。
两种字典转模型方式对比
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| KVC 方式 | setValuesForKeysWithDictionary: |
简单,系统提供 | 必须属性名和 key 一一对应,不对应会崩溃;不支持嵌套模型转换 |
| Runtime 方式 | 遍历属性 + KVC 赋值 | 灵活,支持嵌套模型、数组中模型、key 映射等 | 需要自己写实现 |
KVC 方式(简单但有局限)
@implementation Status
+ (instancetype)statusWithDict:(NSDictionary *)dict {
Status *status = [[self alloc] init];
// KVC 自动赋值
[status setValuesForKeysWithDictionary:dict];
return status;
}
// 必须重写这个方法,否则字典中有模型没有的 key 时会崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
// 空实现,忽略不认识的 key
}
@end
Runtime 方式(完整实现,支持嵌套模型)
这是本文的重点,给出一个支持一级转换、二级转换(字典中嵌套字典转模型)、三级转换(数组中字典转模型)的完整实现。
// NSObject+Model.h
#import <Foundation/Foundation.h>
@protocol ModelProtocol <NSObject>
@optional
// 返回字典中数组字段对应的模型类名
// 如:@{@"users": @"User", @"comments": @"Comment"}
+ (NSDictionary *)arrayContainModelClass;
@end
@interface NSObject (Model) <ModelProtocol>
+ (instancetype)modelWithDict:(NSDictionary *)dict;
@end
// NSObject+Model.m
#import "NSObject+Model.h"
#import <objc/runtime.h>
@implementation NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict {
if (!dict || ![dict isKindOfClass:[NSDictionary class]]) {
return nil;
}
// 1. 创建模型对象
id model = [[self alloc] init];
// 2. 用 Runtime 遍历模型的所有属性
unsigned int count = 0;
// class_copyPropertyList 获取所有属性(注意:是属性,不是成员变量)
// 如果用 class_copyIvarList 获取的是成员变量,名字带下划线 _name
objc_property_t *propertyList = class_copyPropertyList(self, &count);
for (unsigned int i = 0; i < count; i++) {
// 获取属性
objc_property_t property = propertyList[i];
// 获取属性名(C 字符串转 OC 字符串)
const char *propertyNameC = property_getName(property);
NSString *propertyName = [NSString stringWithUTF8String:propertyNameC];
// 根据属性名去字典中取 value
id value = dict[propertyName];
// 如果字典中没有这个 key,跳过
if (!value || value == [NSNull null]) {
continue;
}
// ====== 二级转换:value 是字典,需要转成嵌套模型 ======
if ([value isKindOfClass:[NSDictionary class]]) {
// 获取属性的类型编码,如 @"User"、@"NSDictionary"
// property_getAttributes 返回的格式如:T@"User",&,V_user
const char *attributes = property_getAttributes(property);
NSString *attrString = [NSString stringWithUTF8String:attributes];
// 从类型编码中提取类名
// 格式:T@"User" → 提取 User
NSString *typeName = [self extractTypeNameFromAttributes:attrString];
if (typeName && ![typeName hasPrefix:@"NS"]) {
// 是自定义模型类,递归转换
Class modelClass = NSClassFromString(typeName);
if (modelClass) {
value = [modelClass modelWithDict:value];
}
}
}
// ====== 三级转换:value 是数组,数组中是字典,需要转成模型数组 ======
if ([value isKindOfClass:[NSArray class]]) {
// 检查模型类是否实现了 arrayContainModelClass 协议方法
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
NSDictionary *arrayClassMap = [self arrayContainModelClass];
NSString *modelClassName = arrayClassMap[propertyName];
if (modelClassName) {
Class itemClass = NSClassFromString(modelClassName);
if (itemClass) {
NSMutableArray *modelArray = [NSMutableArray array];
for (NSDictionary *itemDict in value) {
if ([itemDict isKindOfClass:[NSDictionary class]]) {
id itemModel = [itemClass modelWithDict:itemDict];
[modelArray addObject:itemModel];
}
}
value = modelArray;
}
}
}
}
// 3. 用 KVC 给模型属性赋值
[model setValue:value forKey:propertyName];
}
// 释放属性列表(class_copyPropertyList 返回的是 malloc 的内存,需要手动释放)
free(propertyList);
return model;
}
// 从属性的类型编码中提取类名
// 输入:T@"User",&,V_user → 输出:User
+ (NSString *)extractTypeNameFromAttributes:(NSString *)attributes {
if (!attributes || attributes.length == 0) {
return nil;
}
// 查找第一个引号的位置
NSRange firstQuote = [attributes rangeOfString:@"\""];
if (firstQuote.location == NSNotFound) {
return nil;
}
// 从第一个引号后开始截取
NSString *subString = [attributes substringFromIndex:firstQuote.location + 1];
// 查找第二个引号的位置
NSRange secondQuote = [subString rangeOfString:@"\""];
if (secondQuote.location == NSNotFound) {
return nil;
}
// 截取两个引号之间的内容,就是类名
return [subString substringToIndex:secondQuote.location];
}
@end
使用示例
// 模型定义
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@interface Status : NSObject
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) User *user; // 嵌套模型
@property (nonatomic, strong) NSArray *comments; // 数组中嵌套模型
@end
@implementation Status
// 实现协议方法,告诉数组中字段对应的模型类名
+ (NSDictionary *)arrayContainModelClass {
return @{@"comments": @"Comment"};
}
@end
// 字典转模型
NSDictionary *dict = @{
@"text": @"这是一条微博",
@"user": @{@"name": @"张三", @"age": @20},
@"comments": @[
@{@"content": @"评论1", @"likeCount": @10},
@{@"content": @"评论2", @"likeCount": @20}
]
};
Status *status = [Status modelWithDict:dict];
NSLog(@"%@, %@, %lu", status.text, status.user.name, (unsigned long)status.comments.count);
MJExtension / YYModel 底层原理对比
| 框架 | 底层核心 | 特点 |
|---|---|---|
| MJExtension | Runtime 遍历属性 + KVC 赋值 | 简单易用,支持 key 映射、嵌套模型、数组模型,性能中等 |
| YYModel | Runtime 遍历属性 + 直接 ivar 赋值(不用 KVC) | 性能极高,支持 JSON 双向转换、key 映射、黑名单白名单,代码更复杂 |
| 系统 KVC | setValuesForKeysWithDictionary: |
最简单,但不支持嵌套模型,key 不对应会崩溃 |
核心思路都是一样的:Runtime 遍历属性 → 根据属性名取字典 value → 赋值给模型。区别在于赋值方式(KVC vs 直接 ivar)、支持的高级特性(key 映射、嵌套转换等)和性能优化。
六、其他 Runtime 实战场景
1. 自动归档解档(NSCoding)
利用 Runtime 遍历所有属性,自动实现 encodeWithCoder: 和 initWithCoder:,不需要手动为每个属性写归档代码:
@implementation NSObject (Coding)
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(ivarList);
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivarList);
}
return self;
}
@end
2. 获取/修改私有属性
利用 KVC 可以访问对象的私有属性(即使在 .m 中声明的也可以):
// 获取 UITextField 的占位文字 Label(私有属性 _placeholderLabel)
UILabel *placeholderLabel = [textField valueForKey:@"_placeholderLabel"];
placeholderLabel.textColor = [UIColor redColor];
// 修改 UIPageControl 的分页指示器图片(私有属性)
[pageControl setValue:[UIImage imageNamed:@"dot_normal"] forKey:@"_pageImage"];
[pageControl setValue:[UIImage imageNamed:@"dot_current"] forKey:@"_currentPageImage"];
注意:访问私有属性有 App Store 审核风险,且系统版本升级后私有属性名可能变化,导致崩溃。仅在调试或企业内部应用中谨慎使用。
3. 字典转模型属性自动生成(开发辅助)
开发时拿到一个复杂的 JSON,手动写模型属性很慢,可以用 Runtime 遍历字典自动生成属性字符串:
+ (void)generatePropertyStringFromDict:(NSDictionary *)dict {
NSMutableString *result = [NSMutableString string];
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSString *type;
if ([obj isKindOfClass:NSString.class]) {
type = @"NSString *";
} else if ([obj isKindOfClass:NSArray.class]) {
type = @"NSArray *";
} else if ([obj isKindOfClass:NSDictionary.class]) {
type = @"NSDictionary *";
} else if ([obj isKindOfClass:NSNumber.class]) {
// NSNumber 可能是 int、float、BOOL,需要根据实际情况判断
const char *objCType = [obj objCType];
if (strcmp(objCType, @encode(BOOL)) == 0) {
type = @"BOOL ";
} else if (strcmp(objCType, @encode(int)) == 0) {
type = @"NSInteger ";
} else {
type = @"CGFloat ";
}
} else {
type = @"id ";
}
if ([type hasSuffix:@"*"]) {
[result appendFormat:@"@property (nonatomic, strong) %@%@;\n", type, key];
} else {
[result appendFormat:@"@property (nonatomic, assign) %@%@;\n", type, key];
}
}];
NSLog(@"%@", result);
}
七、Swift 版本对照
Runtime 在 Swift 中的使用有一些限制和差异:
1. Swift 中使用 Runtime 的前提
Swift 的方法调用默认是静态派发(编译期确定),不是动态消息发送。要让 Swift 的方法支持 Runtime 动态特性,需要:
- 类继承自
NSObject。 - 方法用
@objc标记,让 Objective-C Runtime 可见。 - 方法用
dynamic标记,强制使用动态派发(支持 Method Swizzling)。
class MyClass: NSObject {
@objc dynamic func originalMethod() {
// 这个方法支持 Runtime 动态派发,可以被 Swizzle
}
}
2. Method Swizzling in Swift
extension UIViewController {
// Swift 中用 @objc 标记的类方法,在类加载时调用
// 注意:Swift 中 +load 方法不能被重写,需要用 @objc 标记的 initialize 或在 AppDelegate 中手动调用
static let swizzle: Void = {
let originalSelector = #selector(viewDidAppear(_:))
let swizzledSelector = #selector(ch_viewDidAppear(_:))
let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector)
let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector)
if let original = originalMethod, let swizzled = swizzledMethod {
method_exchangeImplementations(original, swizzled)
}
}()
@objc func ch_viewDidAppear(_ animated: Bool) {
ch_viewDidAppear(animated) // 调用原方法
print("[Runtime] 页面浏览: \(type(of: self))")
}
}
// 在 AppDelegate 中触发 Swizzle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIViewController.swizzle
return true
}
3. 关联对象 in Swift
import ObjectiveC
extension UIButton {
private struct AssociatedKeys {
static var clickBlock = "clickBlock"
}
var clickBlock: ((UIButton) -> Void)? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.clickBlock) as? (UIButton) -> Void
}
set {
objc_setAssociatedObject(self, &AssociatedKeys.clickBlock, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
Swift 中更推荐用 Swift 的特性(如
extension计算属性、associatedtype、协议扩展等)来替代 Runtime,只有在需要和 Objective-C 交互或实现某些动态特性时才使用 Runtime。
八、常见问题与坑
问题 1:Method Swizzling 会造成递归死循环吗?
答案:如果写法正确就不会。关键是在自定义方法中调用交换后的方法名(如 ch_imageNamed:),而不是原方法名(imageNamed:)。因为交换后,调用 ch_imageNamed: 实际执行的是系统的 imageNamed: 实现,不会递归。
如果在自定义方法中调用了原方法名 imageNamed:,就会递归调用自己,造成死循环崩溃。
问题 2:分类中能添加成员变量吗?
答案:不能直接添加。分类的内存布局在编译期就确定了,不能再增加 ivar。但可以通过关联对象(objc_setAssociatedObject)实现"添加属性"的效果,本质是给对象动态绑定键值对,不是真正的成员变量。
问题 3:class_copyPropertyList 和 class_copyIvarList 有什么区别?
| 方法 | 获取的内容 | 名字格式 |
|---|---|---|
class_copyPropertyList |
所有 @property 声明的属性 |
name(不带下划线) |
class_copyIvarList |
所有成员变量(包括 @property 自动生成的 _name 和手动声明的 ivar) | _name(带下划线) |
字典转模型推荐用 class_copyPropertyList,因为只需要转换声明了的属性,不需要转换手动声明的私有 ivar。
问题 4:class_copyPropertyList 返回的内存需要释放吗?
答案:需要。class_copyPropertyList、class_copyIvarList、class_copyMethodList 等 copy 系列函数返回的是 malloc 分配的内存,使用完后必须调用 free() 释放,否则会造成内存泄漏。
问题 5:动态添加方法后,方法会一直存在吗?
答案:是的。class_addMethod 是给类添加方法,一旦添加成功,这个类的所有后续实例都能响应这个方法,直到 App 进程结束。不是只对当前实例有效。
问题 6:Method Swizzling 有什么风险?
- 审核风险:某些 Swizzling 系统敏感方法可能被 App Store 拒绝。
- 版本兼容风险:系统版本升级后,被交换的方法实现可能变化,导致 Swizzling 失效或崩溃。
- 调试困难:方法交换后,调用栈和预期不一致,排查问题困难。
- 冲突风险:多个分类都交换了同一个方法,可能互相覆盖,导致只有最后一个生效。
- 性能影响:Swizzling 本身不影响性能,但自定义方法中添加的逻辑(如埋点、日志)可能影响性能。
问题 7:关联对象会造成内存泄漏吗?
答案:不会自动泄漏,但需要注意:
- 关联对象的内存管理由关联策略决定(
OBJC_ASSOCIATION_RETAIN会强引用,OBJC_ASSOCIATION_ASSIGN是弱引用)。 - 对象被销毁时,所有关联对象会自动释放。
- 但如果关联对象是 Block,且 Block 中强引用了 self,会造成循环引用(Block 强引用 self,self 通过关联对象强引用 Block),需要用
__weak解决。
九、总结
Runtime 在实际开发中的五大高频使用场景:
| 场景 | 核心 API | 典型用途 |
|---|---|---|
| 发送消息 | objc_msgSend |
方法调用的本质,理解动态性 |
| 方法交换(Swizzling) | method_exchangeImplementations |
埋点 AOP、防崩溃、给系统方法加功能 |
| 动态添加方法 | resolveInstanceMethod: + class_addMethod |
动态方法解析、防止未实现方法崩溃、消息转发 |
| 关联对象 | objc_setAssociatedObject / objc_getAssociatedObject |
给分类添加属性、绑定额外数据 |
| 字典转模型 | class_copyPropertyList + KVC |
MJExtension/YYModel 底层原理,自动字典转模型 |
其他实战场景:自动归档解档、获取/修改私有属性、自动生成模型属性代码。
核心思想:Runtime 让 Objective-C 具备了动态性——可以在运行时修改类的行为、添加方法、交换实现、关联数据。理解这些使用场景,不仅能写出更灵活的代码,也是 iOS 面试的高频考点。

浙公网安备 33010602011771号