Runtime 运行时机制
问题
OC 的 Runtime 是什么?isa 指针、类对象(Class)、元类对象(Meta Class)之间的关系是什么?
答案
Runtime 概述
Objective-C 是一门动态语言,很多操作从编译时推迟到了运行时:
- 方法调用 → 消息发送:
[obj method]编译为objc_msgSend(obj, @selector(method)) - 类型确定 → 运行时:对象类型在运行时通过 isa 指针确定
- 方法实现 → 可替换:Method Swizzling 可在运行时更换方法实现
isa 指针
每个 OC 对象的第一个成员变量都是 isa 指针:
isa 链路与 superclass 链路
- isa 链路(确定类型):实例 → 类 → 元类 → 根元类(NSObject 的元类)→ 自身
- superclass 链路(继承关系):子类 → 父类 → ... → NSObject → nil
- 特殊:根元类的 superclass 指向 NSObject 类对象(这就是为什么 NSObject 的实例方法可以被类方法调用到)
经典的 isa 与 superclass 关系图:
类对象存储内容
// objc_class 结构体(简化版)
struct objc_class {
Class isa; // 指向元类
Class superclass; // 指向父类
cache_t cache; // 方法缓存
class_data_bits_t bits; // 指向 class_rw_t
};
// class_rw_t(read-write)
struct class_rw_t {
const class_ro_t *ro; // 只读数据
method_list_t *methods; // 方法列表(包含分类方法)
property_list_t *properties;
protocol_list_t *protocols;
};
// class_ro_t(read-only,编译时确定)
struct class_ro_t {
const char *name; // 类名
method_list_t *baseMethods; // 原始方法列表
ivar_list_t *ivars; // 成员变量列表
uint32_t instanceSize; // 实例大小
};
Runtime API 常用操作
#import <objc/runtime.h>
// 获取类名
const char *name = class_getName([NSObject class]);
// 获取实例变量列表
unsigned int count;
Ivar *ivars = class_copyIvarList([UIView class], &count);
for (unsigned int i = 0; i < count; i++) {
NSLog(@"%s", ivar_getName(ivars[i]));
}
free(ivars);
// 动态添加方法
class_addMethod([MyClass class],
@selector(newMethod),
(IMP)my_function,
"v@:");
// 获取方法实现
IMP imp = class_getMethodImplementation([NSObject class],
@selector(description));
// 判断是否响应某方法
BOOL responds = class_respondsToSelector([NSObject class],
@selector(init));
Method Swizzling
在运行时交换两个方法的实现:
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSel = @selector(viewDidAppear:);
SEL swizzledSel = @selector(tracking_viewDidAppear:);
Method original = class_getInstanceMethod(cls, originalSel);
Method swizzled = class_getInstanceMethod(cls, swizzledSel);
// 先尝试添加(防止父类方法被影响)
BOOL added = class_addMethod(cls, originalSel,
method_getImplementation(swizzled),
method_getTypeEncoding(swizzled));
if (added) {
class_replaceMethod(cls, swizzledSel,
method_getImplementation(original),
method_getTypeEncoding(original));
} else {
method_exchangeImplementations(original, swizzled);
}
});
}
- (void)tracking_viewDidAppear:(BOOL)animated {
[self tracking_viewDidAppear:animated]; // 实际调用原始实现(方法已交换)
NSLog(@"Page: %@", NSStringFromClass([self class]));
}
@end
Method Swizzling 注意事项
- 必须在
+load中执行(+initialize可能不执行或多次执行) - 用
dispatch_once保证只执行一次 - 先
class_addMethod再交换,避免影响父类 - 命名加前缀防止冲突
关联对象
为已有类动态添加属性(Category 不能添加实例变量):
#import <objc/runtime.h>
static const void *kAssociatedKey = &kAssociatedKey;
@implementation UIView (Badge)
- (void)setBadgeValue:(NSString *)badgeValue {
objc_setAssociatedObject(self, kAssociatedKey, badgeValue,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)badgeValue {
return objc_getAssociatedObject(self, kAssociatedKey);
}
@end
关联策略枚举:
| 策略 | 等价属性修饰符 |
|---|---|
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 |
常见面试问题
Q1: 一个 NSObject 对象占多少内存?
答案:
NSObject *obj = [[NSObject alloc] init];
// 实际分配 16 字节(64位系统)
NSLog(@"%zd", malloc_size((__bridge const void *)obj)); // 16
// NSObject 只有一个 isa 指针(8字节),但内存分配最小 16 字节对齐
NSLog(@"%zd", class_getInstanceSize([NSObject class])); // 8
Q2: object_getClass(obj) 和 [obj class] 的区别?
答案:
object_getClass(obj):返回 isa 指向的类型。对于实例对象返回类对象,对于类对象返回元类[obj class]:返回类对象。对于实例对象和类对象都返回类对象(类方法+class是返回自身)
Q3: isKindOfClass 和 isMemberOfClass 的区别?
答案:
isKindOfClass:判断是否是某个类或其子类的实例isMemberOfClass:判断是否是某个类的实例(不包含子类)
NSMutableArray *arr = [NSMutableArray new];
[arr isKindOfClass:[NSArray class]]; // YES(子类也算)
[arr isMemberOfClass:[NSArray class]]; // NO(不是 NSArray 本身)
[arr isMemberOfClass:[NSMutableArray class]]; // YES
Q4: 为什么 Method Swizzling 必须在 +load 而不是 +initialize 中?
答案:
+load:类加载到内存时调用,每个类只调一次,且在 main 函数之前。子类、父类、Category 的 +load 都会调用+initialize:类第一次接收消息时调用,如果子类没实现会调父类的(可能多次触发),且可能永远不被调用
Swizzle 需要确保执行且只执行一次,+load + dispatch_once 是最安全的方式。
Q5: Runtime 在实际开发中有哪些应用?
答案:
- 埋点/AOP:Method Swizzling hook 页面生命周期
- JSON → Model:通过 Runtime 获取属性列表自动映射
- 热修复:JSPatch 基于消息转发实现
- Crash 防护:消息转发阶段拦截 unrecognized selector
- 字典模型转换:MJExtension 等框架核心原理