在大型 iOS 工程中,设计锁粒度与隔离策略的目标是在数据安全性、执行性能与开发维护成本之间找到平衡。锁粒度过粗会导致主线程卡顿(如你提供的 slowlog_0 所示),锁粒度过细则会增加死锁风险。
以下是一套经过实践检验的设计体系:
1. 锁粒度设计:从“全局”走向“条目”
在大型工程中,应遵循“能用局部锁,不用全局锁”的原则。
-
分段锁(Striped Locking) :参考
SideTable的设计。- 应用场景:大规模缓存、全局状态管理。
- 设计:不要为整个
NSDictionary加锁,而是创建 16 或 64 个桶(Bucket),根据 Key 的 Hash 值分配到对应的锁。这样可以支持多个线程并发访问不同段的数据。
-
属性级对象锁:
- 应用场景:配置模型、单例状态。
- 设计:使用
os_unfair_lock保护特定变量,而不是锁定整个类实例。
2. 核心隔离策略:并发控制的核心
相比于直接使用“锁”,在 iOS 大型工程中更推荐使用执行隔离。
A. 基于 GCD 串行队列的隔离(推荐)
这是 Apple 最推荐的模式,用“消息传递”替代“共享内存”。
- 设计:为每个模块(如
NetworkManager或DatabaseManager)创建一个私有的dispatch_queue_t。 - 优点:消除了竞争,逻辑清晰,且能有效利用
dispatch_async避免主线程被阻塞。
B. 读写隔离(Reader-Writer Pattern)
-
应用场景:频繁读取、偶尔写入的数据(如内存配置表)。
-
设计:使用 GCD 并发队列配合
dispatch_barrier_async。- 读:
dispatch_sync到并发队列,允许多线程同时读。 - 写:
dispatch_barrier_async,确保写入时没有其他读写任务。
- 读:
3. 针对“启动性能”的特殊隔离设计
结合你上传的 slowlog_0 案例(个推 SDK 在主线程初始化导致卡顿),在设计隔离策略时必须考虑:
-
启动链路分级:
- Level 0 (必须同步) :崩溃监控、基础日志。
- Level 1 (异步/并行) :像个推、埋点等第三方 SDK,应强制隔离到专门的
StartupQueue中执行。
-
主线程守卫:
- 严禁在主线程使用
dispatch_sync等待任何后台任务。 - 如果必须等待,应使用“回调/监听”机制,而非“阻塞等待”。
- 严禁在主线程使用
4. 避免死锁的防御性规范
在大型团队协作中,靠自觉是不够的,需要设计规约:
-
锁的层级化:定义一套加锁顺序。例如:
数据库锁 -> 网络缓存锁 -> 内存模型锁。禁止反向加锁。 -
超时机制:在关键路径上使用
pthread_mutex_timedlock或 GCD 的dispatch_semaphore_wait超时版本,即便发生异常也不会永久卡死。 -
无锁设计(Lock-Free) :
- 利用原子操作(Atomic CAS)处理简单的状态位切换。
- 利用不可变对象(Immutable Objects)。当数据更新时,创建一个新对象并替换引用,旧对象随 ARC 释放。这是最安全的隔离策略。
5. 总结建议方案
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 高频并发小数据 | os_unfair_lock | 性能最接近自旋锁,且安全 |
| 复杂逻辑模块 | 串行 GCD 队列隔离 | 将竞态问题转化为顺序执行,降低维护难度 |
| 大型数据/文件 | 分段读写锁 (Barrier) | 提高读取吞吐量 |
| 对象属性保护 | 不可变模型 + 指针替换 | 彻底避免锁,极大提升启动性能 |