Objective-C 复习知识点
站在资深 iOS 工程师视角,按「知识点 → 底层原理 → 实战例子 → 易错坑 → 高频面试题」的脉络,把 Objective-C 从语法基础、Runtime、内存管理、Foundation、性能优化到架构设计串成一条主线。即便 SwiftUI 时代,UIKit、Foundation、Runtime 仍然以 OC 思路实现,理解 OC 仍是深入 iOS 底层的必经之路。代码示例可直接粘到 Xcode 工程运行。
文档导航
- 前言:为什么今天还要学 OC
- 第一部分 · 语言基础
- 第二部分 · Runtime 与动态性
- 第三部分 · 内存与并发
- 第四部分 · Foundation 与 UI
- 第五部分 · 进阶与优化
- 第六部分 · 架构设计
前言
Objective-C(下文简称 OC)诞生于 1980 年代,是 Brad Cox 在 1984 年创造的,1996 年被 NeXTSTEP(乔布斯离开 Apple 后创办的公司)采纳为主力语言,最终随着 Mac OS X 与 iOS 进入 Apple 生态。
今天为什么还要学 OC?
- 底层全是 OC:UIKit、Foundation、CFNetwork、Core Data 全部用 OC 实现。理解 OC 才能真正理解这些框架的运行机制。
- Runtime 是动态性的根基:KVO、KVC、JSPatch、Aspects、BlockHook、AOP、热更新全依赖 OC Runtime。
- 海量存量代码:腾讯、阿里、字节、美团等一线大厂的 App 主体仍是 OC(虽然新功能可能用 Swift)。
- 架构经验沉淀:组件化(CTMediator、BeeHive)、AOP 框架、监控方案几乎都是基于 OC 思想设计。
- 面试必考:Runtime、Run Loop、内存管理、Block 是 iOS 高级岗位的「送分题」也是「分水岭」。
本文档目标:把 OC 从语言基础一路讲到架构设计,让读者既能写好日常代码,也能在面试中游刃有余,还能在大项目中设计出可演进的架构。每章节统一遵循「核心要点 → 底层原理 → 实战例子 → 易错坑 → 高频面试题」五段式。
第一部分 · 语言基础
第 1 章 Objective-C 历史与运行时本质
1.1 核心要点
- OC = C + SmallTalk 风格的消息传递。
- OC 是 C 的超集:所有合法 C 代码都是合法 OC 代码。
- 编译产物是原生机器码,不像 Java 有 VM。
- 运行时维护一套元数据(类对象、方法表、
isa指针),称为 Runtime。 - Runtime 源码:opensource.apple.com 的
objc4,已开源。 - 1984 年由 Brad Cox 与 Tom Love 创造,1996 年随 NeXTSTEP 成为 Apple 主力语言,2014 年 Swift 发布后逐步让位但未消失。
1.2 编译与运行时
OC 程序从源码到运行分两阶段:
.m / .mm 源码
↓ Clang 前端
↓ 词法分析 → 语法分析 → AST
↓ 语义分析 + 类型检查
↓ 代码生成
LLVM IR
↓ LLVM 优化器
↓ LLVM 后端
Mach-O 机器码(与 C 完全相同)
↓ 加载到内存(dyld)
运行时(Runtime 启动:注册类、绑定方法、调用 +load)关键认知:
- OC 在编译期就生成了原生机器码,没有「字节码 → 解释执行」环节。
- 但 OC 在运行时维护了一份元数据(每个类的
objc_class结构体),用于支持动态消息派发。 - 这套元数据 + 操作它们的 C API(
<objc/runtime.h>),统称为 Runtime。
1.3 OC 与 C 的关系
// 纯 C 函数与 OC 方法可以混用
#include <stdio.h>
void cHello(void) {
printf("Hello from C\n");
}
@interface MyClass : NSObject
- (void)ocHello;
@end
@implementation MyClass
- (void)ocHello {
NSLog(@"Hello from OC");
cHello(); // 直接调 C 函数
}
@end- OC 方法最终编译为 C 函数(前两个参数固定为
self、_cmd)。 - OC 类是 C 结构体(
struct objc_class)。 - OC 对象是 C 结构体(
struct objc_object,首成员isa)。
1.4 Runtime 演进
| 版本 | 特点 |
|---|---|
| Legacy(OC 1.0,32 位 Mac) | isa 是纯指针;元数据旧格式 |
| Modern(OC 2.0,64 位,iOS 全部) | isa 是 non-pointer(位域);运行时重写;性能优化 |
OC 2.0 在 iOS 上始终是 modern runtime,无需考虑 legacy。
1.5 高频面试题
Q1:OC 与 C 的关系? A:OC 是 C 的超集,编译产物就是原生机器码(与 C 相同)。OC 在 C 之上加了「面向对象语法 + SmallTalk 风格的消息传递 + Runtime 元数据」。所有 OC 方法本质是 C 函数(前两参是 self 和 _cmd),所有 OC 类本质是 C 结构体。
Q2:OC 与 Java 的运行时区别? A:OC 没有 VM(虚拟机),编译产物直接跑在 CPU 上。Java 字节码需要 JVM 解释执行(或 JIT)。OC 的「动态性」依赖运行时维护的元数据(类对象、方法表),而不是 VM。
Q3:Runtime 是什么?开源吗? A:Runtime 是 OC 提供的一套「运行时元数据 + C API」,用于支持动态消息派发、内省、Method Swizzling 等特性。源码 objc4 已开源,见 opensource.apple.com。
Q4:OC 的「动态性」体现在哪? A:①动态类型(id 类型运行时决定);②动态绑定(方法调用走 objc_msgSend,运行时查找);③动态加载(Category 在运行时合并);④动态添加(class_addMethod、objc_setAssociatedObject);⑤消息转发(动态处理未实现的方法)。
第 2 章 语法基础与消息传递
2.1 核心要点速查
头文件与实现:
// Person.h(公开声明)
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)introduce;
+ (Person *)personWithName:(NSString *)name;
@end
// Person.m(实现)
#import "Person.h"
@implementation Person
- (void)introduce {
NSLog(@"I'm %@, %ld years old", self.name, (long)self.age);
}
+ (Person *)personWithName:(NSString *)name {
Person *p = [Person new];
p.name = name;
return p;
}
@end方法调用语法:
// 无参方法
[obj hello];
// 带参方法(每个参数前都有「标签」)
[obj doSomethingWithArg1:arg1 arg2:arg2];
// 类方法
[Person personWithName:@"Alice"];
// 嵌套调用
[[NSString alloc] init];2.2 方法签名(selector)
OC 方法在语言层面用 selector 唯一标识:
SEL sel = @selector(introduce);
SEL clsSel = @selector(personWithName:);SEL是一个 opaque 类型,本质是经过哈希的 C 字符串。- 同名 selector 在全局只存一份(哈希表)。
- 方法名包含所有标签(
doSomethingWithArg1:arg2:),缺一不可。
2.3 消息传递的编译转换
// 你写的代码
[person introduce];
[person setName:@"Alice"];
// 编译器翻译成
((void(*)(id, SEL))objc_msgSend)(person, @selector(introduce));
((void(*)(id, SEL, NSString *))objc_msgSend)(person, @selector(setName:), @"Alice");objc_msgSend 是核心函数,查找流程见 第 9 章。
易错点:
- 调用
super方法编译为objc_msgSendSuper(不是objc_msgSend),告诉运行时从父类开始查找。但self仍是当前对象。 - 给
nil发消息是合法的——objc_msgSend在函数入口判断 receiver 为 nil 就直接返回(返回 0 / nil / 0.0 等零值)。 - 给
nil发消息返回 struct 时行为平台相关(arm64 上小于等于 16 字节的 struct 安全,更大可能 crash)。
2.4 数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 基本类型 | 与 C 完全相同 | int、float、BOOL(YES/NO) |
id | 任意 OC 对象(动态类型) | id obj = [[NSObject alloc] init]; |
instancetype | 当前类的类型(仅用于方法返回) | + (instancetype)person |
Class | 类对象 | Class cls = [Person class]; |
SEL | 方法选择器 | SEL s = @selector(hello); |
IMP | 函数指针 | IMP imp = method_getImplementation(method); |
NSString * | OC 字符串 | @"hello" |
NSNumber * | 数字对象 | @1(语法糖) |
NSArray * | 数组 | @[@1, @2] |
NSDictionary * | 字典 | @{@"k": @"v"} |
Block | 闭包 | ^{ /* ... */ } |
2.5 控制流与字面量
字面量语法(LLVM 4+ 编译器特性):
// 数字
NSNumber *n1 = @1;
NSNumber *n2 = @'A';
NSNumber *n3 = @YES;
NSNumber *n4 = @(1 + 2); // 表达式
// 数组与字典
NSArray *arr = @[@"a", @"b", @"c"];
NSDictionary *dict = @{@"key": @"value"};
NSMutableArray *marr = [@[@"a"] mutableCopy];
// 下标访问
NSString *first = arr[0];
dict[@"newKey"] = @"newValue";盒装表达式(Boxed Expression):
NSNumber *pi = @(3.14);
NSNumber *timeout = @(someIntVariable);2.6 instancetype 与 id 的区别
// ❌ 用 id 返回,编译器不知道返回什么类型
+ (id)personWithName:(NSString *)name;
// ✅ 用 instancetype,编译器知道返回当前类的类型
+ (instancetype)personWithName:(NSString *)name;
// 子类继承时自动适配
[Person personWithName:@"x"]; // 返回 Person *
[Student personWithName:@"x"]; // 返回 Student *instancetype只能用于方法返回类型,不能用作变量类型。alloc、init、new等约定方法必须返回instancetype。- 现代 OC 编程统一用
instancetype替代id作为初始化方法返回类型。
2.7 高频面试题
Q1:selector 是什么? A:方法名的唯一标识符,本质是经过哈希的 C 字符串。同名 selector 全局只存一份。@selector(name) 在编译期确定,运行时通过 sel_registerName 注册到全局表。
Q2:给 nil 发消息会怎样? A:合法,直接返回零值(0 / nil / 0.0)。但返回大 struct(arm64 上 > 16 字节)可能 crash,因为返回值放在指针里,nil 时无空间。实践中给 nil 发消息是安全且常用的模式,避免大量 nil 检查。
Q3:instancetype 和 id 区别? A:instancetype 是「当前类的类型」,编译器能做类型推断;id 是「任意 OC 对象」,无类型检查。初始化方法用 instancetype,回调参数用 id<Protocol>。
Q4:super 调用本质? A:编译为 objc_msgSendSuper,运行时从父类的 method list 开始查找。但 self 仍是当前对象(不是父类对象)。[super dealloc] 在 ARC 下不能写。
第 3 章 类、对象与 isa 指针
3.1 对象的本质
每个 OC 对象在内存中的首字段都是 isa:
// 简化版结构(objc4 源码 objc-private.h)
struct objc_object {
Class isa;
};
// arm64 上 isa 是位域(non-pointer ISA)
struct objc_object {
uintptr_t isa_taggedptr_or_raw;
// 实际是 union {
// Class cls;
// uintptr_t bits;
// struct { ... 位域 ... };
// };
};实例对象的 isa 指向类对象,类对象存储实例方法、属性、协议、父类指针等元数据。
3.2 类对象的本质
struct objc_class : objc_object {
Class superclass; // 父类指针
cache_t cache; // 方法缓存
class_data_bits_t bits; // 方法列表、属性列表、协议列表
};类对象本身也是对象,所以也有 isa——指向元类(metaclass)。
3.3 isa 链与继承链
instance ──isa──▶ class ──isa──▶ metaclass ──isa──▶ root metaclass
▲ │
└─────────────isa───────────────────────────┘
(root metaclass.isa = self)
│
│ superclass 继承链
▼
superclass (e.g. NSObject)关键认知:
- 实例对象的
isa指向类对象(存实例方法)。 - 类对象的
isa指向元类(存类方法+方法)。 - 元类的
isa指向根元类(NSObject 的元类)。 - 根元类的
isa指向自己,形成闭环。 - 根元类的
superclass是 NSObject 类对象——这就是为什么[SomeClass alloc]这种根类方法所有类都能调(沿元类继承链查到 NSObject 元类,再沿 superclass 链查到 NSObject 类对象)。
3.4 验证 isa 链
#import <objc/runtime.h>
@interface Animal : NSObject @end
@implementation Animal @end
@interface Dog : Animal @end
@implementation Dog @end
int main(int argc, const char *argv[]) {
@autoreleasepool {
Dog *dog = [Dog new];
// 实例的 isa 指向类
NSLog(@"dog.isa = %s", class_getName(object_getClass(dog))); // Dog
// 类的 isa 指向元类
NSLog(@"Dog.isa = %s", class_getName(object_getClass([Dog class]))); // Dog
// 元类的 isa 指向根元类
Class dogMeta = object_getClass([Dog class]);
NSLog(@"DogMeta.isa = %s", class_getName(object_getClass(dogMeta))); // NSObject
// 根元类的 isa 指向自己
Class nsObjMeta = object_getClass([NSObject class]);
NSLog(@"NSObjectMeta.isa = %s", class_getName(object_getClass(nsObjMeta))); // NSObject
// superclass 链
NSLog(@"Dog.super = %s", class_getName(class_getSuperclass([Dog class]))); // Animal
NSLog(@"Animal.super = %s", class_getName(class_getSuperclass([Animal class]))); // NSObject
}
}3.5 non-pointer ISA(arm64)
arm64 上 isa 不是纯指针,而是位域:
// arm64 简化版(实际位段更多)
struct isa_t {
uintptr_t nonpointer : 1; // 0 = 纯指针;1 = 位域
uintptr_t has_assoc : 1; // 是否有关联对象
uintptr_t has_cxx_dtor : 1; // 是否有 C++ 析构
uintptr_t shiftcls : 33; // 类指针(33 位够用)
uintptr_t magic : 6; // 调试用
uintptr_t weakly_referenced : 1; // 是否被弱引用
uintptr_t deallocating : 1; // 是否正在 dealloc
uintptr_t has_sidetable_rc : 1; // 引用计数是否溢出到 SideTable
uintptr_t extra_rc : 19; // 引用计数 - 1
};优化目的:减少内存访问次数,把引用计数、关联对象标志等塞进 isa 一并存储。
取真实类指针:
Class cls = object_getClass(obj); // 等价 (Class)(isa & ISA_MASK)3.6 对象内存布局
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
// 内存布局(64 位):
// offset 0: isa (8 bytes)
// offset 8: _name (8 bytes, NSString *)
// offset 16: _age (8 bytes, NSInteger)- 实例变量按声明顺序排列(编译期决定偏移)。
- 父类的 ivar 在子类前面。
- 8 字节对齐。
- 这就是为什么 Category 不能加 ivar——会破坏所有现有对象的内存布局。
class_getInstanceSize(Person) 返回 24(实际可能因为对齐到 32)。
3.7 高频面试题
Q1:OC 对象的本质是什么? A:指向 struct objc_object 的指针,首字段是 isa。isa 指向类对象,类对象存方法列表、属性、协议、父类指针等元数据。
Q2:实例对象、类对象、元类的关系? A:实例对象的 isa 指向类对象;类对象的 isa 指向元类;元类的 isa 指向根元类;根元类的 isa 指向自己。继承链上,类对象的 superclass 指向父类对象;元类的 superclass 指向父元类;根元类的 superclass 指向根类对象(NSObject)。
Q3:为什么 [SomeClass alloc] 能调到 NSObject 的 alloc? A:SomeClass 是类对象,它的 isa 指向 SomeClass 的元类。元类没找到 alloc,沿 superclass 链查父元类……直到根元类。根元类的 superclass 是 NSObject 类对象,所以能调到 NSObject 的实例方法 +alloc。
Q4:arm64 上 isa 还是纯指针吗? A:不是。arm64 上 isa 是 non-pointer(位域):1 位 nonpointer 标志 + 33 位类指针 + 引用计数 + 标志位。访问真实类指针用 isa & ISA_MASK。
Q5:Category 为什么不能加 ivar? A:类的内存布局在编译期就确定了,所有实例按这个布局分配。Category 在运行时合并到类对象上,此时已注册的类布局无法改变(否则破坏所有现有对象的内存)。可以用关联对象模拟(参见 第 11 章)。
第 4 章 @property 与属性语义
4.1 @property 本质
@property (nonatomic, copy) NSString *name;等价于编译器自动生成:
// 1. 实例变量(默认带下划线)
private NSString *_name;
// 2. getter
- (NSString *)name {
return _name;
}
// 3. setter(根据语义不同)
- (void)setName:(NSString *)name {
if (_name != name) {
NSString *tmp = [name copy]; // copy 语义
[_name release]; // ARC 自动插入
_name = tmp;
}
}也可以手动实现:@synthesize name = _name; 指定 ivar;@dynamic name; 告诉编译器不要生成(运行时再提供,典型场景 Core Data)。
4.2 属性语义速查
| 修饰符 | 行为 | 适用 |
|---|---|---|
strong / retain | 持有,引用计数 +1 | OC 对象,所有权关系 |
weak | 不持有,对象释放后自动置 nil | delegate、避免循环引用 |
assign / unsafe_unretained | 不持有,不置 nil | 基本类型(int/float)、非 OC 对象 |
copy | setter 中 copy 一份新对象 | NSString、block、不可变集合 |
atomic | getter/setter 加锁(默认) | 线程安全的最弱保证 |
nonatomic | 不加锁 | 性能优先(绝大多数场景) |
readonly | 只生成 getter | 不变量 |
readwrite | 生成 getter + setter(默认) | 可变量 |
getter=name / setter=name | 自定义 getter/setter 名 | 如 BOOL isHidden 的 setHidden: |
class | 类属性(OC 不支持存储,只声明) | Swift 互操作时用 |
nullability | nullable / nonnull / null_unspecified / null_resettable | Swift 桥接类型安全 |
4.3 strong / weak / copy 选择决策树
是基本类型(int / float / BOOL)?
├─ 是 → assign
└─ 否 ↓
是 block?
├─ 是 → copy(惯例,ARC 下 strong 也会 copy)
└─ 否 ↓
是不可变类型(NSString / NSArray / NSDictionary),且可能有可变子类?
├─ 是 → copy(防止外部传入可变版本偷偷改)
└─ 否 ↓
是 delegate / 通知者?
├─ 是 → weak(防循环引用)
└─ 否 ↓
普通对象 → strong4.4 copy 深入
// ❌ 用 strong 存 NSMutableString,外部修改会偷偷变
@property (nonatomic, strong) NSString *name;
NSMutableString *m = [NSMutableString stringWithString:@"Alice"];
person.name = m;
[m appendString:@" Bob"];
NSLog(@"%@", person.name); // "Alice Bob"!封装被破坏
// ✅ 用 copy,setter 中拷贝为不可变副本
@property (nonatomic, copy) NSString *name;
person.name = m; // setter 内部执行 _name = [m copy],得到不可变副本
[m appendString:@" Bob"];
NSLog(@"%@", person.name); // "Alice" 安全集合类型的 copy / mutableCopy:
NSArray *arr = @[@1, @2];
NSArray *arr2 = [arr copy]; // 浅拷贝(元素不拷贝)
NSMutableArray *marr = [arr mutableCopy]; // 浅拷贝,得到可变副本
// 深拷贝(元素也拷贝)
NSMutableArray *deepCopy = [NSMutableArray array];
for (id item in arr) {
[deepCopy addObject:[item copy]];
}4.5 atomic 真的安全吗
@property (atomic, strong) NSMutableArray *arr;
// ❌ 即使 atomic,下面操作仍然有竞态
self.arr = [NSMutableArray array]; // OK,setter 加锁
[self.arr addObject:obj]; // addObject 没加锁atomic只保证 setter / getter 各自的原子性。- 复合操作(读 → 改 → 写)仍然有竞态:
self.count = self.count + 1。 atomic比nonatomic慢 20 倍左右(要加锁)。- 几乎所有实践都使用
nonatomic+ 显式加锁。
4.6 自定义 getter / setter
@interface User : NSObject
@property (nonatomic, copy, readonly) NSString *displayName;
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@end
@implementation User
// 自定义 getter,让 displayName 是计算属性
- (NSString *)displayName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
@end自定义 setter 的标准模板(MRC 时代用,ARC 下自动管理):
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release]; // MRC 才需要,ARC 会自动
_name = [name copy];
}
}4.7 nullability 注解
@interface User : NSObject
@property (nonatomic, copy, nullable) NSString *middleName;
@property (nonatomic, copy, nonnull) NSString *firstName;
@property (nonatomic, copy, null_resettable) NSString *displayName;
- (nullable NSString *)optionalName;
- (void)greet:(nonnull NSString *)name;
@end批量注解:
NS_ASSUME_NONNULL_BEGIN
@interface User : NSObject
@property (nonatomic, copy) NSString *name; // 自动 nonnull
- (nullable User *)findUser:(NSString *)name;
@end
NS_ASSUME_NONNULL_END- 作用:让 Swift 桥接时变成 Optional / NonOptional,类型安全。
- iOS 9+ 完整支持。
4.8 高频面试题
Q1:NSString 为什么用 copy? A:可能传入 NSMutableString。如果用 strong,外部修改 mutable string 会让属性值偷偷变化,破坏封装。copy 在 setter 中拷贝一份不可变副本,外部再修改不影响属性。
Q2:atomic 真的线程安全吗? A:不是。atomic 只保证 getter / setter 这一对操作的原子性(加锁)。复合操作(self.count += 1)依然有竞态。所以实践中一律用 nonatomic + 显式加锁(@synchronized、dispatch_semaphore、pthread_mutex、os_unfair_lock)。
Q3:@property 编译器自动生成了什么? A:①实例变量(带下划线前缀);②getter;③setter(根据语义:strong 调 retain,copy 调 copy,weak 注册到 weak 表);④属性的元数据(property_t,含 name、attributes)。
Q4:copy 和 mutableCopy 区别? A:copy 返回不可变副本(无论原对象是否可变),mutableCopy 返回可变副本。两者都是浅拷贝(容器本身拷贝,元素不拷贝),深拷贝需自己实现 NSCopying / NSMutableCopying 协议并递归。
Q5:@synthesize 和 @dynamic 区别? A:@synthesize 让编译器自动合成 ivar + getter/setter(现代 OC 默认行为);@dynamic 告诉编译器「不要合成,运行时会有」,典型场景是 Core Data NSManagedObject 子类(属性由框架动态提供)。
第 5 章 Category 与 Extension
5.1 Category(分类)
// NSString+Reversed.h
#import <Foundation/Foundation.h>
@interface NSString (Reversed)
- (NSString *)reversed;
@end
// NSString+Reversed.m
#import "NSString+Reversed.h"
@implementation NSString (Reversed)
- (NSString *)reversed {
NSUInteger len = self.length;
NSMutableString *result = [NSMutableString stringWithCapacity:len];
for (NSInteger i = len - 1; i >= 0; i--) {
unichar c = [self characterAtIndex:i];
[result appendFormat:@"%C", c];
}
return result;
}
@end
// 使用
NSString *s = @"hello";
NSLog(@"%@", [s reversed]); // "olleh"作用:
- 给现有类添加方法(即使没有源码,包括系统类)。
- 拆分大型类的实现(按功能分文件)。
- 模拟「私有方法」对外隐藏。
- 把 informal protocol 形式化。
限制:
- 不能添加实例变量(破坏内存布局)。
- 可以添加属性,但只生成 getter/setter 声明,不合成 ivar——必须用关联对象手动实现(见 第 11 章)。
- 同一类多个 Category 实现同名方法 → 编译顺序决定哪个生效(最后一个编译的覆盖前面,包括类本体)。
5.2 Category 加载时机
// objc4 源码(简化)
// Runtime 启动时:
// 1. 读取 Mach-O 中的 __objc_catlist section
// 2. 对每个 Category 调用 attachCategories(cls, cat)
// 3. 把 Category 的方法列表合并到 cls 的 method_list_t 头部- Category 在 Runtime 启动阶段(
map_images→_read_images)合并到类对象。 - 合并后方法出现在类的
method_list_t头部,所以 Category 方法「覆盖」原类方法(其实是查找时先找到)。 - 这是运行时行为,所以编译顺序无关运行时(但影响多个 Category 同名方法的覆盖关系)。
5.3 +load 与 +initialize 在 Category 中的行为
| 方法 | 调用次数 | 调用顺序 | Category 行为 |
|---|---|---|---|
+load | 每个类 / Category 各 1 次 | 父类 → 子类 → 各自的 Category | Category 的 +load 与主类的 +load 都会调用 |
+initialize | 每个类最多 1 次(懒加载) | 父类 → 子类 | Category 的 +initialize 覆盖 主类的(同普通方法) |
@interface Person : NSObject @end
@implementation Person
+ (void)load { NSLog(@"Person +load"); }
+ (void)initialize { NSLog(@"Person +initialize"); }
@end
@interface Person (Cat1) @end
@implementation Person (Cat1)
+ (void)load { NSLog(@"Cat1 +load"); }
+ (void)initialize { NSLog(@"Cat1 +initialize"); }
@end
// 输出顺序:
// Person +load
// Cat1 +load
// (Person 首次使用时:)Cat1 +initialize(覆盖了 Person 的)5.4 Extension(扩展 / 匿名分类)
// Person.m
#import "Person.h"
// Extension 必须在主类的 .m 中
@interface Person ()
{
NSString *_privateIvar; // 可以加 ivar
}
@property (nonatomic, copy) NSString *internalProperty; // 私有属性
- (void)privateMethod; // 私有方法声明
@end
@implementation Person
- (void)privateMethod { /* ... */ }
@endExtension 与 Category 的区别:
| 维度 | Category | Extension |
|---|---|---|
| 命名 | 必须有名字 (Name) | 匿名 () |
| 文件位置 | 独立 .h/.m | 主类 .m 内 |
| 加 ivar | ❌ | ✅ |
| 加属性 | 只声明 getter/setter,无 ivar | 完整合成 ivar + getter/setter |
| 编译时合并 | 运行时合并 | 编译时合并 |
| 用途 | 给现有类添加方法 | 声明私有接口 |
5.5 Category 实战:模块拆分
大型类(如 UIViewController 子类几千行)按功能拆分到多个 Category:
// UserViewController.h(公开)
@interface UserViewController : UIViewController
- (void)viewDidLoad;
@end
// UserViewController.m(主实现)
@implementation UserViewController
- (void)viewDidLoad {
[self setupUI];
[self loadData];
}
@end
// UserViewController+UI.h/m
@interface UserViewController (UI)
- (void)setupUI;
@end
// UserViewController+Network.h/m
@interface UserViewController (Network)
- (void)loadData;
@end
// UserViewController+Actions.h/m
@interface UserViewController (Actions)
- (void)onLoginButton:(UIButton *)sender;
@end注意:Category 方法对外可见(声明在 .h 就是公开的),但可以通过只把声明写在 .m 中做「私有方法」。
5.6 Category 同名方法冲突
// Category A
@implementation NSString (ReverseA)
- (NSString *)reversed { return @"A"; }
@end
// Category B
@implementation NSString (ReverseB)
- (NSString *)reversed { return @"B"; }
@end
NSString *s = @"x";
[s reversed]; // 取决于哪个 Category 后编译!规避方案:
- 方法名加前缀(如
xxx_reversed)。 - 用
+load中显式class_addMethod检查存在性。 - 系统类的 Category 用
MY前缀(避免与系统 / 第三方库冲突)。
5.7 高频面试题
Q1:Category 能添加 ivar 吗?为什么? A:不能。类的内存布局在编译期确定,所有实例按这个布局分配。Category 在运行时合并到类对象,此时类的布局无法改变(会破坏所有现有对象的内存)。可以用关联对象(objc_setAssociatedObject)模拟。
Q2:Category 与 Extension 的区别? A:①Extension 匿名,Category 有名;②Extension 编译时合并到主类,Category 运行时合并;③Extension 可以加 ivar,Category 不行;④Extension 必须在主类 .m 中声明,Category 可以独立文件。
Q3:Category 方法覆盖原类方法的原理? A:Runtime 合并 Category 时,把 Category 的方法插入到类的 method_list_t 头部。objc_msgSend 查找时从前往后遍历,先找到 Category 的方法,原方法被「覆盖」(实际是先找到了)。
Q4:多个 Category 实现同名方法怎么办? A:取决于编译顺序(Build Phases → Compile Sources 顺序)。后编译的 Category 在方法列表头部,会覆盖前面的。规避:方法名加前缀,避免冲突。
Q5:+load 和 +initialize 在 Category 中行为? A:+load 主类和每个 Category 各调用一次(不覆盖);+initialize 只调用一次(首次使用该类时),Category 的覆盖主类的。
第 6 章 协议 @protocol
6.1 协议定义与实现
// Drawable.h
@protocol Drawable <NSObject>
@required
- (void)draw;
@property (nonatomic, strong) UIColor *color;
@optional
- (void)drawWithOpacity:(CGFloat)opacity;
@end
// Shape.h
@interface Shape : NSObject <Drawable>
@end
// Shape.m
@implementation Shape
- (void)draw { /* 实现 */ }
@synthesize color = _color; // 协议中的属性在实现类中合成
// drawWithOpacity: 不实现也合法(@optional)
@end6.2 协议特性
@required(默认):必须实现,否则编译警告。@optional:可选实现。- 协议可继承:
@protocol A <B>(A 继承 B 的所有方法声明)。 - 协议可被多类实现(跨继承树的多态)。
- OC 协议没有关联类型(与 Swift 不同),能力弱一些。
6.3 协议的使用方式
// 类型约束
id<Drawable> obj; // 任意实现 Drawable 的对象
NSArray<id<Drawable>> *arr; // 数组元素都是 Drawable
UIViewController<UITableViewDelegate> *vc;
// 检查
if ([obj conformsToProtocol:@protocol(Drawable)]) { ... }
if ([obj respondsToSelector:@selector(drawWithOpacity:)]) { ... }
// 转换
id<Drawable> drawable = (id<Drawable>)shape;6.4 协议作为 Delegate 模式
// 声明
@class MyView;
@protocol MyViewDelegate <NSObject>
@optional
- (void)myView:(MyView *)view didTapButton:(UIButton *)button;
- (BOOL)myViewShouldBeginEditing:(MyView *)view;
@end
@interface MyView : UIView
@property (nonatomic, weak) id<MyViewDelegate> delegate;
@end
// 调用前必须检查
@implementation MyView
- (void)onTap {
if ([self.delegate respondsToSelector:@selector(myView:didTapButton:)]) {
[self.delegate myView:self didTapButton:self.button];
}
}
@enddelegate 用 weak:避免循环引用(MyView 持有 delegate,delegate 通常持有 MyView)。
6.5 informal protocol(非正式协议)
旧 OC 时代(协议不支持 @optional)的做法:分类到 NSObject 上,让所有对象「都可以」实现这些方法。
@interface NSObject (MyInformalProtocol)
- (void)optionalMethod;
@end
// 实现者按需实现,调用前 respondsToSelector: 检查现代 OC 已被正式 protocol 取代,但 UIKit / Foundation 中仍能见到。
6.6 协议对象(Protocol 对象)
Protocol *p = @protocol(Drawable);
NSLog(@"%s", protocol_getName(p)); // "Drawable"
// 检查遵循
[class_conformsToProtocol([Shape class], @protocol(Drawable))];每个协议运行时有一个 Protocol 对象,存方法列表与父协议。
6.7 高频面试题
Q1:OC 协议与 Swift 协议的区别? A:①OC 协议无关联类型;②OC 协议不能有默认实现(要用 Category 模拟);③OC 协议不能要求 init;④OC 协议属性只声明,不合成;⑤Swift 协议可以被 enum / struct 实现,OC 协议只能被 class 实现。
Q2:delegate 为什么用 weak? A:避免循环引用。delegate 通常持有 self(被代理对象),如果被代理对象也 strong 持有 delegate,就形成环。weak 引用不计引用计数,对象释放后自动置 nil。
Q3:调用可选方法前必须做什么? A:用 respondsToSelector: 检查。否则未实现的方法调用会 crash(unrecognized selector)。
Q4:id<Drawable> 和 Shape * 的区别? A:前者只暴露协议要求的方法,调用方不知道具体类型;后者暴露 Shape 的所有方法。前者更解耦,是面向接口编程的体现。
第 7 章 Block 底层剖析
7.1 Block 是什么
Block 是「带捕获环境的函数指针」。底层是一个 C 结构体:
// 简化版(libclosure 源码 Block_private.h)
struct Block_layout {
void *isa; // _NSConcreteStackBlock / _NSConcreteMallocBlock / _NSConcreteGlobalBlock
int flags;
int reserved;
void (*invoke)(void *, ...); // 函数指针,指向 block 体
struct Block_descriptor_1 *descriptor;
// 接下来是捕获的变量(每个变量追加一个字段)
};
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
// 可选:copy / dispose helpers(用于 ARC / 对象捕获)
};Block 的本质:
// 你写的
int x = 10;
void (^block)(void) = ^{
NSLog(@"x = %d", x);
};
block();
// 编译器生成(简化)
struct __block_impl {
void *isa;
int flags;
int reserved;
void *FuncPtr;
};
struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, const struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int x; // 捕获的变量
};
// block 体
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int x = __cself->x;
NSLog(@"x = %d", x);
}7.2 Block 的三种类型
| 类型 | 触发条件 | 内存位置 | isa 类 |
|---|---|---|---|
__NSGlobalBlock__ | 不捕获任何外部变量(或只捕获全局/静态) | 数据段 .data | _NSConcreteGlobalBlock |
__NSStackBlock__ | 捕获自动变量(MRC 下可见) | 栈 | _NSConcreteStackBlock |
__NSMallocBlock__ | 栈 block 被 copy / ARC 下默认捕获 | 堆 | _NSConcreteMallocBlock |
// 全局 block
void (^g)(void) = ^{ NSLog(@"hello"); };
NSLog(@"%@", [g class]); // __NSGlobalBlock__
// ARC 下,赋值给 strong 变量自动 copy,所以是 Malloc
int x = 5;
void (^m)(void) = ^{ NSLog(@"%d", x); };
NSLog(@"%@", [m class]); // __NSMallocBlock__7.3 变量捕获
// 1. 局部基本类型:按值捕获
int x = 10;
void (^b)(void) = ^{ NSLog(@"%d", x); };
x = 20;
b(); // 输出 10(捕获的是当时的值)
// 2. OC 对象:strong 捕获(引用计数 +1)
NSObject *obj = [NSObject new];
void (^b)(void) = ^{ NSLog(@"%@", obj); };
// block 内部强持有 obj 直到 block 释放
// 3. 静态变量:按引用捕获(指针)
static int s = 1;
void (^b)(void) = ^{ s = 100; }; // 可以修改!
b();
// 4. 全局变量:不捕获(直接访问)
int g = 0;
void (^b)(void) = ^{ g = 100; };
// 5. __block 修饰的局部变量:包装为结构体,按引用捕获
__block int y = 10;
void (^b)(void) = ^{ y = 20; }; // 可以修改!
b();
NSLog(@"%d", y); // 207.4 __block 修饰符
__block int x = 10;
void (^b)(void) = ^{
x = 20;
NSLog(@"%d", x);
};
b();编译器生成:
struct __Block_byref_x {
void *__isa;
__Block_byref_x *__forwarding; // 关键:指向自己(堆上)或栈上
int __flags;
int __size;
int x; // 实际的值
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_x *x; // 指向 byref 结构体的指针
};__forwarding 是精髓——block copy 到堆时,栈上的 byref 的 __forwarding 改为指向堆上副本,从此所有读写都通过堆上的副本,保证一致性。
7.5 循环引用
// ❌ 循环引用
@interface MyView : UIView
@property (nonatomic, copy) void (^onTap)(void);
@end
@implementation ViewController
- (void)setup {
self.view.onTap = ^{
NSLog(@"%@", self); // block 强引用 self
};
// self → view → onTap(block) → self ❌
}
@end
// ✅ 用 __weak
- (void)setup {
__weak typeof(self) weakSelf = self;
self.view.onTap = ^{
NSLog(@"%@", weakSelf);
};
}
// ✅✅ 标准写法:__strong 扶正
- (void)setup {
__weak typeof(self) weakSelf = self;
self.view.onTap = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSLog(@"%@", strongSelf);
[strongSelf doSomething];
};
}__strong 扶正的原因:弱引用在回调过程中可能被释放,扶正为强引用保证整个闭包执行期间 self 不被释放。
7.6 Block API
// 作为参数
[UIView animateWithDuration:0.3 animations:^{
view.alpha = 0;
} completion:^(BOOL finished) {
[view removeFromSuperview];
}];
// GCD
dispatch_async(dispatch_get_main_queue(), ^{
/* ... */
});
// 通知
[[NSNotificationCenter defaultCenter] addObserverForName:@"x"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
/* ... */
}];
// 排序
NSArray *sorted = [arr sortedArrayUsingComparator:^NSComparisonResult(id a, id b) {
return [a compare:b];
}];
// Block 作为类型
typedef void (^CompletionHandler)(BOOL success, NSError *error);
- (void)doTaskWithCompletion:(CompletionHandler)completion;7.7 Block 属性为什么用 copy
@property (nonatomic, copy) void (^onTap)(void);历史原因:MRC 时代,block 字面量创建在栈上,赋值给属性需要 copy 到堆。ARC 下 strong 也会自动 copy,但约定俗成写 copy 提醒开发者——block 跨作用域需要拷贝到堆。
7.8 Block 与函数指针
// 函数指针
void (*funcPtr)(void) = someFunction;
funcPtr();
// Block
void (^block)(void) = ^{ /* ... */ };
block();区别:
| 维度 | 函数指针 | Block |
|---|---|---|
| 捕获环境 | 无 | 有(自动变量、__block、self) |
| 内存 | 代码段 | 栈 / 堆 / 数据段 |
| 类型 | 静态 | 动态(每个 block 字面量是独立类型) |
| ABI | C ABI | OC ABI(含 isa、flags) |
7.9 高频面试题
Q1:Block 本质是什么? A:Block 是「带捕获环境的函数指针」。底层是 Block_layout 结构体,含 isa、flags、invoke(函数指针)、descriptor、捕获的变量。 isa 决定它是栈 / 堆 / 全局 block。
Q2:为什么 Block 属性用 copy? A:MRC 时代,栈 block 出作用域就销毁,赋值给属性需要 copy 到堆。ARC 下 strong 也会自动 copy,但写 copy 是惯例与提醒。
Q3:__block 修饰符的原理? A:把被修饰变量包装成 __Block_byref_x 结构体,按引用捕获。结构体内的 __forwarding 在 block copy 到堆时改指堆上副本,保证修改作用于同一份数据。
Q4:Block 中修改外部变量为什么必须用 __block? A:局部变量默认按值捕获(const 副本),修改副本不影响外部。__block 把变量包装为 byref 结构体,按指针捕获,所有读写走同一份内存。
Q5:Block 的循环引用怎么破? A:用 __weak typeof(self) weakSelf = self; 在 block 内部弱引用 self。需要保证执行期间 self 不被释放时,在 block 内 __strong typeof(weakSelf) strongSelf = weakSelf; 扶正。
Q6:Block 内部访问 self 一定会循环引用吗? A:不一定。只有 self 强持有 block 时才循环。如果 block 是临时变量(如 [UIView animateWithDuration:animations:] 的参数),block 释放时引用解除,不会循环。
第二部分 · Runtime 与动态性
第 8 章 KVC 与 KVO 原理
8.1 KVC(Key-Value Coding)
KVC 用字符串 key 访问属性,绕过编译期类型检查。
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
User *user = [User new];
[user setValue:@"Alice" forKey:@"name"];
[user setValue:@30 forKey:@"age"];
NSString *name = [user valueForKey:@"name"];
NSInteger age = [[user valueForKey:@"age"] integerValue];
// keyPath(嵌套)
[user setValue:@50 forKeyPath:@"department.manager.age"];
// 批量
NSDictionary *dict = [user dictionaryWithValuesForKeys:@[@"name", @"age"]];
[user setValuesForKeysWithDictionary:@{@"name": @"Bob", @"age": @25}];8.2 KVC 查找顺序(setValue:forKey:)
1. 试着调用 set<Key>:(标准 setter)
2. 若类方法 + (BOOL)accessInstanceVariablesDirectly 返回 YES(默认):
a. _<key>
b. _is<Key>
c. <key>
d. is<Key>
3. 都没找到 → 调用 setValue:forUndefinedKey:(默认抛 NSUndefinedKeyException)8.3 KVC 查找顺序(valueForKey:)
1. 试着调用 get<Key> / <Key> / is<Key> / _get<Key> / _<Key>
2. 数组相关方法(countOf<Key>、objectIn<Key>AtIndex: 等)→ 返回数组代理
3. 集合相关方法 → 返回集合代理
4. accessInstanceVariablesDirectly == YES:
a. _<key> / _is<Key> / <key> / is<Key>
5. 调用 valueForUndefinedKey:8.4 KVC 易错坑
// ❌ 传 nil 给非对象属性 → 抛 NSInvalidArgumentException
[user setValue:nil forKey:@"age"];
// ✅ 用 nil 处理
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"age"]) {
self.age = 0;
} else {
[super setNilValueForKey:key];
}
}
// ⚠️ KVC 跳过属性语义(copy / strong 等),直接操作 ivar
// 如果直接 [_name release] 然后 KVC setValue:forKey:@"name",
// 不会触发 setter 中的 copy 逻辑,需要小心8.5 KVO(Key-Value Observing)
KVO 监听属性变化,原理是 Runtime 动态创建子类。
// 注册观察者
[self.user addObserver:self
forKeyPath:@"name"
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld)
context:NULL];
// 实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
NSLog(@"old: %@, new: %@", change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]);
}
}
// 触发
self.user.name = @"Bob";
// 移除
[self.user removeObserver:self forKeyPath:@"name"];8.6 KVO 底层原理
addObserver:forKeyPath:options:context:
↓
runtime 动态创建 NSKVONotifying_User(继承自 User)
↓
把 self.user.isa 指向 NSKVONotifying_User
↓
重写 setName: 为 _NSSetObjectValueAndNotify(或类似的 _NSSetXxxValueAndNotify)
↓
_NSSetObjectValueAndNotify 内部:
1. [self willChangeValueForKey:@"name"]
2. 调用 super 的 setName:(即原始 setter)
3. [self didChangeValueForKey:@"name"]
↓
触发 observeValueForKeyPath: 回调验证 KVO 子类:
User *user = [User new];
NSLog(@"%@", [user class]); // User
[user addObserver:self forKeyPath:@"name" options:0 context:NULL];
NSLog(@"%@", [user class]); // 还是 User(class 方法被重写了)
NSLog(@"%@", object_getClass(user)); // NSKVONotifying_User(真实 isa)8.7 手动触发 KVO
@implementation User
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
return NO; // 关闭自动 KVO
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
_name = [name copy];
[self didChangeValueForKey:@"name"];
}
@end8.8 直接修改 ivar 不触发 KVO
- (void)test {
_name = @"no-kvo"; // ❌ 不触发 KVO(绕过 setter)
}KVO 是通过拦截 setter 实现的,直接改 ivar 不会触发 willChange / didChange。
8.9 KVO 易错坑
- 注册与移除必须配对:重复 remove 会 crash。
- dealloc 中先移除观察者:否则对象释放后 KVO 回调到野指针。
- 多观察者 + 多 keyPath:用
context区分(比 keyPath 字符串更可靠)。 - 多线程:KVO 回调在 setter 调用线程,跨线程要小心。
- 可变数组 / 集合:直接调用
addObject:不触发 KVO,要用mutableArrayValueForKey:获取代理。
// ❌ 不触发 KVO
[self.items addObject:item];
// ✅ 触发 KVO
[[self mutableArrayValueForKey:@"items"] addObject:item];8.10 高频面试题
Q1:KVO 的原理? A:addObserver:forKeyPath: 时,Runtime 动态创建子类 NSKVONotifying_<原类>,把对象的 isa 指向它,重写被观察属性的 setter 为 _NSSetXxxValueAndNotify。该函数在调用原 setter 前后分别 willChangeValueForKey: / didChangeValueForKey:,触发观察者回调。
Q2:直接修改成员变量会触发 KVO 吗? A:不会。KVO 通过拦截 setter 实现,直接改 ivar 绕过 setter。手动触发需要 willChangeValueForKey: + 修改 + didChangeValueForKey:。
Q3:KVO 回调在哪个线程? A:setter 调用线程。如果 setter 在子线程被调,回调也在子线程。要主线程回调,需要自己 dispatch。
Q4:如何手动实现一个 KVO? A:①创建子类;②把对象 isa 指向子类;③在子类中重写 setter,前后调用 willChange/didChange;④释放时恢复 isa。
Q5:KVO 注册移除不配对会怎样? A:①重复移除抛 NSRangeException;②不移除导致野指针 crash(被观察对象释放但 KVO 表还留着观察者引用)。
第 9 章 Runtime 进阶:objc_msgSend 与消息转发
这一章是 OC Runtime 的精华。理解了
objc_msgSend与消息转发,几乎所有「魔法」(KVO、JSPatch、Aspects、AOP)都豁然开朗。
9.1 objc_msgSend 工作流程
[obj doSomething:arg];
// 编译器翻译为:
((void(*)(id, SEL, id))objc_msgSend)(obj, @selector(doSomething:), arg);完整查找链:
1. 检查 receiver 是否为 nil → 是则 return 0(nil 消息安全)
↓
2. 从 receiver->isa 找到 class
↓
3. 查 class 的方法缓存(cache_t)
命中 → 跳转到 IMP(汇编 ret)
↓
4. 缓存未命中 → 遍历 class 的 method_list_t
↓
5. 沿 superclass 链一直查到根类
↓
6. 全部未命中 → _class_lookupMethodAndLoadCache3(C 实现)
a. 动态方法解析 resolveInstanceMethod: / resolveClassMethod:
b. 快速转发 forwardingTargetForSelector:
c. 标准转发 methodSignatureForSelector: + forwardInvocation:
↓
7. 都失败 → doesNotRecognizeSelector: → crash(unrecognized selector)9.2 objc_msgSend 为什么用汇编
objc_msgSend 在 objc-msg-arm64.s 中用汇编实现,原因:
- 绕过 C 函数 prologue(栈帧建立),直接控制寄存器,减少开销。
- 参数数量不固定(不同方法参数个数不同),C 函数难处理。
- 热路径性能要求——每条 OC 消息发送都走,几条汇编指令决定整体性能。
汇编伪代码(arm64):
ENTRY _objc_msgSend
cbz x0, LNilReceiver // receiver == nil → return 0
ldr x13, [x0] // x13 = receiver->isa
... // 取出 cls
CacheLookup NORMAL // 查 cache
// cache 命中 → 跳 IMP(br x17)
// cache 未命中 → __objc_msgSend_uncached(C 函数)
LNilReceiver:
mov x0, #0
ret
END ENTRY优化效果:实测一次 objc_msgSend 约 10-20 纳秒,比 C 函数指针慢 2-3 倍,但可接受。
9.3 方法缓存(cache_t)
struct cache_t {
struct bucket_t *_buckets; // 哈希表
mask_t _mask; // 容量 - 1
mask_t _occupied; // 已用槽位数
};
struct bucket_t {
SEL _key; // selector
IMP _imp; // 函数指针
};机制:
- 哈希函数:
index = _mask & ((uintptr_t)sel >> 0)(位运算极快)。 - 负载因子 3/4 时扩容(容量翻倍,重新哈希所有 entry)。
- 扩容时不清空旧 bucket 内存,残留数据可见(逆向工程可挖)。
- 每个类都有独立的 cache(继承时父类方法被调用,也缓存到子类的 cache)。
为什么这么快:哈希表 + 直接函数指针跳转,整个查找过程不到 10 条汇编指令。
9.4 动态方法解析(resolveInstanceMethod:)
@interface DynamicObject : NSObject
@end
@implementation DynamicObject
// 声明要动态实现的方法
- (void)dynamicMethod;
// 实际的 IMP 函数
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"Dynamic method called!");
}
// 动态添加
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
// 使用
DynamicObject *obj = [DynamicObject new];
[obj dynamicMethod]; // 输出 "Dynamic method called!"类型编码(v@:):
v= void(返回值)@= id(self):= SEL(_cmd)
类型编码规则见 Type Encodings。
应用场景:
@dynamic属性(Core Data NSManagedObject 子类)。- 自动生成 getter/setter。
- JSPatch 把 JS 函数注册为 OC 方法。
9.5 快速转发(forwardingTargetForSelector:)
@interface Proxy : NSObject
- (void)hello;
@end
@implementation Proxy
- (void)hello { NSLog(@"Proxy hello"); }
@end
@interface Forwarder : NSObject
@property (nonatomic, strong) Proxy *proxy;
@end
@implementation Forwarder
- (instancetype)init {
if (self = [super init]) { _proxy = [Proxy new]; }
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(hello)) {
return self.proxy; // 直接转发给 proxy
}
return [super forwardingTargetForSelector:aSelector];
}
@end
// 使用
Forwarder *f = [Forwarder new];
[f hello]; // 输出 "Proxy hello"特点:
- 简单——只返回另一个对象。
- 限制:返回的对象方法签名不会被暴露(外部
conformsToProtocol仍按 Forwarder 算)。 - 适合「单一代理」场景。
9.6 标准转发(methodSignatureForSelector: + forwardInvocation:)
@implementation Forwarder
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
// 返回方法的类型签名(参数列表 + 返回值)
if (aSelector == @selector(hello)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
// 也可以委托给内部对象
return [self.proxy methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 重新设置 target,调用 proxy
[anInvocation invokeWithTarget:self.proxy];
}
@endNSInvocation:
- 封装了一次完整的方法调用:target、selector、所有参数、返回值。
- 可以拿到 / 修改任意参数。
- 可以多次 invoke。
- (void)forwardInvocation:(NSInvocation *)inv {
NSUInteger argc = [[inv methodSignature] numberOfArguments];
for (NSUInteger i = 0; i < argc; i++) {
const char *type = [[inv methodSignature] getArgumentTypeAtIndex:i];
// 读取每个参数
}
// 修改参数
NSString *newArg = @"modified";
[inv setArgument:&newArg atIndex:2];
[inv invokeWithTarget:otherObj];
// 获取返回值
void *returnVal = NULL;
[inv getReturnValue:&returnVal];
}9.7 NSProxy 实现 AOP
NSProxy 是与 NSObject 平级的「纯代理」抽象根类:
@interface AspectProxy : NSProxy
@property (nonatomic, strong) id target;
@end
@implementation AspectProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
NSLog(@"before %@", NSStringFromSelector([invocation selector]));
[invocation invokeWithTarget:self.target];
NSLog(@"after");
}
@end
// 使用
Real *real = [Real new];
AspectProxy *proxy = [[AspectProxy alloc] initWithTarget:real];
[proxy anyMethod]; // 触发 before / after为什么用 NSProxy 不用 NSObject:
- NSProxy 是「纯代理」,没有 init、没有大量内置方法。
- NSProxy 的所有方法(除了少数根类方法)都走 forwarding。
- NSObject 大量方法直接实现了,不适合做 proxy(会有「漏网」方法)。
9.8 三种转发方式对比
| 方式 | API | 限制 | 适用场景 |
|---|---|---|---|
| 动态方法解析 | +resolveInstanceMethod: / +resolveClassMethod: | 必须自己提供 IMP | 动态添加方法、JSPatch |
| 快速转发 | -forwardingTargetForSelector: | 单一转发目标 | 简单代理 |
| 标准转发 | -methodSignatureForSelector: + -forwardInvocation: | 灵活但慢 | AOP、消息拦截、日志 |
9.9 Runtime 常用 API
#import <objc/runtime.h>
// 类相关
Class cls = object_getClass(obj);
Class superCls = class_getSuperclass(cls);
const char *name = class_getName(cls);
unsigned int count = 0;
Method *methods = class_copyMethodList(cls, &count); // 需要 free
Ivar *ivars = class_copyIvarList(cls, &count);
objc_property_t *props = class_copyPropertyList(cls, &count);
Protocol **protocols = class_copyProtocolList(cls, &count);
// 方法相关
SEL sel = method_getName(method);
IMP imp = method_getImplementation(method);
const char *typeEncoding = method_getTypeEncoding(method);
unsigned int argCount = method_getNumberOfArguments(method);
IMP origImp = method_setImplementation(method, newImp);
void method_exchangeImplementations(Method m1, Method m2);
// 动态操作
class_addMethod(cls, sel, imp, types);
class_replaceMethod(cls, sel, imp, types);
class_addIvar(cls, name, size, alignment, types); // 仅运行时创建的类
class_addProperty(cls, name, attrs, count);
objc_allocateClassPair(superCls, name, extraBytes); // 动态创建类
objc_registerClassPair(cls); // 注册到 Runtime
// 关联对象(详见第 11 章)
objc_setAssociatedObject(obj, key, value, policy);
id value = objc_getAssociatedObject(obj, key);
objc_removeAssociatedObjects(obj);
// 内省
BOOL isMeta = class_isMetaClass(cls);
BOOL res = [obj isKindOfClass:[SomeClass class]];
BOOL res = [obj respondsToSelector:sel];
BOOL res = [cls conformsToProtocol:@protocol(SomeProtocol)];
// 消息发送(手动)
id result = objc_msgSend(obj, sel, arg1, arg2);9.10 高频面试题
Q1:objc_msgSend 的完整流程? A:①receiver nil 检查;②从 isa 取类;③查 cache(命中即跳 IMP);④遍历 method list;⑤沿 superclass 链查;⑥动态方法解析(resolveInstanceMethod:);⑦快速转发(forwardingTargetForSelector:);⑧标准转发(methodSignatureForSelector: + forwardInvocation:);⑨crash。
Q2:objc_msgSend 为什么用汇编? A:①绕过 C 函数 prologue;②处理不定参数;③热路径性能要求。汇编实现实测每次约 10-20ns,性能可接受。
Q3:方法缓存的扩容机制? A:负载因子 3/4 时扩容,容量翻倍,重新哈希所有 entry。扩容时旧 bucket 内存不清空(残留数据可被逆向)。
Q4:消息转发的三步? A:①+resolveInstanceMethod:(动态方法解析);②-forwardingTargetForSelector:(快速转发,返回另一对象);③-methodSignatureForSelector: + -forwardInvocation:(标准转发,NSInvocation)。
Q5:resolveInstanceMethod 与 forwardInvocation 的区别? A:前者给类一次「动态添加方法」的机会,添加后该 selector 后续走正常派发;后者每次都触发转发,不修改类。前者性能好,后者灵活(可以拦截、修改参数、改 target)。
Q6:Method Swizzling 与 forwardInvocation 都能实现 AOP,区别? A:Swizzling 是修改类的方法表(一次,全局生效);forwardInvocation 是对象级转发(每次消息发送都触发)。Swizzling 性能好(一次后正常派发),forwardInvocation 灵活但每次都走完整转发链(慢)。
第 10 章 Method Swizzling 实战
10.1 Method Swizzling 原理
Method Swizzling = 交换两个方法的 IMP(实现指针)。
#import <objc/runtime.h>
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewWillAppear:);
SEL swizzledSel = @selector(xxx_viewWillAppear:);
Method original = class_getInstanceMethod(cls, originalSel);
Method swizzled = class_getInstanceMethod(cls, swizzledSel);
method_exchangeImplementations(original, swizzled);
});
}
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated]; // 此时实际调原方法
NSLog(@"VC appeared: %@", self);
}
@end执行后:
[vc viewWillAppear:]实际执行xxx_viewWillAppear:的代码。[vc xxx_viewWillAppear:]实际执行原来的viewWillAppear:。- 这就是为什么 swizzled 方法内调自己看起来「递归」其实是调原方法。
10.2 标准写法(防止父类方法被覆盖)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewWillAppear:);
SEL swizzledSel = @selector(xxx_viewWillAppear:);
Method original = class_getInstanceMethod(cls, originalSel);
Method swizzled = class_getInstanceMethod(cls, swizzledSel);
// 先尝试 add,避免父类方法被改
BOOL didAdd = class_addMethod(cls,
originalSel,
method_getImplementation(swizzled),
method_getTypeEncoding(swizzled));
if (didAdd) {
// 类原本没实现该方法(继承自父类),刚才 add 进来了
// 现在把 swizzledSel 替换为原实现
class_replaceMethod(cls,
swizzledSel,
method_getImplementation(original),
method_getTypeEncoding(original));
} else {
// 类原本就实现了,直接交换
method_exchangeImplementations(original, swizzled);
}
});
}为什么这么复杂:
- 如果
cls本身没实现viewWillAppear:(继承自父类),直接method_exchangeImplementations会把父类的实现交换到xxx_viewWillAppear:,导致所有 UIViewController 子类都受影响。 - 先
class_addMethod检查:能 add 进去说明原来没有,那么把 swizzledIMP add 进去,再用class_replaceMethod把 swizzledSel 指向原 IMP。
10.3 注意事项
| 注意点 | 原因 |
|---|---|
在 +load 中 swizzle,不要 +initialize | +load 时机早(main 之前);+initialize 可能多次(子类首次使用) |
dispatch_once 防多次 swizzle | 多个 Category 同时存在时防止重复交换 |
方法名加前缀(xxx_) | 避免与系统 / 第三方库冲突 |
先 class_addMethod 再 method_exchangeImplementations | 防止父类方法被改 |
| 不要 swizzle 系统私有方法 | 审核风险 |
| swizzle 后注意线程安全 | 全局影响,多线程 swizzle 可能丢失 |
| Swizzling 在 Swift 中受限 | Swift 方法默认静态派发,需 @objc dynamic |
10.4 实战案例 1:自动埋点
@implementation UIViewController (Tracking)
+ (void)load {
static dispatch_once_t once;
dispatch_once(&once, ^{
[self swizzle:@selector(viewDidAppear:) with:@selector(xxx_viewDidAppear:)];
});
}
+ (void)swizzle:(SEL)orig with:(SEL)new {
Method m1 = class_getInstanceMethod(self, orig);
Method m2 = class_getInstanceMethod(self, new);
method_exchangeImplementations(m1, m2);
}
- (void)xxx_viewDidAppear:(BOOL)animated {
[self xxx_viewDidAppear:animated];
[Tracker trackPageView:NSStringFromClass([self class])];
}
@end10.5 实战案例 2:防按钮多次点击
@implementation UIButton (AntiMultipleClick)
+ (void)load {
static dispatch_once_t once;
dispatch_once(&once, ^{
[self swizzle:@selector(sendAction:to:forEvent:) with:@selector(xxx_sendAction:to:forEvent:)];
});
}
- (void)xxx_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate];
NSTimeInterval interval = [self eventTimeInterval];
if (now - self.lastClickTime < interval) return; // 拦截
self.lastClickTime = now;
[self xxx_sendAction:action to:target forEvent:event];
}
// 用关联对象存属性(见第 11 章)
- (void)setEventTimeInterval:(NSTimeInterval)interval {
objc_setAssociatedObject(self, @selector(eventTimeInterval), @(interval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSTimeInterval)eventTimeInterval {
return [objc_getAssociatedObject(self, @selector(eventTimeInterval)) doubleValue] ?: 0.5;
}
- (void)setLastClickTime:(NSTimeInterval)t {
objc_setAssociatedObject(self, @selector(lastClickTime), @(t), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSTimeInterval)lastClickTime {
return [objc_getAssociatedObject(self, @selector(lastClickTime)) doubleValue];
}
@end10.6 实战案例 3:字典转模型
@implementation NSObject (Model)
+ (instancetype)modelFromDict:(NSDictionary *)dict {
id obj = [self new];
unsigned int count = 0;
Ivar *ivars = class_copyIvarList(self, &count);
for (unsigned int i = 0; i < count; i++) {
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
// 去掉下划线前缀
if ([key hasPrefix:@"_"]) key = [key substringFromIndex:1];
id value = dict[key];
if (value) [obj setValue:value forKey:key];
}
free(ivars);
return obj;
}
@end10.7 高频面试题
Q1:Method Swizzling 为什么要在 +load 中执行? A:①+load 在 main 之前由 Runtime 调用(类刚加载完),时机早;②+load 每个类只调一次,安全;③+initialize 在类首次被使用时调用,可能晚于使用场景,且子类会触发父类 +initialize 再次调用。
Q2:为什么 Swizzling 要 dispatch_once? A:防止多个 Category 同时 swizzle 时重复交换(导致方法实现「绕一圈」回来)。+load 调用顺序虽确定,但保险起见仍加 dispatch_once。
Q3:为什么先 class_addMethod 再 method_exchangeImplementations? A:如果类本身没实现该方法(继承自父类),直接 exchange 会改父类的方法表,影响所有子类。先 class_addMethod 检查能否添加,能添加说明原类没实现,那就把 swizzled IMP add 进去,再用 class_replaceMethod 把 swizzledSel 指向原 IMP。
Q4:Swift 中还能 Swizzling 吗? A:能,但有限制。Swift 方法默认静态派发(不经过 objc_msgSend),无法 swizzle。只有 @objc dynamic 标注的方法才能 swizzle(强制走 OC runtime)。Swift 想做 AOP 更推荐用协议 + 装饰器。
第 11 章 关联对象与动态特性
11.1 关联对象(Associated Object)
Category 不能加 ivar,但可以用关联对象模拟。
#import <objc/runtime.h>
@implementation UIView (Badge)
- (void)setBadgeValue:(NSInteger)badgeValue {
objc_setAssociatedObject(self,
@selector(badgeValue),
@(badgeValue),
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSInteger)badgeValue {
return [objc_getAssociatedObject(self, @selector(badgeValue)) integerValue];
}
@end
// 使用
view.badgeValue = 5;11.2 关联策略
| 策略 | 等价属性语义 |
|---|---|
OBJC_ASSOCIATION_ASSIGN | @property(assign) |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property(strong, nonatomic) |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property(copy, nonatomic) |
OBJC_ASSOCIATION_RETAIN | @property(strong, atomic) |
OBJC_ASSOCIATION_COPY | @property(copy, atomic) |
注意:没有 weak 关联策略。weak 需要 OBJC_ASSOCIATION_RETAIN + 自己实现 weak 表(或用 NSMapTable weak map)。
11.3 关联对象底层
Runtime 维护一张全局哈希表 AssociationsHashMap:
AssociationsHashMap
key: 对象地址 (disguised_ptr_t)
value: ObjectAssociationMap
key: 关联 key (void *)
value: ObjcAssociation
policy
value (id)- 设置关联:先找对象 → 找 key → 替换 / 添加。
- 对象 dealloc 时,runtime 调用
_object_remove_assocations移除该对象的所有关联。
易错:关联对象释放时机 ≠ 对象 dealloc 调用时机。在 dealloc 流程的某个阶段(call deallocating observers)才清理。
11.4 key 的选择
// 方案 1:用 selector(隐式地址,永不重复)
objc_setAssociatedObject(self, @selector(badgeValue), value, policy);
// 方案 2:用 static 变量地址
static void *kBadgeKey = &kBadgeKey;
objc_setAssociatedObject(self, kBadgeKey, value, policy);
// 方案 3:用字符串字面量(注意:相同字面量是同一份)
objc_setAssociatedObject(self, @"badge", value, policy);最佳实践:方案 1(@selector(...)),隐式地址 + 自描述。
11.5 动态创建类
// 1. 分配类
Class newClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);
// 2. 添加成员(必须在 register 之前)
class_addIvar(newClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
class_addMethod(newClass, @selector(hello), (IMP)helloIMP, "v@:");
// 3. 注册类
objc_registerClassPair(newClass);
// 4. 使用
id obj = [[newClass alloc] init];
[obj setValue:@"Alice" forKey:@"name"];
[obj hello];
// 5. 销毁类(极少用)
objc_disposeClassPair(newClass);应用:KVO 底层(创建 NSKVONotifying_X)、JSPatch、单元测试 mock。
11.6 实战:动态实现 NSCoding
@implementation NSObject (AutoCoding)
- (void)encodeWithCoder:(NSCoder *)coder {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[coder encodeObject:value forKey:key];
}
free(ivars);
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super init]) {
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *name = ivar_getName(ivars[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = [coder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
}
return self;
}
@end11.7 Runtime 应用汇总
| 应用 | 机制 |
|---|---|
| AOP / 埋点 / 监控 | Method Swizzling |
| 字典 → Model | class_copyIvarList + KVC |
| 自动归档 | class_copyIvarList |
| 拦截按钮多次点击 | swizzle sendAction:to:forEvent: |
| JSPatch(动态下发修复) | 动态方法解析 + 消息转发 |
| KVO | 动态创建子类(NSKVONotifying_X) |
| Aspects(AOP 库) | Method Swizzling + NSInvocation |
| Mock 测试 | isa 替换或动态子类 |
11.8 高频面试题
Q1:Category 能加 ivar 吗?怎么模拟? A:不能。用关联对象 objc_setAssociatedObject 模拟,本质是把额外数据存到全局 AssociationsHashMap 里,对外表现为「Category 加属性」。
Q2:关联对象的释放时机? A:在对象 dealloc 流程中(call deallocating observers),runtime 调用 _object_remove_assocations 清理该对象的所有关联。这与对象 dealloc 调用时机略有差异。
Q3:动态创建类的步骤? A:①objc_allocateClassPair 分配;②class_addIvar / class_addMethod 添加成员(必须在注册前);③objc_registerClassPair 注册到 Runtime;④使用;⑤objc_disposeClassPair 销毁(极少用)。
Q4:关联对象能用 weak 吗? A:不能直接用。Runtime 没有 OBJC_ASSOCIATION_WEAK 策略。需要自己实现:用 RETAIN 关联一个 NSMapTable(key 强,value 弱)。
第三部分 · 内存与并发
第 12 章 内存管理深度剖析
12.1 引用计数模型
OC 用**引用计数(Reference Counting)**管理对象生命周期:
| 事件 | 计数变化 |
|---|---|
alloc / new / copy / mutableCopy | +1(返回持引用) |
retain | +1 |
release | -1(为 0 时调用 dealloc) |
autorelease | 不立即 -1,加入 autorelease pool,pool drain 时 -1 |
// NSObject 内部(简化)
struct NSObject {
Class isa;
// 引用计数存在 isa 的 extra_rc 位域(19 位)
// 或在 SideTable 的 refcountMap 中
};
// retain / release 内部:
- (id)retain {
// isa.extra_rc++;溢出则一半复制到 SideTable
return self;
}
- (oneway void)release {
// isa.extra_rc--;不够则从 SideTable 取
// 到 0 时调用 dealloc
}12.2 MRC(手动引用计数)
iOS 4 之前只能用 MRC:
// MRC 规则:
// 1. 凡是 alloc / new / copy / mutableCopy 创建的对象,自己持有
// 2. 其他方法返回的对象不持有(可能 autorelease)
// 3. 想持有就 retain,不想持有就 release / autorelease
- (void)mrcExample {
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1,自己持有
[obj retain]; // 引用计数 = 2
[obj release]; // 引用计数 = 1
[obj release]; // 引用计数 = 0 → dealloc
}
// autorelease 临时对象
- (NSString *)description {
return [[[NSString alloc] initWithFormat:@"..."] autorelease];
}12.3 ARC(自动引用计数)
iOS 5 引入 ARC,编译器自动插入 retain / release / autorelease:
// 你写的
- (void)loadData {
NSObject *obj = [[NSObject alloc] init];
[self process:obj];
}
// ARC 生成(伪代码)
- (void)loadData {
NSObject *obj = [[NSObject alloc] init]; // +1
[self process:obj];
[obj release]; // -1(编译器自动)
}关键认知:
- ARC ≠ GC(垃圾回收)。ARC 是编译期插入,无运行时暂停。
- ARC 仍是引用计数模型,要求开发者理解循环引用。
- ARC 不管理 CoreFoundation 对象(
CFRetain/CFRelease仍需手动)。 - ARC 不管理 malloc / free。
12.4 ARC 修饰符
| 修饰符 | 作用 |
|---|---|
__strong(默认) | 强引用,引用计数 +1 |
__weak | 弱引用,不计计数,对象释放后置 nil |
__unsafe_unretained | 不安全引用,不计计数,不置 nil(野指针风险) |
__autoreleasing | 自动释放,用于 out 参数 |
// __strong(默认)
__strong NSObject *s = [NSObject new]; // 持有
// __weak(弱引用)
__weak NSObject *w = s; // 不持有
s = nil;
NSLog(@"%@", w); // nil(s 释放后 w 自动置 nil)
// __unsafe_unretained
__unsafe_unretained NSObject *u = s;
s = nil;
NSLog(@"%@", u); // ⚠️ 野指针,访问会 crash
// __autoreleasing(out 参数)
- (void)fetchError:(__autoreleasing NSError **)error {
*error = [NSError errorWithDomain:@"" code:0 userInfo:nil]; // 自动注册到 autorelease pool
}12.5 weak 底层实现
__weak 变量怎么做到「对象释放后自动置 nil」?
底层:一张全局哈希表 weak_table_t:
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
...
};
struct weak_entry_t {
DisguisedPointer<objc_object> referent; // 被引用对象
weak_referrer_t *referrers; // 所有弱引用指针数组
...
};工作流程:
__weak NSObject *w = obj;
↓
注册:runtime 把 &w 加入 obj 的 weak_entry
↓
obj dealloc
↓
runtime clearDeallocating()
↓
查 weak_table[obj] → 找到所有 weak 指针 [&w1, &w2, &w3...]
↓
遍历,把每个指针指向的内容置为 nil
↓
从 weak_table 中移除 obj 这一项代价:
- 注册 weak 时修改全局表(加 SideTable spinlock)。
- 多线程并发可能竞争(分片 SideTable 减少冲突)。
- 所以 weak 比 strong 慢(约 2-3 倍),能用
__unsafe_unretained就用(但风险大,慎用)。
12.6 Autorelease 与 autoreleasepool
- (NSString *)description {
return [NSString stringWithFormat:@"..."]; // 返回 autoreleased 对象
}stringWithFormat: 返回的对象引用计数为 1,被注册到当前线程的 autorelease pool。pool drain 时统一 release。
底层:
// autoreleasepool 由 AutoreleasePoolPage 实现(双向链表)
struct AutoreleasePoolPage {
magic_t const magic;
id *next; // 下一个对象的槽位
pthread_t const thread;
AutoreleasePoolPage *parent;
AutoreleasePoolPage *child;
...
};- 每个线程有自己的 autorelease pool 栈。
@autoreleasepool { }编译为objc_autoreleasePoolPush()/objc_autoreleasePoolPop()。- 主线程的 RunLoop 在每次循环开始 push、结束 pop。
12.7 autoreleasepool 实战
// ❌ 内存可能暴涨
for (int i = 0; i < 1000000; i++) {
NSString *s = [NSString stringWithFormat:@"%d", i]; // autorelease
// ... 处理 s
}
// ✅ 用 @autoreleasepool 及时释放
for (int i = 0; i < 1000000; i++) {
@autoreleasepool {
NSString *s = [NSString stringWithFormat:@"%d", i];
// ... 处理 s
} // s 在这里释放
}典型场景:
- 循环中创建大量 autorelease 对象。
- 子线程中没有 RunLoop,没有自动 pool——必须手动包。
- 框架入口函数(API 边界)。
12.8 Tagged Pointer
NSNumber *n = @1;
NSLog(@"%p", n); // 0xb000000000000012 之类的「奇怪地址」
NSLog(@"%@", [n class]); // __NSCFNumber(但其实是 Tagged Pointer)@1 的 NSNumber 不分配堆内存!值直接编码到指针里。这种技术叫 Tagged Pointer。
机制:
- 64 位指针 8 字节,但实际只用低 48 位寻址。
- 剩下高位 + 部分低位编码「类型标记 + 有效负载数据」。
- NSNumber 小整数、短 NSString(≤ 7 字节)都走 Tagged Pointer。
优点:
- 不分配堆,无 malloc / free 开销。
- 无引用计数,无 retain / release。
- 更好的缓存局部性。
坑:
[n class]返回__NSCFNumber(被伪装)。Runtime 通过 isa mask 判断。- 不能用
==比较两个 NSNumber(虽然@(1) == @(1)在某些情况成立,因为 tag 编码后相同),要用isEqual:。 - Tagged Pointer 不参与 weak 表,对它 weak 没意义。
12.9 僵尸对象(Zombie)
NSObject *obj = [[NSObject alloc] init];
[obj release]; // 引用计数 0,dealloc
[obj description]; // ⚠️ 访问已释放对象,野指针调试技巧:Xcode → Edit Scheme → Diagnostics → 勾选 Zombie Objects。开启后:
- 对象 dealloc 不真正释放内存。
- 改写 isa 为
_NSZombie_Class。 - 再次访问时打印
*** -[NSObject description]: message sent to deallocated instance 0x60000xxx并 break。
生产环境禁用,性能开销大。
12.10 循环引用诊断
典型场景:
// 1. delegate(不用 weak)
@property (nonatomic, strong) id<MyDelegate> delegate;
// 2. block(self 持有 block,block 持有 self)
self.completion = ^{
NSLog(@"%@", self);
};
// 3. NSTimer(target-action 强引用 target)
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(tick)
userInfo:nil
repeats:YES];
// 4. 互相持有
a.friend = b;
b.friend = a;诊断工具:Xcode Memory Graph Debugger(Debug Navigator → 选中进程 → 显示内存图)。能可视化所有对象引用关系,自动标出 leak / 静态保留环。
12.11 高频面试题
Q1:ARC 在编译期做了什么? A:自动插入 retain / release / autorelease。局部变量:进入作用域 retain,退出 release。属性赋值:先 retain new,release old。返回值:根据 NS_RETURNS_RETAINED / NS_RETURNS_NOT_RETAINED 决定是否 autorelease。优化器做 ARC 优化,消除冗余调用。
Q2:ARC 还会发生循环引用吗? A:会。ARC 只自动管理计数,不能识别「逻辑上的环」。delegate、block、NSTimer、通知、KVO 都是高发地。
Q3:weak 与 unsafe_unretained 区别? A:weak 对象释放后自动置 nil,访问安全;unsafe_unretained 不置 nil,访问已释放对象是野指针 crash。weak 有额外开销(weak 表),unsafe_unretained 无开销但危险。
Q4:autoreleasepool 何时必须用? A:①循环中产生大量 autorelease 临时对象;②子线程(默认没 pool);③长生命周期线程;④库入口函数(API 边界)。
Q5:Tagged Pointer 是什么? A:64 位系统上把小整数 NSNumber 与短 NSString 的值直接编码到指针中,不分配堆。无 malloc / retain / release 开销。访问时通过 isa mask 判断。
Q6: autorelease 对象何时释放? A:主线程 RunLoop 注册了 Observer,在 BeforeWaiting(即将睡眠)时 pop 当前 autorelease pool,释放这一帧的 autorelease 对象,然后 push 新的。子线程没有 RunLoop 时,线程退出才释放。
第 13 章 RunLoop 深度剖析
13.1 RunLoop 概念
RunLoop 是「保活线程、管理事件、节省 CPU」的机制:
- 没事做时,让线程睡眠(不消耗 CPU)。
- 有事做时,唤醒线程处理。
- 处理完,继续睡眠。
对比 Linux select/epoll:本质类似——都是事件循环。iOS RunLoop 是 Apple 把 select/epoll + Mach port 消息 + Timer + Observer 整合后的产物。
13.2 RunLoop 结构
struct __CFRunLoop {
pthread_t _thread; // 所属线程
CFMutableSetRef _commonModes; // 标记为 common 的 mode 集合
CFMutableSetRef _allModes; // 所有 mode
CFRunLoopModeRef _currentMode; // 当前 mode
};
struct __CFRunLoopMode {
CFStringRef _name; // kCFRunLoopDefaultMode / UITrackingRunLoopMode ...
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};每个 RunLoop 包含多个 Mode,每个 Mode 包含若干 Source / Timer / Observer。RunLoop 一次只能跑一个 Mode。
13.3 Mode 机制
| Mode | 含义 |
|---|---|
kCFRunLoopDefaultMode | 默认模式,空闲时 |
UITrackingRunLoopMode | 滑动 ScrollView / UITableView 时 |
GSEventReceiveRunLoopMode | 接收系统事件(私有) |
NSRunLoopCommonModes(kCFRunLoopCommonModes) | 占位 mode,包含 Default + Tracking |
经典坑:
// ❌ 滑动时定时器停止
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(tick)
userInfo:nil
repeats:YES];
// scheduledTimerWithTimeInterval 默认加入 DefaultMode,滑动时切到 TrackingRunLoopMode 不调用
// ✅ 加入 commonModes
[timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];原因:滑动时主线程 RunLoop 切到 UITrackingRunLoopMode,Default Mode 下的 Timer 不被调用。Common Modes 是「虚拟集合」,加入 common 等于同时加入 Default 与 Tracking。
13.4 Source / Timer / Observer
| 类型 | 说明 | 例子 |
|---|---|---|
| Source0 | 非基于端口事件,需手动触发 | 触摸事件、CFRunLoopSourceCreate |
| Source1 | 基于 mach port 的事件 | 系统内核消息、Port 跨线程通信 |
| Timer | 定时器 | NSTimer、CADisplayLink |
| Observer | 观察者,监听 RunLoop 状态 | 卡顿检测、性能监控 |
13.5 RunLoop Activity
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source0
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将睡眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚被唤醒
kCFRunLoopExit = (1UL << 7), // 退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};13.6 RunLoop 与线程关系
- 每条线程都有且只有一个 RunLoop(懒创建)。
- 主线程的 RunLoop 在
UIApplicationMain中自动启动。 - 子线程的 RunLoop 默认不启动,需手动
[[NSRunLoop currentRunLoop] run]。 - RunLoop 与线程一一对应,存储在全局字典里(key 是 pthread_t)。
13.7 RunLoop 内部循环
1. 通知 Observer:进入 RunLoop(Entry)
2. 通知 Observer:Timer 即将处理(BeforeTimers)
3. 通知 Observer:Source0 即将处理(BeforeSources)
4. 触发 Source0 事件
5. 如果 Source1 有待处理事件,跳到 9
6. 通知 Observer:线程即将睡眠(BeforeWaiting)
7. 睡眠,等待事件唤醒:
- Source1 事件
- Timer 触发
- 外部手动唤醒(CFRunLoopWakeUp)
- 超时
8. 通知 Observer:线程刚被唤醒(AfterWaiting)
9. 处理待处理事件(Source1 / Timer / Source0 手动唤醒)
10. 检查退出条件:超时 / 主线程被杀 / runUntilDate / stop
11. 通知 Observer:退出 RunLoop(Exit),否则回到 113.8 应用场景 1:常驻子线程
@interface NetworkThread : NSObject
+ (instancetype)sharedInstance;
@end
@implementation NetworkThread {
NSThread *_thread;
}
+ (instancetype)sharedInstance {
static NetworkThread *instance;
static dispatch_once_t once;
dispatch_once(&once, ^{
instance = [NetworkThread new];
});
return instance;
}
- (instancetype)init {
if (self = [super init]) {
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[_thread start];
}
return self;
}
- (void)run {
// 子线程默认没 RunLoop,必须手动启动
@autoreleasepool {
NSPort *port = [NSPort new];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run]; // 常驻
}
}
- (void)executeOnNetworkThread:(dispatch_block_t)block {
[self performSelector:@selector(runBlock:)
onThread:_thread
withObject:block
waitUntilDone:NO];
}
- (void)runBlock:(dispatch_block_t)block { block(); }
@end13.9 应用场景 2:滑动不停止定时器
如上 Mode 一节所示,加入 NSRunLoopCommonModes。
13.10 应用场景 3:卡顿监测
@interface LagMonitor : NSObject
@property (nonatomic, assign) NSTimeInterval threshold; // 阈值(秒)
@end
@implementation LagMonitor {
CFRunLoopObserverRef _observer;
dispatch_semaphore_t _semaphore;
NSTimeInterval _lastActivityTime;
}
- (void)start {
self.threshold = 0.05; // 50ms
CFRunLoopObserverContext ctx = {0, (__bridge void *)self, NULL, NULL, NULL};
_observer = CFRunLoopObserverCreate(NULL,
kCFRunLoopAllActivities,
YES,
0,
&observerCallback,
&ctx);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
_semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
while (YES) {
long result = dispatch_semaphore_wait(self->_semaphore,
dispatch_time(DISPATCH_TIME_NOW,
self.threshold * NSEC_PER_SEC));
if (result != 0) {
// 超时,说明主线程在某个 activity 停留过久
NSLog(@"Lag detected, last activity: %@", @(self->_lastActivityTime));
}
}
});
}
static void observerCallback(CFRunLoopObserverRef observer,
CFRunLoopActivity activity,
void *info) {
LagMonitor *monitor = (__bridge LagMonitor *)info;
monitor->_lastActivityTime = [NSDate timeIntervalSinceReferenceDate];
dispatch_semaphore_signal(monitor->_semaphore);
}
@end原理:监测主 RunLoop 状态切换。如果两次状态切换之间超过阈值(如 50ms),说明卡顿。
13.11 应用场景 4:Autorelease pool 机制
主线程 RunLoop 在 Observer 中注册了 autorelease pool 的 push/pop:
- 进入 RunLoop(BeforeWaiting 之前)→ push。
- 即将睡眠(BeforeWaiting)→ pop(释放这一帧的 autorelease 对象)。
- 再 push 一个新的。
这就是为什么主线程 autorelease 对象每帧释放。
13.12 高频面试题
Q1:RunLoop 与线程关系? A:一对一。每条线程有且只有一个 RunLoop(懒创建)。主线程的 RunLoop 在 UIApplicationMain 启动,子线程默认不启动。RunLoop 存在全局字典里(key = pthread_t),离开线程时销毁。
Q2:NSTimer 滑动时为什么停止?怎么解决? A:滑动时主 RunLoop 切到 UITrackingRunLoopMode,Timer 加在 Default Mode 下不被调用。解决:加入 NSRunLoopCommonModes(同时生效于 Default 与 Tracking)。
Q3:RunLoop 内部循环有几个 Observer 钩子? A:6 个:Entry、BeforeTimers、BeforeSources、BeforeWaiting、AfterWaiting、Exit。卡顿监测常用 BeforeWaiting(即将睡眠)+ AfterWaiting(刚唤醒)。
Q4:main 函数之后为什么 App 还能保持运行? A:UIApplicationMain 内部启动了主 RunLoop。RunLoop 自循环,有事做唤醒,没事睡眠,永远不会返回(直到 App 退出)。
Q5:RunLoop 与 GCD 的关系? A:GCD 用 dispatch queue 调度,本身独立于 RunLoop。但 dispatch_async 到主队列的任务最终通过 RunLoop 的 Source1(mach port)唤醒主线程执行。dispatch_after 也是基于 RunLoop 的 timer 机制。
第 14 章 多线程与 GCD
14.1 线程与进程基础
| 概念 | iOS 表现 |
|---|---|
| 进程 | App 启动后是一个进程,有独立地址空间 |
| 主线程 | 启动 UIApplicationMain 的线程,处理 UI 事件、RunLoop |
| 子线程 | NSThread / GCD / NSOperationQueue 创建 |
| 线程栈 | 默认 512KB(主线程 8MB) |
| 内核线程 | Mach 层 thread_t,pthread 封装 |
线程开销:创建一个 pthread 约 90KB(栈 + 内核结构),上下文切换约 1-10μs。GCD 维护线程池,避免频繁创建销毁。
14.2 pthread / NSThread
// pthread(C 接口)
pthread_t thread;
pthread_create(&thread, NULL, &worker, NULL);
pthread_join(thread, NULL);
// NSThread(OC 封装)
NSThread *t = [[NSThread alloc] initWithTarget:self
selector:@selector(work)
object:nil];
[t start];
// 隐式
[self performSelectorInBackground:@selector(work) withObject:nil];NSThread 暴露了底层细节(栈大小、优先级、是否主线程),但缺少线程池/取消机制。实际开发几乎不用,直接上 GCD。
14.3 GCD 全面剖析
GCD(Grand Central Dispatch)是 Apple 在 OS X 10.6 引入的 C 语言并发框架,核心思想:
- 任务(block)提交到队列。
- 队列决定任务调度顺序。
- 系统维护线程池,自动调度。
队列类型
| 队列 | 串行/并发 | 描述 |
|---|---|---|
| 主队列 | 串行 | dispatch_get_main_queue(),绑定主线程 + 主 RunLoop |
| 全局并发队列 | 并发 | dispatch_get_global_queue(flags, 0) |
| 自定义串行队列 | 串行 | dispatch_queue_create("com.example.serial", NULL) |
| 自定义并发队列 | 并发 | dispatch_queue_create("com.example.concurrent", DISPATCH_QUEUE_CONCURRENT) |
同步 vs 异步
// 异步(立即返回,任务在目标队列上调度)
dispatch_async(queue, ^{
NSLog(@"async");
});
// 同步(阻塞当前线程等任务完成)
dispatch_sync(queue, ^{
NSLog(@"sync");
});| 同步 sync | 异步 async | |
|---|---|---|
| 串行 | 当前线程执行,串行 | 新线程执行,串行 |
| 并发 | 当前线程执行,串行(不会并发) | 线程池调度,并发 |
死锁陷阱
// ❌ 死锁
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"main");
});
// main.sync 阻塞主线程等任务,任务在主队列等主线程空闲 → 死锁
// ❌ 死锁(自定义串行队列也类似)
dispatch_queue_t serial = dispatch_queue_create("s", NULL);
dispatch_async(serial, ^{
dispatch_sync(serial, ^{ // 在 serial 上下文 sync serial
NSLog(@"deadlock");
});
});规则:sync 提交到当前正在执行的队列必然死锁。
GCD 常用 API
// 1. 后台处理,回到主线程刷新 UI
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *result = [self computeHeavy];
dispatch_async(dispatch_get_main_queue(), ^{
self.label.text = result;
});
});
// 2. 延迟执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
NSLog(@"0.5s later");
});
// 3. 一次性执行(线程安全单例)
+ (instancetype)sharedInstance {
static MyClass *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
// 4. 并发组
dispatch_group_t group = dispatch_group_create();
for (NSURL *url in urls) {
dispatch_group_enter(group);
[self download:url completion:^{
dispatch_group_leave(group);
}];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"all done");
});
// 5. 信号量(控制并发数)
dispatch_semaphore_t sem = dispatch_semaphore_create(3);
for (NSURL *url in urls) {
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self download:url completion:^{
dispatch_semaphore_signal(sem);
}];
});
}
// 6. barrier(并发队列中独占执行)
dispatch_queue_t queue = dispatch_queue_create("io", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ [self read]; });
dispatch_async(queue, ^{ [self read]; });
dispatch_barrier_async(queue, ^{ [self write]; }); // 等所有读完成,独占执行写
// 7. DispatchSource 定时器(精准)
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,
dispatch_get_global_queue(0, 0));
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, 0),
1.0 * NSEC_PER_SEC,
0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"tick");
});
dispatch_resume(timer);
// 8. apply(并发循环)
dispatch_apply(count, queue, ^(size_t i) {
[self processItem:i];
});QoS(服务质量)
| QoS | 优先级 | 用途 |
|---|---|---|
DISPATCH_QUEUE_PRIORITY_USER_INTERACTIVE | 最高 | UI 动画、事件处理 |
DISPATCH_QUEUE_PRIORITY_USER_INITIATED | 高 | 用户点击立即需要的任务 |
DISPATCH_QUEUE_PRIORITY_UTILITY | 中 | 长任务(解析、网络) |
DISPATCH_QUEUE_PRIORITY_BACKGROUND | 低 | 后台同步、预取 |
DISPATCH_QUEUE_PRIORITY_DEFAULT | 默认 | 介于 userInitiated 与 utility |
GCD 底层
- 实现于
libdispatch.dylib,源码在 swift-corelibs-libdispatch。 - 队列是
dispatch_queue_t,内部串接到 root queue(线程池)。 - 任务是
dispatch_continuation_t(带函数指针 + 上下文)。 - 线程池基于 pthread workqueue(
pthread_workqueue_*)。 - XNU 内核提供
workq_kernreturn系统调用配合调度。
14.4 NSOperationQueue
NSOperation + NSOperationQueue 是 GCD 的 OC 封装:
NSOperationQueue *queue = [NSOperationQueue new];
queue.maxConcurrentOperationCount = 2;
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op1");
}];
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"op2");
}];
[op2 addDependency:op1]; // op1 完成后 op2 才开始
[queue addOperations:@[op1, op2] waitUntilFinished:NO];优势:
- 任务对象化(可取消、可观察状态、可设优先级)。
- 任务依赖。
- 最大并发数。
- KVO 友好。
自定义 Operation:
@interface DownloadOperation : NSOperation
@property (nonatomic, copy) NSString *url;
@property (nonatomic, copy) void (^completion)(NSData *);
@end
@implementation DownloadOperation {
BOOL _executing;
BOOL _finished;
}
- (void)start {
if (self.isCancelled) {
[self done];
return;
}
self.executing = YES;
[self main];
}
- (void)main {
[self download:self.url completion:^(NSData *data) {
if (self.completion) self.completion(data);
[self done];
}];
}
- (void)done {
self.executing = NO;
self.finished = YES;
}
// 并发 operation 需重写这两个属性(KVO 通知)
@synthesize executing = _executing;
@synthesize finished = _finished;
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
@end14.5 选型对比
| 场景 | 推荐 |
|---|---|
| 简单 fire-and-forget | GCD dispatch_async |
| 复杂依赖、可取消 | NSOperationQueue |
| 限并发数 | NSOperationQueue 或信号量 |
| 一次性单例 | dispatch_once |
| 定时器 | dispatch_source_t 或 NSTimer |
| 后台常驻线程 | NSThread + RunLoop |
14.6 高频面试题
Q1:GCD 死锁条件? A:sync 提交到当前正在执行的队列。例如主线程 dispatch_sync(main_queue, ...),子线程同步到自己的串行队列同理。
Q2:GCD 的并发队列底层线程数是多少? A:由系统决定,受 QoS 与系统负载影响。一般等于 CPU 核心数(不是无限)。线程过多会触发 GCD 的「线程爆炸」警告(threads proliferated warning)。
Q3:dispatch_once 为什么是线程安全的? A:底层用原子操作 + 双重检查锁。第一次调用进入慢路径(加锁、执行 block、置标记);后续调用走快路径(读标记判断)。即使多线程同时进入,也能保证 block 只执行一次。
Q4:dispatch_barrier_async 的使用场景? A:并发队列中的「读写隔离」。读操作用 dispatch_async,写操作用 dispatch_barrier_async(等所有正在执行的读完成,独占执行写,写完再恢复并发读)。常用于线程安全的字典 / 缓存。
Q5:NSTimer 与 CADisplayLink 区别? A:NSTimer 基于时间间隔触发(精度受 RunLoop 影响,约 50-100ms 误差);CADisplayLink 与屏幕刷新率同步(60Hz = 16.67ms,ProMotion 120Hz),适合动画。
第 15 章 锁机制全面对比
15.1 锁分类
| 锁 | 类型 | 性能 | 特点 |
|---|---|---|---|
OSSpinLock(已弃用) | 自旋 | 最高 | 不安全,优先级反转风险 |
os_unfair_lock | 自旋替代 | 极高 | iOS 10+ 推荐 |
dispatch_semaphore_t | 信号量 | 高 | 跨平台,简单 |
pthread_mutex_t | 互斥 | 高 | C 接口,灵活 |
NSLock | 互斥 | 中 | OC 封装 |
NSRecursiveLock | 递归互斥 | 中 | 同线程可多次 lock |
NSCondition | 条件变量 | 中 | 等待/通知模型 |
NSConditionLock | 条件锁 | 中 | 条件触发解锁 |
@synchronized | OC 语法糖 | 低 | 简单,慢 |
| 串行队列 | 隐式锁 | 中 | 用队列替代锁 |
pthread_rwlock_t | 读写锁 | 中 | 读多写少 |
dispatch_barrier | 写独占 | 中 | 并发队列 + barrier |
15.2 os_unfair_lock(推荐)
#import <os/lock.h>
@interface ThreadSafeCounter ()
@property (nonatomic, assign) os_unfair_lock lock;
@property (nonatomic, assign) NSInteger count;
@end
@implementation ThreadSafeCounter
- (instancetype)init {
if (self = [super init]) {
_lock = OS_UNFAIR_LOCK_INIT;
_count = 0;
}
return self;
}
- (NSInteger)count {
os_unfair_lock_lock(&_lock);
NSInteger c = _count;
os_unfair_lock_unlock(&_lock);
return c;
}
- (void)increment {
os_unfair_lock_lock(&_lock);
_count++;
os_unfair_lock_unlock(&_lock);
}
@end特点:
- iOS 10+ 取代 OSSpinLock。
- 内核协调优先级,避免反转。
- 自旋替代品——不会一直忙等,必要时会睡眠。
- 性能接近 OSSpinLock。
15.3 pthread_mutex
#import <pthread.h>
@interface ThreadSafeArray ()
@property (nonatomic, assign) pthread_mutex_t lock;
@property (nonatomic, strong) NSMutableArray *items;
@end
@implementation ThreadSafeArray
- (instancetype)init {
if (self = [super init]) {
pthread_mutex_init(&_lock, NULL);
_items = [NSMutableArray array];
}
return self;
}
- (void)dealloc {
pthread_mutex_destroy(&_lock);
}
- (void)addObject:(id)obj {
pthread_mutex_lock(&_lock);
[_items addObject:obj];
pthread_mutex_unlock(&_lock);
}
@end特点:C 接口,性能高,可配递归 / 默认 / 错误检查三种类型。
15.4 NSLock / NSRecursiveLock
// NSLock
NSLock *lock = [NSLock new];
[lock lock];
// 临界区
[lock unlock];
// NSRecursiveLock(同线程可多次 lock,避免递归死锁)
NSRecursiveLock *rLock = [NSRecursiveLock new];
- (void)recursive:(NSInteger)n {
[rLock lock];
if (n > 0) [self recursive:n - 1];
[rLock unlock];
}15.5 @synchronized
@synchronized(obj) {
// 临界区
}底层:递归互斥锁,封装在 objc-sync 中。
int objc_sync_enter(id obj);
int objc_sync_exit(id obj);- 内部维护一张哈希表:
obj → SyncList(含锁)。 - 优点:语法简单,可重入。
- 缺点:最慢的锁(约比 os_unfair_lock 慢 10 倍),因为要查表。
15.6 串行队列作为锁
@interface ThreadSafeCounter ()
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, assign) NSInteger count;
@end
@implementation ThreadSafeCounter
- (instancetype)init {
if (self = [super init]) {
_queue = dispatch_queue_create("counter", DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (NSInteger)count {
__block NSInteger c;
dispatch_sync(_queue, ^{ c = _count; });
return c;
}
- (void)increment {
dispatch_async(_queue, ^{ _count++; });
}
@end特点:用串行队列串行化访问,避免显式加锁。优点:易读、可重入;缺点:异步任务时无法立即返回。
15.7 读写锁(pthread_rwlock_t)
#import <pthread.h>
@interface ThreadSafeCache ()
@property (nonatomic, assign) pthread_rwlock_t lock;
@property (nonatomic, strong) NSMutableDictionary *cache;
@end
@implementation ThreadSafeCache
- (instancetype)init {
if (self = [super init]) {
pthread_rwlock_init(&_lock, NULL);
_cache = [NSMutableDictionary dictionary];
}
return self;
}
- (id)objectForKey:(id)key {
pthread_rwlock_rdlock(&_lock); // 读锁(多个读可并发)
id v = _cache[key];
pthread_rwlock_unlock(&_lock);
return v;
}
- (void)setObject:(id)obj forKey:(id)key {
pthread_rwlock_wrlock(&_lock); // 写锁(独占)
_cache[key] = obj;
pthread_rwlock_unlock(&_lock);
}
@end15.8 barrier 实现读写锁
- (id)objectForKey:(id)key {
__block id v;
dispatch_sync(_concurrentQueue, ^{ v = _cache[key]; });
return v;
}
- (void)setObject:(id)obj forKey:(id)key {
dispatch_barrier_async(_concurrentQueue, ^{ _cache[key] = obj; });
}并发队列 + barrier 等价于读写锁——读可并发,写独占。
15.9 优先级反转
问题:低优先级线程持锁,高优先级线程等锁,中优先级线程抢占低优先级 → 高优先级被「中」阻塞。
OSSpinLock 弃用的原因:自旋锁忙等,加剧反转。
os_unfair_lock 解决方案:内核在持锁期间提升持锁者优先级(priority inheritance)。
15.10 锁的性能基准
实测(每秒操作数,越大越好):
| 锁 | 简单场景 | 复杂场景 |
|---|---|---|
| OSSpinLock(弃用) | 1200 万 | 1100 万 |
| os_unfair_lock | 1100 万 | 1000 万 |
| dispatch_semaphore | 950 万 | 900 万 |
| pthread_mutex | 800 万 | 750 万 |
| NSLock | 600 万 | 550 万 |
| NSRecursiveLock | 500 万 | 450 万 |
| @synchronized | 200 万 | 180 万 |
15.11 高频面试题
Q1:为什么 OSSpinLock 不再安全? A:优先级反转。低优先级线程持锁自旋,高优先级线程等锁(也自旋,CPU 不释放),中优先级线程抢占低优先级 → 高优先级被无限阻塞。改用 os_unfair_lock:内核在持锁期间提升持锁者优先级(priority inheritance)。
Q2:@synchronized 为什么慢? A:内部维护哈希表 obj → SyncList,每次加锁都要查表。锁本身是递归互斥锁,封装层多。但简单场景仍推荐使用,可读性最高。
Q3:怎么实现一个读写锁? A:①pthread_rwlock_t(C 接口);②dispatch_barrier_async(并发队列 + barrier,读用 dispatch_sync,写用 dispatch_barrier_async);③自定义(读计数 + 写标志 + 互斥锁)。
Q4:自旋锁和互斥锁的区别? A:自旋锁:等锁时不睡眠,循环检查(CPU 不释放);适合临界区极短的场景。互斥锁:等锁时睡眠,被唤醒再继续;适合临界区长。自旋锁在低优先级可能反转,互斥锁有上下文切换开销。
Q5:死锁的四个必要条件? A:①互斥(资源独占);②请求与保持(持锁同时请求新锁);③不可剥夺(锁不能被强制释放);④循环等待(多个线程形成环)。打破任何一个即可避免死锁。
第四部分 · Foundation 与 UI
第 16 章 Foundation 框架深度
16.1 NSObject 与根类
NSObject 是绝大多数 OC 类的根类,承担三大职能:
- Runtime 入口:
+alloc、-init、-class、-isKindOfClass:、-respondsToSelector:等。 - 协议方法:
<NSObject>协议定义了相等性、描述、自省。 - 默认实现:
-description、-hash、-isEqual:。
注意:NSObject 类 ≠ NSObject 协议。后者是协议,任何类可遵循(不一定继承 NSObject)。
16.2 NSObject 核心方法
// 实例创建
+ (instancetype)alloc; // 分配内存(refcount = 1)
- (instancetype)init; // 初始化(约定:init 失败返回 nil)
+ (instancetype)new; // = alloc + init
// 内省
- (BOOL)isKindOfClass:(Class)aClass; // 包含父类
- (BOOL)isMemberOfClass:(Class)aClass; // 仅当前类
- (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)conformsToProtocol:(Protocol *)protocol;
// 消息转发辅助
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
// 描述
- (NSString *)description; // 调试用,NSLog(%@)
- (NSString *)debugDescription; // LLDB po 用
// 相等性
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;16.3 相等性契约
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation Person
- (BOOL)isEqual:(id)object {
if (self == object) return YES;
if (![object isKindOfClass:[Person class]]) return NO;
Person *other = object;
return [self.name isEqualToString:other.name] && self.age == other.age;
}
- (NSUInteger)hash {
return [self.name hash] ^ self.age;
}
@end契约:
isEqual:返回 YES,则两者hash必须相等。- 反过来不要求(hash 相等不一定 isEqual)。
- 用作 NSDictionary key / NSSet 元素时依赖这两个方法。
16.4 Class Cluster(类簇)
NSArray *arr = @[@1, @2, @3];
NSLog(@"%@", NSStringFromClass([arr class])); // __NSArrayI
NSMutableArray *marr = [NSMutableArray array];
NSLog(@"%@", NSStringFromClass([marr class])); // __NSArrayM
NSArray *empty = @[];
NSLog(@"%@", NSStringFromClass([empty class])); // __NSArray0
NSArray *single = @[@1];
NSLog(@"%@", NSStringFromClass([single class])); // __NSSingleObjectArrayI类簇本质:抽象工厂模式。NSArray 是抽象基类,工厂方法 +arrayWith... 返回的是 __NSArrayI / __NSArrayM / __NSSingleObjectArrayI / __NSArray0 等具体子类。
优势:对外稳定接口,对内根据使用场景选择最优实现。
代价:不能直接子类化 NSArray——必须重写所有 primitive 方法(count、objectAtIndex:)并实现自己的存储。
16.5 集合类底层
| 公开类型 | 底层结构 | 特点 |
|---|---|---|
NSArray / __NSArrayI | C 数组(连续内存) | O(1) 索引访问 |
NSMutableArray / __NSArrayM | 环形缓冲区(circular buffer) | 头尾插入 O(1) |
__NSSingleObjectArrayI | 单元素优化 | 只装一个元素时不分配数组 |
__NSArray0 | 空数组单例 | [NSArray array] 永远返回同一实例 |
NSDictionary / __NSDictionaryI | 开放寻址哈希表 | 负载因子 0.75 |
NSMutableDictionary / __NSDictionaryM | 同上 + 可扩容 | rehash 时复制 |
NSSet | 哈希表 | O(1) 查找 |
NSOrderedSet | 数组 + 哈希表 | 既保留顺序又 O(1) 查找 |
16.6 NSMapTable / NSHashTable / NSCache
| 类 | 特点 | 典型场景 |
|---|---|---|
NSCache | 线程安全、自动响应内存警告 evict | 图片缓存(替代字典) |
NSMapTable | 可配 key/value 引用语义 | weak map(防循环引用) |
NSHashTable | 可配元素引用语义 | weak collection |
// NSMapTable:key 强,value 弱(防循环引用)
NSMapTable *map = [NSMapTable strongToWeakObjectsMapTable];
[map setObject:delegateObj forKey:@"k"];
// 当 delegateObj 释放后,自动从 map 移除
// NSCache:图片缓存
NSCache *imageCache = [NSCache new];
imageCache.countLimit = 100;
imageCache.totalCostLimit = 50 * 1024 * 1024; // 50MB
[imageCache setObject:img forKey:key cost:image.size.width * image.size.height];
id cached = [imageCache objectForKey:key];NSCache vs NSDictionary:
- NSCache 线程安全(不需要自己加锁)。
- 接收内存警告自动 evict。
- 不 retain key(key 可以是任意对象)。
- 不计 retain,存代理对象不会改变引用计数。
16.7 NSString 与 NSAttributedString
NSString:Unicode 字符串,对 UTF-16 编码做了优化的不可变类。
NSString *s = @"Hello";
NSLog(@"%lu", (unsigned long)[s length]); // 5
NSString *emoji = @"👋"; // U+1F44B
NSLog(@"%lu", (unsigned long)[emoji length]); // 2!代理对占 2 个 UTF-16 unitlength是 UTF-16 unit 数,不是字符数。- 遍历字形簇要用
rangeOfComposedCharacterSequenceAtIndex:。 CFString与NSString是 toll-free bridged,可互相强转。NSMutableString是可变子类。
NSAttributedString / NSMutableAttributedString:带属性的字符串。
NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:@"Hello World"];
[attr addAttribute:NSFontAttributeName
value:[UIFont systemFontOfSize:17]
range:NSMakeRange(0, 5)];
[attr addAttribute:NSForegroundColorAttributeName
value:[UIColor redColor]
range:NSMakeRange(6, 5)];
UILabel *label = [UILabel new];
label.attributedText = attr;16.8 通知中心 NSNotificationCenter
// 注册
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onChange:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
// 发送
[[NSNotificationCenter defaultCenter] postNotificationName:@"MyEvent"
object:nil
userInfo:@{@"key": @"value"}];
// 实现
- (void)onChange:(NSNotification *)note {
NSLog(@"%@", note.userInfo);
}
// iOS 9+ 后,对象 dealloc 时自动移除观察者底层:一张哈希表 name → [observers],post 时遍历调用。默认在发送线程同步调用所有观察者——发送在子线程,回调也在子线程。
// 跨线程发送:用 NotificationQueue 异步合并
[[NSNotificationQueue defaultQueue] enqueueNotification:note
postingStyle:NSPostASAP];16.9 文件系统与序列化
沙盒目录:
{App Home}/
├── {App}.app/ ← 只读,签名内容
├── Documents/ ← 用户数据,会被 iTunes / iCloud 备份
├── Library/
│ ├── Caches/ ← 缓存,可被系统清理,不备份
│ ├── Preferences/ ← NSUserDefaults 的 plist
│ └── Application Support/ ← App 配置与数据,会备份
├── tmp/ ← 临时文件,系统随时清理
└── SystemData/ ← 系统数据(iOS 11+)NSString *docs = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
NSString *tmp = NSTemporaryDirectory();序列化:
NSCoding:实现encodeWithCoder:/initWithCoder:。NSSecureCoding:安全版本,防止 substitution attack。NSKeyedArchiver/NSKeyedUnarchiver:二进制 plist 序列化。
// 实现 NSSecureCoding
@interface User : NSObject <NSSecureCoding>
@property (nonatomic, copy) NSString *name;
@end
@implementation User
+ (BOOL)supportsSecureCoding { return YES; }
- (void)encodeWithCoder:(NSCoder *)coder {
[coder encodeObject:self.name forKey:@"name"];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super init]) {
_name = [coder decodeObjectOfClass:[NSString class] forKey:@"name"];
}
return self;
}
@end
// 归档
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:user
requiringSecureCoding:YES
error:nil];
[NSKeyedArchiver archiveRootObject:user toFile:path]; // 简化版
// 解档
User *user = [NSKeyedUnarchiver unarchivedObjectOfClass:[User class]
fromData:data
error:nil];16.10 NSDate / NSCalendar
NSDate *now = [NSDate date];
NSDate *tomorrow = [now dateByAddingTimeInterval:86400];
NSCalendar *cal = [NSCalendar currentCalendar];
NSDateComponents *comps = [cal components:NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay
fromDate:now];
NSLog(@"%ld-%ld-%ld", (long)comps.year, (long)comps.month, (long)comps.day);
// 日期格式化
NSDateFormatter *fmt = [NSDateFormatter new];
fmt.dateFormat = @"yyyy-MM-dd HH:mm:ss";
fmt.locale = [NSLocale localeWithLocaleIdentifier:@"zh_CN"];
NSString *str = [fmt stringFromDate:now];易错:
NSDate是绝对时间(UTC),格式化时区影响显示。NSDateFormatter创建开销大,建议复用。- iOS 8+ 用
NSISO8601DateFormatter处理 ISO8601。
16.11 高频面试题
Q1:NSArray 的底层是什么?为什么 __NSArrayM 用环形缓冲区? A:__NSArrayI(不可变)是连续 C 数组;__NSArrayM(可变)是环形缓冲区(circular buffer)。环形 buffer 的好处:头尾插入都是 O(1),避免了普通数组头插的 O(n) 位移。
Q2:NSString 的 length 是字符数吗? A:不是,是 UTF-16 code unit 数。emoji 占 2 个 unit(代理对),所以 "👋".length == 2。要数实际字形数,用 rangeOfComposedCharacterSequencesInRange:。
Q3:NSCache 与 NSDictionary 区别? A:①NSCache 线程安全;②接收内存警告自动 evict;③不 retain key;④不计 retain,存代理对象不改变引用计数。
Q4:通知中心默认在哪个线程回调? A:发送线程同步回调。如果发送在后台线程,回调也在后台线程。要主线程回调,用 NotificationQueue 或 dispatch_async(main) 包一层。
Q5:isEqual: 与 hash 的契约? A:isEqual: 返回 YES 则 hash 必须相等;反过来不要求(hash 相等不一定 isEqual)。用作 NSDictionary key / NSSet 元素时依赖这两个方法。
第 17 章 UIKit 实践要点
17.1 UIView 与 CALayer
核心认知:UIView 是 CALayer 的上层封装。
UIView:负责事件响应、命中测试(hit-testing)、参与响应链。CALayer:负责绘制内容、动画、变换(GPU 渲染单元)。- 每个 UIView 都有一个
layer属性,UIView 是 layer 的 delegate。
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
view.backgroundColor = [UIColor redColor];
view.layer.cornerRadius = 8;
view.layer.borderColor = [UIColor blackColor].CGColor;
view.layer.borderWidth = 1;为什么分两层? macOS / iOS 共享 CoreAnimation 框架(CALayer),但 UI 事件响应在不同平台有不同实现。把「响应链 + 命中测试」放在平台专属的 UIView,把「绘制 + 动画」放在跨平台 CALayer。
17.2 事件传递与响应链
两阶段:命中测试(Hit Test) → 响应链(Responder Chain)。
// UIView 的 hitTest 实现(简化)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!self.userInteractionEnabled || self.hidden || self.alpha <= 0.01) return nil;
if (![self pointInside:point withEvent:event]) return nil;
for (UIView *subview in [self.subviews reverseObjectEnumerator]) { // 从后往前(顶部的在前)
UIView *hit = [subview hitTest:[self convertPoint:point toView:subview] withEvent:event];
if (hit) return hit;
}
return self;
}实战 1:扩展按钮点击区域。
@interface LargerButton : UIButton @end
@implementation LargerButton
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
CGFloat insets = 20;
CGRect area = CGRectInset(self.bounds, -insets, -insets);
return CGRectContainsPoint(area, point);
}
@end实战 2:穿透点击(让父视图忽略点击,让下面的 view 接收)。
@interface PassthroughView : UIView @end
@implementation PassthroughView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hit = [super hitTest:point withEvent:event];
return hit == self ? nil : hit; // 自己不接收,让父级找下一个
}
@end17.3 响应链(Responder Chain)
UIView → UIView → ... → UIViewController.view → UIViewController → UIWindow → UIApplication → AppDelegate事件(touch / motion / press)从 hit-test 找到的 view 开始,沿 nextResponder 一路向上:
@implementation MyView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event]; // 不处理就让上级处理
}
@end17.4 UIViewController 生命周期
init(coder: / initWithNibName:)
↓
loadView ← 创建 self.view(不要主动调 super)
↓
viewDidLoad ← 视图加载完成,做一次性初始化
↓
viewWillAppear: ← 即将显示
↓
viewWillLayoutSubviews
↓
viewDidLayoutSubviews ← 子视图布局完成,frame 已确定
↓
viewDidAppear: ← 已显示
↓
viewWillDisappear:
↓
viewDidDisappear:
↓
dealloc ← 释放易错坑:
- 不要在
viewDidLoad写依赖 frame 的逻辑(此时 Auto Layout 还没计算)。 - 不要主动调用
[super loadView];如果你想完全自定义 view,重写loadView并赋值给self.view。 viewWillAppear:可能多次调用(push / pop / present / dismiss),不能做一次性逻辑。
17.5 UITableView / UICollectionView 性能优化
| 优化点 | 手段 |
|---|---|
| 高度缓存 | 提前算好 cell 高度并缓存 |
| 异步布局 | AsyncDisplayKit / AsyncCollectionView |
| 离屏渲染规避 | 避免同时 cornerRadius + maskToBounds |
| 圆角优化 | 用带圆角的图代替 mask |
| 图片解码 | 子线程强制解码 |
| 减少层级 | 扁平化 cell 内 subviews |
| 复用 | prepareForReuse 重置状态 |
| 异步绘制 | layer.drawsAsynchronously = YES |
子线程解码图片:
@implementation UIImage (Decode)
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (image.images.count > 0) return image; // GIF 不解码
CGImageRef imageRef = image.CGImage;
if (!imageRef) return image;
CGSize size = image.size;
if (size.width == 0 || size.height == 0) return image;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef ctx = CGBitmapContextCreate(NULL,
size.width,
size.height,
8,
0,
colorSpace,
kCGImageAlphaPremultipliedLast);
CGColorSpaceRelease(colorSpace);
if (!ctx) return image;
CGRect rect = (CGRect){.origin = CGPointZero, .size = size};
CGContextDrawImage(ctx, rect, imageRef);
CGImageRef decoded = CGBitmapContextCreateImage(ctx);
CGContextRelease(ctx);
UIImage *result = [UIImage imageWithCGImage:decoded
scale:image.scale
orientation:image.imageOrientation];
CGImageRelease(decoded);
return result;
}
@end17.6 Auto Layout 原理
Auto Layout 是基于约束(线性等式 / 不等式)的布局系统。底层算法叫 Cassowary(约束求解器,1998 年论文)。
- Cassowary 把约束转化为线性规划,用单纯形法求最小化误差的解。
- 复杂度随约束数 N² 增长。
- iOS 12 之后 Apple 重写了 Auto Layout,性能大幅提升。
- 大量 cell + 复杂 Auto Layout 仍是性能瓶颈。
优化:
- 改用 UIStackView(减少约束)。
- 子线程计算(部分场景)。
- 完全手动 frame(最快)。
17.7 高频面试题
Q1:UI 渲染为什么必须在主线程? A:UIKit 非线程安全;事件分发、响应链、视图树修改依赖单线程串行;与 VSync 协调。子线程操作 UI 会引发未定义行为(崩溃、UI 错乱)。
Q2:UIView 与 CALayer 关系? A:UIView 是 CALayer 的事件响应包装。1:1。Layer 是渲染核心。UIView 提供 hit-test、响应链、触摸;CALayer 提供内容、动画、变换。
Q3:Hit Test 流程? A:从 window 递归调用 hitTest(_:with:),point(inside:with:) 判断点是否在视图内,从后往前遍历 subviews。第一个返回非 nil 的就是接收事件的 view。
Q4:Auto Layout 性能瓶颈在哪? A:Cassowary 算法复杂度 O(N²)。约束越多,子视图越多,求解越慢。优化:UIStackView、减少约束、手动 frame、提前算好高度。
第 18 章 Core Animation 与渲染
18.1 Core Animation 架构
Core Animation 是 iOS / macOS 共享的图形动画框架。本身不直接渲染,而是「layer tree → RenderServer(独立进程)→ GPU」的代理。
App Process
↓ layer tree(model + presentation)
IPC(提交)
BackBoard / RenderServer Process
↓ OpenGL ES / Metal 调用
GPU
↓ framebuffer
Display跨进程提交的代价就是为什么「不要在主线程做太多 layer 修改」——每次修改触发 commit。
18.2 渲染流程与帧率
App (CPU)
│
│ ① Layout(frame / constraint 计算)
│ ② Display(文本绘制、图片解码)
│ ③ Prepare(图片格式转换、解码)
│ ④ Commit(打包 layer tree,序列化跨进程)
▼
Render Server(BackBoard.app 进程)
│
│ ⑤ OpenGL ES / Metal 接收 layer tree
│ ⑥ 顶点着色器 / 片元着色器 / 光栅化
▼
GPU
│
│ ⑦ 合成图层、混合
│ ⑧ 写入 framebuffer
▼
Display(按 VSync 刷新到屏幕)关键概念:
- VSync(垂直同步):屏幕每 16.67ms(60Hz)/ 8.33ms(120Hz ProMotion)刷新一次。错过一帧就掉帧。
- 双缓冲机制:GPU 同时维护两块 framebuffer,避免撕裂。
- CPU + GPU 必须在一个 VSync 周期内完成,否则这一帧被丢弃。
18.3 Layer 三棵树
| 树 | 名 | 作用 |
|---|---|---|
| Model Tree | layer(默认访问) | 你设定的「最终值」 |
| Presentation Tree | layer.presentationLayer | 动画过程中每一刻的「显示值」 |
| Render Tree | RenderServer 私有 | GPU 实际渲染的数据 |
[UIView animateWithDuration:1.0 animations:^{
view.frame = CGRectMake(0, 0, 200, 200);
}];
// 动画进行中读当前位置
CALayer *presentation = view.layer.presentationLayer;
CGRect currentFrame = presentation.frame;18.4 隐式动画与显式动画
隐式动画:直接修改 CALayer 的 animatable 属性(position / opacity / bounds / backgroundColor 等),CoreAnimation 自动加上 0.25s 默认动画。
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
layer.opacity = 0.0;
[CATransaction commit];UIView 动画:
[UIView animateWithDuration:0.3
animations:^{
view.frame = newFrame;
view.alpha = 0.5;
}
completion:^(BOOL finished) {
[view removeFromSuperview];
}];
// Spring 动画(iOS 7+)
[UIView animateWithDuration:0.5
delay:0
usingSpringWithDamping:0.7
initialSpringVelocity:0.5
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
view.center = newCenter;
}
completion:nil];18.5 显式动画
// CABasicAnimation
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"opacity"];
anim.fromValue = @0;
anim.toValue = @1;
anim.duration = 0.3;
[layer addAnimation:anim forKey:@"fade"];
// CAKeyframeAnimation
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
UIBezierPath *path = [UIBezierPath bezierPath];
// ... 构造路径
anim.path = path.CGPath;
anim.duration = 2;
[layer addAnimation:anim forKey:@"move"];
// CAAnimationGroup
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = @[fadeAnim, scaleAnim];
group.duration = 0.5;
[layer addAnimation:group forKey:@"combined"];
// CASpringAnimation(iOS 9+)
CASpringAnimation *spring = [CASpringAnimation animationWithKeyPath:@"position.y"];
spring.damping = 10;
spring.stiffness = 100;
spring.mass = 1;
spring.initialVelocity = 0;
spring.duration = spring.settlingDuration;
[layer addAnimation:spring forKey:@"spring"];关键认知:CAAnimation 结束后,layer 的 model 值不变。要保留动画后状态:
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
// 或者动画结束时显式 set model18.6 离屏渲染
触发离屏渲染的操作:
layer.cornerRadius+masksToBounds(同时设才有问题)layer.masklayer.shadow(无 shadowPath)shouldRasterize(光栅化)- Group opacity(多个 layer 合成 + 透明度)
- 文字
UITextView多种样式
为什么慢:
- 额外分配离屏 buffer(图像大小的几倍)。
- 渲染从直接 pipeline → 中断切换。
- 拷贝回 framebuffer 额外开销。
优化:
| 场景 | 替代方案 |
|---|---|
| 圆角 | 带圆角的图(服务端合成 / CoreGraphics 预处理) |
| 阴影 | layer.shadowPath = [UIBezierPath ...].CGPath |
| 模糊 | UIVisualEffectView(系统 GPU 优化) |
| 多效果 | 预合成图 / 异步绘制 |
检测:模拟器 → Debug → Color Offscreen-Rendered Yellow。黄色区域即离屏渲染。
// 阴影优化
layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:layer.bounds
cornerRadius:layer.cornerRadius].CGPath;
// 圆角优化(避免离屏)
// 1. 服务端返回带圆角的图片
// 2. 用 CoreGraphics 在子线程预先合成
+ (UIImage *)imageWithRoundCorner:(UIImage *)image radius:(CGFloat)radius {
UIGraphicsBeginImageContextWithOptions(image.size, NO, [UIScreen mainScreen].scale);
CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect
cornerRadius:radius];
[path addClip];
[image drawInRect:rect];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}18.7 高频面试题
Q1:什么是离屏渲染?为什么会慢? A:GPU 不能直接把圆角/模糊/阴影结果写到 framebuffer,需要在屏幕外开缓冲区渲染,再合成。代价:①额外内存 ②拷贝回 framebuffer ③上下文切换。
Q2:CAAnimation 结束后 layer 会回到原位置吗?为什么? A:会,因为动画只改 presentation tree,不改 model tree。要保留动画后的状态,要在动画结束时显式 set model layer,或用 fillMode = kCAFillModeForwards + removedOnCompletion = NO。
Q3:CALayer 的 presentation 与 model 区别? A:model tree 是「最终值」,presentation tree 是「动画过程中每一刻的显示值」。动画期间访问 layer.presentationLayer.position 拿到当前实际位置,访问 layer.position 拿到目标值。
Q4:为什么 cornerRadius + masksToBounds 触发离屏? A:因为圆角裁剪需要在 layer 内容(图片/背景/子 layer)合成后才能应用。GPU 不能在一次 pass 中完成,需要先合成到离屏 buffer、再裁剪、再输出。
第五部分 · 进阶与优化
第 19 章 性能优化
19.1 启动优化
启动阶段
用户点击图标
↓
【pre-main 阶段】
1. dyld 加载 Mach-O
2. 加载依赖的动态库(递归加载)
3. Rebase / Bind(地址修正、符号绑定)
4. ObjC Runtime setup(注册类、Category)
5. +load 方法
6. C++ 静态构造函数、__attribute__((constructor))
↓
main()
↓
【post-main 阶段】
7. UIApplicationMain
8. AppDelegate didFinishLaunching
9. RootViewController 创建 + viewDidLoad
10. 首屏数据请求
11. 首屏渲染(第一帧提交到 RenderServer)优化手段
| 阶段 | 手段 |
|---|---|
| dyld | 减少动态库数量(合并;改静态库;iOS 13+ dyld 3 闭包缓存) |
| Rebase/Bind | 减少指针修正(数据段 / ObjC 类的指针);减少 OC 类数量 |
| +load | 把 +load 中的逻辑延后到 +initialize 或首屏后;改用 dispatch_once 全局变量 |
| Category | 减少 +load 中的 swizzle |
| 通用 | 用 -Wl,-order_file 排列代码段顺序,提升 page-in 命中率 |
| didFinishLaunching | 延后第三方 SDK 初始化;按需初始化;分阶段启动 |
| 首屏 | 简化首屏视图层级;预加载图片解码 |
| 数据 | 关键数据预请求;本地缓存优先 |
测量工具:
- Xcode → Edit Scheme → Run → Diagnostics → 勾选 Dynamic Library Loading 与 Pre-main statistics。
- 启动时控制台输出
Total pre-main time: ...ms。
dyld 3 闭包(iOS 13+):iOS 13 之前 dyld 2.x 每次启动都解析 Mach-O、绑定符号。iOS 13+ dyld 3 引入闭包缓存——首次启动分析后,结果缓存到 /var/db/dyld/,后续启动直接读取。
Page-In 优化:
- App 启动时按 16KB 一页从磁盘加载到内存。
- 启动相关的代码/数据若分散在多个页,page-in 次数多 → 启动慢。
- 用
-order_file把启动相关函数集中到前几页。
19.2 卡顿监测
卡顿本质
VSync 周期内(60Hz = 16.67ms)CPU + GPU 没完成一帧渲染 → 掉帧 → 卡顿。
主线程耗时监测
方案 1:CADisplayLink 测 FPS:
@interface FPSMonitor : NSObject
@end
@implementation FPSMonitor {
CADisplayLink *_link;
NSTimeInterval _lastTime;
NSInteger _count;
}
- (instancetype)init {
if (self = [super init]) {
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
return self;
}
- (void)tick:(CADisplayLink *)link {
_count++;
NSTimeInterval now = link.timestamp;
if (now - _lastTime >= 1.0) {
NSLog(@"FPS: %ld", (long)_count);
_lastTime = now;
_count = 0;
}
}
@end方案 2:RunLoop Observer(详细见 第 13 章):监测主线程 RunLoop 状态切换,超过阈值即卡顿。
方案 3:子线程抓栈:
// 子线程定时抓主线程栈
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
while (YES) {
[NSThread sleepForTimeInterval:0.05]; // 50ms
// 用 mach thread_get_state 获取主线程栈
NSArray *symbols = [NSThread callStackSymbols];
// 上报
}
});第三方工具:
- Matrix(微信):RunLoop + 信号量 + 栈抓取。
- BSBacktraceLogger:抓主线程栈。
19.3 内存优化
| 维度 | 手段 |
|---|---|
| 减少 malloc | 用栈对象、值类型(OC 中的 struct)、内存池 |
| 图片 | 子线程解码 + 下采样 |
| 列表 | 复用 cell、缓存高度、按需加载 |
| 缓存 | NSCache 限大小 + 监听 memory warning 清理 |
| 字符串 | 大量小字符串用 __NSCFConstantString |
| autorelease | 循环外包 @autoreleasepool { } |
| 释放 | 后台线程释放对象(weak + 异步) |
| 检测 | Xcode Memory Graph、Instruments Allocations/Leaks |
OOM(Out Of Memory):
- 系统内存紧张时主动杀掉内存占用大的 App(Jetsam 机制)。
- 监控:用
os_signpost/ MetricKit 捕获内存压力事件。 - 阈值:iPhone 物理内存 × 一定比例(不同机型不同)。
19.4 包体优化
| 维度 | 手段 | 收益估计 |
|---|---|---|
| 资源图片 | WebP 替代 PNG/JPEG(30-50%) | 高 |
| 大图 | 资源 → 服务端下载(动态化) | 高 |
| App Icon | 不在 Asset Catalog 之外重复 | 中 |
| 启动图 | 用 LaunchScreen storyboard 而非静态图 | 中 |
| 二进制 | -Osize 优化(10-20% 体积) | 中 |
| 死代码 | Strip、Linkmap 分析 | 低-中 |
| 动态库改静态库 | 减少多个 dylib | 中 |
| 不用的架构 | lipo 移除 simulator 架构 | 中 |
| 资源按需 | On-Demand Resources | 高 |
| 字体 | 子集化(Fontmin 等工具) | 中 |
| Assets.car | 资源压缩、删除冗余 | 中 |
测量:Xcode → Archive → Distribute App → Generate Size Report。
19.5 工具链
| 工具 | 用途 |
|---|---|
| Instruments | Time Profiler / Allocations / Leaks / Core Animation / System Trace |
| Xcode Memory Graph | 可视化对象引用关系,找 leak |
| MetricKit | iOS 13+ 线上性能数据收集 |
| App Launch(Instruments) | 启动分析 |
| Hangs(Instruments) | 卡顿分析 |
| Network(Instruments) | 网络性能 |
| CTMalloc | 内存分配追踪 |
| OS Signpost | 自定义埋点 |
19.6 高频面试题
Q1:启动优化有哪些关键点? A:①减少动态库;②少 +load 方法;③减少 ObjC 类数量;④延后非首屏任务;⑤首屏数据预取;⑥order_file 优化 page-in。本质上把 pre-main 时间和首屏渲染时间都压缩。
Q2:怎么检测主线程卡顿? A:①CADisplayLink 测 FPS(粗粒度);②RunLoop Observer 测 BeforeWaiting → AfterWaiting 耗时(中粒度);③子线程定时抓主线程栈(细粒度,定位到函数)。线上方案常用 ②+③。
Q3:什么是离屏渲染?为什么会慢? A:GPU 不能直接把圆角/模糊/阴影结果写到 framebuffer,需要在屏幕外开缓冲区渲染,再合成。代价:①额外内存 ②拷贝回 framebuffer ③上下文切换。优化:用带圆角的图、用 shadowPath、避免 mask。
Q4:App 包体太大怎么减? A:①资源用 WebP / 服务端动态下载;②删除未使用资源;③二进制 strip、-Osize;④字体子集化;⑤按需资源(ODR)。
第 20 章 与 Swift 混编
20.1 文件类型与桥接
| 文件 | 用途 |
|---|---|
.h / .m | OC 头文件 / 实现 |
.swift | Swift 源文件 |
{Module}-Bridging-Header.h | OC → Swift 桥接(OC 暴露给 Swift) |
{Module}-Swift.h | Swift → OC 桥接(编译器自动生成) |
.modulemap | Module 化 OC 框架 |
20.2 Swift 调用 OC
方式 1:Bridging Header:
- 创建
{Module}-Bridging-Header.h。 - 在 Bridging Header 中
#import所有要暴露给 Swift 的 OC 头文件。 - Swift 代码直接使用(无需 import)。
// {Module}-Bridging-Header.h
#import "Person.h"
#import "MyTools.h"// Swift 中直接用
let p = Person(name: "Alice")
p.greet()方式 2:Module Map(推荐):
把 OC 代码打包成 framework,定义 module map:
// MyOCModule.modulemap
module MyOCModule {
umbrella header "MyOCModule.h"
export *
}Swift 中:
import MyOCModule20.3 OC 调用 Swift
步骤:
- Swift 类继承
NSObject或标注@objc/@objcMembers。 - OC 中
#import "{Module}-Swift.h"(编译器自动生成的头文件)。
// Swift
@objc class Calculator: NSObject {
@objc func add(_ a: Int, b: Int) -> Int { a + b }
@objc static let shared = Calculator()
}
@objcMembers class MyVC: UIViewController { // 所有成员一次性暴露
func someMethod() { }
}// OC
#import "MyModule-Swift.h"
Calculator *c = [Calculator new];
[c add:1 b:2];20.4 互操作注意点
| Swift 特性 | OC 能否调用 |
|---|---|
| class 继承 NSObject / @objc | ✅ |
| struct / enum | ❌(需 @objc,且 enum 必须 Int 原始值) |
| Tuple | ❌ |
| 泛型 | ❌(部分场景,需 @objc 兼容) |
| Optional | ✅(被映射为 nullable) |
| Closure | ✅(被映射为 block) |
@objc dynamic | ✅,可被 KVO / Runtime 拦截 |
| 默认参数 | 部分(编译器生成多份) |
20.5 命名转换
| Swift | OC |
|---|---|
func greet(name: String) | - (void)greetWithName:(NSString *)name |
init(name: String) | - (instancetype)initWithName:(NSString *)name |
static func shared() | + (instancetype)shared |
enum Color { case red } | typedef SWIFT_ENUM(NSInteger, Color) { ColorRed = 0 } |
Swift 方法第一个标签会被合并到方法名(greet(name:) → greetWithName:)。
20.6 nullability 注解
// OC
@property (nonatomic, copy, nullable) NSString *middleName;
@property (nonatomic, copy, nonnull) NSString *firstName;
NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy) NSString *name; // 自动 nonnull
NS_ASSUME_NONNULL_ENDSwift 中:
var middleName: String? // nullable → Optional
var firstName: String // nonnull
var name: String // 在 NS_ASSUME_NONNULL_BEGIN 区域内20.7 Swift / OC 混编最佳实践
- 新项目优先 Swift:Swift 是未来,OC 是历史。
- 新文件优先 Swift:除非必须 OC(如与 OC 框架深度集成)。
- 关键模型层用 Swift:值类型 / Optional / 错误处理更安全。
- 大型 OC 项目渐进迁移:新模块用 Swift,老模块逐步替换。
- 避免 Swift ↔ OC 频繁跨边界:跨边界有性能开销(参数转换)。
- 公共接口尽量 Swift:Swift → OC 限制少,OC → Swift 限制多。
20.8 高频面试题
Q1:Swift 调 OC 怎么做? A:通过 Bridging Header(小项目)或 Module Map(中大型项目)。Bridging Header 中 #import 所有要暴露的 OC 头文件,Swift 代码直接使用,无需 import。
Q2:OC 调 Swift 的限制? A:Swift 类必须继承 NSObject 或 @objc;struct / enum / tuple / 泛型 / 协议默认不能从 OC 调用。Swift 独有特性 OC 看不到。
Q3:@objc 和 @objc dynamic 区别? A:@objc 把方法 / 类暴露给 OC,但默认仍走静态派发(除非 OC 调用)。@objc dynamic 强制走 OC runtime(消息派发),可被 KVO / Runtime / Swizzling 拦截。
Q4:Swift 与 OC 互操作的性能开销? A:跨边界调用涉及参数转换(Optional / String / Array 的桥接),有少量开销。频繁跨边界的代码要谨慎设计。
第 21 章 安全与防护
21.1 数据存储安全
| 数据类型 | 推荐方案 |
|---|---|
| 密码、token、密钥 | Keychain |
| 用户偏好 | NSUserDefaults(不含敏感) |
| 业务数据 | SQLite + 字段加密 |
| 缓存图片 | 文件系统(无需加密) |
| 文档、PDF | 文件系统 + Data Protection |
iOS Data Protection(文件级加密):
NSData *data = ...;
NSError *err;
[data writeToFile:path
options:NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication
error:&err];| 级别 | 说明 |
|---|---|
NSDataWritingFileProtectionComplete | 设备锁定后立即不可访问 |
NSDataWritingFileProtectionCompleteUnlessOpen | 已打开文件可继续写 |
NSDataWritingFileProtectionCompleteUntilFirstUserAuthentication | 第一次解锁后保持可访问(推荐) |
算法:iOS 用 AES-XTS,硬件级加密(Secure Enclave)。
21.2 网络安全
- HTTPS + ATS(强制 TLS 1.2+)。
- SSL Pinning(防 MITM)。
- 请求签名(HMAC-SHA256)。
- Token 体系:JWT、OAuth2。
- 敏感参数加密:RSA 公钥加密(密钥传输)+ AES 对称加密(大数据)。
SSL Pinning 实现:
@interface PinningDelegate : NSObject <NSURLSessionDelegate>
@end
@implementation PinningDelegate
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
if (!serverTrust) {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}
SecCertificateRef cert = SecTrustGetCertificateAtIndex(serverTrust, 0);
NSData *remoteCertData = CFBridgingRelease(SecCertificateCopyData(cert));
NSData *localCertData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"server" ofType:@"cer"]];
if ([remoteCertData isEqualToData:localCertData]) {
completionHandler(NSURLSessionAuthChallengeUseCredential,
[NSURLCredential credentialForTrust:serverTrust]);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
@end21.3 反调试
// 检测 ptrace 附加
#import <sys/ptrace.h>
static const int PT_DENY_ATTACH = 31;
static void disableDebugger(void) {
ptrace(PT_DENY_ATTACH, 0, 0, 0); // 主动拒绝附加
}
// 检测 sysctl P_TRACED 标志
#import <sys/sysctl.h>
static BOOL isBeingTraced(void) {
struct kinfo_proc info;
size_t size = sizeof(info);
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
sysctl(mib, 4, &info, &size, NULL, 0);
return (info.kp_proc.p_flag & P_TRACED) != 0;
}21.4 越狱检测
+ (BOOL)isJailbroken {
NSArray *paths = @[@"/Applications/Cydia.app",
@"/Library/MobileSubstrate/MobileSubstrate.dylib",
@"/bin/bash",
@"/usr/sbin/sshd",
@"/etc/apt",
@"/private/var/lib/apt/",
@"/usr/sbin/sshd",
@"/Applications/Sileo.app"];
for (NSString *path in paths) {
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
return YES;
}
}
// 检测能否打开 cydia:// scheme
if ([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]) {
return YES;
}
// 检测能否写入 /private (非越狱不可写)
NSString *test = @"/private/jailbreak_test";
[@"" writeToFile:test atomically:YES encoding:NSUTF8StringEncoding error:nil];
if ([[NSFileManager defaultManager] fileExistsAtPath:test]) {
[[NSFileManager defaultManager] removeItemAtPath:test error:nil];
return YES;
}
return NO;
}21.5 dyld 注入检测
#import <mach-o/dyld.h>
static BOOL isDylibInjected {
uint32_t count = _dyld_image_count();
for (uint32_t i = 0; i < count; i++) {
const char *name = _dyld_get_image_name(i);
if (strstr(name, "Substrate") ||
strstr(name, "FridaGadget") ||
strstr(name, "libfrida") ||
strstr(name, "TweakInject")) {
return YES;
}
}
return NO;
}21.6 代码混淆
C/C++ 层:
- 静态字符串加密(编译期混淆宏)。
- 函数内联、控制流平坦化。
- LLVM Pass(O-LLVM / Hikari)。
OC 层:
- 类名 / 方法名混淆(如
MyClass→a_b_c_)。 - 字符串不要硬编码(用解码函数)。
- 关键算法用 C 实现 + 静态库。
资源:
- 图片资源加密。
- SQLite 数据库加密(SQLCipher)。
21.7 高频面试题
Q1:iOS App 加密机制是什么? A:FairPlay DRM + Code Signature。Apple 用私钥对 App 加密,设备用预置公钥验证。越狱可绕过签名校验,但 FairPlay 不能直接解密。
Q2:怎么防止 App 被逆向? A:①代码混淆;②反调试(ptrace + sysctl);③反注入(dyld 列表检测);④完整性校验;⑤关键算法服务端化;⑥越狱检测。没有任何方法能完全防住有经验的逆向工程师——目标是提高门槛。
Q3:SSL Pinning 怎么做? A:在 URLSession:didReceive:challenge:completionHandler: 中校验服务端证书/公钥,匹配则信任,不匹配则取消。可绑定完整证书或仅公钥。
Q4:iOS 上 AES 用哪个库? A:①CommonCrypto(C 接口,老);②CryptoKit(iOS 13+ Swift 友好);③Security.framework(底层)。OC 项目一般用 CommonCrypto。
第六部分 · 架构设计
写到这里,语言、Runtime、Foundation 都是「术」。架构是「道」——它决定了代码长期可维护性、团队协作效率、App 的演进上限。本章从设计原则出发,一路走到大型项目的组件化、架构演进,并提供真实的架构案例。
第 22 章 设计原则 SOLID
SOLID 是面向对象设计的五个基本原则,由 Robert C. Martin(Uncle Bob)提出。理解这些原则是写出可维护代码的前提。
22.1 Single Responsibility Principle(单一职责)
一个类应该只有一个引起它变化的原因。
反例:
// ❌ 一个类干了所有事
@interface UserViewController : UIViewController
- (void)loadUsers; // 网络
- (NSData *)parseJSON:(NSData *)data; // 解析
- (void)saveToDB:(NSArray *)users; // 数据库
- (void)renderUI; // UI
- (void)trackEvent:(NSString *)event; // 埋点
- (UIImage *)cacheImage:(NSURL *)url; // 图片缓存
@end重构:
// ✅ 拆分为多个职责单一的类
@interface UserService : NSObject
- (NSArray *)loadUsers;
@end
@interface UserRepository : NSObject
- (void)save:(NSArray *)users;
@end
@interface Analytics : NSObject
- (void)track:(NSString *)event;
@end
@interface ImageCache : NSObject
- (UIImage *)imageForURL:(NSURL *)url;
@end
// Controller 只负责 UI 协调
@interface UserViewController : UIViewController
@property (nonatomic, strong) UserService *service;
@property (nonatomic, strong) Analytics *analytics;
@end22.2 Open-Closed Principle(开闭原则)
软件实体应该对扩展开放,对修改关闭。
反例:
// ❌ 加新形状要改 calculateArea
- (CGFloat)calculateArea:(id)shape {
if ([shape isKindOfClass:[Circle class]]) return ...;
else if ([shape isKindOfClass:[Rectangle class]]) return ...;
// 加 Triangle 要改这里
}重构:
// ✅ 抽象协议
@protocol Shape <NSObject>
- (CGFloat)area;
@end
@interface Circle : NSObject <Shape>
@end
@implementation Circle
- (CGFloat)area { return M_PI * r * r; }
@end
@interface Triangle : NSObject <Shape> // 新增不改旧
@end
- (CGFloat)totalArea:(NSArray<id<Shape>> *)shapes {
CGFloat total = 0;
for (id<Shape> s in shapes) total += [s area];
return total;
}22.3 Liskov Substitution Principle(里氏替换)
子类必须能够替换其父类,而不破坏程序正确性。
反例:
@interface Rectangle : NSObject
@property NSInteger width;
@property NSInteger height;
- (NSInteger)area;
@end
@interface Square : Rectangle // ❌ Square 强制宽高相等,违反 Rectangle 契约
@end
@implementation Square
- (void)setWidth:(NSInteger)w { self.height = w; [super setWidth:w]; }
@end教训:继承不是为了复用代码,而是表达「真正的 is-a」关系。如果行为不一致,用组合而非继承。
22.4 Interface Segregation Principle(接口隔离)
客户不应被迫依赖它不使用的方法。
反例:
// ❌ 胖协议
@protocol Worker <NSObject>
- (void)code;
- (void)design;
- (void)test;
- (void)manage;
- (void)market;
@end
@interface Programmer : NSObject <Worker>
// 被迫实现 design/test/manage/market
@end重构:
// ✅ 拆为小协议
@protocol Coder <NSObject>
- (void)code;
@end
@protocol Designer <NSObject>
- (void)design;
@end
@interface Programmer : NSObject <Coder>
- (void)code { /* ... */ }
@endOC 协议天然适合接口隔离——多个小协议组合优于一个大协议。
22.5 Dependency Inversion Principle(依赖倒置)
高层模块不应依赖低层模块。两者都应依赖抽象。
反例:
@interface OrderService : NSObject
@property (nonatomic, strong) MySQLDatabase *db; // 写死
- (void)create:(Order *)order;
@end重构:
// ✅ 依赖抽象(协议)
@protocol Database <NSObject>
- (void)insertOrder:(Order *)order;
@end
@interface MySQLDatabase : NSObject <Database>
@end
@interface PostgreSQLDatabase : NSObject <Database>
@end
@interface OrderService : NSObject
@property (nonatomic, strong) id<Database> db;
- (void)create:(Order *)order;
@end
// 注入具体实现
OrderService *svc = [OrderService new];
svc.db = [MySQLDatabase new];依赖倒置是**依赖注入(DI)与控制反转(IoC)**的基础。
22.6 其他重要原则
| 原则 | 含义 |
|---|---|
| DRY(Don't Repeat Yourself) | 每个知识点在系统中只有一份表达 |
| KISS(Keep It Simple, Stupid) | 简单优于复杂 |
| YAGNI(You Aren't Gonna Need It) | 别提前为「未来需求」做设计 |
| Composition Over Inheritance | 优先组合而非继承 |
| Law of Demeter(最少知识原则) | 一个对象只与其直接朋友通信 |
| Hollywood Principle | 别调用我,我会调用你(IoC) |
第 23 章 Objective-C 中的设计模式
设计模式是「针对反复出现的设计问题的经典解法」。GoF 23 个模式分三类。
23.1 创建型模式
单例(Singleton)
@interface Analytics : NSObject
+ (instancetype)sharedInstance;
- (void)track:(NSString *)event;
@end
@implementation Analytics
+ (instancetype)sharedInstance {
static Analytics *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone:NULL] init];
});
return instance;
}
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
- (id)copyWithZone:(NSZone *)zone {
return self;
}
@endiOS 中的单例:[UIApplication sharedApplication]、[NSUserDefaults standardUserDefaults]、[NSFileManager defaultManager]、[NSURLSession sharedSession]。
警告:单例是反模式(被滥用),主要问题——全局状态、隐式依赖、难测试、生命周期等于 App。
工厂方法(Factory Method)
@protocol Payment <NSObject>
- (void)payWithAmount:(NSDecimalNumber *)amount;
@end
@interface AliPay : NSObject <Payment> @end
@interface WeChatPay : NSObject <Payment> @end
@interface PaymentFactory : NSObject
+ (id<Payment>)createWithType:(NSString *)type;
@end
@implementation PaymentFactory
+ (id<Payment>)createWithType:(NSString *)type {
if ([type isEqualToString:@"alipay"]) return [AliPay new];
if ([type isEqualToString:@"wechat"]) return [WeChatPay new];
return nil;
}
@end抽象工厂(Abstract Factory)
@protocol UIFactory <NSObject>
- (UIButton *)createButton;
- (UITextField *)createTextField;
@end
@interface iOSFactory : NSObject <UIFactory> @end
@interface AndroidStyleFactory : NSObject <UIFactory> @end建造者(Builder)
@interface PizzaBuilder : NSObject
@property (nonatomic) NSInteger size;
@property (nonatomic) BOOL cheese;
@property (nonatomic) BOOL pepperoni;
- (Pizza *)build;
@end
Pizza *pizza = [[[[[PizzaBuilder new] setSize:16] setCheese:YES] setPepperoni:NO] build];原型(Prototype)
@interface Document : NSObject <NSCopying>
@property (nonatomic, copy) NSString *content;
- (id)copyWithZone:(NSZone *)zone;
@end
Document *template = ...;
Document *draft = [template copy];23.2 结构型模式
适配器(Adapter)
// 旧接口
@interface OldJSONService : NSObject
- (NSData *)fetchJSON:(NSURL *)url;
@end
// 新协议
@protocol DataService <NSObject>
- (NSDictionary *)fetch:(NSURL *)url;
@end
// 适配器
@interface JSONServiceAdapter : NSObject <DataService>
@property (nonatomic, strong) OldJSONService *old;
- (NSDictionary *)fetch:(NSURL *)url {
NSData *data = [self.old fetchJSON:url];
return [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
}
@end装饰器(Decorator)
OC 中常用 Category + 关联对象实现:
@interface UIImageView (Cache)
- (void)loadImageFromURL:(NSURL *)url; // 给 UIImageView 加缓存能力
@end
@implementation UIImageView (Cache)
- (void)loadImageFromURL:(NSURL *)url {
// 检查缓存 → 加载 → 设置 image
}
@end代理(Proxy)
// 图片懒加载代理
@interface ImageProxy : NSObject
@property (nonatomic, strong) NSString *path;
@property (nonatomic, strong) RealImage *realImage;
- (void)draw;
@end
@implementation ImageProxy
- (void)draw {
if (!self.realImage) {
self.realImage = [[RealImage alloc] initWithPath:self.path];
}
[self.realImage draw];
}
@end外观(Facade)
// 复杂子系统
@interface AuthService : NSObject @end
@interface UserService : NSObject @end
@interface AnalyticsService : NSObject @end
// 统一入口
@interface AppFacade : NSObject
- (void)loginWithUsername:(NSString *)username password:(NSString *)password;
@end
@implementation AppFacade
- (void)loginWithUsername:(NSString *)username password:(NSString *)password {
[[[AuthService new] authenticate:username password:password] then:^{
[[UserService new] loadProfile];
[[AnalyticsService new] track:@"login"];
}];
}
@end享元(Flyweight)
@interface CharFactory : NSObject
+ (instancetype)sharedInstance;
- (Char *)charWithCharacter:(NSString *)c;
@end
@implementation CharFactory {
NSMutableDictionary<NSString *, Char *> *_pool;
}
- (Char *)charWithCharacter:(NSString *)c {
if (!_pool[c]) _pool[c] = [[Char alloc] initWithChar:c];
return _pool[c];
}
@end组合(Composite)
@protocol FileSystemNode <NSObject>
@property (nonatomic, readonly) NSUInteger size;
@end
@interface FileNode : NSObject <FileSystemNode> @end
@interface FolderNode : NSObject <FileSystemNode>
@property (nonatomic, strong) NSArray<id<FileSystemNode>> *children;
@end
@implementation FolderNode
- (NSUInteger)size {
NSUInteger total = 0;
for (id<FileSystemNode> n in self.children) total += n.size;
return total;
}
@end23.3 行为型模式
策略(Strategy)
@protocol SortingStrategy <NSObject>
- (NSArray *)sort:(NSArray *)arr;
@end
@interface QuickSortStrategy : NSObject <SortingStrategy> @end
@interface MergeSortStrategy : NSObject <SortingStrategy> @end
@interface Sorter : NSObject
@property (nonatomic, strong) id<SortingStrategy> strategy;
- (NSArray *)sort:(NSArray *)arr;
@end观察者(Observer)
// 通知中心
[[NSNotificationCenter defaultCenter] postNotificationName:@"userLogin" object:nil];
// KVO(详见第 8 章)
[user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];命令(Command)
@protocol Command <NSObject>
- (void)execute;
- (void)undo;
@end
@interface LightOnCommand : NSObject <Command>
@property (nonatomic, strong) Light *light;
- (void)execute { [self.light turnOn]; }
- (void)undo { [self.light turnOff]; }
@end
@interface RemoteControl : NSObject
@property (nonatomic, strong) id<Command> button;
- (void)press { [self.button execute]; }
@end状态(State)
@protocol TCPState <NSObject>
- (void)handle:(TCPConnection *)connection;
@end
@interface TCPClosed : NSObject <TCPState>
- (void)handle:(TCPConnection *)c { c.state = [TCPOpen new]; }
@end
@interface TCPConnection : NSObject
@property (nonatomic, strong) id<TCPState> state;
- (void)request { [self.state handle:self]; }
@end责任链(Chain of Responsibility)
@protocol Handler <NSObject>
@property (nonatomic, strong) id<Handler> next;
- (BOOL)handle:(Request *)request;
@end
@interface AuthHandler : NSObject <Handler>
- (BOOL)handle:(Request *)req {
if (!req.token) return NO;
return [self.next handle:req];
}
@end
@interface LogHandler : NSObject <Handler> @end
@interface BusinessHandler : NSObject <Handler> @end
// 组装责任链
AuthHandler *h1 = [AuthHandler new];
h1.next = [LogHandler new];
h1.next.next = [BusinessHandler new];
[h1 handle:request];UIKit 响应链、URLSession 的认证挑战、Express / Flask 中间件都是责任链。
迭代器(Iterator)
// NSEnumerator 是迭代器
NSEnumerator *enumerator = [arr objectEnumerator];
id obj;
while ((obj = [enumerator nextObject])) { /* ... */ }
// 快速枚举(语法糖)
for (id obj in arr) { /* ... */ }中介者(Mediator)
@interface ChatRoom : NSObject
- (void)sendMessage:(NSString *)msg from:(User *)from to:(User *)to;
@end
@interface User : NSObject
@property (nonatomic, strong) ChatRoom *room;
- (void)send:(NSString *)msg to:(User *)to {
[self.room sendMessage:msg from:self to:to];
}
@end备忘录(Memento)
@interface EditorMemento : NSObject
@property (nonatomic, copy) NSString *content;
@end
@interface Editor : NSObject
@property (nonatomic, copy) NSString *content;
- (EditorMemento *)save;
- (void)restore:(EditorMemento *)m;
@end模板方法(Template Method)
@interface Game : NSObject
- (void)play; // 模板(不可重写)
- (void)initialize; // 钩子(子类重写)
- (void)startPlay;
- (void)endPlay;
@end
@implementation Game
- (void)play { // 模板方法
[self initialize];
[self startPlay];
[self endPlay];
}
- (void)initialize { } // 默认实现
@endOC 中用 Category 而非继承也能实现类似效果。
访问者(Visitor)
@protocol Visitor <NSObject>
- (void)visitFile:(File *)file;
- (void)visitFolder:(Folder *)folder;
@end
@interface SizeCalculator : NSObject <Visitor>
@property (nonatomic) NSUInteger total;
- (void)visitFile:(File *)f { self.total += f.size; }
- (void)visitFolder:(Folder *)folder {
for (id node in folder.children) {
if ([node respondsToSelector:@selector(accept:)]) {
[node accept:self];
}
}
}
@end23.4 OC 中的设计模式落地
| GoF 模式 | OC 中的体现 |
|---|---|
| 单例 | UIApplication.sharedApplication、UserDefaults.standard |
| 工厂 | 类簇(NSNumber、NSArray) |
| 抽象工厂 | UIAppearance |
| 适配器 | Toll-free bridging(NSString / CFString) |
| 装饰器 | Category + 关联对象 |
| 代理 | NSURLConnectionDelegate、NSURLSessionDelegate |
| 外观 | UIKit 整体封装 CoreAnimation |
| 享元 | __NSCFConstantString(小字符串常量池) |
| 组合 | UIView / CALayer 树 |
| 策略 | JSONSerialization.ReadingOptions |
| 观察者 | KVO、NotificationCenter |
| 命令 | NSInvocation、Block |
| 状态 | UIViewController 状态机 |
| 责任链 | Responder Chain |
| 迭代器 | NSEnumerator、fast enumeration |
| 中介者 | NotificationCenter |
| 备忘录 | NSUndoManager |
| 模板方法 | UIView drawRect: |
| 访问者 | 较少使用(OC 中麻烦) |
第 24 章 架构模式演进
24.1 Cocoa MVC(Apple MVC)
┌──────────────┐
│ Controller │
└─┬─────────┬──┘
┌───▼───┐ ┌────▼───┐
│ Model │ │ View │
└───────┘ └────────┘- Controller 是粘合剂,处理 View 与 Model 通信。
- View 通过 Target-Action 接收事件,通过 Outlet 更新。
Massive View Controller 问题:Controller 经常几千行。
OC 典型示例:
// Model
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
// View(用 XIB / SB 或纯代码)
@interface UserCell : UITableViewCell
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UILabel *ageLabel;
@end
// Controller
@interface UserViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, strong) NSMutableArray<User *> *users;
@property (nonatomic, weak) IBOutlet UITableView *tableView;
@end
@implementation UserViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self loadData];
}
- (void)loadData {
[APIClient fetchUsers:^(NSArray *users) {
self.users = [users mutableCopy];
[self.tableView reloadData];
}];
}
- (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)s {
return self.users.count;
}
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)ip {
UserCell *cell = [tv dequeueReusableCellWithIdentifier:@"UserCell"];
User *u = self.users[ip.row];
cell.nameLabel.text = u.name;
cell.ageLabel.text = [NSString stringWithFormat:@"%ld", (long)u.age];
return cell;
}
@end24.2 MVP(Model-View-Presenter)
┌──────────┐ ┌──────────┐
│ View │◀───────▶│Presenter │
└──────────┘ └────┬─────┘
│
┌────▼─────┐
│ Model │
└──────────┘- View 完全被动,只更新 UI。
- Presenter 处理所有业务逻辑。
- View 与 Presenter 一对一。
OC 示例:
// View(UIViewController 充当)
@interface UserView : UIViewController
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UIButton *refreshButton;
@property (nonatomic, strong) id<UserViewDelegate> delegate;
- (IBAction)onRefresh:(id)sender;
- (void)showName:(NSString *)name;
@end
// Presenter
@protocol UserViewDelegate <NSObject>
- (void)didTapRefresh;
@end
@interface UserPresenter : NSObject <UserViewDelegate>
@property (nonatomic, weak) id view;
@property (nonatomic, strong) UserService *service;
- (void)load;
@end
@implementation UserPresenter
- (void)load {
[self.service fetch:^(User *user) {
// 通过协议回调 View
if ([self.view respondsToSelector:@selector(showName:)]) {
[self.view showName:user.name];
}
}];
}
- (void)didTapRefresh { [self load]; }
@end24.3 MVVM(Model-View-ViewModel)
┌──────────┐◀─Data──┌──────────┐◀─Data─┌──────────┐
│ View │ │ViewModel │ │ Model │
└──────────┘─Event─▶└──────────┘─Cmd──▶└──────────┘- ViewModel 不持 View 引用,只暴露数据流。
- View 订阅 ViewModel 状态(KVO / ReactiveCocoa / RxObjC)。
OC 中用 RAC(ReactiveCocoa)实现 MVVM:
// ViewModel
@interface LoginViewModel : NSObject
@property (nonatomic, strong, readonly) RACSignal *username;
@property (nonatomic, strong, readonly) RACSignal *password;
@property (nonatomic, strong, readonly) RACSignal *isValid;
@property (nonatomic, strong, readonly) RACCommand *loginCommand;
@end
@implementation LoginViewModel
- (instancetype)init {
if (self = [super init]) {
_username = [RACSubject subject];
_password = [RACSubject subject];
_isValid = [RACSignal combineLatest:@[_username, _password]
reduce:^(NSString *u, NSString *p) {
return @(u.length > 0 && p.length > 0);
}];
_loginCommand = [[RACCommand alloc] initWithEnabled:_isValid
signalBlock:^RACSignal *(id _) {
return [self loginSignal];
}];
}
return self;
}
- (RACSignal *)loginSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[APIClient login:self.username.first password:self.password.first
completion:^(BOOL success) {
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
@end
// View
@implementation LoginViewController
- (void)viewDidLoad {
[super viewDidLoad];
RAC(self.loginButton, enabled) = self.viewModel.isValid;
[self.loginButton.rac_command subscribeNext:^(id _) {
NSLog(@"Login success");
}];
}
@endMVVM 的代价:
- 学习曲线(响应式编程)。
- 调试难度(异步链路多)。
- 简单场景过度设计。
24.4 VIPER(View-Interactor-Presenter-Entity-Router)
View ─── Presenter ─── Interactor ─── Entity
│
Router| 角色 | 职责 |
|---|---|
| View | UI 展示、用户事件 |
| Presenter | UI 逻辑、View ↔ Interactor 转换 |
| Interactor | 业务逻辑、网络/数据库访问 |
| Entity | 数据模型 |
| Router | 导航/路由 |
特点:
- 职责极致拆分,每个模块单一职责。
- 可测试性强(每个角色都可单测)。
- 模块化友好。
- 代码量大。
OC 示例(简化):
// Entity
@interface User : NSObject
@property (nonatomic, copy) NSString *id;
@property (nonatomic, copy) NSString *name;
@end
// Interactor
@protocol UserInteractorInput <NSObject>
- (void)fetchUserWithID:(NSString *)userID;
@end
@protocol UserInteractorOutput <NSObject>
- (void)didFetchUser:(User *)user;
@end
@interface UserInteractor : NSObject <UserInteractorInput>
@property (nonatomic, weak) id<UserInteractorOutput> output;
@property (nonatomic, strong) UserService *service;
@end
@implementation UserInteractor
- (void)fetchUserWithID:(NSString *)userID {
[self.service fetchUser:userID completion:^(User *u) {
[self.output didFetchUser:u];
}];
}
@end
// Presenter
@interface UserPresenter : NSObject <UserInteractorOutput>
@property (nonatomic, weak) id<UserViewInput> view;
@property (nonatomic, strong) id<UserInteractorInput> interactor;
@property (nonatomic, strong) id<UserRouterInput> router;
- (void)viewDidLoad;
@end
@implementation UserPresenter
- (void)viewDidLoad {
[self.interactor fetchUserWithID:@"123"];
}
- (void)didFetchUser:(User *)user {
[self.view showUserName:user.name];
}
@end
// View
@protocol UserViewInput <NSObject>
- (void)showUserName:(NSString *)name;
@end
@interface UserViewController : UIViewController <UserViewInput>
@property (nonatomic, strong) UserPresenter *presenter;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@end
@implementation UserViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.presenter viewDidLoad];
}
- (void)showUserName:(NSString *)name {
self.nameLabel.text = name;
}
@end
// Router
@protocol UserRouterInput <NSObject>
- (void)navigateToDetail:(User *)user;
@end
@interface UserRouter : NSObject <UserRouterInput>
@property (nonatomic, weak) UIViewController *viewController;
- (void)navigateToDetail:(User *)user {
// push detail VC
}
@end适用场景:大型项目、多人协作、长期维护。
24.5 架构选型决策树
项目规模?
│
├─ 小(个人项目 / Demo)
│ └─ MVC(最简单)
│
├─ 中(团队 5-10 人)
│ └─ MVC + Service 层(瘦 Controller)
│ 或 MVP / MVVM(如已有 RAC 经验)
│
├─ 大(团队 10+ 人 / 多模块)
│ └─ VIPER + 组件化
│
└─ 已有大量 RAC 经验
└─ MVVM + RAC24.6 高频面试题
Q1:MVVM 比 MVC 好在哪? A:①View 与 Model 完全解耦;②ViewModel 易测试(不依赖 UIKit);③数据绑定减少样板代码;④ViewModel 可跨平台复用。代价是引入响应式编程学习成本。
Q2:MVVM 中 ViewModel 应不应该引用 UIKit? A:不应该。这是 MVVM 与 MVP 的关键区别。ViewModel 只暴露纯数据,View 自己决定如何展示。
Q3:VIPER 的优势与劣势? A:优势——①单一职责到极致;②可测试性强;③模块化好;④易团队协作。劣势——①样板代码极多;②简单页面过重;③学习曲线陡;④路由管理复杂。
Q4:OC 项目选 MVVM 还是 MVC? A:①小项目 MVC;②中型项目 MVC + Service 层(瘦 Controller);③已有 RAC 经验或团队接受响应式 → MVVM + RAC;④巨型项目多人协作 → VIPER。
第 25 章 组件化设计
25.1 组件化动机
随着项目变大,单工程模式遇到瓶颈:
- 编译慢:一行改动触发整个工程重编。
- 耦合高:业务之间互相依赖,改一处影响多处。
- 复用难:功能不能独立复用、独立测试。
- 协作冲突:多人改同一文件频繁冲突。
组件化目标:把一个大 App 拆分为多个独立组件(framework / 静态库),每个组件独立开发、独立测试、独立编译,最终通过路由 / 接口组装成完整 App。
25.2 三大路由方案对比
方案 1:URL Router(如 MGJRouter、HHRouter)
// 注册
[MGJRouter registerURLPattern:@"mgj://user/detail" toHandler:^(NSDictionary *params) {
UserDetailViewController *vc = [UserDetailViewController new];
vc.userId = params[@"id"];
return vc;
}];
// 调用
UIViewController *vc = [MGJRouter objectForURL:@"mgj://user/detail"
withParameters:@{@"id": @"123"}];优点:①实现简单;②与 H5 / 外部 Scheme 自然兼容;③跨 App 路由。
缺点:①字符串硬编码,编译期不检查;②参数传递靠字典,类型不安全;③难以表达复杂依赖关系。
方案 2:Target-Action(CTMediator)
// 业务模块(Target)
@interface CT_User : NSObject
- (UIViewController *)detailView:(NSDictionary *)params {
UserDetailViewController *vc = [UserDetailViewController new];
vc.userId = params[@"id"];
return vc;
}
@end
// 中介者 category(暴露强类型接口)
@interface CTMediator (User)
- (UIViewController *)CT_userDetailViewWithID:(NSString *)userID;
@end
@implementation CTMediator (User)
- (UIViewController *)CT_userDetailViewWithID:(NSString *)userID {
return [self performTarget:@"User"
action:@"detailView"
params:@{@"id": userID}];
}
@end
// 调用
UIViewController *vc = [CTMediator sharedInstance CT_userDetailViewWithID:@"123"];优点:①无需运行时注册;②字符串仅用于命名;③category 隔离模块接口;④硬编码集中。
缺点:①仍是字符串运行时调用;②OC runtime 依赖。
方案 3:Protocol-Class(BeeHive、ServiceLoader)
// 定义协议
@protocol UserServiceProtocol <NSObject>
- (UIViewController *)userDetailWithID:(NSString *)userID;
@end
// 实现
@interface UserServiceImpl : NSObject <UserServiceProtocol>
@end
@implementation UserServiceImpl
- (UIViewController *)userDetailWithID:(NSString *)userID {
UserDetailViewController *vc = [UserDetailViewController new];
vc.userId = userID;
return vc;
}
@end
// 注册
[ServiceLocator registerService:@protocol(UserServiceProtocol)
implementation:[UserServiceImpl class]];
// 调用
id<UserServiceProtocol> svc = [ServiceLocator service:@protocol(UserServiceProtocol)];
UIViewController *vc = [svc userDetailWithID:@"123"];优点:①强类型,编译期检查;②接口与实现分离;③易测试(注册 Mock)。
缺点:①需预先注册;②接口协议可能产生跨模块依赖。
选型建议
| 场景 | 推荐 |
|---|---|
| 小项目,简单跳转 | URL Router |
| 已有 OC 大型项目 | Target-Action |
| 新项目 / 强类型需求 | Protocol-Class |
| 跨 App / H5 跳转 | URL Router(必须) |
混合方案:实践中常组合——内部用 Protocol-Class 调用业务接口,对外(H5、Push)用 URL Router。
25.3 依赖注入
// 简化版 DI 容器
@interface Container : NSObject
+ (instancetype)sharedInstance;
- (void)registerProtocol:(Protocol *)protocol implementation:(Class)impl;
- (id)resolve:(Protocol *)protocol;
@end
@implementation Container {
NSMutableDictionary<NSString *, Class> *_map;
}
+ (instancetype)sharedInstance {
static Container *c;
static dispatch_once_t once;
dispatch_once(&once, ^{ c = [Container new]; });
return c;
}
- (instancetype)init {
if (self = [super init]) _map = [NSMutableDictionary dictionary];
return self;
}
- (void)registerProtocol:(Protocol *)protocol implementation:(Class)impl {
_map[NSStringFromProtocol(protocol)] = impl;
}
- (id)resolve:(Protocol *)protocol {
Class cls = _map[NSStringFromProtocol(protocol)];
return cls ? [cls new] : nil;
}
@end
// 注册
[[Container sharedInstance] registerProtocol:@protocol(UserServiceProtocol)
implementation:[UserServiceImpl class]];
// 使用
id<UserServiceProtocol> svc = [[Container sharedInstance] resolve:@protocol(UserServiceProtocol)];25.4 工程化
包管理器
| 工具 | 特点 |
|---|---|
| Cocoapods | 最流行,集中式 Podfile,方便,编译略慢 |
| Carthage | 去中心化,只编译不集成,需手动接入 |
| Swift Package Manager(SPM) | Apple 官方,对 OC 支持有限 |
| Bazel | 大型构建系统,多语言、分布式缓存、增量编译 |
OC 项目主流仍是 Cocoapods。大型团队可能选 Bazel。
组件分层
┌─────────────────────────────────┐
│ App(壳工程,组装 + 路由初始化) │
├─────────────────────────────────┤
│ Business Modules(业务层) │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ Home │ │ User │ │ Order│ ... │
│ └──┬───┘ └──┬───┘ └──┬───┘ │
│ └────────┼────────┘ │
│ │ │
├──────────────▼───────────────────┤
│ Core / Service(核心服务层) │
│ Network / DB / Cache / Logger │
├─────────────────────────────────┤
│ Foundation(基础层) │
│ Extensions / Utils / Models │
├─────────────────────────────────┤
│ Third Party(第三方依赖) │
└─────────────────────────────────┘规则:
- 上层依赖下层,下层不能反向依赖。
- 同层之间通过协议解耦。
- 业务模块之间通过路由通信。
模块通信
| 通信方式 | 场景 |
|---|---|
| 协议 + 注册 | 业务模块互相调用接口(推荐) |
| URL Router | H5 / Push / 跨 App 跳转 |
| 通知 / 总线 | 一对多广播,松耦合 |
| 共享 Service | 全局状态(登录态、用户信息) |
| EventBus | 跨模块事件传递 |
25.5 高频面试题
Q1:组件化的最大挑战是什么? A:①历史代码耦合的拆分成本;②接口设计(粒度、稳定性);③路由方案选型;④CI/CD 流水线(每个组件独立打包);⑤版本管理(podspec 版本兼容);⑥团队协作(owner 制度)。
Q2:URL Router 与 Target-Action 的核心区别? A:URL Router 用字符串 URL,强调「寻址」;Target-Action 用字符串 + 类名 + 方法名,强调「方法调用」。Target-Action 由 CTMediator 主导,结合 category 暴露强类型接口(category 文件由业务方提供,调用方依赖 category 后获得类型安全)。
Q3:组件化一定要用 framework 吗? A:不一定。小型项目可用「文件夹隔离 + 协议」做轻量组件化。中大型项目才需要 framework + 独立仓库 + 版本管理。过早组件化反而拖慢迭代。
第 26 章 大型 OC 项目架构演进
26.1 分层架构
经典三层:
┌─────────────────────┐
│ Presentation │ ← UI、Controller / Presenter / ViewModel
├─────────────────────┤
│ Domain │ ← 业务规则、实体、用例
├─────────────────────┤
│ Data │ ← 数据访问(网络、DB、缓存)
└─────────────────────┘26.2 Clean Architecture(Uncle Bob)
┌────────────────────────────────────┐
│ Frameworks & Drivers (UI/DB/Web) │ ← 外层
├────────────────────────────────────┤
│ Interface Adapters │
├────────────────────────────────────┤
│ Use Cases │
├────────────────────────────────────┤
│ Entities │ ← 内层
└────────────────────────────────────┘
依赖方向:外 → 内(内层不知道外层)26.3 领域驱动设计(DDD)
DDD 强调用「业务领域」划分模块,而非按技术分层。
| 概念 | 说明 |
|---|---|
| Entity | 有唯一标识的领域对象(User、Order) |
| Value Object | 无标识、不可变(Money、Address) |
| Aggregate | 一组关联实体的集合,统一通过 Aggregate Root 访问 |
| Repository | 数据访问接口(接口在领域层,实现在基础设施层) |
| Domain Service | 不属于任何实体的业务逻辑 |
| Application Service | 用例编排,事务边界 |
| Domain Event | 领域中发生的事件 |
OC 中的 DDD 实践:
// Domain(纯业务,不依赖 UIKit)
@interface Money : NSObject // Value Object
@property (nonatomic, readonly) NSDecimalNumber *amount;
@property (nonatomic, readonly) NSString *currency;
@end
@protocol OrderRepository <NSObject>
- (void)saveOrder:(Order *)order;
- (Order *)orderWithID:(NSString *)ID;
@end
@interface Order : NSObject // Entity + Aggregate Root
@property (nonatomic, copy) NSString *ID;
@property (nonatomic, strong) NSMutableArray<OrderItem *> *items;
- (void)applyDiscount:(Discount *)discount;
- (NSDecimalNumber *)total;
@end
// Application Service
@interface PlaceOrderUseCase : NSObject
@property (nonatomic, strong) id<OrderRepository> repo;
- (void)execute:(Order *)order;
@end
// Infrastructure
@interface SQLOrderRepository : NSObject <OrderRepository>
- (void)saveOrder:(Order *)order { /* SQLite 写入 */ }
@end
// Presentation
@interface OrderViewModel : NSObject
@property (nonatomic, strong) PlaceOrderUseCase *useCase;
@end26.4 架构演进路径
阶段 1:单工程 MVC
- 几人小项目。
- 所有代码堆在一起。
- 编译快,迭代快。
阶段 2:MVC + 基础分层
- 引入 Service / Repository 层。
- Controller 不再写网络、数据库。
- 几十人团队,多个业务线。
阶段 3:MVVM / MVP + 组件化
- 引入 MVVM,Controller 瘦身。
- 拆分为 Pod / 静态库组件。
- 业务组件间通过路由通信。
- 启用 CI(每个组件独立构建)。
阶段 4:模块化 + BFF + 微服务
- 业务模块化为独立 App 或独立团队。
- 后端 BFF 层聚合数据。
- 多端统一架构(Flutter / RN / Hybrid)。
阶段 5:插件化 + 动态化
- 业务插件化(按需下发)。
- 部分模块用 RN / JSPatch 动态化(绕过审核)。
- LSP / JIT 等高级技术。
26.5 案例研究
电商类 App(淘宝、京东、拼多多)
特点:
- 业务线极多(首页、搜索、详情、购物车、订单、支付、物流、会员、IM、直播)。
- 团队几百到上千人。
- 双 11 流量峰值。
架构:
- 极致组件化:每个业务线独立组件,独立 owner。
- Atlas / BeeHive 类路由。
- BFF + 服务化:后端 BFF 聚合接口,前端按需取数据。
- 动态化:Weex / Tangram / 增量资源下发。
- AOP:埋点、监控、性能统一拦截。
- JSPatch(历史):热修复。
社交类 App(微信、QQ)
特点:
- 实时性强(IM、推送)。
- 数据复杂(联系人、消息、动态、朋友圈)。
- 多端统一(手机、平板、Web、桌面)。
架构:
- IM 长连接:自研协议(如微信 Mars,C++ 实现)。
- 本地优先:所有数据先写本地,再同步服务器。
- CRDT:消息合并冲突解决。
- 模块化:分 Native + Web + Mini Program。
内容类 App(抖音、B 站)
特点:
- 视频为主,性能要求高。
- 推荐算法决定用户体验。
- 海量 UGC / PGC。
架构:
- 视频管线:FFmpeg / AVPlayer 自研播放器。
- ABTest:UI / 算法动态切换。
- 动态化:Lynx(抖音)/ Tangram(内容卡片化)。
- 缓存策略:预加载、分段缓存、CDN 调度。
26.6 高频面试题
Q1:你所在项目的架构是什么样的?为什么这么选? A:(开放题)要能说清楚:①分层方式(MVC / MVVM / VIPER);②组件化方案(Pod / SPM / 自研);③路由方案;④状态管理;⑤选型理由(业务规模、团队规模、历史包袱)。
Q2:组件化拆分粒度怎么定? A:原则:①单一职责:一个组件一个业务模块;②可独立测试;③接口稳定;④适度原则(太细增复杂度,太粗失去组件化收益);⑤通常按业务领域(用户、订单、支付)+ 技术基础(网络、存储、UI 组件库)划分。
Q3:组件 A 依赖组件 B,B 又依赖 A,怎么解? A:①接口下沉,把公共协议抽到 Foundation;②依赖倒置(DIP),让两者都依赖抽象;③事件总线(observer + 通知);④引入新模块作为中间层。
Q4:架构演进的过程中怎么保证业务不中断? A:①绞杀者模式:新功能用新架构,旧功能逐步迁移;②双写双读:状态层做兼容;③金丝雀发布;④灰度迁移,监控核心指标;⑤随时可回滚。
第 27 章 高频面试题速查
把全书知识点浓缩到 50 道高频面试题,便于最后一轮扫盲。
语言基础(10 题)
Q1:OC 与 C 的关系? A:OC 是 C 的超集,编译产物就是原生机器码。OC 在 C 之上加了「面向对象语法 + SmallTalk 风格的消息传递 + Runtime 元数据」。
Q2:OC 与 Java 的运行时区别? A:OC 没有 VM(虚拟机),编译产物直接跑在 CPU 上。Java 字节码需要 JVM 解释执行。OC 的「动态性」依赖运行时维护的元数据,而不是 VM。
Q3:selector 是什么? A:方法名的唯一标识符,本质是经过哈希的 C 字符串。同名 selector 全局只存一份。@selector(name) 在编译期确定。
Q4:给 nil 发消息会怎样? A:合法,直接返回零值(0 / nil / 0.0)。返回大 struct(arm64 上 > 16 字节)可能 crash。
Q5:instancetype 和 id 区别? A:instancetype 是「当前类的类型」,编译器能做类型推断;id 是「任意 OC 对象」,无类型检查。初始化方法用 instancetype。
Q6:super 调用本质? A:编译为 objc_msgSendSuper,运行时从父类的 method list 开始查找。但 self 仍是当前对象。
Q7:OC 对象的本质? A:指向 struct objc_object 的指针,首字段是 isa。isa 指向类对象,类对象存方法列表、属性、协议、父类指针。
Q8:isa 还是纯指针吗(arm64)? A:不是。是 non-pointer(位域):1 位 nonpointer 标志 + 33 位类指针 + 引用计数 + 标志位。访问真实类指针用 isa & ISA_MASK。
Q9:Category 为什么不能加 ivar? A:类的内存布局在编译期确定。Category 在运行时合并到类对象,无法改变已注册的布局。可用关联对象模拟。
Q10:NSString 为什么用 copy? A:可能传入 NSMutableString,外部修改会让属性偷偷变化,破坏封装。copy 在 setter 中拷贝一份不可变副本。
Block 与 Runtime(10 题)
Q11:Block 本质? A:Block 是「带捕获环境的函数指针」。底层是 Block_layout 结构体,含 isa、flags、invoke(函数指针)、descriptor、捕获的变量。
Q12:Block 三种类型? A:①__NSGlobalBlock__(不捕获外部变量);②__NSStackBlock__(MRC 下捕获自动变量);③__NSMallocBlock__(栈 block 被 copy / ARC 下默认捕获)。
Q13:__block 修饰符原理? A:把被修饰变量包装成 __Block_byref_x 结构体,按引用捕获。__forwarding 在 block copy 到堆时改指堆上副本,保证修改作用于同一份数据。
Q14:Block 属性为什么用 copy? A:MRC 时代栈 block 出作用域就销毁,赋值给属性需要 copy 到堆。ARC 下 strong 也会自动 copy,但写 copy 是惯例。
Q15:objc_msgSend 完整流程? A:①nil 检查;②从 isa 取类;③查 cache(命中即跳 IMP);④遍历 method list;⑤沿 superclass 链查;⑥动态方法解析;⑦快速转发;⑧标准转发;⑨crash。
Q16:objc_msgSend 为什么用汇编? A:①绕过 C 函数 prologue;②处理不定参数;③热路径性能要求。
Q17:消息转发三步? A:①+resolveInstanceMethod:(动态方法解析);②-forwardingTargetForSelector:(快速转发);③-methodSignatureForSelector: + -forwardInvocation:(标准转发)。
Q18:Method Swizzling 为什么要在 +load 中? A:①+load 在 main 之前由 Runtime 调用,时机早;②每个类只调一次;③+initialize 可能多次调用(子类首次使用)。
Q19:为什么先 class_addMethod 再 method_exchangeImplementations? A:如果类本身没实现该方法(继承自父类),直接 exchange 会改父类的方法表,影响所有子类。先 add 检查,能 add 说明原来没有。
Q20:KVO 原理? A:Runtime 动态创建子类 NSKVONotifying_X,重写 setter 为 _NSSetXxxValueAndNotify,调用前后触发 will/did change。直接修改 ivar 不触发 KVO。
内存与并发(10 题)
Q21:ARC 在编译期做了什么? A:自动插入 retain / release / autorelease。ARC ≠ GC,无运行时暂停。
Q22:weak 底层实现? A:全局 weak_table_t,key 是对象地址,value 是该对象的所有 weak 引用数组。对象 dealloc 时遍历置 nil。
Q23:Tagged Pointer 是什么? A:64 位系统把小整数 NSNumber 与短 NSString(≤ 7 字节)的值直接编码到指针中,不分配堆。无 malloc / retain 开销。
Q24:autoreleasepool 何时必须用? A:①循环中产生大量 autorelease 临时对象;②子线程(默认没 pool);③长生命周期线程;④库入口函数。
Q25:GCD 死锁条件? A:sync 提交到当前正在执行的队列。
Q26:dispatch_barrier_async 的使用场景? A:并发队列中的「读写隔离」。读用 dispatch_async,写用 dispatch_barrier_async(等所有正在执行的读完成,独占执行写)。
Q27:dispatch_once 为什么线程安全? A:底层用原子操作 + 双重检查锁。第一次调用进入慢路径(加锁、执行 block、置标记);后续走快路径。
Q28:为什么 OSSpinLock 不再安全? A:优先级反转。低优先级持锁自旋,高优先级等锁,中优先级抢占低优先级 → 高优先级被无限阻塞。改用 os_unfair_lock。
Q29:RunLoop 与线程关系? A:一对一,懒创建。主线程自动启动,子线程需手动。
Q30:NSTimer 滑动时为什么停止? A:滑动时主 RunLoop 切到 UITrackingRunLoopMode,Default Mode 下的 Timer 不被调用。解决:加入 NSRunLoopCommonModes。
Foundation 与 UI(8 题)
Q31:NSArray 底层结构? A:__NSArrayI(不可变)连续 C 数组;__NSArrayM(可变)环形缓冲区,头尾插入 O(1)。
Q32:NSString length 是字符数吗? A:不是,是 UTF-16 code unit 数。emoji 占 2 个 unit(代理对)。
Q33:NSCache 与 NSDictionary 区别? A:①线程安全;②接收内存警告自动 evict;③不 retain key;④不计 retain。
Q34:通知中心默认在哪个线程回调? A:发送线程同步回调。要主线程回调,用 NotificationQueue 或 dispatch_async(main) 包一层。
Q35:UI 为什么必须在主线程? A:UIKit 非线程安全;事件分发与响应链依赖主线程串行;与 VSync 协调。
Q36:UIView 与 CALayer 关系? A:1:1。UIView 是 CALayer 的事件响应包装。Layer 是渲染核心。
Q37:离屏渲染触发条件? A:cornerRadius + masksToBounds、mask、shadow(无 shadowPath)、shouldRasterize、模糊。
Q38:Hit Test 流程? A:从 window 递归调用 hitTest:withEvent:,pointInside:withEvent: 判断点是否在视图内,从后往前遍历 subviews。
进阶与优化(7 题)
Q39:启动优化关键点? A:减少动态库、少 +load、减少 OC 类、延后 SDK 初始化、首屏数据预取、order_file 优化 page-in。
Q40:怎么检测主线程卡顿? A:①CADisplayLink 测 FPS;②RunLoop Observer 测 BeforeWaiting → AfterWaiting;③子线程定时抓主线程栈。
Q41:iOS App 加密机制? A:FairPlay DRM + Code Signature。Apple 用私钥对 App 加密,设备用预置公钥验证。
Q42:SSL Pinning 怎么做? A:在 URLSession:didReceive:completionHandler: 中校验服务端证书/公钥,匹配则信任,不匹配则取消。
Q43:OC 调 Swift 的限制? A:Swift 类必须继承 NSObject 或 @objc;struct / enum / tuple / 泛型默认不能从 OC 调用。
Q44:@objc 与 @objc dynamic 区别? A:@objc 暴露给 OC(默认仍静态派发);@objc dynamic 强制走 OC runtime,可被 KVO / Swizzling。
Q45:怎么防止 App 被逆向? A:代码混淆、反调试、反注入、完整性校验、关键算法服务端化、越狱检测。
架构设计(5 题)
Q46:MVVM 比 MVC 好在哪? A:①View 与 Model 解耦;②ViewModel 易测试;③数据绑定减少样板代码;④ViewModel 可跨平台复用。代价是响应式编程学习成本。
Q47:组件化三大路由方案? A:URL Router(字符串寻址)、Target-Action(中介者调方法)、Protocol-Class(接口与实现分离)。
Q48:依赖倒置原则? A:高层不依赖低层具体,依赖抽象。控制反转(IoC)+ 依赖注入(DI)是其实现。
Q49:怎么选架构? A:小项目 MVC;中等 MVC + Service 层;大型 VIPER + 组件化。考虑团队规模、业务复杂度、长期成本。
Q50:架构演进过程中怎么保证业务不中断? A:①绞杀者模式:新功能用新架构;②双写双读:状态层兼容;③金丝雀发布;④灰度迁移,监控核心指标;⑤随时可回滚。
结语
Objective-C 是一门「看似古老实则深刻」的语言。它把动态性做到极致,让 Runtime 成为运行时的灵魂。理解 OC 不仅是写好 OC 代码的前提,也是深入理解 UIKit、Foundation、KVO、AOP 等系统机制的基础。
真正的成长路径:
- 会用——能写出跑得起来的代码。
- 懂原理——能解释为什么这么写、底层怎么实现。
- 能优化——能定位性能瓶颈、设计可维护的架构。
- 能创造——能从零设计一个模块、一个架构、一个框架。
每个阶段都需要刻意练习。建议:
- 每周读一份 Apple 官方文档(《Programming with Objective-C》《Runtime Programming Guide》)。
- 每月读一份 objc4 / CFNetwork / Foundation 源码。
- 每个季度写一个完整的 Demo 项目,把所学知识用进去。
- 参与开源项目(ReactiveCocoa、Aspects、CTMediator、BeeHive),看一线团队的代码风格。
OC 不会过时。Swift 是未来,但 OC 是底座。掌握 OC 让你在面对任何 iOS 框架 / 系统机制时都能游刃有余。
📚 推荐资源
- Apple 官方文档:Programming with Objective-C
- objc4 源码:opensource.apple.com
- objc.io 中国:高质量 iOS / OC 深度文章
- NSHipster:OC / Swift 边角料
- GitHub 开源 OC 项目:AFNetworking、SDWebImage、ReactiveCocoa、Aspects、CTMediator、BeeHive
- 一线大厂技术博客(美团、字节、阿里)
最后更新:2026 年 · 持续修订中。如有错误或建议,欢迎指正。