读完前两篇后,这些问题自然会冒出来。这里逐一回答,每个问题都尽量说清楚"为什么"。
Q1:[obj method] 底层发生了什么?(消息发送完整流程)
一句话答案
OC 的方法调用本质是发消息,编译器把 [obj method] 翻译成 objc_msgSend(obj, @selector(method))。
完整流程
[obj doSomething]
↓ 编译器翻译
objc_msgSend(obj, @selector(doSomething))
↓
1. 【快速路径】查 cache_t 哈希表
命中 → 直接跳转执行 IMP,结束
↓ 未命中
2. 【慢速路径】lookUpImpOrForward()
a. 在当前类的 method_list 里遍历查找
b. 找不到 → 沿 superclass 往上找
c. 找到 → 写入 cache,然后执行
↓ 还找不到(到 NSObject 都没有)
3. 【消息转发】
a. resolveInstanceMethod: / resolveClassMethod:
→ 动态添加方法的机会(返回 YES 则重新查找)
b. forwardingTargetForSelector:
→ 把消息转发给另一个对象
c. methodSignatureForSelector: + forwardInvocation:
→ 完全自定义转发(最慢,但最灵活)
d. 以上都没处理 → 抛出 unrecognized selector 异常
objc_msgSend 为什么用汇编写?
因为它极其频繁(每次方法调用都走这里),而且需要:
- 在不知道参数类型/数量的情况下转发所有参数
- 函数调用约定要求参数在寄存器里,C 语言做不到"透明转发"
- 需要精确控制寄存器,避免破坏调用约定
ARM64 的 objc_msgSend 汇编大约 50 行,是 runtime 里最精心优化的代码。
Q2:方法查找时,方法列表是如何遍历的?
// 精简版
static method_t *search_method_list(const method_list_t *mlist, SEL sel) {
// 如果方法列表已排序(编译期会排序),用二分查找
if (mlist->isFixedUp()) {
return findMethodInSortedMethodList(sel, mlist);
}
// 否则线性遍历
for (auto& meth : *mlist) {
if (meth.name() == sel) return &meth;
}
return nil;
}
- 编译器会对方法列表排序(按 SEL 地址值排序)
- 运行时用二分查找,O(log n)
- Category 方法在运行时动态插入,插入后会重新排序
Q3:Category 的方法是怎么"加"进类里的?
编译期
Category 的方法被编译成一个独立的结构体 category_t:
struct category_t {
const char *name; // category 名字
classref_t cls; // 属于哪个类
struct method_list_t *instanceMethods; // 实例方法列表
struct method_list_t *classMethods; // 类方法列表
struct protocol_list_t *protocols; // 协议
struct property_list_t *instanceProperties; // 属性
};
运行时
App 启动时,_objc_init → map_images → load_images 流程里:
1. 遍历所有 category_t
2. 找到对应的类 objc_class
3. 把 category 的方法列表**插到** class_rw_t 的方法数组最前面
4. (不是替换,是插到最前面!)
为什么 Category 能"覆盖"原方法?
因为方法查找是从前往后找到第一个就停止。Category 的方法插在最前面,所以先被找到。
但原方法没有被删除,还可以通过 super 或 Method Swizzle 拿到。
多个 Category 都定义了同一个方法,谁赢?
编译顺序决定的——最后被编译(链接)的 Category 里的方法插在最前面,所以它"赢"。这就是为什么多个 Category 冲突时行为是不确定的。
Q4:Method Swizzle 的底层原理
你用的 API
Method original = class_getInstanceMethod(cls, @selector(viewDidLoad));
Method swizzled = class_getInstanceMethod(cls, @selector(my_viewDidLoad));
method_exchangeImplementations(original, swizzled);
底层做了什么
void method_exchangeImplementations(Method m1, Method m2) {
// 就是交换两个 method_t 的 IMP 字段
IMP m1_imp = m1->imp(false);
m1->setImp(m2->imp(false));
m2->setImp(m1_imp);
// 清空方法缓存!否则旧 IMP 还在 cache 里
flushCaches(nil, __func__, [](Class c){ return true; });
}
为什么 Swizzle 后要清缓存?
cache_t 里缓存的是 SEL → IMP 的映射。Swizzle 交换了 IMP,但 cache 里的旧映射还在。如果不清空,下次调用方法时命中缓存,还是走旧的 IMP。
所以 method_exchangeImplementations 内部会调用 flushCaches 把相关类的方法缓存全清掉。
Swizzle 要注意什么?
- 必须在
+load里执行,不能在+initialize(initialize 是懒加载,时机不确定) - dispatch_once 保证只执行一次
- 注意调用
[self my_viewDidLoad]实际上是在调原来的viewDidLoad(因为 IMP 已经交换了) - 继承链问题:子类 Swizzle 父类方法,要先用
class_addMethod检查子类自己有没有这个方法
Q5:retain / release 的底层实现
retain(引用计数 +1)
id objc_object::retain() {
// TaggedPointer 直接返回,不需要引用计数
if (isTaggedPointer()) return (id)this;
return rootRetain(false, RRVariant::Default);
}
ALWAYS_INLINE id objc_object::rootRetain(...) {
// 先尝试在 isa.extra_rc 里加
uintptr_t oldBits = LoadExclusive(&isa.bits);
uintptr_t newBits;
do {
// extra_rc + 1
newBits = oldBits + RC_ONE; // RC_ONE 是 extra_rc 的最低位掩码
if (slowpath(newBits & RC_HALF)) {
// extra_rc 快溢出了,一半移到 SideTable
return rootRetain_overflow(...);
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldBits, newBits)));
return (id)this;
}
release(引用计数 -1)
bool objc_object::rootRelease(...) {
uintptr_t oldBits = LoadExclusive(&isa.bits);
uintptr_t newBits;
do {
newBits = oldBits - RC_ONE; // extra_rc - 1
if (slowpath(newBits & RC_DEALLOCATING_MASK)) {
// 引用计数到0,触发 dealloc
return rootReleaseShouldDealloc(...);
}
} while (slowpath(!StoreExclusive(&isa.bits, &oldBits, newBits)));
return false;
}
为什么用 LoadExclusive/StoreExclusive?
这是 ARM64 的原子操作指令(LDXR/STXR),实现了无锁的 CAS(Compare And Swap) 。在多线程下修改引用计数是安全的,不需要加锁。
Q6:对象的 dealloc 流程(完整版)
// NSObject.mm
- (void)dealloc {
_objc_rootDealloc(self);
}
void _objc_rootDealloc(id obj) {
obj->rootDealloc();
}
void objc_object::rootDealloc() {
// 快速路径:如果这些条件都满足,直接 free
if (fastpath(isa.nonpointer // 是 non-pointer isa
&& !isa.weakly_referenced // 没有弱引用
&& !isa.has_assoc // 没有关联对象
&& !isa.has_cxx_dtor // 没有 C++ 析构
&& !isa.has_sidetable_rc)) // 引用计数没溢出到 SideTable
{
free(this); // 直接释放内存,非常快
return;
}
// 慢速路径
object_dispose(this);
}
id object_dispose(id obj) {
// 1. 调用 .cxx_destruct(清理 ARC strong 变量)
if (isa.has_cxx_dtor) {
object_cxxDestruct(obj);
}
// 2. 清理关联对象
if (isa.has_assoc) {
_object_remove_assocations(obj, ...);
}
// 3. 清零弱引用
if (isa.weakly_referenced) {
weak_clear_no_lock(&table.weak_table, (id)obj);
}
// 4. 清理 SideTable 里的引用计数记录
if (isa.has_sidetable_rc) {
table.refcnts.erase(obj);
}
// 5. 释放内存
free(obj);
}
.cxx_destruct 是什么?
编译器为每个有 strong 成员变量的类自动生成的方法,内容大概是:
- (void).cxx_destruct {
// 对每个 strong ivar 执行 objc_storeStrong(&ivar, nil)
// 相当于 self.name = nil; self.title = nil; ...
}
这就是 ARC 下不需要手写 dealloc 的原因——编译器帮你生成了。
Q7:AutoreleasePool 怎么工作的?
本质
AutoreleasePool 是一个栈式结构,每个 pool 在栈上插一个哨兵(POOL_BOUNDARY),autorelease 的对象入栈,pop 时从栈顶到哨兵之间的所有对象全部 release。
AutoreleasePoolPage
class AutoreleasePoolPage : private AutoreleasePoolPageData {
// 每个 Page 是 4096 字节(一个虚拟内存页)
// Page 之间用双向链表连接
id *next; // 指向下一个空位
// 剩余空间存放 autorelease 对象的指针
};
Page 内存布局(4096字节):
[header(56字节)][POOL_BOUNDARY][obj1][obj2][obj3]...[next指针]
@autoreleasepool {} 做了什么?
@autoreleasepool {
// 代码
}
// 编译后等同于:
void *pool = objc_autoreleasePoolPush();
// 代码
objc_autoreleasePoolPop(pool);
Push:在栈上插入POOL_BOUNDARY哨兵,返回哨兵地址Pop:从栈顶开始,对每个对象调用release,直到遇到哨兵
RunLoop 和 AutoreleasePool 的关系
主线程的 RunLoop 在每次循环开始时 Push 一个 pool,循环结束时 Pop。这意味着在一次 RunLoop 迭代内产生的 autorelease 对象,会在这次迭代结束时被释放。
Q8:load 和 initialize 的区别
+load | +initialize | |
|---|---|---|
| 调用时机 | App 启动,类被加载进内存时 | 类第一次收到消息时 |
| 是否走 objc_msgSend | 不走,直接调用函数指针 | 走 |
| 是否继承 | 不继承,父类和子类各自调用 | 继承(子类没实现时调父类的) |
| 线程安全 | 是,但所有类的 load 串行执行 | 是,runtime 加锁保证只调一次 |
| Category 是否调用 | 是,Category 的 load 也会调 | Category 会覆盖类的 initialize |
为什么 Swizzle 要在 load 里?
因为 load 是在类被加载时调用,时机早、确定、每个类只一次。initialize 是懒加载,时机不确定,而且如果子类没有实现,父类的 initialize 会被多次调用(每个触发的子类调一次)。
Q9:为什么不能在 OC 类里定义同名不同参数的方法(方法重载)?
因为 OC 的方法查找是靠 SEL(字符串)来的,不是靠完整的函数签名。
- (void)doSomething:(int)a;
- (void)doSomething:(NSString *)a;
// 这两个的 SEL 都是 @selector(doSomething:),是同一个!
// 无法区分
C++ 可以重载是因为编译器会对函数名做 Name Mangling,把参数类型编码进函数名里,变成不同的符号。OC 没有这个机制。
Q10:class_getInstanceMethod vs class_getMethodImplementation
// 返回 method_t 结构体指针(含 SEL、types、IMP)
Method class_getInstanceMethod(Class cls, SEL name);
// 只返回 IMP(函数指针)
IMP class_getMethodImplementation(Class cls, SEL name);
区别:
class_getInstanceMethod:只在当前类查找,不走消息转发class_getMethodImplementation:会走完整的消息发送流程(包括转发),找不到方法会返回_objc_msgForward(消息转发入口)而不是 nil
Q11:isKindOfClass vs isMemberOfClass
// 检查对象是否是某个类或其子类的实例
- (BOOL)isKindOfClass:(Class)cls;
// 检查对象是否恰好是某个类的实例(不含子类)
- (BOOL)isMemberOfClass:(Class)cls;
底层实现:
- (BOOL)isKindOfClass:(Class)cls {
// 沿着 isa 链和继承链往上找
for (Class tcls = self->getIsa(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isMemberOfClass:(Class)cls {
// 只比较直接的类
return self->getIsa() == cls;
}
一个经典面试题
BOOL res1 = [[NSObject class] isKindOfClass:[NSObject class]]; // YES
BOOL res2 = [[NSObject class] isMemberOfClass:[NSObject class]]; // NO
// 解析:
// [NSObject class] 返回的是 NSObject 类对象
// 类对象的 isa 指向元类(Meta-NSObject),不是 NSObject 本身
// isKindOfClass 沿 isa 链找:Meta-NSObject → Meta-NSObject(根元类 isa 自指)
// → superclass → NSObject → 找到了!所以 YES
// isMemberOfClass 只比较直接 isa:Meta-NSObject ≠ NSObject,所以 NO
Q12:objc_object 和 NSObject 的关系
// C++ 层
struct objc_object {
isa_t isa;
};
struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
};
// OC 层
@interface NSObject {
Class isa; // 就是 objc_object.isa
}
NSObject 在 OC 层的定义和 objc_object 在 C++ 层的定义是同一块内存的两种视角:
- C++ runtime 通过
objc_object操作 - OC 代码通过
NSObject操作 - 两者 binary 兼容(内存布局完全一致)
总结:读完这三篇你应该理解的核心
- 类是结构体:
objc_class就是一个 C++ struct,所有 OC 的"面向对象"都建立在这上面 - isa 是钥匙:方法查找靠 isa 找到类,类靠 superclass 找到父类
- 方法调用是查表:先查缓存(哈希表),再查方法列表(二分查找),再往上走继承链
- 引用计数在 isa 里:extra_rc 19位,溢出才去 SideTable
- dealloc 是有序的:cxx_destruct → 关联对象 → 弱引用清零 → SideTable → free
- 元类让"类方法"逻辑自洽:类方法存在元类里,和实例方法的查找机制完全一样