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 有什么风险?

  1. 审核风险:某些 Swizzling 系统敏感方法可能被 App Store 拒绝。
  2. 版本兼容风险:系统版本升级后,被交换的方法实现可能变化,导致 Swizzling 失效或崩溃。
  3. 调试困难:方法交换后,调用栈和预期不一致,排查问题困难。
  4. 冲突风险:多个分类都交换了同一个方法,可能互相覆盖,导致只有最后一个生效。
  5. 性能影响: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 面试的高频考点。


posted @ 2017-04-17 08:36  Mr.陳  阅读(560)  评论(0)    收藏  举报