跳到主要内容

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 注意事项
  1. 必须在 +load 中执行+initialize 可能不执行或多次执行)
  2. dispatch_once 保证只执行一次
  3. class_addMethod 再交换,避免影响父类
  4. 命名加前缀防止冲突

关联对象

为已有类动态添加属性(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_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICstrong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMICcopy, nonatomic
OBJC_ASSOCIATION_RETAINstrong, atomic
OBJC_ASSOCIATION_COPYcopy, 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 在实际开发中有哪些应用?

答案

  1. 埋点/AOP:Method Swizzling hook 页面生命周期
  2. JSON → Model:通过 Runtime 获取属性列表自动映射
  3. 热修复:JSPatch 基于消息转发实现
  4. Crash 防护:消息转发阶段拦截 unrecognized selector
  5. 字典模型转换:MJExtension 等框架核心原理

相关链接