Skip to content

Objective-C 复习知识点

站在资深 iOS 工程师视角,按「知识点 → 底层原理 → 实战例子 → 易错坑 → 高频面试题」的脉络,把 Objective-C 从语法基础、Runtime、内存管理、Foundation、性能优化到架构设计串成一条主线。即便 SwiftUI 时代,UIKit、Foundation、Runtime 仍然以 OC 思路实现,理解 OC 仍是深入 iOS 底层的必经之路。代码示例可直接粘到 Xcode 工程运行。


文档导航


前言

Objective-C(下文简称 OC)诞生于 1980 年代,是 Brad Cox 在 1984 年创造的,1996 年被 NeXTSTEP(乔布斯离开 Apple 后创办的公司)采纳为主力语言,最终随着 Mac OS X 与 iOS 进入 Apple 生态。

今天为什么还要学 OC?

  1. 底层全是 OC:UIKit、Foundation、CFNetwork、Core Data 全部用 OC 实现。理解 OC 才能真正理解这些框架的运行机制。
  2. Runtime 是动态性的根基:KVO、KVC、JSPatch、Aspects、BlockHook、AOP、热更新全依赖 OC Runtime。
  3. 海量存量代码:腾讯、阿里、字节、美团等一线大厂的 App 主体仍是 OC(虽然新功能可能用 Swift)。
  4. 架构经验沉淀:组件化(CTMediator、BeeHive)、AOP 框架、监控方案几乎都是基于 OC 思想设计。
  5. 面试必考: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.comobjc4,已开源。
  • 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 的关系

objective-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_addMethodobjc_setAssociatedObject);⑤消息转发(动态处理未实现的方法)。


第 2 章 语法基础与消息传递

2.1 核心要点速查

头文件与实现

objective-c
// 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

方法调用语法

objective-c
// 无参方法
[obj hello];

// 带参方法(每个参数前都有「标签」)
[obj doSomethingWithArg1:arg1 arg2:arg2];

// 类方法
[Person personWithName:@"Alice"];

// 嵌套调用
[[NSString alloc] init];

2.2 方法签名(selector)

OC 方法在语言层面用 selector 唯一标识:

objective-c
SEL sel = @selector(introduce);
SEL clsSel = @selector(personWithName:);
  • SEL 是一个 opaque 类型,本质是经过哈希的 C 字符串。
  • 同名 selector 在全局只存一份(哈希表)。
  • 方法名包含所有标签(doSomethingWithArg1:arg2:),缺一不可。

2.3 消息传递的编译转换

objective-c
// 你写的代码
[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 完全相同intfloatBOOL(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+ 编译器特性)

objective-c
// 数字
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)

objective-c
NSNumber *pi = @(3.14);
NSNumber *timeout = @(someIntVariable);

2.6 instancetype 与 id 的区别

objective-c
// ❌ 用 id 返回,编译器不知道返回什么类型
+ (id)personWithName:(NSString *)name;

// ✅ 用 instancetype,编译器知道返回当前类的类型
+ (instancetype)personWithName:(NSString *)name;

// 子类继承时自动适配
[Person personWithName:@"x"];   // 返回 Person *
[Student personWithName:@"x"];  // 返回 Student *
  • instancetype 只能用于方法返回类型,不能用作变量类型。
  • allocinitnew 等约定方法必须返回 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

c
// 简化版结构(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 类对象的本质

c
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 链

objective-c
#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 不是纯指针,而是位域:

c
// 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 一并存储。

取真实类指针

objective-c
Class cls = object_getClass(obj);  // 等价 (Class)(isa & ISA_MASK)

3.6 对象内存布局

objective-c
@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 的指针,首字段是 isaisa 指向类对象,类对象存方法列表、属性、协议、父类指针等元数据。

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 本质

objective-c
@property (nonatomic, copy) NSString *name;

等价于编译器自动生成:

objective-c
// 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持有,引用计数 +1OC 对象,所有权关系
weak不持有,对象释放后自动置 nildelegate、避免循环引用
assign / unsafe_unretained不持有,不置 nil基本类型(int/float)、非 OC 对象
copysetter 中 copy 一份新对象NSString、block、不可变集合
atomicgetter/setter 加锁(默认)线程安全的最弱保证
nonatomic不加锁性能优先(绝大多数场景)
readonly只生成 getter不变量
readwrite生成 getter + setter(默认)可变量
getter=name / setter=name自定义 getter/setter 名BOOL isHiddensetHidden:
class类属性(OC 不支持存储,只声明)Swift 互操作时用
nullabilitynullable / nonnull / null_unspecified / null_resettableSwift 桥接类型安全

4.3 strong / weak / copy 选择决策树

是基本类型(int / float / BOOL)?
├─ 是 → assign
└─ 否 ↓
是 block?
├─ 是 → copy(惯例,ARC 下 strong 也会 copy)
└─ 否 ↓
是不可变类型(NSString / NSArray / NSDictionary),且可能有可变子类?
├─ 是 → copy(防止外部传入可变版本偷偷改)
└─ 否 ↓
是 delegate / 通知者?
├─ 是 → weak(防循环引用)
└─ 否 ↓
普通对象 → strong

4.4 copy 深入

objective-c
// ❌ 用 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

objective-c
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 真的安全吗

objective-c
@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
  • atomicnonatomic 慢 20 倍左右(要加锁)。
  • 几乎所有实践都使用 nonatomic + 显式加锁

4.6 自定义 getter / setter

objective-c
@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 下自动管理):

objective-c
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];           // MRC 才需要,ARC 会自动
        _name = [name copy];
    }
}

4.7 nullability 注解

objective-c
@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

批量注解

objective-c
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 + 显式加锁(@synchronizeddispatch_semaphorepthread_mutexos_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(分类)

objective-c
// 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"

作用

  1. 给现有类添加方法(即使没有源码,包括系统类)。
  2. 拆分大型类的实现(按功能分文件)。
  3. 模拟「私有方法」对外隐藏。
  4. 把 informal protocol 形式化。

限制

  • 不能添加实例变量(破坏内存布局)。
  • 可以添加属性,但只生成 getter/setter 声明,不合成 ivar——必须用关联对象手动实现(见 第 11 章)。
  • 同一类多个 Category 实现同名方法 → 编译顺序决定哪个生效(最后一个编译的覆盖前面,包括类本体)。

5.2 Category 加载时机

c
// 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 次父类 → 子类 → 各自的 CategoryCategory 的 +load 与主类的 +load 都会调用
+initialize每个类最多 1 次(懒加载)父类 → 子类Category 的 +initialize 覆盖 主类的(同普通方法)
objective-c
@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(扩展 / 匿名分类)

objective-c
// Person.m
#import "Person.h"

// Extension 必须在主类的 .m 中
@interface Person ()
{
    NSString *_privateIvar;     // 可以加 ivar
}
@property (nonatomic, copy) NSString *internalProperty;  // 私有属性
- (void)privateMethod;          // 私有方法声明
@end

@implementation Person
- (void)privateMethod { /* ... */ }
@end

Extension 与 Category 的区别

维度CategoryExtension
命名必须有名字 (Name)匿名 ()
文件位置独立 .h/.m主类 .m
加 ivar
加属性只声明 getter/setter,无 ivar完整合成 ivar + getter/setter
编译时合并运行时合并编译时合并
用途给现有类添加方法声明私有接口

5.5 Category 实战:模块拆分

大型类(如 UIViewController 子类几千行)按功能拆分到多个 Category:

objective-c
// 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 同名方法冲突

objective-c
// 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 后编译!

规避方案

  1. 方法名加前缀(如 xxx_reversed)。
  2. +load 中显式 class_addMethod 检查存在性。
  3. 系统类的 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 协议定义与实现

objective-c
// 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)
@end

6.2 协议特性

  • @required(默认):必须实现,否则编译警告。
  • @optional:可选实现。
  • 协议可继承:@protocol A <B>(A 继承 B 的所有方法声明)。
  • 协议可被多类实现(跨继承树的多态)。
  • OC 协议没有关联类型(与 Swift 不同),能力弱一些。

6.3 协议的使用方式

objective-c
// 类型约束
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 模式

objective-c
// 声明
@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];
    }
}
@end

delegate 用 weak:避免循环引用(MyView 持有 delegate,delegate 通常持有 MyView)。

6.5 informal protocol(非正式协议)

旧 OC 时代(协议不支持 @optional)的做法:分类到 NSObject 上,让所有对象「都可以」实现这些方法。

objective-c
@interface NSObject (MyInformalProtocol)
- (void)optionalMethod;
@end
// 实现者按需实现,调用前 respondsToSelector: 检查

现代 OC 已被正式 protocol 取代,但 UIKit / Foundation 中仍能见到。

6.6 协议对象(Protocol 对象)

objective-c
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 结构体:

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 的本质:

objective-c
// 你写的
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
objective-c
// 全局 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 变量捕获

objective-c
// 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);  // 20

7.4 __block 修饰符

objective-c
__block int x = 10;
void (^b)(void) = ^{
    x = 20;
    NSLog(@"%d", x);
};
b();

编译器生成:

c
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 循环引用

objective-c
// ❌ 循环引用
@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

objective-c
// 作为参数
[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

objective-c
@property (nonatomic, copy) void (^onTap)(void);

历史原因:MRC 时代,block 字面量创建在栈上,赋值给属性需要 copy 到堆。ARC 下 strong 也会自动 copy,但约定俗成写 copy 提醒开发者——block 跨作用域需要拷贝到堆。

7.8 Block 与函数指针

objective-c
// 函数指针
void (*funcPtr)(void) = someFunction;
funcPtr();

// Block
void (^block)(void) = ^{ /* ... */ };
block();

区别:

维度函数指针Block
捕获环境有(自动变量、__blockself
内存代码段栈 / 堆 / 数据段
类型静态动态(每个 block 字面量是独立类型)
ABIC ABIOC 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 访问属性,绕过编译期类型检查。

objective-c
@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 易错坑

objective-c
// ❌ 传 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 动态创建子类。

objective-c
// 注册观察者
[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 子类

objective-c
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

objective-c
@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"];
}

@end

8.8 直接修改 ivar 不触发 KVO

objective-c
- (void)test {
    _name = @"no-kvo";   // ❌ 不触发 KVO(绕过 setter)
}

KVO 是通过拦截 setter 实现的,直接改 ivar 不会触发 willChange / didChange。

8.9 KVO 易错坑

  1. 注册与移除必须配对:重复 remove 会 crash。
  2. dealloc 中先移除观察者:否则对象释放后 KVO 回调到野指针。
  3. 多观察者 + 多 keyPath:用 context 区分(比 keyPath 字符串更可靠)。
  4. 多线程:KVO 回调在 setter 调用线程,跨线程要小心。
  5. 可变数组 / 集合:直接调用 addObject: 不触发 KVO,要用 mutableArrayValueForKey: 获取代理。
objective-c
// ❌ 不触发 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 工作流程

objective-c
[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_msgSendobjc-msg-arm64.s 中用汇编实现,原因:

  1. 绕过 C 函数 prologue(栈帧建立),直接控制寄存器,减少开销。
  2. 参数数量不固定(不同方法参数个数不同),C 函数难处理。
  3. 热路径性能要求——每条 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)

c
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:)

objective-c
@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:)

objective-c
@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:)

objective-c
@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];
}

@end

NSInvocation

  • 封装了一次完整的方法调用:target、selector、所有参数、返回值。
  • 可以拿到 / 修改任意参数。
  • 可以多次 invoke。
objective-c
- (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 平级的「纯代理」抽象根类:

objective-c
@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

objective-c
#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(实现指针)。

objective-c
#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 标准写法(防止父类方法被覆盖)

objective-c
+ (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_addMethodmethod_exchangeImplementations防止父类方法被改
不要 swizzle 系统私有方法审核风险
swizzle 后注意线程安全全局影响,多线程 swizzle 可能丢失
Swizzling 在 Swift 中受限Swift 方法默认静态派发,需 @objc dynamic

10.4 实战案例 1:自动埋点

objective-c
@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])];
}

@end

10.5 实战案例 2:防按钮多次点击

objective-c
@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];
}

@end

10.6 实战案例 3:字典转模型

objective-c
@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;
}

@end

10.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,但可以用关联对象模拟。

objective-c
#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 的选择

objective-c
// 方案 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 动态创建类

objective-c
// 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

objective-c
@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;
}

@end

11.7 Runtime 应用汇总

应用机制
AOP / 埋点 / 监控Method Swizzling
字典 → Modelclass_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
c
// 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:

objective-c
// 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

objective-c
// 你写的
- (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 参数
objective-c
// __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

c
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

objective-c
- (NSString *)description {
    return [NSString stringWithFormat:@"..."];   // 返回 autoreleased 对象
}

stringWithFormat: 返回的对象引用计数为 1,被注册到当前线程的 autorelease pool。pool drain 时统一 release。

底层

c
// 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 实战

objective-c
// ❌ 内存可能暴涨
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

objective-c
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。

优点

  1. 不分配堆,无 malloc / free 开销。
  2. 无引用计数,无 retain / release。
  3. 更好的缓存局部性。

  • [n class] 返回 __NSCFNumber(被伪装)。Runtime 通过 isa mask 判断。
  • 不能用 == 比较两个 NSNumber(虽然 @(1) == @(1) 在某些情况成立,因为 tag 编码后相同),要用 isEqual:
  • Tagged Pointer 不参与 weak 表,对它 weak 没意义。

12.9 僵尸对象(Zombie)

objective-c
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 循环引用诊断

典型场景

objective-c
// 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 结构

c
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接收系统事件(私有)
NSRunLoopCommonModeskCFRunLoopCommonModes占位 mode,包含 Default + Tracking

经典坑

objective-c
// ❌ 滑动时定时器停止
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

c
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),否则回到 1

13.8 应用场景 1:常驻子线程

objective-c
@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(); }

@end

13.9 应用场景 2:滑动不停止定时器

如上 Mode 一节所示,加入 NSRunLoopCommonModes

13.10 应用场景 3:卡顿监测

objective-c
@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

objective-c
// 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 异步

objective-c
// 异步(立即返回,任务在目标队列上调度)
dispatch_async(queue, ^{
    NSLog(@"async");
});

// 同步(阻塞当前线程等任务完成)
dispatch_sync(queue, ^{
    NSLog(@"sync");
});
同步 sync异步 async
串行当前线程执行,串行新线程执行,串行
并发当前线程执行,串行(不会并发)线程池调度,并发

死锁陷阱

objective-c
// ❌ 死锁
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

objective-c
// 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 封装:

objective-c
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

objective-c
@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"];
}

@end

14.5 选型对比

场景推荐
简单 fire-and-forgetGCD 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条件锁条件触发解锁
@synchronizedOC 语法糖简单,慢
串行队列隐式锁用队列替代锁
pthread_rwlock_t读写锁读多写少
dispatch_barrier写独占并发队列 + barrier

15.2 os_unfair_lock(推荐)

objective-c
#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

objective-c
#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

objective-c
// 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

objective-c
@synchronized(obj) {
    // 临界区
}

底层:递归互斥锁,封装在 objc-sync 中。

c
int objc_sync_enter(id obj);
int objc_sync_exit(id obj);
  • 内部维护一张哈希表:obj → SyncList(含锁)。
  • 优点:语法简单,可重入。
  • 缺点:最慢的锁(约比 os_unfair_lock 慢 10 倍),因为要查表。

15.6 串行队列作为锁

objective-c
@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)

objective-c
#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);
}

@end

15.8 barrier 实现读写锁

objective-c
- (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_lock1100 万1000 万
dispatch_semaphore950 万900 万
pthread_mutex800 万750 万
NSLock600 万550 万
NSRecursiveLock500 万450 万
@synchronized200 万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 类的根类,承担三大职能:

  1. Runtime 入口+alloc-init-class-isKindOfClass:-respondsToSelector: 等。
  2. 协议方法<NSObject> 协议定义了相等性、描述、自省。
  3. 默认实现-description-hash-isEqual:

注意NSObject 类 ≠ NSObject 协议。后者是协议,任何类可遵循(不一定继承 NSObject)。

16.2 NSObject 核心方法

objective-c
// 实例创建
+ (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 相等性契约

objective-c
@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(类簇)

objective-c
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 方法(countobjectAtIndex:)并实现自己的存储。

16.5 集合类底层

公开类型底层结构特点
NSArray / __NSArrayIC 数组(连续内存)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
objective-c
// 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 编码做了优化的不可变类。

objective-c
NSString *s = @"Hello";
NSLog(@"%lu", (unsigned long)[s length]);   // 5

NSString *emoji = @"👋";   // U+1F44B
NSLog(@"%lu", (unsigned long)[emoji length]);   // 2!代理对占 2 个 UTF-16 unit
  • length 是 UTF-16 unit 数,不是字符数。
  • 遍历字形簇要用 rangeOfComposedCharacterSequenceAtIndex:
  • CFStringNSString 是 toll-free bridged,可互相强转。
  • NSMutableString 是可变子类。

NSAttributedString / NSMutableAttributedString:带属性的字符串。

objective-c
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

objective-c
// 注册
[[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 时遍历调用。默认在发送线程同步调用所有观察者——发送在子线程,回调也在子线程。

objective-c
// 跨线程发送:用 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+)
objective-c
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 序列化。
objective-c
// 实现 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

objective-c
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:发送线程同步回调。如果发送在后台线程,回调也在后台线程。要主线程回调,用 NotificationQueuedispatch_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。
objective-c
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)。

objective-c
// 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:扩展按钮点击区域

objective-c
@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 接收)。

objective-c
@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;   // 自己不接收,让父级找下一个
}
@end

17.3 响应链(Responder Chain)

UIView → UIView → ... → UIViewController.view → UIViewController → UIWindow → UIApplication → AppDelegate

事件(touch / motion / press)从 hit-test 找到的 view 开始,沿 nextResponder 一路向上:

objective-c
@implementation MyView
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];   // 不处理就让上级处理
}
@end

17.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

子线程解码图片

objective-c
@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;
}

@end

17.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 Treelayer(默认访问)你设定的「最终值」
Presentation Treelayer.presentationLayer动画过程中每一刻的「显示值」
Render TreeRenderServer 私有GPU 实际渲染的数据
objective-c
[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 默认动画。

objective-c
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
layer.opacity = 0.0;
[CATransaction commit];

UIView 动画

objective-c
[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 显式动画

objective-c
// 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 值不变。要保留动画后状态:

objective-c
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
// 或者动画结束时显式 set model

18.6 离屏渲染

触发离屏渲染的操作

  • layer.cornerRadius + masksToBounds(同时设才有问题)
  • layer.mask
  • layer.shadow(无 shadowPath)
  • shouldRasterize(光栅化)
  • Group opacity(多个 layer 合成 + 透明度)
  • 文字 UITextView 多种样式

为什么慢

  1. 额外分配离屏 buffer(图像大小的几倍)。
  2. 渲染从直接 pipeline → 中断切换。
  3. 拷贝回 framebuffer 额外开销。

优化

场景替代方案
圆角带圆角的图(服务端合成 / CoreGraphics 预处理)
阴影layer.shadowPath = [UIBezierPath ...].CGPath
模糊UIVisualEffectView(系统 GPU 优化)
多效果预合成图 / 异步绘制

检测:模拟器 → Debug → Color Offscreen-Rendered Yellow。黄色区域即离屏渲染。

objective-c
// 阴影优化
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 LoadingPre-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

objective-c
@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:子线程抓栈

objective-c
// 子线程定时抓主线程栈
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];
        // 上报
    }
});

第三方工具

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 工具链

工具用途
InstrumentsTime Profiler / Allocations / Leaks / Core Animation / System Trace
Xcode Memory Graph可视化对象引用关系,找 leak
MetricKitiOS 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 / .mOC 头文件 / 实现
.swiftSwift 源文件
{Module}-Bridging-Header.hOC → Swift 桥接(OC 暴露给 Swift)
{Module}-Swift.hSwift → OC 桥接(编译器自动生成)
.modulemapModule 化 OC 框架

20.2 Swift 调用 OC

方式 1:Bridging Header

  1. 创建 {Module}-Bridging-Header.h
  2. 在 Bridging Header 中 #import 所有要暴露给 Swift 的 OC 头文件。
  3. Swift 代码直接使用(无需 import)。
objective-c
// {Module}-Bridging-Header.h
#import "Person.h"
#import "MyTools.h"
swift
// 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 中:

swift
import MyOCModule

20.3 OC 调用 Swift

步骤

  1. Swift 类继承 NSObject 或标注 @objc / @objcMembers
  2. OC 中 #import "{Module}-Swift.h"(编译器自动生成的头文件)。
swift
// 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() { }
}
objective-c
// 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 命名转换

SwiftOC
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 注解

objective-c
// 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_END

Swift 中:

swift
var middleName: String?     // nullable → Optional
var firstName: String       // nonnull
var name: String            // 在 NS_ASSUME_NONNULL_BEGIN 区域内

20.7 Swift / OC 混编最佳实践

  1. 新项目优先 Swift:Swift 是未来,OC 是历史。
  2. 新文件优先 Swift:除非必须 OC(如与 OC 框架深度集成)。
  3. 关键模型层用 Swift:值类型 / Optional / 错误处理更安全。
  4. 大型 OC 项目渐进迁移:新模块用 Swift,老模块逐步替换。
  5. 避免 Swift ↔ OC 频繁跨边界:跨边界有性能开销(参数转换)。
  6. 公共接口尽量 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(文件级加密):

objective-c
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 实现

objective-c
@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);
    }
}

@end

21.3 反调试

c
// 检测 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 越狱检测

objective-c
+ (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 注入检测

c
#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 层

  • 类名 / 方法名混淆(如 MyClassa_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(单一职责)

一个类应该只有一个引起它变化的原因。

反例

objective-c
// ❌ 一个类干了所有事
@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

重构

objective-c
// ✅ 拆分为多个职责单一的类
@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;
@end

22.2 Open-Closed Principle(开闭原则)

软件实体应该对扩展开放,对修改关闭。

反例

objective-c
// ❌ 加新形状要改 calculateArea
- (CGFloat)calculateArea:(id)shape {
    if ([shape isKindOfClass:[Circle class]]) return ...;
    else if ([shape isKindOfClass:[Rectangle class]]) return ...;
    // 加 Triangle 要改这里
}

重构

objective-c
// ✅ 抽象协议
@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(里氏替换)

子类必须能够替换其父类,而不破坏程序正确性。

反例

objective-c
@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(接口隔离)

客户不应被迫依赖它不使用的方法。

反例

objective-c
// ❌ 胖协议
@protocol Worker <NSObject>
- (void)code;
- (void)design;
- (void)test;
- (void)manage;
- (void)market;
@end

@interface Programmer : NSObject <Worker>
// 被迫实现 design/test/manage/market
@end

重构

objective-c
// ✅ 拆为小协议
@protocol Coder <NSObject>
- (void)code;
@end

@protocol Designer <NSObject>
- (void)design;
@end

@interface Programmer : NSObject <Coder>
- (void)code { /* ... */ }
@end

OC 协议天然适合接口隔离——多个小协议组合优于一个大协议。

22.5 Dependency Inversion Principle(依赖倒置)

高层模块不应依赖低层模块。两者都应依赖抽象。

反例

objective-c
@interface OrderService : NSObject
@property (nonatomic, strong) MySQLDatabase *db;   // 写死
- (void)create:(Order *)order;
@end

重构

objective-c
// ✅ 依赖抽象(协议)
@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)

objective-c
@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;
}

@end

iOS 中的单例[UIApplication sharedApplication][NSUserDefaults standardUserDefaults][NSFileManager defaultManager][NSURLSession sharedSession]

警告:单例是反模式(被滥用),主要问题——全局状态、隐式依赖、难测试、生命周期等于 App。

工厂方法(Factory Method)

objective-c
@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)

objective-c
@protocol UIFactory <NSObject>
- (UIButton *)createButton;
- (UITextField *)createTextField;
@end

@interface iOSFactory : NSObject <UIFactory> @end
@interface AndroidStyleFactory : NSObject <UIFactory> @end

建造者(Builder)

objective-c
@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)

objective-c
@interface Document : NSObject <NSCopying>
@property (nonatomic, copy) NSString *content;
- (id)copyWithZone:(NSZone *)zone;
@end

Document *template = ...;
Document *draft = [template copy];

23.2 结构型模式

适配器(Adapter)

objective-c
// 旧接口
@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 + 关联对象实现:

objective-c
@interface UIImageView (Cache)
- (void)loadImageFromURL:(NSURL *)url;   // 给 UIImageView 加缓存能力
@end

@implementation UIImageView (Cache)
- (void)loadImageFromURL:(NSURL *)url {
    // 检查缓存 → 加载 → 设置 image
}
@end

代理(Proxy)

objective-c
// 图片懒加载代理
@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)

objective-c
// 复杂子系统
@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)

objective-c
@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)

objective-c
@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;
}
@end

23.3 行为型模式

策略(Strategy)

objective-c
@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)

objective-c
// 通知中心
[[NSNotificationCenter defaultCenter] postNotificationName:@"userLogin" object:nil];

// KVO(详见第 8 章)
[user addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];

命令(Command)

objective-c
@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)

objective-c
@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)

objective-c
@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)

objective-c
// NSEnumerator 是迭代器
NSEnumerator *enumerator = [arr objectEnumerator];
id obj;
while ((obj = [enumerator nextObject])) { /* ... */ }

// 快速枚举(语法糖)
for (id obj in arr) { /* ... */ }

中介者(Mediator)

objective-c
@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)

objective-c
@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)

objective-c
@interface Game : NSObject
- (void)play;          // 模板(不可重写)
- (void)initialize;    // 钩子(子类重写)
- (void)startPlay;
- (void)endPlay;
@end

@implementation Game
- (void)play {   // 模板方法
    [self initialize];
    [self startPlay];
    [self endPlay];
}
- (void)initialize { }   // 默认实现
@end

OC 中用 Category 而非继承也能实现类似效果。

访问者(Visitor)

objective-c
@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];
        }
    }
}
@end

23.4 OC 中的设计模式落地

GoF 模式OC 中的体现
单例UIApplication.sharedApplicationUserDefaults.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 典型示例

objective-c
// 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;
}
@end

24.2 MVP(Model-View-Presenter)

┌──────────┐         ┌──────────┐
│   View   │◀───────▶│Presenter │
└──────────┘         └────┬─────┘

                     ┌────▼─────┐
                     │  Model   │
                     └──────────┘
  • View 完全被动,只更新 UI。
  • Presenter 处理所有业务逻辑。
  • View 与 Presenter 一对一。

OC 示例

objective-c
// 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]; }
@end

24.3 MVVM(Model-View-ViewModel)

┌──────────┐◀─Data──┌──────────┐◀─Data─┌──────────┐
│   View   │        │ViewModel │       │  Model   │
└──────────┘─Event─▶└──────────┘─Cmd──▶└──────────┘
  • ViewModel 不持 View 引用,只暴露数据流。
  • View 订阅 ViewModel 状态(KVO / ReactiveCocoa / RxObjC)。

OC 中用 RAC(ReactiveCocoa)实现 MVVM

objective-c
// 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");
    }];
}
@end

MVVM 的代价

  • 学习曲线(响应式编程)。
  • 调试难度(异步链路多)。
  • 简单场景过度设计。

24.4 VIPER(View-Interactor-Presenter-Entity-Router)

View ─── Presenter ─── Interactor ─── Entity

            Router
角色职责
ViewUI 展示、用户事件
PresenterUI 逻辑、View ↔ Interactor 转换
Interactor业务逻辑、网络/数据库访问
Entity数据模型
Router导航/路由

特点

  • 职责极致拆分,每个模块单一职责。
  • 可测试性强(每个角色都可单测)。
  • 模块化友好。
  • 代码量大。

OC 示例(简化)

objective-c
// 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 + RAC

24.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)

objective-c
// 注册
[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)

objective-c
// 业务模块(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)

objective-c
// 定义协议
@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 依赖注入

objective-c
// 简化版 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 RouterH5 / 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 实践

objective-c
// 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;
@end

26.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 的指针,首字段是 isaisa 指向类对象,类对象存方法列表、属性、协议、父类指针。

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 等系统机制的基础。

真正的成长路径

  1. 会用——能写出跑得起来的代码。
  2. 懂原理——能解释为什么这么写、底层怎么实现。
  3. 能优化——能定位性能瓶颈、设计可维护的架构。
  4. 能创造——能从零设计一个模块、一个架构、一个框架。

每个阶段都需要刻意练习。建议:

  • 每周读一份 Apple 官方文档(《Programming with Objective-C》《Runtime Programming Guide》)。
  • 每月读一份 objc4 / CFNetwork / Foundation 源码。
  • 每个季度写一个完整的 Demo 项目,把所学知识用进去。
  • 参与开源项目(ReactiveCocoa、Aspects、CTMediator、BeeHive),看一线团队的代码风格。

OC 不会过时。Swift 是未来,但 OC 是底座。掌握 OC 让你在面对任何 iOS 框架 / 系统机制时都能游刃有余。

📚 推荐资源


最后更新:2026 年 · 持续修订中。如有错误或建议,欢迎指正。

基于 VitePress 构建 · 部署于 Cloudflare Pages