延伸问题

3 阅读9分钟

读完前两篇后,这些问题自然会冒出来。这里逐一回答,每个问题都尽量说清楚"为什么"。


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_initmap_imagesload_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 要注意什么?

  1. 必须在 +load 里执行,不能在 +initialize(initialize 是懒加载,时机不确定)
  2. dispatch_once 保证只执行一次
  3. 注意调用 [self my_viewDidLoad] 实际上是在调原来的 viewDidLoad(因为 IMP 已经交换了)
  4. 继承链问题:子类 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 兼容(内存布局完全一致)

总结:读完这三篇你应该理解的核心

  1. 类是结构体objc_class 就是一个 C++ struct,所有 OC 的"面向对象"都建立在这上面
  2. isa 是钥匙:方法查找靠 isa 找到类,类靠 superclass 找到父类
  3. 方法调用是查表:先查缓存(哈希表),再查方法列表(二分查找),再往上走继承链
  4. 引用计数在 isa 里:extra_rc 19位,溢出才去 SideTable
  5. dealloc 是有序的:cxx_destruct → 关联对象 → 弱引用清零 → SideTable → free
  6. 元类让"类方法"逻辑自洽:类方法存在元类里,和实例方法的查找机制完全一样