融会贯通 · 深入浅出 · 横向对比 · 纵向深度 · 底层源码级
目录
第一章:iOS 最全面最底层基础八股文
一、Objective-C 对象模型与 Runtime
1.1 对象的本质——从源码到内存
OC 对象的 C 语言底层定义:
// objc-private.h
struct objc_object {
isa_t isa; // 联合体,不再是简单指针
};
typedef struct objc_class *Class;
struct objc_class : objc_object {
Class superclass;
cache_t cache; // 方法缓存,核心性能关键
class_data_bits_t bits; // 指向 class_rw_t
};
**isa_t 联合体的完整 bit-field 定义(arm64): **
union isa_t {
Class cls;
uintptr_t bits;
struct {
uintptr_t nonpointer : 1; // 0=纯指针 1=优化的isa
uintptr_t has_assoc : 1; // 是否有关联对象
uintptr_t has_cxx_dtor : 1; // 是否有C++/OC析构器(.cxx_destruct)
uintptr_t shiftcls : 33; // 类指针地址(右移3位存储,因为类地址8字节对齐)
uintptr_t magic : 6; // 固定值0x1a,用于标识是否已初始化
uintptr_t weakly_referenced : 1; // 是否有weak指针指向
uintptr_t unused : 1; // 未使用
uintptr_t has_sidetable_rc : 1; // 引用计数是否溢出到SideTable
uintptr_t extra_rc : 19; // 引用计数值-1(能存储0~524287)
};
};
**为什么 shiftcls 只需要 33 位就能存储 64 位指针? **
-
类对象在内存中是8字节对齐的,所以地址的后 3 位一定为 0
-
高位一般也用不到(用户空间地址不会超过 2^36)
-
所以存储时右移 3 位,读取时左移 3 位还原,33 位足够
-
具体取出 class 指针:
(isa.bits >> 3) << 3即isa.bits & ISA_MASK -
arm64 的
ISA_MASK = 0x0000000ffffffff8ULL
**extra_rc 的工作方式: **
-
对象初始化时
extra_rc = 0(表示引用计数为 1) -
retain时extra_rc++,如果溢出(超过 19 bit 最大值 524287):
- 将 extra_rc 的一半搬移到 SideTable 的 refcnts 哈希表中
- 设置 has_sidetable_rc = 1
- extra_rc 设为原来的一半(留出空间继续增长,避免频繁操作 SideTable)
release时extra_rc--,如果变为负数:
- 从 SideTable 借一批回来
- 如果 SideTable 中也为 0 → dealloc
1.2 class_rw_t 与 class_ro_t 的完整结构
// class_ro_t:编译期生成,只读
struct class_ro_t {
uint32_t flags; // 标志位(是否为元类、是否为根类等)
uint32_t instanceStart; // 成员变量起始偏移量
uint32_t instanceSize; // 实例大小(包含 isa + 所有成员变量)
uint32_t reserved; // 保留
union {
const uint8_t *ivarLayout; // ivar 布局(用于 GC,ARC 下也有用)
Class nonMetaclass;
};
explicit_atomic<const char *> name; // 类名
WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods; // 编译期的方法列表
protocol_list_t *baseProtocols; // 编译期的协议列表
const ivar_list_t *ivars; // 成员变量列表(运行时不可修改!)
const uint8_t *weakIvarLayout; // weak ivar 布局
property_list_t *baseProperties; // 属性列表
};
// class_rw_t:运行时创建
struct class_rw_t {
uint32_t flags;
uint16_t witness;
explicit_atomic<uintptr_t> ro_or_rw_ext; // 指向 class_ro_t 或 class_rw_ext_t
Class firstSubclass;
Class nextSiblingClass;
// 以下方法用于获取方法/属性/协议列表
// 如果没有 class_rw_ext_t,直接从 class_ro_t 获取
// 如果有 class_rw_ext_t,从 ext 获取(ext 包含动态添加的)
};
// class_rw_ext_t:iOS 14+ 优化,按需创建
// 只有当运行时需要修改类信息时才创建(如 category 加载、class_addMethod 等)
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods; // 方法列表(二维数组:多个 method_list_t)
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
char *demangledName;
uint32_t version;
};
**iOS 14 class_rw_ext_t 优化的意义: **
-
实测发现约 90% 的类在运行时不会被动态修改
-
之前每个类都要创建完整的
class_rw_t(含方法/属性/协议数组),浪费内存 -
优化后,
class_rw_t只存基本信息,ro_or_rw_ext默认指向class_ro_t -
只有需要动态修改时才创建
class_rw_ext_t -
全系统节省约 14MB 内存
1.3 方法缓存 cache_t 的底层实现
struct cache_t {
// arm64:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // buckets 指针 + mask
// _bucketsAndMaybeMask 的高 48 位存储 buckets 指针
// 低 16 位存储 mask(buckets 数量 - 1)
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 真正的 mask
uint16_t _flags;
uint16_t _occupied; // 已使用的 bucket 数量
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 共享缓存中的预优化缓存
};
};
struct bucket_t {
// arm64:
explicit_atomic<uintptr_t> _imp; // 方法实现指针(PAC 签名加密的)
explicit_atomic<SEL> _sel; // 方法选择器
};
**缓存查找的汇编实现流程(arm64,CacheLookup 宏): **
1. 从 isa 中取出 class 指针
2. 从 class 偏移固定字节取出 cache_t
3. 从 cache_t 中取出 buckets 指针和 mask
4. 计算 index = selector & mask
5. 从 buckets[index] 取出 bucket
6. 比较 bucket._sel 是否等于目标 selector
7. 相等 → CacheHit:直接跳转到 bucket._imp(br x17)
8. 不相等 → 线性探测:index = (index - 1) & mask(注意是减一,往前找)
9. 如果 bucket._sel == 0 → CacheMiss:跳转到 __objc_msgSend_uncached
10. 如果绕了一圈回到起点 → CacheMiss
**为什么线性探测是 index - 1(往前找)而不是 + 1? **
-
arm64 下的 bucket_t 结构是 sel 在高位、imp 在低位
-
从后往前遍历可以利用 ldp 指令同时加载两个连续的 64 位值,减少内存访问次数
-
同时方便判断数组边界
**扩容策略细节: **
-
初始容量:4(INIT_CACHE_SIZE)
-
当
_occupied达到容量的 7/8(arm64)或 3/4(其他)时扩容 -
新容量 = 旧容量 × 2(但有最大限制)
-
扩容时完全丢弃旧缓存,不做 rehash
-
为什么不 rehash?因为方法调用具有时间局部性,最近调用的方法更可能再被调用,旧缓存的价值不大
1.4 消息发送机制——从汇编到 C++
**objc_msgSend 的完整汇编实现(arm64 简化版): **
// 入口:x0 = receiver, x1 = selector
_objc_msgSend:
// 1. 检查 receiver 是否为 nil 或 tagged pointer
cmp p0, #0
b.le LNilOrTagged
// 2. 从 receiver 取出 isa
ldr p13, [x0] // p13 = isa
// 3. 从 isa 中取出 class(ISA_MASK)
and p16, p13, #ISA_MASK // p16 = class
// 4. CacheLookup(内联宏,见上面的描述)
CacheLookup NORMAL
LNilOrTagged:
// tagged pointer 走特殊路径
// nil receiver 直接返回 0
**lookUpImpOrForward(C++ 函数,缓存未命中时调用): **
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior) {
IMP imp = nil;
Class curClass;
// 1. 确保类已经 realized(class_rw_t 已创建)
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftMaybeRelock(cls, runtimeLock);
}
// 2. 确保类已经 initialized
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.lock();
// 3. 再次检查缓存(加锁期间可能被其他线程填充)
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 4. 在当前类的方法列表中查找
{
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
}
// 5. 沿 superclass 链查找
for (curClass = cls->getSuperclass();
curClass != nil;
curClass = curClass->getSuperclass()) {
// 先查父类缓存
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp == (IMP)_objc_msgForward_impcache) break;
goto done;
}
// 再查父类方法列表
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
}
// 6. 未找到:进入动态方法解析
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER; // 确保只解析一次
if (resolveMethod_locked(inst, sel, cls, behavior)) {
goto retry; // 解析成功,重走查找流程
}
}
// 7. 进入消息转发
imp = (IMP)_objc_msgForward_impcache;
done:
// 8. 将找到的 IMP 缓存到当前类的 cache_t 中
if (imp != (IMP)_objc_msgForward_impcache) {
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
runtimeLock.unlock();
return imp;
}
**方法列表的查找算法细节: **
static method_t *search_method_list(const method_list_t *mlist, SEL sel) {
if (mlist->isFixedUp()) {
// 已排序:使用二分查找
// 二分查找的 key 是 selector 的地址值(SEL 本质是 const char*)
return findMethodInSortedMethodList(sel, mlist);
} else {
// 未排序:线性查找
for (auto& meth : *mlist) {
if (meth.name() == sel) return &meth;
}
}
return nil;
}
**为什么先查缓存再查方法列表? **
-
缓存查找在汇编中实现,仅需 3
5 条指令(约 12 个 CPU 周期) -
方法列表查找需要加锁、遍历(即使二分也是 O(logN))、可能还需遍历父类
-
实测方法调用的****缓存命中率超过 90%** **,大多数调用直接在汇编中完成
1.5 动态方法解析的底层
static void resolveMethod_locked(id inst, SEL sel, Class cls, int behavior) {
if (!cls->isMetaClass()) {
// 实例方法解析
resolveInstanceMethod(inst, sel, cls);
} else {
// 类方法解析
resolveClassMethod(inst, sel, cls);
// 如果类方法解析失败,尝试实例方法解析
// 因为类方法存储在元类中,元类也是对象
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
}
static void resolveInstanceMethod(id inst, SEL sel, Class cls) {
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 检查元类中是否实现了 resolveInstanceMethod:
if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA())) {
return; // 没有实现,直接返回
}
// 调用 +resolveInstanceMethod:
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 再次查找(开发者可能在 resolveInstanceMethod: 中调用了 class_addMethod)
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
// ...
}
1.6 消息转发的底层( __ ***forwarding* **__)
___forwarding___ 是 CoreFoundation 中的私有函数,伪代码还原:
int ___forwarding___(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
Class cls = object_getClass(receiver);
// 1. 快速转发
if (class_respondsToSelector(cls, @selector(forwardingTargetForSelector:))) {
id forwardTarget = [receiver forwardingTargetForSelector:sel];
if (forwardTarget && forwardTarget != receiver) {
// 将 receiver 替换为 forwardTarget,重新走 objc_msgSend
return objc_msgSend(forwardTarget, sel, ...);
}
}
// 2. 完整转发
if (class_respondsToSelector(cls, @selector(methodSignatureForSelector:))) {
NSMethodSignature *sig = [receiver methodSignatureForSelector:sel];
if (sig) {
// 创建 NSInvocation
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
[inv setTarget:receiver];
[inv setSelector:sel];
// 设置参数...
if (class_respondsToSelector(cls, @selector(forwardInvocation:))) {
[receiver forwardInvocation:inv];
// 取出返回值并返回
return;
}
}
}
// 3. 都没处理 → 崩溃
[receiver doesNotRecognizeSelector:sel];
__builtin_unreachable();
}
**NSInvocation 底层原理: **
-
NSInvocation 封装了一次方法调用的所有信息:target、selector、参数、返回值
-
底层维护一块参数缓冲区(
_frame),按照方法签名的格式存储参数 -
invoke时使用objc_msgSend或objc_msgSend_stret发送消息 -
参数的类型编码使用
@encode()格式(如@表示 id,:表示 SEL,i表示 int)
1.7 Method Swizzling 的底层实现
void method_exchangeImplementations(Method m1, Method m2) {
mutex_locker_t lock(runtimeLock);
// 交换两个 Method 结构体中的 imp 字段
IMP imp1 = m1->imp(false);
IMP imp2 = m2->imp(false);
SEL sel1 = m1->name();
SEL sel2 = m2->name();
m1->setImp(imp2);
m2->setImp(imp1);
// 关键:清除相关类的方法缓存!
// 因为缓存中可能还存着旧的 IMP
flushCaches(nil, __func__, [sel1, sel2](Class c) {
return c->cache.occupied() > 0;
});
// 更新自定义 RR/AWZ 标志
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
**关键细节:为什么要清除缓存? **
-
cache_t中存储的是SEL → IMP映射 -
交换 IMP 后,缓存中的 IMP 指向了错误的函数
-
如果不清除缓存,已缓存的方法调用仍然走旧的 IMP
-
flushCaches(nil, ...)传 nil 表示清除所有类的缓存(因为子类可能也缓存了)
1.8 关联对象的完整数据结构
// 全局关联对象管理器
class AssociationsManager {
static AssociationsHashMap *_map; // 全局唯一的哈希表
static StripedLock _lock; // 分离锁(不是单一锁,减少竞争)
};
// 一级哈希表:对象地址 → ObjectAssociationMap
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>
AssociationsHashMap;
// 二级哈希表:key → ObjcAssociation
typedef DenseMap<const void *, ObjcAssociation>
ObjectAssociationMap;
// 关联值
class ObjcAssociation {
uintptr_t _policy; // 关联策略(ASSIGN/RETAIN/COPY + NONATOMIC/ATOMIC)
id _value; // 关联的值
};
**关联对象的设置流程(objc_setAssociatedObject): **
1. 创建 AssociationsManager(获取锁)
2. 获取全局 AssociationsHashMap
3. 以对象地址的 DisguisedPtr 为 key,查找 ObjectAssociationMap
4. 如果不存在,创建新的 ObjectAssociationMap 并插入
5. 以传入的 key 为 key,创建 ObjcAssociation(policy + value)
6. 插入到 ObjectAssociationMap 中
7. 设置对象的 isa.has_assoc = 1(标记有关联对象)
8. 释放旧的关联值(如果有)
9. 释放锁
**关联对象的清除(对象释放时): **
dealloc → objc_destructInstance → _object_remove_assocations
→ 在 AssociationsHashMap 中以对象地址为 key 查找
→ 取出该对象的所有关联值
→ 释放锁
→ 逐个 release 关联值(在锁外释放,避免死锁)
1.9 Category 的底层加载过程
**编译后的 Category 结构体: **
struct category_t {
const char *name; // 分类名
classref_t cls; // 宿主类
WrappedPtr<method_list_t> instanceMethods; // 实例方法列表
WrappedPtr<method_list_t> classMethods; // 类方法列表
struct protocol_list_t *protocols; // 协议列表
struct property_list_t *instanceProperties; // 属性列表
struct property_list_t *_classProperties; // 类属性列表
method_list_t *methodsForMeta(bool isMeta) {
return isMeta ? classMethods : instanceMethods;
}
};
**Runtime 加载 Category 的流程: **
_objc_init
→ map_images(dyld 通知 Runtime 镜像被映射到内存)
→ _read_images
→ 遍历所有 category_t
→ 如果宿主类已经 realized:
→ attachCategories(cls, cats, ...)
→ 如果宿主类尚未 realized:
→ 存入 unattachedCategories 表,等待类 realize 时再加载
attachCategories:
1. 分配新的 method_list_t 数组(二维数组的形式)
2. 将所有 category 的方法列表按逆序添加(后编译的在前面)
3. 将新方法列表**插入到 class_rw_ext_t.methods 的最前面**
4. 触发 class_rw_ext_t 的创建(如果还没有)
5. 对属性列表和协议列表做同样操作
**为什么分类方法"覆盖"宿主类方法? **
-
实际上方法没有被覆盖,宿主类的方法仍然存在
-
只是分类的方法列表被插入到了
methods数组的前面 -
lookUpImpOrForward查找方法时是从前往后遍历方法列表数组 -
所以先找到分类的方法,就不会继续找宿主类的了
-
可以通过遍历方法列表找到原始方法并调用
1.10 +load 与 +initialize 的底层实现差异
** +load 的调用链: **
_objc_init
→ load_images(每次加载新的 image 时调用)
→ prepare_load_methods
→ 将所有类的 +load 方法添加到 loadable_classes 数组
(按继承层级排序,父类在前)
→ 将所有分类的 +load 方法添加到 loadable_categories 数组
→ call_load_methods
→ 先调用所有类的 +load(do while 循环)
→ (*load_method)(cls, @selector(load)) // 直接函数指针调用!
→ 再调用所有分类的 +load
→ (*load_method)(cat, @selector(load)) // 直接函数指针调用!
** +initialize 的调用链: **
objc_msgSend → lookUpImpOrForward
→ if (!cls->isInitialized())
→ initializeAndLeaveLocked
→ initializeAndMaybeRelock
→ initializeNonMetaClass
→ 递归初始化父类(如果父类未初始化)
→ callInitialize(cls)
→ ((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize))
// 走 objc_msgSend!所以会被分类覆盖
**关键区别的本质原因: **
-
+load是 IMP 直接调用((*load_method)(cls, SEL)),所以不走消息发送,分类不会覆盖 -
+initialize是 objc_msgSend 调用,走完整的消息发送流程,分类会覆盖
二、内存管理
2.1 SideTable 的完整结构
struct SideTable {
spinlock_t slock; // 自旋锁(实际是 os_unfair_lock)
RefcountMap refcnts; // 引用计数表(DenseMap<DisguisedPtr, size_t>)
weak_table_t weak_table; // 弱引用表
};
// 全局 SideTable 数组
static StripedMap<SideTable> SideTables;
// StripedMap 内部是一个固定大小的数组,64 个元素
// 通过对象地址哈希取模确定使用哪个 SideTable
// 哈希函数:((addr >> 4) ^ (addr >> 9)) % StripeCount
struct weak_table_t {
weak_entry_t *weak_entries; // hash 数组
size_t num_entries; // 已使用数量
uintptr_t mask; // 容量 - 1
uintptr_t max_hash_displacement; // 最大哈希冲突偏移量
};
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被弱引用的对象地址
// 存储弱引用指针(指向 weak 变量的指针的指针)
union {
struct {
weak_referrer_t *referrers; // 动态数组(超过4个时)
uintptr_t out_of_line_ness : 2;
uintptr_t num_refs : PTR_MINUS_2;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// 内联数组(4个以内时,避免额外分配内存)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; // WEAK_INLINE_COUNT = 4
};
};
};
**weak 指针置 nil 的完整流程(dealloc → clearDeallocating): **
void objc_object::clearDeallocating_slow() {
SideTable& table = SideTables()[this];
table.lock();
// 1. 清除弱引用
if (isa.weakly_referenced) {
// 在 weak_table 中以当前对象地址为 key 查找 weak_entry_t
weak_entry_t *entry = weak_entry_for_referent(&table.weak_table, this);
if (entry) {
// 遍历所有弱引用指针,置为 nil
weak_referrer_t *referrers = entry->referrers;
for (size_t i = 0; i < TABLE_SIZE(entry); i++) {
objc_object **referrer = referrers[i];
if (referrer) {
if (*referrer == this) {
*referrer = nil; // 将 weak 变量置 nil
}
}
}
// 从 weak_table 中移除该 entry
weak_entry_remove(&table.weak_table, entry);
}
}
// 2. 清除引用计数
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
2.2 retain/release 的底层实现
**objc_retain: **
id objc_retain(id obj) {
if (obj->isTaggedPointerOrNil()) return obj; // TaggedPointer 直接返回
return obj->retain();
}
objc_object::retain() {
// 快速路径:Non-pointer isa
if (fastpath(!isTaggedPointer() && isa.nonpointer)) {
// 原子操作:extra_rc++
uintptr_t newisa = __sync_fetch_and_add(&isa.bits, RC_ONE);
// 检查是否溢出
if (slowpath(newisa.extra_rc == 0)) { // 溢出了
return rootRetain_overflow(false);
}
return (id)this;
}
// 慢速路径:操作 SideTable
return rootRetain(false, RRVariant::Fast);
}
// rootRetain 溢出处理:
// 将 extra_rc 的一半(RC_HALF = 524288/2)搬到 SideTable
// extra_rc 设为另一半
// 设置 has_sidetable_rc = 1
**objc_release: **
void objc_release(id obj) {
if (obj->isTaggedPointerOrNil()) return; // TaggedPointer 直接返回
obj->release();
}
objc_object::release() {
if (fastpath(isa.nonpointer)) {
// 原子操作:extra_rc--
uintptr_t newisa = __sync_sub_and_fetch(&isa.bits, RC_ONE);
if (slowpath(newisa.extra_rc == -1ULL)) { // 下溢了
return rootRelease_underflow(false);
}
return;
}
return rootRelease(false, RRVariant::Fast);
}
// rootRelease 下溢处理:
// 尝试从 SideTable 借 RC_HALF 到 extra_rc
// 如果 SideTable 中也为 0:
// → 设置 deallocating 标志
// → 调用 ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc))
2.3 AutoreleasePool 的底层实现
class AutoreleasePoolPage : private AutoreleasePoolPageData {
// 每页大小 = 4096 字节(一个虚拟内存页)
static size_t const SIZE = PAGE_MIN_SIZE; // 4096
// 页头结构(AutoreleasePoolPageData):
magic_t const magic; // 校验值
__unsafe_unretained id *next; // 指向下一个可存放对象的位置
pthread_t const thread; // 所属线程
AutoreleasePoolPage * const parent; // 上一页(双向链表)
AutoreleasePoolPage *child; // 下一页(双向链表)
uint32_t const depth; // 链表深度
uint32_t hiwat; // 最大入栈数量(high water mark)
// 页头之后的剩余空间用于存放 autorelease 对象指针
// 第一页的可用空间 = 4096 - sizeof(AutoreleasePoolPageData) ≈ 4096 - 56 = 4040
// 可存放 4040 / 8 = 505 个对象指针
};
// POOL_BOUNDARY 就是 nil(0x0)
#define POOL_BOUNDARY nil
// push 操作
static inline void *push() {
// 获取当前线程的 hotPage(最新的 page)
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
// 插入 POOL_BOUNDARY 标记
return page->add(POOL_BOUNDARY);
}
return autoreleaseFullPage(POOL_BOUNDARY, page);
}
// autorelease 操作
static inline id autorelease(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj); // 将对象指针存入 next 位置,next++
}
return autoreleaseFullPage(obj, page);
}
// pop 操作
static inline void pop(void *token) { // token = push 时返回的 POOL_BOUNDARY 位置
AutoreleasePoolPage *page = pageForPointer(token);
// 从 next-1 开始,逐个取出对象指针并调用 release
// 直到遇到 token(POOL_BOUNDARY)
page->releaseUntil(token);
// 释放空的子页(保留一个空子页作为缓存)
if (page->child) {
if (page->child->child) {
page->child->child->kill(); // 释放孙子页及其后续
}
}
}
**autorelease 与 RunLoop 的关系(底层源码): **
// main RunLoop 注册了两个 Observer:
// Observer 1: kCFRunLoopEntry(优先级最高 = 0x7FFFFFFF)
// 回调:_objc_autoreleasePoolPush()
// Observer 2: kCFRunLoopBeforeWaiting | kCFRunLoopExit(优先级最低 = -0x7FFFFFFF)
// BeforeWaiting 回调:
// _objc_autoreleasePoolPop(oldPool) // 释放旧池中的对象
// _objc_autoreleasePoolPush() // 创建新池
// Exit 回调:
// _objc_autoreleasePoolPop(pool) // 释放池中所有对象
**这意味着每次 RunLoop 循环(处理事件 → 休眠 → 唤醒),autorelease 对象都会被释放。 **
2.4 TaggedPointer 的完整实现
// arm64 下判断 TaggedPointer
static inline bool isTaggedPointer(const void *ptr) {
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
// _OBJC_TAG_MASK = 1ULL << 63 (最高位为 1)
}
// TaggedPointer 的位布局(arm64):
// [63] : 1 = TaggedPointer 标志
// [62-60] : tag index(0~6,表示类型:NSNumber=0, NSDate=1, NSString=5 等)
// tag=7 时为扩展类型,使用 [59-52] 作为扩展 tag
// [59-4] : payload(实际数据)
// [3-0] : payload 继续或 tagged pointer 的特殊信息
// 安全措施:TaggedPointer 的值经过了混淆(XOR 一个随机值)
// objc_debug_taggedpointer_obfuscator 在启动时随机生成
// 编码:encoded = ptr ^ objc_debug_taggedpointer_obfuscator
// 解码:decoded = encoded ^ objc_debug_taggedpointer_obfuscator
**为什么要混淆? ** 防止攻击者构造特定的 TaggedPointer 值来欺骗 Runtime。
**TaggedPointer 优化效果: **
-
NSNumber存储小整数、float:无需 malloc/free,无引用计数操作 -
NSString存储 7 字节以内的 ASCII 字符串(NSTaggedPointerString) -
NSDate存储时间戳 -
性能提升 3-100 倍(取决于操作类型)
三、Block
3.1 Block 的完整底层结构
// Block 编译后的结构
struct __block_impl {
void *isa; // 指向 _NSConcreteStackBlock / _NSConcreteGlobalBlock / _NSConcreteMallocBlock
int Flags; // 标志位:
// bit 0: BLOCK_DEALLOCATING
// bit 16: BLOCK_REFCOUNT_MASK(引用计数掩码)
// bit 24: BLOCK_NEEDS_FREE(堆 Block)
// bit 25: BLOCK_HAS_COPY_DISPOSE(有 copy/dispose 辅助函数)
// bit 26: BLOCK_HAS_CTOR(有 C++ 构造函数)
// bit 27: BLOCK_IS_GC(GC 相关,iOS 不用)
// bit 28: BLOCK_IS_GLOBAL(全局 Block)
// bit 30: BLOCK_HAS_SIGNATURE(有方法签名)
int Reserved;
void (*FuncPtr)(void *, ...); // 函数指针,指向 Block 的实现代码
};
// Block 描述信息
struct __block_desc_0 {
unsigned long reserved;
unsigned long Block_size; // Block 结构体大小
// 以下两个函数只在 BLOCK_HAS_COPY_DISPOSE 时存在:
void (*copy)(void *dst, void *src); // 从栈拷贝到堆时调用
void (*dispose)(void *); // 堆 Block 释放时调用
// 以下在 BLOCK_HAS_SIGNATURE 时存在:
const char *signature; // Block 的类型签名(参数/返回值编码)
};
**Block 捕获 OC 对象时的 copy 函数实现: **
static void __main_block_copy_0(struct __main_block_impl_0 *dst,
struct __main_block_impl_0 *src) {
// _Block_object_assign 相当于 retain(根据对象的所有权修饰符决定)
// BLOCK_FIELD_IS_OBJECT = 3:对象类型
// BLOCK_FIELD_IS_BLOCK = 7:Block 类型
// BLOCK_FIELD_IS_BYREF = 8:__block 变量
_Block_object_assign(&dst->obj, src->obj, BLOCK_FIELD_IS_OBJECT);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
// _Block_object_dispose 相当于 release
_Block_object_dispose(src->obj, BLOCK_FIELD_IS_OBJECT);
}
3.2 __block 变量的底层细节****
// __block int a = 10; 编译后生成的结构体
struct __Block_byref_a_0 {
void *__isa; // nil
__Block_byref_a_0 *__forwarding; // 核心!
int __flags;
int __size;
int a; // 原始变量的值
};
// 如果 __block 修饰的是 OC 对象
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
// 以下两个函数用于管理捕获的对象的内存
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
id obj;
};
****** __forwarding 指针的状态变化: **
栈上的 Block 未 copy 时:
栈上 byref.__forwarding → 栈上 byref(指向自己)
Block copy 到堆上后:
栈上 byref.__forwarding → 堆上 byref(指向堆上的副本)
堆上 byref.__forwarding → 堆上 byref(指向自己)
访问 __block 变量时:
a->__forwarding->a
无论是从栈上还是堆上访问,都通过 __forwarding 访问到堆上的同一份数据
****** __block 变量与对象内存管理: **
- 当 __block 变量被 Block copy 到堆上时:****
****- __Block_byref_id_object_copy 被调用
- 对捕获的对象执行 _Block_object_assign
- 根据变量的所有权修饰符(__strong/__weak)决定 retain 还是创建 weak 引用
- 当堆上 __block 变量释放时:****
****- __Block_byref_id_object_dispose 被调用
- 对捕获的对象执行 _Block_object_dispose(release / 移除 weak 引用)
3.3 Block 的 copy 流程
// _Block_copy 的简化实现
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return nil;
// 如果已经在堆上,引用计数 +1 后返回
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果是全局 Block,直接返回(不需要 copy)
if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 栈 Block → 堆 Block
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return nil;
// 内存拷贝
memmove(result, aBlock, aBlock->descriptor->size);
// 修改 flags:设置 BLOCK_NEEDS_FREE,引用计数设为 1
result->flags &= ~(BLOCK_REFCOUNT_MASK | BLOCK_DEALLOCATING);
result->flags |= BLOCK_NEEDS_FREE | 2; // 2 = 初始引用计数
// 修改 isa 为 _NSConcreteMallocBlock
_Block_call_copy_helper(result, aBlock);
result->isa = _NSConcreteMallocBlock;
return result;
}
四、RunLoop
4.1 RunLoop 的底层源码实现(CFRunLoopRun 简化版)
// CFRunLoopRun 的核心逻辑(简化版)
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName,
CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
// 1. 根据 modeName 找到对应的 CFRunLoopModeRef
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (!currentMode || __CFRunLoopModeIsEmpty(rl, currentMode)) {
return kCFRunLoopRunFinished; // Mode 中没有 Source/Timer/Observer
}
// 2. 通知 Observer:即将进入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 3. 进入核心循环
SInt32 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled);
// 4. 通知 Observer:即将退出 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm,
CFTimeInterval seconds, Boolean stopAfterHandle) {
// 获取基于 mach_port 的唤醒端口
mach_port_name_t dispatchPort = _dispatch_get_main_queue_port_4CF();
// 使用 GCD timer 或 mk_timer 设置超时
dispatch_source_t timeout_timer = NULL;
// ...
Boolean didDispatchPortLastTime = true;
int32_t retVal = 0;
do {
// 2. 通知 Observer:即将处理 Timer
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 3. 通知 Observer:即将处理 Source0
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 4. 处理 Blocks(dispatch_async 到主线程的 block)
__CFRunLoopDoBlocks(rl, rlm);
// 5. 处理 Source0(非端口事件)
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}
// 6. 如果有 Source1 就绪,跳到步骤 9 处理
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL, rl, rlm)) {
goto handle_msg;
}
// 7. 通知 Observer:即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// 8. 调用 mach_msg 等待唤醒(用户态 → 内核态)
// 这里会阻塞线程,直到:
// - 收到 mach_port 消息(Source1 / Timer / dispatch_main_queue)
// - 超时
// - 被外部唤醒(CFRunLoopWakeUp)
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer),
&livePort, poll ? 0 : TIMEOUT_INFINITY,
&voucherState, &voucherCopy, rl, rlm);
__CFRunLoopUnsetSleeping(rl);
// 9. 通知 Observer:已经唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
// 10. 根据唤醒来源处理事件
if (livePort == dispatchPort) {
// GCD 事件(dispatch_async 到主线程的)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else if (livePort == rl->_timerPort || (livePort == modeQueuePort)) {
// Timer 事件
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else {
// Source1 事件
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
}
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 判断是否退出
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm)) {
retVal = kCFRunLoopRunFinished;
}
} while (retVal == 0);
return retVal;
}
**mach_msg 系统调用——RunLoop 休眠的本质: **
// mach_msg 是 Mach 内核的消息收发函数
// 发送消息:将消息放入端口的消息队列
// 接收消息:从端口的消息队列取出消息,如果队列为空则阻塞
mach_msg_return_t mach_msg(
mach_msg_header_t *msg, // 消息头
mach_msg_option_t option, // MACH_SEND_MSG / MACH_RCV_MSG
mach_msg_size_t send_size, // 发送大小
mach_msg_size_t rcv_size, // 接收缓冲区大小
mach_port_name_t rcv_name, // 接收端口
mach_msg_timeout_t timeout, // 超时时间
mach_port_name_t notify // 通知端口
);
// RunLoop 休眠时调用:
mach_msg(msg, MACH_RCV_MSG | MACH_RCV_LARGE, 0, size, port, 0, MACH_PORT_NULL);
// MACH_RCV_MSG:接收模式
// 没有消息时线程被内核挂起(不消耗 CPU)
// 有消息时内核唤醒线程,mach_msg 返回
**CFRunLoopWakeUp 的实现(唤醒休眠的 RunLoop): **
void CFRunLoopWakeUp(CFRunLoopRef rl) {
// 向 RunLoop 的 _wakeUpPort 发送一个 mach 消息
kern_return_t ret = __CFSendTrivialMachMessage(rl->_wakeUpPort, 0, MACH_SEND_TIMEOUT, 0);
// 这会唤醒正在 mach_msg 中等待的线程
}
4.2 NSTimer 的底层
-
NSTimer 本质是
CFRunLoopTimerRef -
内部维护一个
_fireTSR(下次触发的绝对时间,mach_absolute_time 格式) -
RunLoop 每次循环时检查 Timer 是否到期(
__CFRunLoopDoTimers) -
不精确的原因:
1. RunLoop 可能在处理其他事件(Source/Observer),错过了精确触发时间
2. 到期时间的计算基于上次循环的时间
3. 如果 RunLoop 处于非 Timer 注册的 Mode 中,Timer 不会触发
-
**Tolerance(容差) **:iOS 7+ 引入,允许系统合并多个 Timer 的触发时机,减少 CPU 唤醒次数,节省电量
4.3 GCD 与 RunLoop 的关系
-
dispatch_async(dispatch_get_main_queue(), block)的 block 最终在 RunLoop 中执行 -
当有 block 被提交到主队列时,libdispatch 会向主线程的 RunLoop 发送一个 mach_msg
-
RunLoop 被唤醒后,在
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__中执行 block -
这就是为什么
dispatch_async到主线程的 block 不会立即执行,而是等到下一次 RunLoop 循环
五、多线程
5.1 GCD 的底层实现
**dispatch_queue 的底层结构: **
struct dispatch_queue_s {
DISPATCH_QUEUE_HEADER;
// 关键字段:
// dq_serialnum:队列序列号(主队列=1,全局队列=2~5)
// dq_width:并发宽度(串行=1,并发=DISPATCH_QUEUE_WIDTH_MAX=0xfff)
// dq_items_head/tail:任务链表头尾
// dq_state:队列状态(原子变量)
// do_targetq:目标队列(层级关系)
};
**dispatch_async 的底层流程: **
dispatch_async(queue, block)
→ _dispatch_continuation_init:将 block 包装成 dispatch_continuation_t(轻量级对象)
→ 设置 voucher(优先级传递信息)
→ 设置 dc_func = block 的函数指针
→ 设置 dc_ctxt = block 的上下文
→ _dispatch_continuation_async
→ dx_push(queue, continuation) // 将任务推入队列
→ 串行队列:_dispatch_lane_push
→ 如果队列为空且未执行中:直接 drain(执行)
→ 否则:追加到任务链表尾部
→ 并发队列:_dispatch_lane_concurrent_push
→ 追加到任务链表尾部
→ 唤醒线程池中的工作线程
// 工作线程的执行循环:
_dispatch_worker_thread3
→ _dispatch_root_queue_drain
→ _dispatch_queue_override_invoke
→ _dispatch_client_callout
→ block 的函数指针(block 的上下文) // 最终执行 block
**dispatch_sync 的死锁检测: **
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block) {
// 检查是否会死锁(当前线程是否正在执行该串行队列的任务)
if (unlikely(_dispatch_queue_try_acquire_barrier_sync(queue, tid))) {
// 不会死锁,直接在当前线程执行 block
_dispatch_sync_invoke_and_complete(queue, block);
} else {
// 会死锁!
// 但实际上 GCD 的死锁检测不够完善,只检测了直接死锁
// dispatch_sync 到自己所在的串行队列 → 直接 crash:
DISPATCH_CLIENT_CRASH(0, "dispatch_sync called on queue already owned by current thread");
}
}
**线程池管理: **
-
GCD 维护一个全局线程池
-
线程数量由 libdispatch 动态管理,上限通常为 64 或 512(取决于系统)
-
当所有线程都忙时,新提交的任务排队等待
-
空闲线程超过一定时间后被回收
-
线程爆炸问题:大量 dispatch_async 可能创建过多线程,导致性能下降(上下文切换开销)
5.2 dispatch_once 的底层实现
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block) {
// predicate 的值:
// 0 = 未执行
// ~0 = 已完成
// 其他值 = 正在执行(存储等待线程链表信息)
if (likely(os_atomic_load(predicate, acquire) == DONCE_DONE)) {
return; // 快速路径:已经执行过
}
return dispatch_once_f(predicate, block, _dispatch_Block_invoke(block));
}
void dispatch_once_f(dispatch_once_t *predicate, void *ctxt,
dispatch_function_t func) {
// 尝试原子比较交换:0 → locked value
if (os_atomic_cmpxchg(predicate, 0, locked_value, relaxed)) {
// 成功获取"锁",执行 block
func(ctxt);
// 标记为完成
os_atomic_store(predicate, DONCE_DONE, release);
// 唤醒所有等待的线程
_dispatch_once_wake(predicate);
} else {
// 已经有线程在执行,当前线程等待
_dispatch_once_wait(predicate);
// 等待完成后返回(block 已被另一个线程执行)
}
}
**为什么 dispatch_once 是线程安全的? **
-
使用****原子操作(CAS)** **保证只有一个线程能获取执行权
-
其他线程通过自旋 + 信号量等待
-
os_atomic_store使用release语义,确保 block 的副作用对其他线程可见 -
os_atomic_load使用acquire语义,确保能看到 block 的所有副作用
5.3 dispatch_semaphore 的底层实现
struct dispatch_semaphore_s {
DISPATCH_SEMAPHORE_HEADER;
long volatile dsema_value; // 当前信号量值
long dsema_orig; // 初始信号量值
_dispatch_sema4_t dsema_sema; // 底层信号量(内核信号量或 UL 信号量)
};
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout) {
// 原子减 1
long value = os_atomic_dec2o(dsema, dsema_value, acquire);
if (likely(value >= 0)) {
return 0; // 信号量充足,直接返回
}
// 信号量不足,进入慢速路径
return _dispatch_semaphore_wait_slow(dsema, timeout);
// 内部调用 semaphore_wait / WaitForSingleObject
// 线程被内核挂起
}
long dispatch_semaphore_signal(dispatch_semaphore_t dsema) {
// 原子加 1
long value = os_atomic_inc2o(dsema, dsema_value, release);
if (likely(value > 0)) {
return 0; // 没有等待的线程
}
// 有线程在等待,唤醒一个
return _dispatch_semaphore_signal_slow(dsema);
// 内部调用 semaphore_signal / ReleaseSemaphore
}
5.4 @synchronized 的底层实现
// @synchronized(obj) { ... } 编译后:
objc_sync_enter(obj);
// ... 代码 ...
objc_sync_exit(obj);
// objc_sync_enter 实现:
int objc_sync_enter(id obj) {
if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
// data 中包含一个 recursive_mutex_t
data->mutex.lock();
}
// obj == nil 时什么都不做(不会加锁!这是一个常见陷阱)
return 0;
}
// id2data 函数:
// 1. 先查 Thread Local Storage(TLS)中的缓存
// 每个线程缓存最近使用的 SyncData(快速链表 SyncList)
// 2. TLS 未命中,查全局哈希表 sDataLists
// key = obj 地址的哈希值
// value = SyncData 链表
// 3. 在链表中找到或创建 SyncData
// SyncData 包含:object 指针 + threadCount + recursive_mutex_t
// 4. 缓存到 TLS 中
** @synchronized 性能差的原因: **
-
每次加锁都要做哈希查找(全局表 + TLS)
-
recursive_mutex_t本身比os_unfair_lock重 -
obj == nil 时不加锁,存在安全隐患
-
使用全局哈希表,存在锁竞争
六、KVO 底层——更深入
6.1 动态子类的完整创建过程
addObserver:forKeyPath:
→ _NSKeyValueRetainedObservationInfoForObject
→ _NSKeyValueObservationInfoCreateByAdding
→ _NSSetObjectValueAndNotify 的生成过程:
1. 获取对象的 class
2. 检查是否已有 NSKVONotifying_ 子类
3. 没有则创建:
a. objc_allocateClassPair(originalClass, "NSKVONotifying_ClassName", 0)
b. 添加方法:
- setter 方法(如 -setAge:)→ IMP = _NSSetIntValueAndNotify / _NSSetObjectValueAndNotify 等
(根据属性类型选择不同的 setter IMP,不同类型有不同的实现)
- -class → 返回原类(伪装)
- -dealloc → 在释放时恢复 isa
- -_isKVOA → 返回 YES
- -description → 返回原类信息
c. objc_registerClassPair(kvoClass)
4. object_setClass(obj, kvoClass) // 修改 isa
****** _**NSSetXxxValueAndNotify 的内部实现(根据属性类型不同): ****
_NSSetIntValueAndNotify:
→ [self willChangeValueForKey:key]
→ _NSKeyValueWillChange
→ 记录旧值(如果 options 包含 NSKeyValueObservingOptionOld)
→ 入栈 changeDictionary
→ 调用原始 setter([super setAge:newAge])
→ [self didChangeValueForKey:key]
→ _NSKeyValueDidChange
→ 从栈中取出 changeDictionary
→ 填充新值(如果 options 包含 NSKeyValueObservingOptionNew)
→ 遍历所有观察者,调用:
[observer observeValueForKeyPath:keyPath
ofObject:self
change:changeDictionary
context:context]
**集合类型的 KVO(NSMutableArray 等): **
-
直接
[self.array addObject:]不会触发 KVO -
需要通过
[[self mutableArrayValueForKey:@"array"] addObject:] -
mutableArrayValueForKey:返回一个代理对象(NSKeyValueMutableArray) -
代理对象在增删改时自动调用
willChange:valuesAtIndexes:forKey:和didChange:valuesAtIndexes:forKey:
七、事件响应链底层
7.1 触摸事件的完整传递路径
用户触摸屏幕
→ 硬件产生触摸信号
→ IOKit.framework 将触摸事件封装为 IOHIDEvent
→ 通过 mach port 发送给 SpringBoard 进程
→ SpringBoard 判断前台 App,通过 mach port 发送给 App 进程
→ App 主线程 RunLoop 的 Source1 回调被触发
→ __IOHIDEventSystemClientQueueCallback
→ _UIApplicationHandleEventQueue
→ 将 IOHIDEvent 包装为 UIEvent(包含 UITouch)
→ [UIApplication sendEvent:event]
→ [UIWindow sendEvent:event]
→ [UIWindow hitTest:withEvent:] // 开始 Hit-Testing
**hitTest:withEvent: 的完整实现: **
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// 1. 检查是否能接收事件
if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
return nil;
}
// 2. 检查触摸点是否在自身范围内
if (![self pointInside:point withEvent:event]) {
return nil;
}
// 3. 从后往前遍历子视图(最上面的子视图优先)
for (NSInteger i = self.subviews.count - 1; i >= 0; i--) {
UIView *subview = self.subviews[i];
// 坐标转换到子视图坐标系
CGPoint convertedPoint = [subview convertPoint:point fromView:self];
// 递归调用子视图的 hitTest
UIView *hitView = [subview hitTest:convertedPoint withEvent:event];
if (hitView) {
return hitView; // 找到了最合适的视图
}
}
// 4. 没有子视图能处理,返回自己
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return CGRectContainsPoint(self.bounds, point);
}
**手势识别器与 UIResponder 的竞争关系(底层): **
UIWindow 收到 UIEvent 后:
1. 将事件同时发送给 hit-tested view 和相关的 UIGestureRecognizer
2. UIGestureRecognizer 进入 .began 状态,开始识别
3. 同时 UIView 收到 touchesBegan:
4. 如果手势识别器识别成功(.recognized / .changed):
→ 调用手势的 target-action
→ 同时调用 hit-tested view 的 touchesCancelled:(如果 cancelsTouchesInView = YES)
5. 如果手势识别器识别失败(.failed):
→ UIView 正常接收后续 touch 事件
UIGestureRecognizer 状态机:
Possible → Recognized(离散手势)
Possible → Began → Changed → Ended(连续手势)
Possible → Failed
任意状态 → Cancelled
八、UI 渲染底层
8.1 CALayer 的渲染原理
UIView 与 CALayer 的关系:
- UIView 是 CALayer 的 delegate(layer.delegate = view)
- UIView 负责事件处理和响应链
- CALayer 负责内容的存储和渲染
- 每个 CALayer 对应一个 backing store(位图,存储在 GPU 纹理缓存中)
一帧的完整渲染流程:
1. CPU 阶段(App 进程):
a. Handle Events → 触发 UI 更新
b. Commit Transaction(在 RunLoop 的 BeforeWaiting 回调中):
- Layout 阶段:
→ [UIView layoutSubviews]
→ [CALayer layoutSublayers]
→ Auto Layout 约束求解
- Display 阶段:
→ [CALayer display]
→ 如果实现了 -displayLayer:,交给代理绘制
→ 否则 [CALayer drawInContext:]
→ [UIView drawRect:](如果重写了)
→ 创建 CGContext(backing store)
→ Core Graphics 绘制
- Prepare 阶段:
→ 图片解码(JPEG/PNG → 位图)
→ 图片格式转换(如果 GPU 不支持的格式)
- Commit 阶段:
→ 将图层树(layer tree)编码打包
→ 通过 IPC(mach_msg)发送到 Render Server
2. Render Server 进程(backboardd):
a. 解码图层树
b. 构建渲染树(render tree)
c. 调用 GPU:
- 生成 GPU 命令缓冲区
- 提交到 GPU 命令队列
3. GPU 阶段:
a. 顶点着色器(Vertex Shader)
b. 图元装配(Primitive Assembly)
c. 光栅化(Rasterization)→ 将几何图形转换为像素
d. 片段着色器(Fragment Shader)→ 计算每个像素的颜色
e. 写入帧缓冲区(Frame Buffer)
4. 显示器:
a. VSync 信号到来(每秒 60 次 = 16.67ms 一帧)
b. 视频控制器从帧缓冲区读取数据
c. 逐行显示
8.2 离屏渲染底层原理
**正常渲染(On-Screen Rendering): **
GPU 直接将渲染结果写入帧缓冲区(Frame Buffer)
一次遍历即可完成
**离屏渲染(Off-Screen Rendering): **
GPU 需要额外的离屏缓冲区(Offscreen Buffer)来临时存储中间结果
以圆角 + masksToBounds 为例:
1. GPU 先渲染 layer 的内容到离屏缓冲区
2. GPU 再渲染 layer 的所有子 layer 到离屏缓冲区
3. 等所有子 layer 都渲染完毕后
4. 应用圆角裁剪(mask)
5. 将裁剪后的结果从离屏缓冲区 → 帧缓冲区
问题在于:
- 正常渲染是"画家算法",从后往前逐层绘制到帧缓冲区,不需要保存中间结果
- 但 mask 操作需要等所有内容都渲染完成后才能应用
- 这就需要额外的缓冲区来保存中间结果
- 缓冲区的创建/销毁 + 上下文切换(from on-screen to off-screen and back)开销很大
**各种情况的离屏渲染分析: **
| 操作 | 是否离屏渲染 | 原因 |
|------|-------------|------|
| cornerRadius(单独使用) | 否 | 只影响背景和边框,不需要裁剪子内容 |
| cornerRadius + masksToBounds | 是 | 需要裁剪所有子内容 |
| shadow(无 shadowPath) | 是 | 阴影需要根据内容轮廓计算,需要等内容渲染完成 |
| shadow(有 shadowPath) | 否 | 直接根据 path 绘制阴影,不需要等内容 |
| mask | 是 | 需要等内容渲染完成后应用遮罩 |
| shouldRasterize | 是(首次)| 首次渲染时离屏渲染并缓存,后续直接使用缓存 |
| opacity < 1(group opacity) | 是 | 需要等所有子 layer 渲染完成后统一设置透明度 |
**iOS 9+ 的圆角优化: **
-
UIImageView和UIButton的cornerRadius + masksToBounds不再触发离屏渲染 -
前提:没有设置
backgroundColor,图层内容是静态图片 -
系统对这些简单情况做了优化,直接在 GPU 中完成裁剪
8.3 隐式动画的底层原理
-
CALayer 的可动画属性(如 position、opacity、backgroundColor)被修改时,默认会产生隐式动画
-
原理:
1. CALayer 的 actionForKey: 被调用
2. 如果返回一个 CAAction 对象(通常是 CABasicAnimation)
3. 将动画添加到 layer 的动画队列中
4. 在下一个 commit transaction 中提交给 Render Server
-
UIView 的属性修改不会产生隐式动画(因为 UIView 在
actionForLayer:forKey:中返回 NSNull) -
[UIView animateWithDuration:animations:]的原理:
1. 内部调用 [CATransaction begin]
2. 设置 animationDuration、animationTimingFunction 等
3. 在 animations block 中修改属性时,actionForKey: 返回的不是 NSNull 而是 CABasicAnimation
4. [CATransaction commit] 提交
九、App 启动底层
9.1 dyld 加载的完整流程
内核(XNU):
1. exec() 系统调用
2. 创建进程,分配虚拟地址空间
3. 将 App 的 Mach-O 映射到虚拟内存(mmap)
4. 将 dyld 映射到虚拟内存
5. 设置进程入口为 dyld 的入口(__dyld_start)
6. 切换到用户态
dyld(动态链接器):
7. __dyld_start → dyldbootstrap::start → dyld::_main
8. 加载可执行文件(读取 Mach-O header 和 load commands)
9. 加载所有依赖的动态库(递归):
- 从 LC_LOAD_DYLIB 中读取依赖列表
- 检查共享缓存(dyld shared cache)中是否已有
- 未在缓存中:mmap 加载 dylib
- 验证签名
- 递归加载 dylib 的依赖
10. Rebase(内部指针修正):
- ASLR 为每个镜像分配随机 slide 值
- 遍历 __DATA 段中所有需要 rebase 的指针
- 将每个指针加上 slide 值
- 这些指针信息存储在 LC_DYLD_INFO_ONLY 的 rebase info 中
11. Bind(外部符号绑定):
- 遍历 bind info(存储在 LC_DYLD_INFO_ONLY 中)
- 在依赖的 dylib 中查找符号的实际地址
- 将 __DATA 段中的符号指针修改为实际地址
- 懒绑定(Lazy Binding):延迟到第一次调用时才绑定(通过 stub_helper)
12. 通知 objc runtime:
- dyld 调用 _objc_init 中注册的 map_images 回调
- Runtime 注册所有 ObjC 类、Category、Protocol
- 处理 +load 方法
13. 初始化器(Initializers):
- C++ 全局变量构造函数(__mod_init_func)
- __attribute__((constructor)) 函数
- +load 方法(已在步骤 12 中调用)
14. 调用 main() 函数
**dyld shared cache(共享缓存): **
-
所有系统动态库被预先链接并合并为一个大文件
-
路径:
/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 -
优点:
- 加载速度快(已预链接,不需要 rebase/bind)
- 内存共享(所有进程共享同一份映射)
- 大小约 1GB+
9.2 _objc_init 的完整流程**
void _objc_init(void) {
// 1. 初始化锁
// 2. 初始化 tagged pointer 混淆器
// 3. 向 dyld 注册三个回调:
_dyld_objc_notify_register(
&map_images, // 当新的 image 被映射到内存时调用
&load_images, // 当 image 初始化时调用(调用 +load)
&unmap_image // 当 image 被卸载时调用
);
}
// map_images:读取镜像中的 ObjC 元数据
void map_images(unsigned count, const char * const paths[],
const struct mach_header * const mhdrs[]) {
mutex_locker_t lock(runtimeLock);
map_images_nolock(count, paths, mhdrs);
}
void map_images_nolock(unsigned mhCount, const char * const mhPaths[],
const struct mach_header * const mhdrs[]) {
// ...
_read_images(hList, hCount, totalClasses, unoptimizedTotalClasses);
}
void _read_images(header_info **hList, uint32_t hCount,
int totalClasses, int unoptimizedTotalClasses) {
// 1. 读取所有类(__objc_classlist 段)
for (EACH_HEADER) {
classref_t const *classlist = _getObjc2ClassList(hi, &count);
for (i = 0; i < count; i++) {
Class cls = (Class)classlist[i];
Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
// 将类添加到 gdb_objc_realized_classes 哈希表中
}
}
// 2. 修正重映射的类(Remapped Classes)
// 3. 读取所有 Category(__objc_catlist 段)
for (EACH_HEADER) {
auto catlist = _getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
// 如果宿主类已 realized,立即 attach
// 否则存入 unattachedCategories
}
}
// 4. 读取协议(__objc_protolist 段)
// 5. 读取 selector 引用(__objc_selrefs 段)→ 唯一化
// 6. 读取类引用(__objc_classrefs 段)
// 7. 实现所有未 realized 的类(懒加载 vs 非懒加载)
// 非懒加载类(实现了 +load 或有静态实例):立即 realize
// 懒加载类:等第一次使用时再 realize
}
十、Swift 底层
10.1 Swift 对象的完整内存布局
Swift Class Object(堆上):
┌──────────────────────────────┐
│ Metadata Pointer (8 bytes) │ → 类似 OC 的 isa
│ 指向 Full Metadata │
├──────────────────────────────┤
│ Reference Count (8 bytes) │
│ ┌─ Strong RC (32 bits) │ 位域:上半部分
│ ├─ Unowned RC (32 bits) │ 位域:下半部分
│ └─ Flags │ 是否使用 side table、是否正在 deinit
├──────────────────────────────┤
│ Stored Property 1 │
│ Stored Property 2 │
│ ... │
└──────────────────────────────┘
Full Metadata(类元数据):
┌──────────────────────────────┐
│ Kind(类型种类) │ 0 = Class, 1 = Struct, 2 = Enum...
├──────────────────────────────┤
│ Type Descriptor Pointer │ → 类型描述(包含名称、字段信息等)
├──────────────────────────────┤
│ Superclass Metadata Pointer │ → 父类元数据(OC 类也有对应的)
├──────────────────────────────┤
│ Negative Stride Flags │
├──────────────────────────────┤
│ Instance Address Point │
├──────────────────────────────┤
│ Instance Size │ 实例大小
├──────────────────────────────┤
│ Instance Alignment │ 实例对齐
├──────────────────────────────┤
│ VTable │ 虚函数表
│ entry 0: func ptr │
│ entry 1: func ptr │
│ ... │
└──────────────────────────────┘
**Swift 引用计数的三种状态: **
状态 1:Inline(内联,大部分情况)
- Strong RC 和 Unowned RC 直接存储在对象头的 refCounts 字段中
- 原子操作读写
状态 2:Side Table(当有 weak 引用时)
- 创建 HeapObjectSideTableEntry
- refCounts 字段变为指向 side table 的指针
- Side Table 包含:Strong RC + Unowned RC + Weak RC + 对象指针
- Weak 引用指向 Side Table 而非对象本身
- 对象 deinit 后,Side Table 仍然存在(直到最后一个 weak 引用消失)
- 这就是为什么 weak 引用访问已释放对象返回 nil 而不是 crash
状态 3:Immortal(永生对象)
- Strong RC = max,永远不会被释放
- 用于全局变量、字符串常量等
10.2 Swift 方法派发的底层
**VTable 派发(class 的方法): **
调用 obj.method() 时:
1. 从 obj 的第一个字段取出 metadata pointer
2. 从 metadata 中根据方法的 VTable offset 取出函数指针
3. 直接调用函数指针
// 整个过程只需要 2-3 次内存访问 + 1 次间接跳转
// 比 OC 的 objc_msgSend 快得多(不需要哈希查找)
**Protocol Witness Table 派发: **
protocol Drawable {
func draw()
func area() -> Double
}
// 每个遵循 Drawable 的具体类型都会生成一个 Witness Table:
struct DrawableWitnessTable {
func_ptr draw; // → Circle.draw 的实际函数指针
func_ptr area; // → Circle.area 的实际函数指针
};
// 调用 existential.draw() 时:
1. 从 Existential Container 中取出 Protocol Witness Table 指针
2. 从表中根据方法的偏移量取出函数指针
3. 从 Existential Container 中取出值(或值的指针)
4. 调用函数指针,传入值作为 self
10.3 Swift 的 Existential Container 深入
// 5 个 word = 40 字节
// 小值(<= 3 words = 24 bytes):
┌────────────────────────┐
│ Value Buffer (24 bytes) │ ← 直接存储值(inline storage)
│ word 0: value data │
│ word 1: value data │
│ word 2: value data │
├────────────────────────┤
│ Value Witness Table ptr │ ← 值操作表(copy/destroy/size/alignment...)
├────────────────────────┤
│ Protocol Witness Table │ ← 协议方法表
└────────────────────────┘
// 大值(> 3 words):
┌────────────────────────┐
│ Value Buffer (24 bytes) │
│ word 0: heap ptr ─────→ [堆上分配的值]
│ word 1: (unused) │
│ word 2: (unused) │
├────────────────────────┤
│ Value Witness Table ptr │
├────────────────────────┤
│ Protocol Witness Table │
└────────────────────────┘
// 多个协议组合(如 Drawable & Printable):
// 每多一个协议,就多一个 Protocol Witness Table 指针
// 容器大小 = 24 + 8 + 8 * N(N = 协议数量)
**为什么泛型约束比 Existential Type 性能好? **
// 方式 1:Existential Type(协议类型)
func draw(shape: Drawable) {
shape.draw() // 通过 Existential Container → PWT → 间接调用
}
// 方式 2:泛型约束
func draw<T: Drawable>(shape: T) {
shape.draw() // 编译器在 Whole Module Optimization 下可以特化
// 特化后变成:
// shape.draw() // 直接调用 Circle.draw(),零开销!
}
-
Existential:每次调用都要通过容器 → 表 → 间接跳转,无法内联优化
-
泛型:编译器可以生成特化版本,直接调用具体类型的方法,甚至内联
-
泛型在未特化时也比 Existential 好:不需要装箱/拆箱(Value Buffer 的分配/释放)
第二章:第三方常用库原理与八股文
一、SDWebImage 源码级分析
1.1 SDWebImageManager 的核心流程
sd_setImageWithURL:
→ UIView+WebCache 分类方法
→ sd_internalSetImageWithURL:...
→ 取消当前正在进行的下载操作(sd_cancelImageLoadOperationWithKey:)
→ [SDWebImageManager loadImageWithURL:options:progress:completed:]
→ 1. 生成缓存 key(默认 = URL.absoluteString)
→ 2. [SDImageCache queryImageForKey:options:context:completion:]
→ 2a. 内存缓存查找(SDMemoryCache: NSCache 子类)
→ [self.memoryCache objectForKey:key]
→ NSCache 底层使用 pthread_mutex + 双向链表 + 哈希表
→ LRU 淘汰(NSCache 自动管理,但不保证 LRU)
→ 2b. 磁盘缓存查找(在 ioQueue 串行队列中异步执行)
→ 文件名 = MD5(key)
→ NSFileManager 查找文件
→ 读取 NSData
→ [SDImageCodersManager.sharedManager decodedImageWithData:]
→ 根据图片格式选择解码器(PNG/JPEG/WebP/GIF/HEIF)
→ 解码后的 UIImage 存入内存缓存
→ 3. 缓存未命中 → [SDWebImageDownloader downloadImageWithURL:...]
→ 创建 SDWebImageDownloaderOperation(NSOperation 子类)
→ 添加到 downloadQueue(NSOperationQueue,默认 maxConcurrentOperationCount = 6)
→ 内部使用 NSURLSession dataTask
→ 下载完成后:
a. 解码(子线程)
b. 存入磁盘缓存(ioQueue 串行队列)
c. 存入内存缓存
d. 回调主线程
1.2 SDWebImage 的图片解码细节
为什么需要预解码?
UIImage 默认使用"惰性解码":
1. imageWithData: 只是创建了 UIImage 对象,持有压缩数据的引用
2. 图片实际显示时(CALayer 提交到 GPU 前),系统才进行解码
3. 解码发生在主线程,可能导致卡顿
SDWebImage 的预解码(在子线程):
1. 创建 CGBitmapContext(位图上下文)
→ CGBitmapContextCreate(NULL, width, height, 8, bytesPerRow, colorSpace, bitmapInfo)
→ bytesPerRow = 对齐到 64 字节(GPU 优化)
2. 将压缩图片绘制到 context 中
→ CGContextDrawImage(context, rect, imageRef)
→ 这一步触发解码
3. 从 context 创建新的 CGImageRef
→ CGBitmapContextCreateImage(context)
→ 得到的是已解码的位图
4. 用解码后的 CGImageRef 创建 UIImage
5. 缓存解码后的 UIImage 到内存中
内存代价:
解码后的位图大小 = width × height × 4(RGBA)
例如:1000×1000 的图 = 4MB
这就是为什么大图需要降采样
1.3 SDWebImage 的请求合并
同一 URL 的多个请求不会重复下载:
SDWebImageDownloader 维护一个字典:
_URLOperations: { URL → SDWebImageDownloaderOperation }
当请求到来时:
1. 检查 _URLOperations 中是否已有该 URL 的 operation
2. 如果有:将新的 progressBlock 和 completedBlock 添加到现有 operation 的回调数组中
3. 如果没有:创建新的 operation
operation 完成时:
遍历所有回调,逐个调用
从 _URLOperations 中移除该 URL
二、YYCache 源码级分析
2.1 YYMemoryCache 的 LRU 实现
核心数据结构:
_YYLinkedMap:双向链表 + CFMutableDictionary
_YYLinkedMapNode:
- _prev, _next(链表指针)
- _key, _value
- _cost(内存代价)
- _time(最后访问时间)
_YYLinkedMap:
- _head, _tail(链表头尾)
- _dic(CFMutableDictionary,key → node)
- _totalCost, _totalCount
- _releaseOnMainThread(是否在主线程释放对象)
- _releaseAsynchronously(是否异步释放对象)
操作时间复杂度:
- 插入:O(1)(插入链表头部 + 哈希表 put)
- 查找:O(1)(哈希表 get + 移到链表头部)
- 删除最久未使用:O(1)(删除链表尾部 + 哈希表 remove)
为什么不用 NSCache?
1. NSCache 的 LRU 策略不可靠(只保证"大致 LRU")
2. NSCache 不提供 cost limit 的精确控制
3. NSCache 不支持超时清理
4. YYMemoryCache 额外提供了 age-based 清理
2.2 YYDiskCache 的 SQLite + File 混合存储
性能测试数据(YY 作者的 benchmark):
写入性能(数据大小 → 最优方案):
< 20KB → SQLite 更快(文件系统的 inode 创建开销大于 SQLite 事务开销)
> 20KB → 文件更快(SQLite 对大 BLOB 处理效率低)
读取性能:
< 20KB → SQLite 更快(单次 IO 完成)
> 20KB → 文件更快(SQLite 需要将整个 BLOB 加载到内存后再返回)
YYKVStorage 的实现:
- SQLite 存储:key + value(BLOB)+ size + modifiedTime + accessTime + extendedData
- 文件存储:key → 文件名,文件路径 = basePath/data/MD5(key)
- SQLite 中仍然存储文件的元数据(key、size、time),只是 value 为空
- 查询时先查 SQLite 获取元数据,如果 value 非空直接返回,否则读文件
三、fishhook 底层实现详解
3.1 Mach-O 中的符号间接寻址机制
当代码调用 NSLog(@"Hello") 时:
编译后的指令(__TEXT 段):
bl __stub_NSLog // 跳转到 stub
__stubs 段:
__stub_NSLog:
ldr x16, [x16, #offset] // 从 __la_symbol_ptr 加载地址
br x16 // 跳转
__la_symbol_ptr 段(__DATA):
[slot for NSLog] = __stub_helper + offset // 初始指向 stub_helper
__stub_helper 段:
__stub_helper_NSLog:
ldr w16, literal_pool // 加载符号的 lazy bind info 偏移
b __stub_helper_common // 跳转到公共 helper
__stub_helper_common:
// 调用 dyld::fastBindLazySymbol
// dyld 在符号表中查找 NSLog 的实际地址
// 将地址写回 __la_symbol_ptr 对应的 slot
// 跳转到实际地址
第二次调用时:
__la_symbol_ptr 已被修改为实际地址
ldr + br 直接跳转到 Foundation 中的 NSLog 实现
3.2 fishhook 的符号查找算法
// fishhook 的核心函数
static void rebind_symbols_for_image(struct rebinding rebindings[],
size_t rebindings_nel,
const struct mach_header *header,
intptr_t slide) {
// 1. 获取 __LINKEDIT 段的加载命令
// 2. 获取符号表(LC_SYMTAB → symtab)
// 3. 获取动态符号表(LC_DYSYMTAB → dysymtab)
// 4. 获取字符串表(strtab)
// 5. 遍历 __DATA 段中的所有 section
for (每个 __la_symbol_ptr 或 __nl_symbol_ptr section) {
// 6. 获取 indirect symbol table 的对应区域
uint32_t *indirect_symtab = dysymtab->indirectsymoff + linkedit_base;
// section->reserved1 记录了该 section 在 indirect symbol table 中的起始索引
// 7. 遍历每个指针 slot
for (uint i = 0; i < section->size / sizeof(void *); i++) {
// 8. 从 indirect symbol table 获取符号索引
uint32_t symtab_index = indirect_symtab[section->reserved1 + i];
// 9. 从符号表获取符号名的字符串表偏移
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
// 10. 从字符串表获取符号名
char *symbol_name = strtab + strtab_offset;
// 11. 与要替换的符号名比较
if (strcmp(symbol_name, target_name) == 0) {
// 12. 保存原始指针
*(rebinding->replaced) = indirect_symbol_bindings[i];
// 13. 替换为新指针
indirect_symbol_bindings[i] = rebinding->replacement;
}
}
}
}
四、Combine 底层原理
4.1 Publisher-Subscriber 的连接过程
let publisher = [1, 2, 3].publisher
let subscriber = Subscribers.Sink<Int, Never>(receiveCompletion: { _ in },
receiveValue: { print($0) })
publisher.subscribe(subscriber)
底层流程:
1. publisher.subscribe(subscriber)
→ publisher 调用 subscriber.receive(subscription:)
→ 传入一个 Subscription 对象(协议要求)
2. subscriber 收到 subscription 后,调用:
subscription.request(.unlimited) // 请求数据
→ 这就是 Backpressure 机制:subscriber 控制接收速率
3. subscription 根据请求量发送数据:
subscriber.receive(1) // 返回 Subscribers.Demand,表示还需要多少
subscriber.receive(2)
subscriber.receive(3)
subscriber.receive(completion: .finished)
Backpressure(背压)的意义:
- subscriber 可以控制 publisher 的数据发送速率
- .unlimited:不限制
- .none:不再需要更多
- .max(n):再发 n 个
- 这是 Combine 与 RxSwift 的核心区别之一
- RxSwift 没有背压,数据流出过快时可能导致缓冲区爆满
第三章:iOS 开发难点问题与解决方案
一、内存相关难题
1.1 循环引用的底层排查——MLeaksFinder 原理详解
MLeaksFinder 的核心原理:
1. 利用 Method Swizzling hook 以下方法:
- UINavigationController 的 popViewControllerAnimated:
- UINavigationController 的 dismissViewControllerAnimated:completion:
- UIViewController 的 viewDidDisappear:
2. 当 VC 被 pop/dismiss 时:
- 调用 [vc willDealloc]
- willDealloc 内部:
__weak id weakSelf = self;
dispatch_after(2秒, dispatch_get_main_queue(), ^{
// 2秒后检查 weakSelf 是否还有值
if (weakSelf) {
// 对象还在!说明没有被正确释放 → 可能有循环引用
[weakSelf alertLeakInfo];
}
});
- 同时递归调用所有子视图和子控制器的 willDealloc
3. 为什么延迟 2 秒?
- 给系统足够的时间完成 dealloc
- 考虑到动画时间和 autorelease pool 的释放时机
4. 配合 FBRetainCycleDetector(可选):
- 检测到泄漏后,使用 FBRetainCycleDetector 分析循环引用链
- FBRetainCycleDetector 原理:
a. 从泄漏对象开始,使用 Runtime 获取所有强引用的成员变量
b. 对每个强引用对象递归获取其强引用
c. 使用 DFS(深度优先搜索)检测引用图中是否有环
d. 找到环后输出循环引用链
1.2 NSTimer 循环引用的三种解决方案详解
方案一:NSProxy 中间对象
NSProxy 特殊之处:
- 继承自 NSProxy 而非 NSObject
- 不走 OC 的方法查找流程
- 直接进入消息转发(-methodSignatureForSelector: / -forwardInvocation:)
- 转发效率比 NSObject 高(NSObject 要先查方法列表,查不到才转发)
实现:
WeakProxy 持有 weak target
Timer 强引用 WeakProxy
WeakProxy 将所有消息转发给 target
target 释放后,WeakProxy.target = nil
Timer fire 时调用 WeakProxy 的方法,转发给 nil → 无操作
为什么用 NSProxy 而不是 NSObject?
- NSObject 子类需要重写 forwardingTargetForSelector:
- 但 NSObject 可能已有同名方法(如 respondsToSelector:),不会走转发
- NSProxy 完全没有自己的实例方法,所有消息都走转发
- 更纯粹、更安全
方案二:iOS 10+ Block API
// iOS 10+ 提供的 block-based API
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer *timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
[timer invalidate];
return;
}
[strongSelf doSomething];
}];
// 底层原理:Timer 强引用 block,block 弱引用 self
// self 释放后,block 中 weakSelf 为 nil,可手动 invalidate timer
方案三:在合适的时机 invalidate
问题:在 dealloc 中 invalidate 是行不通的
- Timer 强引用 self → self 引用计数永远 > 0 → dealloc 永远不会调用
正确时机:
- viewWillDisappear:(但注意 push 新页面也会调用)
- viewDidDisappear:(配合判断是否是被 pop)
- didMoveToParentViewController:(参数为 nil 时表示被移除)
1.3 野指针的底层检测方案
自定义野指针检测系统(类似 Zombie Objects 但更强大):
原理:
1. Hook NSObject 的 dealloc 方法
2. 在 dealloc 中:
a. 记录对象的类名和地址
b. 不调用原始的 dealloc(对象不会真正释放!)
c. 使用 objc_destructInstance(self) 释放关联对象和弱引用
d. 用 memset 将对象内存填充为特殊值(如 0x55)
e. 将对象的 isa 修改为自定义的"僵尸类"
f. 将对象放入"延迟释放队列"
3. 僵尸类实现了 -forwardInvocation: 等方法:
a. 当野指针访问时,消息发送到僵尸类
b. 僵尸类捕获信息:被调用的方法名 + 对象原始类名 + 地址
c. 输出详细的错误日志
d. 可选择 crash 或继续运行
4. 延迟释放队列管理:
a. 队列有大小限制(如 100 个对象 或 10MB)
b. 超出限制时,释放最早加入的对象(先进先出)
c. 释放时恢复 isa 并调用原始 free
优势(vs Xcode Zombie Objects):
- 可以在 Release 环境使用
- 可以控制内存消耗
- 可以上报到崩溃监控系统
- 可以记录更多上下文信息(调用栈等)
二、线程安全深度问题
2.1 多线程读写 crash 的底层原因
NSMutableArray 多线程 crash 的根本原因:
场景:线程 A 读取 array[5],线程 B 同时 removeAllObjects
crash 原因分析:
1. NSMutableArray 内部使用 C 数组存储元素指针
2. removeAllObjects 会:
a. 逐个 release 元素
b. 修改 count
c. 可能 realloc 内部数组(缩容)
3. 如果线程 A 在步骤 2c 之后访问 array[5]:
- 内部数组可能已被 realloc,原地址失效 → EXC_BAD_ACCESS
4. 如果线程 A 在步骤 2a 的过程中访问 array[5]:
- 元素可能已被 release 但指针还在数组中 → 野指针访问
NSDictionary 多线程 crash 的根本原因:
1. NSDictionary 内部使用哈希表
2. 插入/删除可能触发 rehash(扩容/缩容)
3. rehash 过程中:
- 分配新的哈希表
- 将旧表的元素重新哈希到新表
- 释放旧表
4. 如果读线程正在遍历旧表,而写线程触发了 rehash
- 旧表被释放 → 读线程访问已释放的内存 → crash
2.2 dispatch_barrier_async 实现读写锁
读写锁的语义:
- 多个读取可以同时进行
- 写入时独占,不允许其他读取或写入
GCD 实现:
// 创建自定义并发队列(必须是自定义的!不能用全局队列)
dispatch_queue_t rwQueue = dispatch_queue_create("com.example.rw",
DISPATCH_QUEUE_CONCURRENT);
// 读取操作(可并发)
- (id)readData {
__block id result;
dispatch_sync(rwQueue, ^{ // sync:等待结果返回
result = self.data;
});
return result;
}
// 写入操作(独占)
- (void)writeData:(id)data {
dispatch_barrier_async(rwQueue, ^{ // barrier:等前面的都完成,自己独占执行
self.data = data;
});
}
底层原理:
barrier task 提交到并发队列后:
1. 队列标记进入 barrier 模式
2. 等待所有正在执行的 task 完成
3. 执行 barrier task(此时不允许新 task 执行)
4. barrier task 完成后,解除 barrier 模式
5. 后续 task 恢复正常并发执行
为什么不能用全局队列?
全局队列被所有代码共享,如果使用 barrier:
- 会阻塞其他不相关代码提交到全局队列的 task
- GCD 会直接忽略全局队列上的 barrier 标记(按普通 async 执行)
三、架构难题深入
3.1 CTMediator 的底层实现原理
核心思想:利用 Runtime 动态调用 Target-Action
流程:
1. 调用方通过 CTMediator 的分类方法:
[CTMediator performTarget:@"UserModule"
action:@"showUserProfile"
params:@{@"userId": @"123"}]
2. CTMediator 内部:
// 拼接 Target 类名
NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
// 拼接 Action 方法名
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
// 利用 Runtime 获取类和方法
Class targetClass = NSClassFromString(targetClassString);
id target = [[targetClass alloc] init];
SEL action = NSSelectorFromString(actionString);
// 调用
if ([target respondsToSelector:action]) {
return [target performSelector:action withObject:params];
}
3. Target 类(每个组件提供):
@implementation Target_UserModule
- (UIViewController *)Action_showUserProfile:(NSDictionary *)params {
NSString *userId = params[@"userId"];
UserProfileViewController *vc = [[UserProfileViewController alloc] initWithUserId:userId];
return vc;
}
@end
4. 分类(供调用方使用,提供编译期检查):
@implementation CTMediator (UserModule)
- (UIViewController *)showUserProfileWithUserId:(NSString *)userId {
return [self performTarget:@"UserModule"
action:@"showUserProfile"
params:@{@"userId": userId}];
}
@end
优势:
- Target 类不需要注册,Runtime 动态查找
- 组件间完全无依赖(只依赖 CTMediator)
- 分类提供了类型安全的调用接口
劣势:
- 字符串硬编码(target name + action name)
- 需要维护分类和 Target 类的一致性
- performSelector 最多传一个参数,复杂参数只能用字典
第四章:性能优化八股文与深入细节
一、启动优化——二进制重排原理深入
1.1 Page Fault 的底层原理
虚拟内存 → 物理内存的映射:
1. App 的 Mach-O 被 mmap 到虚拟地址空间
- mmap 不会立即加载文件到物理内存
- 只是建立虚拟地址 → 文件偏移的映射关系
2. 当 CPU 访问某个虚拟地址时:
a. MMU(内存管理单元)查找页表
b. 如果页表中有映射 → 直接访问物理内存(命中 TLB 缓存更快)
c. 如果页表中无映射 → Page Fault(缺页中断)
3. Page Fault 处理流程:
a. CPU 暂停当前执行,陷入内核态
b. 内核的 page fault handler 处理:
- 找到空闲的物理内存页
- 从磁盘读取对应的文件数据到物理内存页
- 对代码页进行签名验证(iOS 安全要求)
- 更新页表(虚拟页 → 物理页的映射)
c. 返回用户态,CPU 从中断处继续执行
4. 耗时分析:
- 从磁盘读取数据:约 0.1~0.5ms
- 签名验证:约 0.1~0.3ms
- 页表更新:约 0.01ms
- 总计:一次 Page Fault 约 0.3~1ms
5. 启动时的 Page Fault:
- App 启动时需要执行大量代码
- 这些代码分布在多个不同的内存页中
- 每个未加载的页都会触发一次 Page Fault
- 如果启动相关的函数分散在 100 个页中 → 100 次 Page Fault → 30~100ms
1.2 Clang 插桩获取启动函数调用顺序
原理:
编译时在每个函数入口插入一个回调函数调用
运行时记录函数调用顺序
步骤:
1. 编译设置:
Other C Flags: -fsanitize-coverage=func,trace-pc-guard
Other Swift Flags: -sanitize-coverage=func -sanitize=undefined
2. 实现回调函数:
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop) {
// start 和 stop 是编译器为所有插桩点分配的 guard 数组的起止指针
// 初始化 guard 值(非零表示启用,零表示禁用)
if (start == stop || *start) return;
for (uint32_t *x = start; x < stop; x++) {
*x = 1; // 启用所有插桩点
}
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // guard 为 0 表示不需要记录
*guard = 0; // 每个函数只记录一次
// 获取调用者的地址
void *PC = __builtin_return_address(0);
// 存储到线程安全的数组中
// 使用 atomic 操作或 OSAtomicEnqueue
OSAtomicEnqueue(&symbolQueue, createNode(PC), offsetof(Node, next));
}
3. 在 App 启动完成后,收集所有记录的 PC 地址:
- 将 PC 转换为符号名(使用 dladdr)
- 去重(保留首次出现)
- 输出 order 文件
4. 配置 Xcode:
Build Settings → Order File → 设置为生成的 order 文件路径
链接器(ld)会按照 order 文件中的函数顺序排列代码
效果:
将启动时调用的所有函数紧凑地排列在连续的内存页中
Page Fault 次数可从 100+ 减少到 10-20 次
启动耗时可减少 10-30%
二、卡顿监测——底层实现
2.1 基于 RunLoop Observer 的卡顿监测
完整实现:
// 子线程监测
- (void)startMonitor {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (self.isMonitoring) {
// 信号量等待,超时时间 = 阈值(如 50ms)
long result = dispatch_semaphore_wait(self.semaphore,
dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
if (result != 0) {
// 超时!说明主线程在 50ms 内没有触发 RunLoop 状态变化
if (self.currentActivity == kCFRunLoopBeforeSources ||
self.currentActivity == kCFRunLoopAfterWaiting) {
// 连续超时 N 次才上报(避免偶发误报)
if (++self.timeoutCount < 3) continue;
self.timeoutCount = 0;
// 收集主线程调用栈
NSString *stack = [self captureMainThreadStack];
// 上报
[self reportLag:stack];
}
}
self.timeoutCount = 0;
}
});
// 主线程 RunLoop Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
NULL, kCFRunLoopAllActivities, YES, 0,
^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
self.currentActivity = activity;
// 每次 RunLoop 状态变化,发送信号
dispatch_semaphore_signal(self.semaphore);
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
// 获取主线程调用栈的底层实现
- (NSString *)captureMainThreadStack {
thread_t mainThread = mach_thread_self(); // 需要保存主线程的 thread port
// 挂起主线程(暂停执行,保证栈不变)
thread_suspend(mainThread);
// 获取主线程的寄存器状态
_STRUCT_MCONTEXT machineContext;
mach_msg_type_number_t stateCount = MACHINE_THREAD_STATE_COUNT;
thread_get_state(mainThread, MACHINE_THREAD_STATE,
(thread_state_t)&machineContext.__ss, &stateCount);
// 从 FP(Frame Pointer)寄存器开始回溯调用栈
uintptr_t fp = machineContext.__ss.__fp;
uintptr_t pc = machineContext.__ss.__pc;
NSMutableArray *frames = [NSMutableArray array];
while (fp && pc) {
Dl_info info;
if (dladdr((void *)pc, &info)) {
[frames addObject:[NSString stringWithFormat:@"%s %s + %ld",
info.dli_fname, info.dli_sname,
(long)(pc - (uintptr_t)info.dli_saddr)]];
}
// 沿栈帧链回溯
pc = *(uintptr_t *)(fp + 8); // 返回地址(LR)
fp = *(uintptr_t *)fp; // 上一个栈帧的 FP
}
// 恢复主线程
thread_resume(mainThread);
return [frames componentsJoinedByString:@"\n"];
}
三、图片降采样的底层实现
ImageIO 降采样(推荐方式):
// 问题:加载 4000x3000 的图片
// 解码后内存:4000 × 3000 × 4 bytes = 48MB !!!
// 但 UIImageView 只有 200x150,实际只需要 400x300(@2x)
// 降采样方案:在解码时就按目标大小读取
func downsample(imageURL: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL,
imageSourceOptions) else { return nil }
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true, // 立即解码,避免主线程惰性解码
kCGImageSourceCreateThumbnailWithTransform: true, // 应用 EXIF 方向
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(
imageSource, 0, downsampleOptions) else { return nil }
return UIImage(cgImage: downsampledImage)
}
// 底层原理:
// CGImageSourceCreateThumbnailAtIndex 在解码时就按照目标尺寸读取
// 不需要先完整解码再缩放
// 对于 JPEG:利用 JPEG 的 DCT 分块特性,可以在解码时跳过不需要的块
// 内存使用:400 × 300 × 4 = 0.48MB(vs 48MB,减少 99%)
四、Auto Layout 底层性能
Auto Layout 的约束求解引擎:Cassowary
原理:
1. 每个约束表示为一个线性方程或不等式:
view.left = superview.left + 20
view.width = superview.width * 0.5
view.height >= 44
2. Cassowary 算法使用单纯形法(Simplex Method)求解线性规划问题
3. 性能复杂度:
iOS 11 及之前:O(N³)(最坏情况),一般 O(N²)
iOS 12+:Apple 重写了引擎,某些场景下可达到 O(N)
- 通过识别独立的约束子系统
- 缓存中间计算结果
- 增量更新(只重新计算变化的约束)
4. 性能建议:
- 避免约束的动态添加/删除(开销大),用优先级 + 激活/禁用代替
- 减少约束数量
- 使用 intrinsicContentSize 减少显式约束
- 避免大量使用 "priority" 产生的不等式(增加方程数量)
第五章:八股文中的对比总结
一、内存管理对比(深入底层差异)
1.1 weak vs unowned(Swift,底层差异)
weak 的底层实现:
1. 创建 HeapObjectSideTableEntry(如果还没有)
2. 对象的 refCounts 从 inline 模式切换为 side table 模式
3. weak 引用指向 Side Table Entry(不是对象本身)
4. Side Table 中维护 weak reference count
5. 对象 deinit 后:
- Strong RC = 0
- Side Table 仍然存在(因为 weak RC > 0)
- weak 引用通过 Side Table 检测到对象已 deinit → 返回 nil
6. 最后一个 weak 引用消失后:
- weak RC = 0
- Side Table 释放
- 对象内存释放
unowned 的底层实现:
1. 不需要 Side Table(如果没有 weak 引用的话)
2. 使用 inline 的 unowned reference count
3. 对象 deinit 后:
- Strong RC = 0
- Unowned RC > 0 → 对象内存不释放(保持分配状态)
- 对象被标记为"已释放但未回收"
4. 访问 unowned 引用时:
- 检查 Strong RC 是否为 0
- 如果是 → fatal error: "Attempted to read an unowned reference but object was already deallocated"
5. 最后一个 unowned 引用消失后:
- Unowned RC = 0
- 对象内存真正释放(free)
性能对比:
weak:
- 额外开销:创建 Side Table + 维护 weak RC + 清除时遍历
- 每次访问需要检查 Side Table + 可选值解包
unowned:
- 额外开销:仅 unowned RC 的原子操作
- 每次访问只需检查标志位
- 比 weak 快约 2-3 倍
选择建议:
weak:不确定生命周期关系时使用(安全)
unowned:确定引用对象生命周期更长时使用(性能)
unowned(unsafe):等价于 OC 的 __unsafe_unretained(最快但不安全)
1.2 atomic 底层实现
// objc 源码中 atomic 的 getter/setter 实现
// Getter
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
id *slot = (id *)((char *)self + offset);
if (!atomic) return *slot; // nonatomic:直接读取
// atomic:加锁读取
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
return objc_autoreleaseReturnValue(value);
// 注意:这里 retain + autorelease 保证返回值在使用期间不被释放
}
// Setter
void objc_setProperty_atomic(id self, SEL _cmd, id newValue, ptrdiff_t offset) {
reallySetProperty(self, _cmd, newValue, offset, true, false);
}
static void reallySetProperty(id self, SEL _cmd, id newValue,
ptrdiff_t offset, bool atomic, bool copy) {
id oldValue;
id *slot = (id *)((char *)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else {
newValue = [newValue retain];
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
[oldValue release];
}
// PropertyLocks 是一个全局的 StripedMap<spinlock_t>
// 不同属性可能共用同一个锁(哈希冲突)
// 这也是 atomic 性能不太好的原因之一
二、锁的底层对比
2.1 os_unfair_lock vs pthread_mutex 底层差异
os_unfair_lock(iOS 10+):
- 底层:使用 __ulock_wait / __ulock_wake 系统调用(darwin 内核)
- 大小:4 字节(一个 uint32_t)
- 等待方式:互斥等待(不自旋)
- 特点:
- "unfair" 意味着不保证 FIFO 公平性
- 刚释放锁的线程可能立即再次获取(减少上下文切换)
- 在低竞争场景下,这种"不公平"反而提高了吞吐量
- 性能:最快的互斥锁
pthread_mutex:
- 底层:POSIX 标准实现,内部也使用 __ulock_wait / __ulock_wake
- 大小:64 字节(包含更多状态信息)
- 类型:
- PTHREAD_MUTEX_NORMAL:普通互斥锁(同线程重复加锁 → 死锁)
- PTHREAD_MUTEX_RECURSIVE:递归锁(同线程可重复加锁)
- PTHREAD_MUTEX_ERRORCHECK:检查锁(同线程重复加锁 → 返回错误)
- 性能:比 os_unfair_lock 慢 10-20%(额外的状态管理和错误检查)
dispatch_semaphore:
- 底层:
- 快速路径:原子操作 dsema_value(用户态,无系统调用)
- 慢速路径:semaphore_wait / semaphore_signal(Mach 内核信号量)
- 特点:
- 可用于跨进程同步(Mach 信号量是内核对象)
- 但通常只用于线程同步
- 支持超时等待
- 性能:快速路径与 os_unfair_lock 相当,慢速路径开销较大
2.2 OSSpinLock 的优先级反转问题(深入分析)
优先级反转的完整场景:
线程优先级:A(高) > B(中) > C(低)
时间线:
t1: 线程 C 获取 spinlock,开始执行临界区
t2: 线程 A 就绪,抢占 C 的 CPU 时间
t3: 线程 A 尝试获取 spinlock → 自旋等待
- A 持续占用 CPU 自旋(因为 A 优先级最高)
- C 被剥夺了 CPU 时间,无法执行和释放锁
t4: 线程 B 就绪
- B 的优先级高于 C,所以 B 也抢占 C
- C 更加无法获得 CPU 时间
t5: 死锁状态
- A 自旋等待 C 释放锁
- C 无法获得 CPU 时间释放锁(被 A 和 B 抢占)
- B 虽然不需要锁,但也在运行
os_unfair_lock 如何解决?
- 不使用自旋,而是让等待线程休眠
- 内核知道哪个线程持有锁(通过 __ulock_wait 的 owner 参数)
- 如果高优先级线程在等待低优先级线程的锁:
- 内核临时提升低优先级线程的优先级(Priority Inheritance)
- 让低优先级线程能获得 CPU 时间并释放锁
- 释放锁后恢复原始优先级
三、GCD vs NSOperation 底层差异
GCD 底层:
- C 语言 API
- libdispatch 库实现
- 直接管理内核线程(pthread)
- 使用工作窃取(work-stealing)算法
- 每个 CPU 核心有一个线程
- 任务以 block 形式封装(dispatch_continuation_t)
NSOperation 底层:
- Objective-C 封装
- 内部使用 GCD(dispatch_queue)来管理线程
- NSOperationQueue 使用 dispatch_queue 作为底层执行器
- 每个 NSOperation 有状态机(ready → executing → finished)
- 使用 KVO 通知状态变化
// NSOperationQueue 内部大致实现:
@implementation NSOperationQueue
- (void)addOperation:(NSOperation *)op {
[self.lock lock];
// 检查依赖是否满足
if ([op isReady]) {
[self _scheduleOperation:op];
} else {
// 添加到等待队列,监听 isReady 变化
[op addObserver:self forKeyPath:@"isReady" options:0 context:nil];
[self.pendingOperations addObject:op];
}
[self.lock unlock];
}
- (void)_scheduleOperation:(NSOperation *)op {
dispatch_async(self.underlyingQueue, ^{
if ([op isCancelled]) return;
[op start]; // start 内部设置 isExecuting = YES
// 调用 main
// 完成后设置 isFinished = YES
// isFinished KVO 触发后检查依赖方是否 ready
});
}
@end
四、事件传递方向 vs 事件响应方向
这是一个经典的对比,方向完全相反:
事件传递(Hit-Testing):从上往下
UIApplication → UIWindow → RootView → SubView → ... → 最深层的 View
目的:找到最合适的事件处理者(firstResponder)
方向:父 → 子(但在同级子视图中从后往前,即最上面的优先)
事件响应(Responder Chain):从下往上
Hit-tested View → SuperView → ... → ViewController → UIWindow → UIApplication → AppDelegate
目的:如果 firstResponder 不处理,向上传递找到能处理的
方向:子 → 父
实际应用示例:
问题:按钮超出父视图 bounds,点击无响应
原因:hitTest 时 pointInside 判定点不在父视图内 → 直接返回 nil
解决:重写父视图的 pointInside,扩大判定范围
问题:让不规则形状视图只响应可见区域
解决:重写 pointInside,使用路径判断点是否在不规则区域内
五、RunLoop Mode 对比
NSDefaultRunLoopMode vs UITrackingRunLoopMode vs NSRunLoopCommonModes
底层实现:
CFRunLoopMode 包含的数据:
struct __CFRunLoopMode {
CFStringRef _name; // Mode 名称
CFMutableSetRef _sources0; // Source0 集合
CFMutableSetRef _sources1; // Source1 集合
CFMutableArrayRef _observers; // Observer 数组
CFMutableArrayRef _timers; // Timer 数组
CFMutableDictionaryRef _portToV1SourceMap; // mach port → Source1 映射
mach_port_t _timerPort; // Timer 专用端口
// ...
};
NSRunLoopCommonModes 的底层:
- 不是一个真正的 Mode
- RunLoop 结构体中有一个 _commonModes 集合,存放标记为 Common 的 Mode 名称
- 默认包含:kCFRunLoopDefaultMode + UITrackingRunLoopMode
- 还有一个 _commonModeItems 集合,存放被添加到 CommonModes 的 Source/Timer/Observer
- 当一个 Timer 被添加到 CommonModes 时:
1. 将 Timer 添加到 _commonModeItems
2. 遍历 _commonModes 中的每个 Mode
3. 将 Timer 添加到每个 Mode 的 _timers 中
- 效果:Timer 在 Default 和 Tracking 模式下都能触发
滑动时 Timer 不触发的原因:
1. UIScrollView 滑动时,RunLoop 从 kCFRunLoopDefaultMode 切换到 UITrackingRunLoopMode
2. 如果 Timer 只添加到了 DefaultMode → 在 TrackingMode 下不在 _timers 中
3. RunLoop 不会执行不在当前 Mode 中的 Timer
六、深拷贝 vs 浅拷贝(底层实现)
NSString 的 copy 底层:
// 不可变字符串的 copy
[immutableString copy]
→ 实际上调用的是 -copyWithZone:
→ NSString 的实现:return [self retain] // 浅拷贝!
→ 因为不可变字符串不需要真正复制(内容永远不会变)
// 不可变字符串的 mutableCopy
[immutableString mutableCopy]
→ 调用 -mutableCopyWithZone:
→ 创建新的 NSMutableString → 深拷贝
// 可变字符串的 copy
[mutableString copy]
→ 调用 -copyWithZone:
→ 创建新的 NSString → 深拷贝(返回不可变类型)
→ 如果不深拷贝,返回的"不可变"对象可能被原可变对象修改
// property copy 修饰符的底层实现(setter)
- (void)setName:(NSString *)name {
id old = _name;
_name = [name copy]; // 如果 name 是 NSString → retain(浅拷贝)
// 如果 name 是 NSMutableString → 深拷贝
[old release];
}
第六章:高难度深底层原理问题
一、Objective-C 类的 Realize 过程
什么是 Realize?
将类从"未初始化状态"(只有编译期的 class_ro_t)转变为"可用状态"(有 class_rw_t)
Non-lazy class vs Lazy class:
- Non-lazy:实现了 +load 方法 → 在 _read_images 中立即 realize
- Lazy:大部分类 → 延迟到第一次使用时(lookUpImpOrForward 中)
realizeClassWithoutSwift(cls, nil):
1. 从 class_data_bits_t 中取出 class_ro_t
2. 分配 class_rw_t 结构体
3. 设置 rw->ro = ro
4. 设置 rw->flags(根据 ro->flags 和其他信息)
5. cls->bits = rw(现在 bits 指向 rw 而非 ro)
6. 递归 realize 父类和元类:
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);
7. 设置正确的父类和元类指针:
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);
8. 调整 ivar 布局(如果父类大小变化了——Non-Fragile ABI)
9. 将类插入父类的子类链表
10. 加载所有未附加的 Category:
methodizeClass(cls, previously);
methodizeClass(cls, previously):
1. 从 class_ro_t 中获取基础方法列表
2. 将方法列表按 selector 地址排序(用于二分查找)
3. 添加到 class_rw_t 的方法列表中
4. 加载属性列表和协议列表
5. 附加所有 category:
attachCategories(cls, cats, ...)
二、Non-Fragile ABI(非脆弱ABI)
问题背景(Fragile ABI 的问题):
// Objective-C 1.0 的脆弱 ABI
@interface NSObject { Class isa; } // isa 在偏移 0
@interface UIView : NSObject { id _layer; } // _layer 在偏移 8
@interface UIButton : UIView { id _title; } // _title 在偏移 16
如果 Apple 给 UIView 添加新的成员变量:
@interface UIView : NSObject { id _layer; id _newIvar; }
// _layer 偏移 8, _newIvar 偏移 16
问题:UIButton 的 _title 编译时确定偏移是 16
但现在 UIView 多了一个 ivar,_title 应该在偏移 24
→ UIButton 的二进制代码仍然用偏移 16 访问 _title → 读到的是 UIView._newIvar → crash
Non-Fragile ABI 的解决方案(Objective-C 2.0):
1. 编译器不再硬编码成员变量的偏移量
2. 而是生成一个"偏移量变量"(ivar offset variable)
3. 运行时在 realize 时重新计算偏移量
// 编译后的代码:
// 硬编码方式:*(id *)((char *)obj + 16) = value;
// Non-Fragile 方式:*(id *)((char *)obj + OBJC_IVAR_$_UIButton._title) = value;
// OBJC_IVAR_$_UIButton._title 是一个全局变量,运行时修正
// realizeClassWithoutSwift 中的步骤 8:
// 如果父类的 instanceSize 与编译时不同:
// 重新计算所有成员变量的偏移量
// 更新 OBJC_IVAR_$ 变量的值
// 更新 instanceSize
三、Swift 的 Type Metadata 系统
Swift 中每个类型都有对应的 Metadata(类型元数据)
Metadata 的种类:
- Class Metadata:类的元数据,包含 VTable
- Struct Metadata:值类型元数据
- Enum Metadata:枚举元数据
- Function Metadata:函数类型元数据
- Existential Metadata:协议类型元数据
- Metatype Metadata:类型的类型元数据
- Tuple Metadata:元组元数据
Metadata 的获取方式:
- 编译时已知类型:直接嵌入二进制(存储在 __TEXT 段或 __DATA 段)
- 泛型实例化:运行时通过 swift_getGenericMetadata 创建
→ 使用 MetadataCache 缓存已创建的泛型元数据
→ 例如 Array<Int> 和 Array<String> 有不同的元数据
Value Witness Table(VWT):
每个类型都有一个 VWT,包含操作该类型值的函数指针:
struct ValueWitnessTable {
initializeBufferWithCopyOfBuffer // 在 buffer 中拷贝初始化
destroy // 销毁值
initializeWithCopy // 拷贝初始化
assignWithCopy // 拷贝赋值
initializeWithTake // 移动初始化(take = 移动语义)
assignWithTake // 移动赋值
getEnumTagSinglePayload // 枚举相关
storeEnumTagSinglePayload // 枚举相关
size // 值的大小
stride // 对齐后的步幅
flags // 标志(是否 POD、是否 BitwiseTakable 等)
extraInhabitantCount // 额外的无效值数量(用于 Optional 优化)
};
Optional 优化(Extra Inhabitants):
- Bool 只使用 0 和 1 两个值
- UInt8 有 256 个值,其中 254 个可以作为"无效值"
- Optional<Bool> 不需要额外的存储空间
→ 使用值 2 表示 .none(nil)
→ 大小 = 1 字节(与 Bool 相同!)
- 类引用类型:nil(0x0)就是"无效值"
→ Optional<AnyObject> 大小 = 8 字节(与指针相同!)
→ 这就是 Optional 的"零开销"优化
四、ARM64 调用约定(函数调用的底层)
ARM64(AArch64)调用约定:
参数传递:
- x0~x7:前 8 个整数/指针参数
- d0~d7:前 8 个浮点参数
- 超过 8 个的参数通过栈传递
返回值:
- x0:整数/指针返回值
- d0:浮点返回值
- 大结构体:通过 x8 传入返回值地址(caller 分配空间)
特殊寄存器:
- x29(FP):帧指针(Frame Pointer),指向当前栈帧底部
- x30(LR):链接寄存器,保存返回地址
- SP:栈指针
ObjC 方法调用:
// [obj doSomething:arg1 with:arg2]
// 编译为:
// x0 = obj(self)
// x1 = @selector(doSomething:with:)(_cmd)
// x2 = arg1
// x3 = arg2
// bl _objc_msgSend
栈帧结构:
高地址
┌─────────────────┐
│ 参数 N(如果 > 8 个)│
│ ... │
│ 参数 9 │
├─────────────────┤ ← 调用者的 SP
│ 保存的 LR(x30) │ ← FP + 8
│ 保存的 FP(x29) │ ← FP(当前帧指针)
├─────────────────┤
│ 局部变量 │
│ ... │
├─────────────────┤ ← SP(当前栈指针)
低地址
// 回溯调用栈就是沿着 FP 链向上走:
// 当前 FP → 上一个 FP → 上上一个 FP → ...
// 每个 FP + 8 处是对应帧的返回地址(LR)
五、PAC(Pointer Authentication Codes)——A12+ 安全特性
arm64e 架构(iPhone XS 及以后)引入了 PAC
原理:
- 利用 64 位指针中未使用的高位存储签名
- 函数指针、返回地址在使用前会被验证签名
- 如果签名不匹配(被篡改)→ 进程立即崩溃
对 objc_msgSend 的影响:
- bucket_t 中的 IMP 是经过 PAC 签名的
- 调用 IMP 前需要先验证签名
- 这增加了一些性能开销(约 1-2%),但大幅提升了安全性
对 fishhook 的影响:
- fishhook 替换函数指针时,需要使用 ptrauth_sign_unauthenticated 重新签名
- 否则签名不匹配 → crash
六、Swift Concurrency 的底层——Cooperative Thread Pool
传统 GCD 线程模型:
- 每个 dispatch_async 可能创建新线程
- 线程数量可能远超 CPU 核心数
- 大量线程 → 频繁上下文切换 → 性能下降
- 线程爆炸问题(Thread Explosion)
Swift Concurrency 的协作式线程池:
- 线程数量 = CPU 核心数(严格限制)
- 任务不绑定到特定线程
- 任务在 await 处挂起时:
1. 保存任务的状态到"续体帧"(continuation frame)
2. 释放当前线程(线程可执行其他任务)
3. 当异步操作完成时,任务被重新调度
4. 可能在不同的线程上恢复执行
续体帧(Continuation Frame):
- 类似函数调用的栈帧,但分配在堆上
- 包含 await 后续代码的执行状态(局部变量、PC 等)
- 多个续体帧形成"异步栈"
- 不同于传统线程栈的固定大小(通常 512KB),续体帧按需分配
Job 和 Executor:
- Task 被分解为多个 Job(在每个 await 点之间是一个 Job)
- 每个 Job 提交到 Executor 执行
- Global Executor:默认的全局线程池
- Serial Executor:Actor 使用的串行执行器
- Main Actor:在主线程执行的特殊 Actor
七、Crash 收集的底层实现
三种层级的异常捕获:
层级 1:Mach 异常(最底层)
// 注册异常端口
task_set_exception_ports(
mach_task_self(), // 当前进程
EXC_MASK_ALL, // 所有异常类型
exceptionPort, // 自定义端口
EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES,
THREAD_STATE_NONE
);
// 在专门的线程中等待异常消息
mach_msg(&msg, MACH_RCV_MSG, 0, sizeof(msg), exceptionPort, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
// 收到异常消息后:
// msg 包含:异常类型、异常代码、线程 ID、任务 ID
// 可以获取崩溃线程的寄存器状态和调用栈
层级 2:Unix Signal(中间层)
// 内核将未处理的 Mach 异常转换为 Signal
// 映射关系:
// EXC_BAD_ACCESS → SIGSEGV / SIGBUS
// EXC_BAD_INSTRUCTION → SIGILL
// EXC_ARITHMETIC → SIGFPE
// abort() → SIGABRT
struct sigaction sa;
sa.sa_sigaction = signalHandler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGBUS, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
void signalHandler(int sig, siginfo_t *info, void *context) {
// sig:信号类型
// info->si_addr:导致信号的内存地址
// context:CPU 寄存器上下文
// 在这里收集调用栈并写入文件
// 注意:signal handler 中只能调用 async-signal-safe 函数!
// 不能 malloc、不能 objc_msgSend、不能 NSLog
// 只能 write()、_exit() 等
}
层级 3:NSException(OC 层)
NSSetUncaughtExceptionHandler(&exceptionHandler);
void exceptionHandler(NSException *exception) {
// exception.name:异常名称(如 NSInvalidArgumentException)
// exception.reason:异常原因
// exception.callStackSymbols:调用栈符号
// 在这里保存崩溃信息
}
为什么需要三个层级?
1. Mach 异常最底层,能捕获所有类型
2. 但 Mach 异常处理复杂,且某些异常(如 SIGABRT)不是 Mach 异常
3. Signal 处理 SIGABRT 和其他信号
4. NSException 提供最详细的 OC 层信息(name、reason、userInfo)
5. 三者互补,确保不遗漏任何崩溃
八、符号化(Symbolication)的底层过程
从崩溃地址到函数名的完整过程:
1. 崩溃日志中的原始信息:
0x0000000104a8c3d4 MyApp + 0x1243d4
2. 计算文件偏移地址:
文件偏移 = 崩溃地址 - 加载基地址
= 0x104a8c3d4 - 0x104968000
= 0x1243D4
3. 在 dSYM 文件中查找:
dSYM 中包含 DWARF 调试信息,结构:
.debug_info:编译单元、函数、变量等的描述
.debug_line:地址到源代码行号的映射表
.debug_abbrev:调试信息的缩写表
.debug_str:字符串表
4. 查找过程:
a. 在 .debug_aranges 中二分查找 0x1243D4 属于哪个编译单元
b. 在该编译单元的 .debug_info 中找到包含该地址的 DW_TAG_subprogram(函数)
c. DW_TAG_subprogram 包含:
- DW_AT_name:函数名
- DW_AT_low_pc / DW_AT_high_pc:函数地址范围
- DW_AT_decl_file:源文件名
- DW_AT_decl_line:声明行号
d. 在 .debug_line 中查找精确的源代码行号
5. UUID 匹配:
- 每次编译生成唯一的 UUID(128 位)
- 存储在 Mach-O 的 LC_UUID load command 中
- 和 dSYM 中的 UUID 必须一致
- 不一致说明 dSYM 不是这个 binary 对应的,符号化结果不可信
6. atos 命令行工具的原理:
atos -arch arm64 -o MyApp.app.dSYM/Contents/Resources/DWARF/MyApp -l 0x104968000 0x104a8c3d4
// 输出:-[ViewController viewDidLoad] (in MyApp) (ViewController.m:42)
附录:高频面试题速查(带底层深度)
必问题目 + 追问
-
消息发送机制 → 追问:objc_msgSend 为什么用汇编?cache_t 的哈希冲突如何解决?扩容时为什么清空缓存?
-
weak 原理 → 追问:SideTable 为什么有 64 个?weak_entry_t 为什么有 inline 优化?
-
Block → 追问:__forwarding 指针的两种状态?Block copy 时 capture 的对象怎么处理?****
4. RunLoop → 追问:mach_msg 是什么?休眠为什么不消耗 CPU?autorelease 对象什么时候释放?****
5. Category → 追问:运行时如何加载?为什么"覆盖"原方法?class_rw_ext_t 是什么?****
6. KVO → 追问:为什么用 isa swizzling 而不是 method swizzling?动态子类重写了哪些方法?****
7. AutoreleasePool → 追问:page 大小是多少?怎么找到 POOL_BOUNDARY?子页的释放策略?****
8. GCD 死锁 → 追问:dispatch_sync 底层如何检测死锁?线程池大小是多少?****
9. 启动优化 → 追问:Page Fault 一次多少毫秒?Clang 插桩的原理?dyld3 闭包是什么?****
10. 卡顿优化 → 追问:如何在子线程获取主线程调用栈?为什么离屏渲染慢?VSync 机制?****
11. 循环引用 → 追问:MLeaksFinder 原理?NSProxy vs NSObject 的消息转发差异?****
12. 线程安全 → 追问:OSSpinLock 优先级反转的完整过程?os_unfair_lock 如何解决?****
13. 事件传递 → 追问:hitTest 为什么从后往前遍历子视图?手势识别器和 touch 事件的竞争?****
14. HTTPS → 追问:TLS 1.2 vs 1.3 的区别?证书固定的两种方式?****
15. Swift 内存 → 追问:weak 和 unowned 的底层区别?Side Table Entry?****
本文档深入到源码级和汇编级,覆盖 iOS 面试中 99% 以上的深度知识点。理解"为什么这样设计"比记住"怎么实现"更重要。面试时能从表层讲到底层,从使用讲到原理,展现出对技术的深度理解,这才是高分答案。