02-编程范式和编程思想学习@iOS |【Effective Objective-C】精华导读

1 阅读51分钟

image.png

📋 目录


一、概述与定位

《Effective Objective-C 2.0: 52 Specific Ways to Write Better iOS and OS X Programs》(以下简称「本书」)由 Matt Galloway 撰写,2013 年由 Addison-Wesley Professional 出版,隶属 Effective Software Development Series(Scott Meyers 主编),与《Effective C++》《Effective Java》等同属「以条目化、可操作建议」提升代码质量的经典技术书 [1][2]。

1.1 目标读者与写作方式

  • 目标读者:具备 Objective-C 与 Cocoa/Cocoa Touch 基础的中高级开发者,不侧重语法入门,而侧重在既有知识基础上写出更安全、可维护、符合范式的代码 [2]。
  • 写作方式:全书分为 52 条(Item) 独立建议,每条聚焦一个具体问题或原则,可单独阅读;条目间有交叉引用,便于形成体系 [1]。

1.2 与「编程范式」的关系

本书所涉的编程范式涵盖:

  • 面向对象范式:对象、消息传递、继承与多态在 Objective-C 中的实现方式(动态类型、运行时)。
  • 内存管理范式:从手动引用计数(MRR)到自动引用计数(ARC)的演进,以及所有权与对象图思维。
  • 并发与异步范式:Block 闭包与 Grand Central Dispatch(GCD)所代表的「任务 + 队列」模型。
  • 接口与 API 设计范式:命名、不可变性、委托与协议、分类与扩展等 Cocoa 惯例。

下文从历史演进核心原理图示与算法应用场景四方面系统梳理本书内容,并引用权威文献与业界实践。本文档同时参考了掘金上的「《Effective Objective-C》干货三部曲」(概念篇、规范篇、技巧篇)[13][14][15],对部分条目的示例与归类做了补充。文档兼顾学术严谨性(概念定义、引用来源)与可读性(结构分条、图示与伪代码),便于既作速查又作体系化学习。


二、技术演进与历史脉络

2.1 Objective-C 与 Cocoa 的渊源

Objective-C 在 C 语言之上增加了单继承的面向对象动态消息传递(dynamic messaging)。对象收到「消息」后,由运行时根据**选择子(selector)**查找并执行对应方法实现;这种「发消息」而非「调函数」的模型,使得方法解析、转发、替换(如 method swizzling)均在运行时完成,构成本书所述「对象、消息与运行时」的基础 [3][4]。

2.2 内存管理范式的演进

阶段时期范式说明
MRR早期至 iOS 4 / Mac OS X 10.6手动引用计数开发者显式调用 retain / release / autorelease,所有权由命名约定(如 allocnewcopy 返回持有)约定 [5][6]
ARCiOS 5 / Mac OS X 10.7 起自动引用计数编译器在编译期插入合适的 retain/release,仍为引用计数语义,非追踪式 GC;循环引用需开发者用 __weak 等打破 [5][7]
GC 弃用OS X Mountain Lion 起垃圾回收在 OS X 上被弃用,macOS Sierra 后从运行时移除,ARC 成为官方推荐 [7]

要点:ARC 与 MRR 共享同一套所有权与引用计数概念;理解「谁拥有对象、何时释放」有助于写出 ARC 下仍正确的代码(尤其 Block、delegate、timer 等易产生循环引用的场景)[5][8]。

2.3 Block 与 GCD 的引入

  • Block:Apple 在 C、Objective-C、C++ 中引入的闭包语法,可捕获上下文变量并延迟执行,是回调、动画、GCD 任务的基础。本书强调 Block 的循环引用(block 捕获 self、self 又持有 block)及 weak–strong dance 的规范写法 [9][10]。
  • Grand Central Dispatch (GCD):基于队列的并发抽象,将任务(block)派发到串行/并发队列,由系统管理线程。与 performSelector: 相比,GCD 支持异步、取消语义与队列层次,成为 iOS/macOS 并发编程的主流范式 [2][11]。

2.4 内存管理范式演进(时间线)

时期范式/事件
早期 CocoaMRR:手动 retain / release / autorelease
iOS 5 / Mac OS X 10.7 (Xcode 4.2)ARC 完整支持,编译期插入引用计数调用
OS X Mountain Lion 起垃圾回收(GC)弃用
macOS Sierra 起GC 从运行时移除,ARC 为唯一推荐方式

三、全书结构与 52 条建议总览

本书共 7 章、52 条,下表给出每章主题与条目范围,便于按需查阅 [1][2]。

主题条目核心内容概要
1熟悉 Objective-C1–5语言根源、头文件与导入、字面量语法、类型常量与枚举
2对象、消息与运行时6–14属性与实例变量、相等性、类簇、关联对象、消息机制、方法转发、method swizzling、类对象
3接口与 API 设计15–22命名、指定初始化器、description、不可变优先、命名一致性、私有方法、错误处理、NSCopying
4协议与分类23–28委托模式、分段实现、分类前缀、分类中避免属性、类扩展、匿名对象
5内存管理29–36引用计数、ARC、dealloc、异常安全、弱引用、autorelease 池、僵尸对象、retainCount
6Block 与 GCD37–46Block 语法与 typedef、handler block、循环引用、dispatch 队列、GCD 与 performSelector、NSOperation、dispatch group、dispatch_once、当前队列
7系统框架47–52框架使用、块枚举、桥接、NSCache、+load/+initialize、NSTimer

四、核心原理与精华条目

4.1 第一章:熟悉 Objective-C

  • 语言根源与运行期组件:Objective-C 采用消息结构,运行时才查找要执行的方法;运行期组件是与开发者代码链接的动态库,包含面向对象所需的数据结构与函数,更新运行期组件即可提升应用性能。对象分配在、指针在;不含 * 的变量可能用栈,结构体保存非对象类型 [13]。
  • 头文件与向前声明:在类的头文件中尽量少引用其他头文件;若仅需声明某类型为属性,使用 向前声明@class EOCEmployer;),在 .m 中再 #import,可减少编译时间并避免循环引用。继承或遵从协议时必须在头文件中引入对应头文件 [14]。
  • 字面量与装箱:使用 @""@[]@{}@() 等字面量可减少冗长代码并降低错误;字面量若含 nil 会立即抛异常,而 arrayWithObjects:nil 会截断,易埋坑。字面量创建的集合为不可变 [2][14]。
  • 常量与枚举:用 static const(编译单元内可见)或 extern const(对外公开)定义常量,避免 #define(无类型、易被改)。对外常量命名建议带类名前缀;枚举用 NS_ENUM / NS_OPTIONSswitch 中不要写 default,以便新增枚举成员时编译器提示未处理 [2][14][15]。

4.2 第二章:对象、消息与运行时

4.2.1 属性与实例变量

属性(@property)是编译器自动生成存取器与(可选)实例变量的语法糖。要点 [2][4]:

  • 读写语义strong(默认对象)、copy(如 NSString/Block 防外部修改)、weak(避免循环引用)、assign(非对象类型)。
  • 原子性atomic(默认)在存取时加锁,多数场景下 nonatomic 更高效且足够;若需线程安全,应结合更高级的同步手段。
  • 属性关键字小结copy 用于 NSString/Block 等需拷贝语义的类型;unsafe_unretained 类似 assign 但用于对象,对象释放后不会清空;在非 setter 中给属性赋值时也需遵循其语义(如 copy 属性在 init 里应对传入值 copy)[13]。

4.2.2 对象相等性与 isEqual / hash

  • 相等性:若逻辑上「相等」需自定义,应实现 isEqual:hashhash 在对象被放入集合(如 NSSet、NSDictionary key)时使用,相等对象必须有相同 hash,反之不要求;hash 应稳定、计算量小 [2]。
  • 类簇(Class Cluster):公开接口是抽象基类(如 NSString、NSArray),实际返回私有子类实例。自定义子类需继承簇的「抽象基类」并实现其工厂方法所依赖的初始器;直接比较类时要注意类簇的多种子类 [2][4]。

4.2.3 关联对象(Associated Objects)

运行时允许在不修改类定义的前提下,给对象关联键值对。常用于:给分类「添加」存储、给系统类绑定上下文数据。需注意键的唯一性与内存语义(如 OBJC_ASSOCIATION_RETAIN_NONATOMIC)[2][4]。

4.2.4 objc_msgSend 与消息查找

[someObject messageName:parameter] 在底层转为 C 函数调用:objc_msgSend(someObject, @selector(messageName:), parameter)。该函数在接收者所属类及父类链的方法列表中查找与选择子相符的 IMP;找到则执行并缓存到类的快速映射表,下次同消息更快;找不到则进入消息转发 [3][13]。

4.2.5 消息转发(Message Forwarding)

当对象收到无法识别的消息时,运行时在报错前会给予二次机会 [3][12][13]:

  1. 动态方法解析+resolveInstanceMethod: / +resolveClassMethod:,可为该类动态添加方法实现(如 class_addMethod);典型应用是 @dynamic 属性 + 内部字典存储(EOCAutoDictionary 模式)。
  2. 快速转发-forwardingTargetForSelector:,返回备援接收者,运行期将消息转给该对象。
  3. 完整转发-methodSignatureForSelector:-forwardInvocation:,将消息封装为 NSInvocation,可修改目标、参数或返回值,实现代理、多继承等。

应用:代理对象、惰性加载大型对象、将未识别消息转发到后备对象等 [12]。

4.2.6 类对象与类型查询

运行期用 objc_class 结构描述类(含 isa、super_class、methodLists、cache 等)。isMemberOfClass: 判断是否为某特定类的实例;isKindOfClass: 判断是否为某类或其派生类的实例。从集合取出对象后往往需做类型判断再调用方法,避免向错误类型发消息 [13]。

4.2.7 Method Swizzling

通过运行时交换两个方法的实现(IMP),从而在不修改原类源码的情况下「注入」或「替换」行为;常用于 AOP、调试、无埋点统计。注意:交换应在 +load 等单次执行路径执行,并考虑继承与多线程安全 [2][4]。

4.3 第三章:接口与 API 设计

  • 命名:方法名应语义清晰、读起来像句子,如 initWithWidth:height: 优于 initWithSize::;布尔 getter 用 is/has 前缀(如 isEqualToString:hasPrefix:)。每个冒号左侧的方法部分最好与右侧参数名对应 [14]。
  • 指定初始化器(Designated Initializer):选定全能初始化方法(参数最多的那个),其他 init 及子类 init 均委托到它;子类若有自己的全能初始化器,需覆写父类的全能初始化器并转调自己的,避免用父类 init 产生非法状态(如 Square 覆写 initWithWidth:andHeight: 转调 initWithDimension:)。实现 initWithCoder: 时也应调用超类对应方法 [2][15]。
  • description:覆写 description 返回类名、地址与关键属性(或字典形式),便于调试时在控制台看到有意义信息 [15]。
  • 不可变优先:对外属性设为 readonly,在类扩展中改为 readwrite;集合对外暴露不可变类型(如 NSSet *friends),内部用 NSMutableSet,通过定制 addFriend:/removeFriend: 等接口修改,getter 返回 [_internalFriends copy],避免外部直接改底层数据 [14]。
  • 私有方法前缀:实现文件中的私有方法加前缀(如 p_privateMethod),便于与公共方法区分;不要用单下划线(与 Apple API 冲突)[14]。
  • NSError:用 NSError 封装错误域(domain)、错误码(code)、用户信息(userInfo);作为「输出参数」传递时用 (NSError **)error,调用方检查 *error;可定义 extern NSString *const EOCErrorDomainNS_ENUM 错误码 [13]。
  • NSCopying:实现 copyWithZone:(及可变版的 mutableCopyWithZone:);Foundation 集合默认浅拷贝,深拷贝需自己遍历并 copyItems:YES 或实现 deepCopy [13][15]。

4.4 第四章:协议与分类

  • 委托(Delegate):用 @protocol 定义回调接口,属性用 weak 避免循环引用;delegate 可选方法用 @optional,调用前先判断 delegate 是否存在再 respondsToSelector:,例如 if (_delegate && [_delegate respondsToSelector:@selector(...)]) { ... } [14]。委托模式与数据源模式:信息从类流向委托者 vs 从数据源流向类。
  • 分类(Category):按逻辑将类方法分散到多个分类(如 Friendship、Work、Play),便于管理;可为「私有方法」建 Private 分类。勿在分类中声明属性(仅 class-continuation 可增加实例变量);为第三方或系统类加分类时,分类名与方法名均加前缀(如 ABC_HTTP),避免覆盖原实现 [2][14]。
  • 类扩展(Class Continuation):在 .m 中的匿名分类,可遵循协议而不暴露、将只读属性改为读写、增加实例变量 [14]。
  • 匿名对象id<EOCDelegate> 表示「遵从某协议的对象」而非「某类的实例」,用作 delegate 属性或方法参数(如 setObject:forKey:(id<NSCopying>)key),强调协议契约 [15]。

4.5 第五章:内存管理

4.5.1 引用计数与 ARC

  • 所有权:谁创建(alloc/new/copy/mutableCopy)、谁持有;谁不再需要,谁释放(在 ARC 下由编译器插入)[5][6]。
  • ARC 规则:不能显式调用 retain/release/autorelease;不能使用 retainCount(仅调试用且不可靠);Core Foundation 与 Objective-C 对象混用需注意桥接(__bridge / __bridge_retained / __bridge_transfer)[5][7]。

4.5.2 循环引用与 weak

典型循环:对象 A 强引用 B,B 强引用 A(或通过 block/delegate 形成环)。解决:将其中一侧改为 weak(如 delegate、block 内对 self 的引用)[8][10]。

4.5.3 其他要点

  • dealloc:在 ARC 下仅用于释放 Core Foundation 对象(如 CFRelease)、移除 KVO/通知(如 removeObserver:self)等;不要在 dealloc 中调用其他方法或属性存取器,可能触发异步回调或 KVO 导致使用已释放对象 [2][5][14]。
  • autorelease 池:对象 autorelease 后在下一次事件循环清空池时才会 release;在循环中创建大量临时对象时,在循环内使用 @autoreleasepool { ... }降低内存峰值 [5][8][15]。
  • 僵尸对象(Zombie):开启后,已释放对象的 isa 被改为指向特殊僵尸类,不回收内存、不覆写;再次向该对象发消息会抛出异常并描述原对象与消息,便于排查野指针 [2][6][15]。
  • retainCount:不应使用;ARC 下已废弃,且其返回值只能反映某一时刻的计数,无法反映自动释放池等后续变化 [14]。
  • 异常安全:MRC 下 try 中 retain 的对象若在 release 前抛异常会泄漏,应在 @finally 中 release;ARC 下需 -fobjc-arc-exceptions 才会在异常路径插入清理代码,会增大体积并影响性能 [15]。

4.6 第六章:Block 与 GCD

4.6.1 Block 类型与循环引用

  • Block 三种类型栈 block(定义时在栈上,离开作用域可能失效);堆 block(对栈 block 发 copy 后拷贝到堆,带引用计数);全局 block(不捕获外部变量时可为全局块)。需长期持有的 block 应 copy 到堆 [13][15]。
  • Block 会捕获其使用的局部变量;对对象默认是强引用。若 block 被当前对象持有(如属性、成员变量),且 block 内又使用了 self_ivar(等价于 self),则形成循环引用 [9][10]。
  • 规范写法:在 block 外先 __weak typeof(self) weakSelf = self;,在 block 内使用 weakSelf;若需在 block 执行过程中保证 self 存活,可在 block 内再 __strong typeof(weakSelf) strongSelf = weakSelf; 后使用 strongSelf(weak–strong dance)。也可在 block 末尾将持有 block 的成员置为 nil 以打破环(如 completion 内 _networkFetcher = nil)[10][15]。
  • handler block 与 typedef:用 completion handler 块替代 delegate 回调可让「发起请求」与「处理结果」写在一起;对常用块签名使用 typedef void(^EOCCompletionHandler)(NSData *data, NSError *error); 便于复用与修改 [15]。

4.6.2 GCD 队列与任务

  • 队列类型:串行队列(同一时间只执行一个任务)、并发队列(可多任务并发);主队列(main queue)为串行,用于 UI 更新。不要使用 dispatch_get_current_queue 判断「当前队列」,因队列有层级关系,结果不可靠 [11][14]。
  • 常用 APIdispatch_asyncdispatch_syncdispatch_afterdispatch_once(单例等)、dispatch_group_async + dispatch_group_notify(多任务完成后汇总)[2][11][15]。
  • 同步与锁:可用串行队列统一读写(读写都 dispatch_sync 到同一队列);或并发队列 + dispatch_barrier_async 写、普通 async/sync 读,保证写互斥、读可并发 [15]。
  • 与 performSelector 对比:GCD 不依赖 selector、可传多参数与返回值;延后执行用 dispatch_after,回主线程用 dispatch_async(main_queue, ^{ ... }),替代 performSelector:withObject:afterDelay:performSelectorOnMainThread: [2][15]。
  • NSOperation 适用场景:需取消任务、设置依赖、指定优先级或 KVO 监听 isFinished/isCancelled 时,用 NSOperationQueue 更合适;GCD 为「fire and forget」[15]。

4.7 第七章:系统框架

  • 块枚举:使用 enumerateObjectsUsingBlock: 可获下标、键值对及 *stop 提前终止;比 for 循环简洁,且可修改块签名以做类型检查。遍历 Dictionary/Set 时无需先 allKeys/allObjects 再遍历,减少临时数组 [2][14]。
  • NSCache:线程安全、不拷贝 key(保留)、内存紧张时自动删减(含「最久未用」策略);可设置 countLimittotalCostLimit。与 NSPurgeableData 配合时,访问前 beginContentAccess、用毕 endContentAccess,便于系统回收内存 [2][14]。
  • +load 与 +initialize+load 在类/分类加入运行期时各调用一次,尽量不要在 load 里调用其他类(加载顺序未定义)。+initialize 在类首次收到消息前调用,子类未实现会调用父类,因此需判断 if (self == [EOCBaseClass class]) 再执行逻辑,避免子类触发父类 initialize [2][4][14]。
  • NSTimer:会强引用 target,若 target 是 self 且 self 又持有 timer,则形成保留环;dealloc 中 invalidate 可能无法执行(因环未打破)。推荐:用 NSTimer 的 block 封装(Category 提供 eoc_scheduledTimerWithTimeInterval:block:repeats:,timer 的 target 为类对象,userInfo 存 [block copy]),在 block 内用 weakSelf/strongSelf 调用业务逻辑,这样 self 释放后 block 中 weakSelf 为 nil,或 dealloc 中 invalidate 即可打破环 [2][14]。
  • 无缝桥接:Foundation 与 Core Foundation 间用 __bridge(不转移所有权)、__bridge_retained__bridge_transfer 转换;创建 CF 集合时可指定回调以自定义内存管理语义,再桥接到 OC 使用 [15]。

五、关键概念图示与流程

5.1 消息发送与查找流程

Objective-C 中 [obj message] 在运行时转化为 objc_msgSend(obj, selector, ...),随后在类的方法表及父类链中查找 IMP;若未找到,进入消息转发 [3][4]。

flowchart TD
    A[obj 收到消息] --> B{在类及父类中查找 IMP}
    B -->|找到| C[调用 IMP]
    B -->|未找到| D[动态方法解析 resolveInstanceMethod:]
    D --> E{添加方法?}
    E -->|是| C
    E -->|否| F[forwardingTargetForSelector:]
    F --> G{返回非 nil 目标?}
    G -->|是| H[向目标转发消息]
    G -->|否| I[methodSignatureForSelector: + forwardInvocation:]
    I --> J[开发者可转发到其他对象或处理]

5.2 消息转发(forwardInvocation)概念

当使用 forwardInvocation: 时,运行时将原始消息封装为 NSInvocation,传给接收者;接收者可修改目标、参数或返回值,实现「代理」「多继承」等 [12]。

sequenceDiagram
    participant C as 调用方
    participant R as 接收者
    participant T as 转发目标

    C->>R: 发送未知消息
    R->>R: methodSignatureForSelector:
    R->>R: forwardInvocation:(invocation)
    R->>T: [invocation invokeWithTarget:T]
    T-->>R: 返回值
    R-->>C: 返回

5.3 引用计数与所有权(概念)

flowchart LR
    subgraph 创建
        A[alloc/new/copy] --> B[引用计数 = 1]
    end
    subgraph 持有
        B --> C[retain +1]
        C --> D[release -1]
    end
    subgraph 释放
        D --> E{计数 = 0?}
        E -->|是| F[dealloc 释放对象]
        E -->|否| G[仍存活]
    end

5.4 Block 循环引用

flowchart LR
    subgraph 循环
        S[self] --> B[block]
        B --> S
    end
    subgraph 打破
        W[weakSelf] --> B2[block]
        S2[self] -.->|弱引用| W
        B2 -.->|捕获 weakSelf| W
    end

5.5 GCD 队列层次(概念)

flowchart TB
    subgraph 主队列
        M[Main Queue - UI]
    end
    subgraph 全局并发队列
        G[Global Concurrent Queue]
    end
    subgraph 自定义
        Q1[Serial Queue]
        Q2[Concurrent Queue]
    end
    M --> G
    G --> Q1
    G --> Q2

六、伪代码与算法说明

6.1 对象相等性与 hash(约定)

约定(Effective Objective-C 与 Cocoa 惯例):
1. 若 [a isEqual:b] 为 YES,则 [a hash] == [b hash] 必须成立。
2. hash 在对象生命周期内应稳定(不变)。
3. hash 不必唯一,但应尽量均匀以减少冲突。

算法(示例,仅说明思路):
- 对关键属性分别求 hash(如 NSString 的 hash、数值的 hash),再组合(如异或、乘质数相加)。
- 避免在 hash 中做重计算或依赖可变状态。

6.2 weak–strong 避免 Block 循环引用(伪代码)

// 错误:block 被 self 持有,block 内又强引用 self
self.block = ^{ [self doSomething]; };  // 循环引用

// 正确:block 外 weak,block 内 strong(可选,防止执行过程中 self 被释放)
__weak typeof(self) weakSelf = self;
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) { [strongSelf doSomething]; }
};

6.3 dispatch_once 单例(典型写法)

+ (instancetype)sharedInstance {
    static MyClass *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[MyClass alloc] init];
    });
    return instance;
}
// dispatch_once 保证块只执行一次,且线程安全。

6.4 forwardInvocation 转发到后备对象(伪代码)

// 根据 Apple 文档 [12],简化实现思路:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature *sig = [super methodSignatureForSelector:selector];
    if (!sig) sig = [backupObject methodSignatureForSelector:selector];
    return sig;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([backupObject respondsToSelector:[invocation selector]])
        [invocation invokeWithTarget:backupObject];
    else
        [super forwardInvocation:invocation];
}

七、应用场景与最佳实践

7.1 场景与条目对照

场景本书建议概要典型条目
网络/异步回调使用 Block + GCD,避免 performSelector;在 block 内用 weak–strong 避免循环引用37–40, 42–43
自定义集合元素实现 isEqual: 与 hash;若需复制实现 NSCopying8, 22
为系统类添加方法用 Category,方法名加前缀;需存储用关联对象或类扩展25–26
单例或一次性初始化dispatch_once45
缓存图片/数据NSCache,不手写 NSDictionary + 淘汰50
定时任务NSTimer 注意与 target 的循环引用,及时 invalidate 或拆开52
调试内存/野指针僵尸对象、Instruments、静态分析35–36
多任务完成后统一处理dispatch_group + dispatch_group_notify44

7.2 高级应用场景简述

  • AOP / 无埋点:通过 Method Swizzling 在系统或业务方法前后插入逻辑(如统计、日志),需注意交换时机与线程安全;可与 +load 配合 [2][4]。
  • 跨框架混用(Core Foundation ↔ Objective-C):使用 __bridge(不转移所有权)、__bridge_retained(CF 侧持有)、__bridge_transfer(OC 侧持有)正确管理生命周期,避免重复释放或泄漏 [5][7]。
  • 大循环中的临时对象:在循环内使用 @autoreleasepool { ... } 及时排空自动释放池,降低内存峰值 [5][8]。
  • 委托与数据源:delegate 属性声明为 weak,在 dealloc 中无需显式置 nil(weak 会自动清空);调用可选方法前用 respondsToSelector: 判断 [2]。

八、其它补充

第2条: 在类的头文件中尽量少引用其他头文件

有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头文件,而应该使用向前声明(forward declaring)使用class关键字,并且在A的实现文件引用B的头文件。

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;//将EOCEmployer作为属性

@end

// EOCPerson.m
#import "EOCEmployer.h"

这样做有什么优点呢:

  • 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
  • 可以避免循环引用:因为如果两个类在自己的头文件中都引入了对方的头文件,那么就会导致其中一个类无法被正确编译。

但是个别的时候,必须在头文件中引入其他类的头文件:

主要有两种情况:

  1. 该类继承于某个类,则应该引入父类的头文件。
  2. 该类遵从某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中。

第3条:多用字面量语法,少用与之等价的方法

1. 声明时的字面量语法:

在声明NSNumber,NSArray,NSDictionary时,应该尽量使用简洁字面量语法。

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSArray *animals =[NSArray arrayWithObjects:@"cat", @"dog",@"mouse", @"badger", nil];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};

2. 集合类取下标的字面量语法:

NSArray,NSDictionary,NSMutableArray,NSMutableDictionary 的取下标操作也应该尽量使用字面量语法。

NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];

使用字面量语法的优点:

  1. 代码看起来更加简洁。
  2. 如果存在nil值,则会立即抛出异常。如果在不用字面量语法定义数组的情况下,如果数组内部存在nil,则系统会将其设为数组最后一个元素并终止。所以当这个nil不是最后一个元素的话,就会出现难以排查的错误。

注意: 字面量语法创建出来的字符串,数组,字典对象都是不可变的。

第4条:多用类型常量,少用#define预处理命令

在OC中,定义常量通常使用预处理命令,但是并不建议使用它,而是使用类型常量的方法。 首先比较一下这两种方法的区别:

  • 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
  • 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。

我们可以看出来,使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:不具备类型 + 可以被任意修改,总之给人一种不安全的感觉。

知道了它们的长短处,我们再来简单看一下它们的具体使用方法:

预处理命令:

#define W_LABEL (W_SCREEN - 2*GAP)

这里,(W_SCREEN - 2*GAP)替换了W_LABEL,它不具备W_LABEL的类型信息。而且要注意一下:如果替换式中存在运算符号,以笔者的经验最好用括号括起来,不然容易出现错误(有体会)。

类型常量:

static const NSTimeIntervalDuration = 0.3;

这里: const 将其设置为常量,不可更改。 static意味着该变量仅仅在定义此变量的编译单元中可见。如果不声明static,编译器会为它创建一个外部符号(external symbol)。我们来看一下对外公开的常量的声明方法:

对外公开某个常量:

如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串,那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。

//header file
extern NSString *const NotificationString;

//implementation file
NSString *const  NotificationString = @"Finish Download";

这里NSString *const NotificationString是指针常量。 extern关键字告诉编译器,在全局符号表中将会有一个名叫NotificationString的符号。

我们通常在头文件声明常量,在其实现文件里定义该常量。由实现文件生成目标文件时,编译器会在“数据段”为字符串分配存储空间。

最后注意一下公开和非公开的常量的命名规范:

公开的常量:常量的名字最好用与之相关的类名做前缀。 非公开的常量:局限于某个编译单元(tanslation unit,实现文件 implementation file)内,在签名加上字母k。

第5条:用枚举表示状态,选项,状态码

我们经常需要给类定义几个状态,这些状态码可以用枚举来管理。下面是关于网络连接状态的状态码枚举:

typedef NS_ENUM(NSUInteger, EOCConnectionState) {
  EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};

需要注意的一点是: 在枚举类型的switch语句中不要实现default分支。它的好处是,当我们给枚举增加成员时,编译器就会提示开发者:switch语句并未处理所有的枚举。对此,笔者有个教训,又一次在switch语句中将“默认分支”设置为枚举中的第一项,自以为这样写可以让程序更健壮,结果后来导致了严重的崩溃。

第21条:理解Objective-C错误类型

在OC中,我们可以用NSError描述错误。 使用NSError可以封装三种信息:

  • Error domain:错误范围,类型是字符串
  • Error code :错误码,类型是整数
  • User info:用户信息,类型是字典

1. NSError的使用

用法:

1.通过委托协议来传递NSError,告诉代理错误类型。

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

2.作为方法的“输出参数”返回给调用者

- (BOOL)doSomething:(NSError**)error

使用范例:


NSError *error = nil;
BOOL ret = [object doSomething:&error];

if (error) {
    // There was an error
}

2. 自定义NSError

我们可以设置属于我们自己程序的错误范围和错误码

  • 错误范围可以用全局常量字符串来定义。
  • 错误码可以用枚举来定义。

// EOCErrors.h
extern NSString *const EOCErrorDomain;

//定义错误码
typedef NS_ENUM(NSUInteger, EOCError) {

    EOCErrorUnknown = –1,
    EOCErrorInternalInconsistency = 100,
    EOCErrorGeneralFault = 105,
    EOCErrorBadInput = 500,
};



// EOCErrors.m
NSString *const EOCErrorDomain = @"EOCErrorDomain"; //定义错误范围

第22条:理解NSCopying协议

如果我们想令自己的类支持拷贝操作,那就要实现NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(NSZone*)zone

作者举了个:


- (id)copyWithZone:(NSZone*)zone {

     EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName  andLastName:_lastName];
    copy->_friends = [_friends mutableCopy];
     return copy;
}

之所以是copy->_friends,而不是copy.friends是因为friends并不是属性,而是一个内部使用的实例变量。

1. 复制可变的版本:

遵从协议

而且要执行:

- (id)mutableCopyWithZone:(NSZone*)zone;

注意:拷贝可变型和不可变型发送的是copymutableCopy消息,而我们实现的却是- (id)copyWithZone:(NSZone*)zone- (id)mutableCopyWithZone:(NSZone*)zone 方法。

而且,如果我们想获得某对象的不可变型,统一调用copy方法;获得某对象的可变型,统一调用mutableCopy方法。

例如数组的拷贝:

-[NSMutableArray copy] => NSArray
-[NSArray mutableCopy] => NSMutableArray

2. 浅拷贝和深拷贝

Foundation框架中的集合类默认都执行浅拷贝:只拷贝容器对象本身,而不复制其中的数据。 而深拷贝的意思是连同对象本身和它的底层数据都要拷贝。

作者用一个图很形象地体现了浅拷贝和深拷贝的区别:

图片来自:《Effective Objective-C》

浅拷贝后的内容和原始内容指向同一个对象 深拷贝后的内容所指的对象是原始内容对应对象的拷贝

3. 如何深拷贝?

我们需要自己编写深拷贝的方法:遍历每个元素并复制,然后将复制后的所有元素重新组成一个新的集合。

- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems;

在这里,我们自己提供了一个深拷贝的方法:该方法需要传入两个参数:需要拷贝的数组和是否拷贝元素(是否深拷贝)


- (id)deepCopy {
       EOCPerson *copy = [[[self class] alloc] initWithFirstName:_firstName andLastName:_lastName];
        copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
        return copy;
}

第47条:熟悉系统框架

如果我们使用了系统提供的现成的框架,那么用户在升级系统后,就可以直接享受系统升级所带来的改进。

主要的系统框架:

  • Foundation:NSObject,NSArray,NSDictionary等
  • CFoundation框架:C语言API,Foundation框架中的许多功能,都可以在这里找到对应的C语言API
  • CFNetwork框架:C语言API,提供了C语言级别的网络通信能力
  • CoreAudio:C语言API,操作设备上的音频硬件
  • AVFoundation框架:提供的OC对象可以回放并录制音频和视频
  • CoreData框架:OC的API,将对象写入数据库
  • CoreText框架:C语言API,高效执行文字排版和渲染操作

用C语言来实现API的好处:可以绕过OC的运行期系统,从而提升执行速度

第7条: 在对象内部尽量直接访问实例变量

关于实例变量的访问,可以直接访问,也可以通过属性的方式(点语法)来访问。书中作者建议在读取实例变量时采用直接访问的形式,而在设置实例变量的时候通过属性来做。

1. 直接访问属性的特点:

  • 绕过set,get语义,速度快;

2. 通过属性访问属性的特点:

  • 不会绕过属性定义的内存管理语义
  • 有助于打断点排查错误
  • 可以触发KVO

因此,有个关于折中的方案:

设置属性:通过属性 读取属性:直接访问

不过有两个特例:

  1. 初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
  2. 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。

第15条:用前缀 避免命名空间冲突

Apple宣称其保留使用所有"两字母前缀"的权利,所以我们选用的前缀应该是三个字母的。 而且,如果自己开发的程序使用到了第三方库,也应该加上前缀。

第18条:尽量使用不可变对象

书中作者建议尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:

在头文件中,设置对象属性为readonly,在实现文件中设置为readwrite。这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。

而且,对于集合类的对象,更应该仔细考虑是否可以将其设为可变的。

如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型。这样一来,当在外部获取这个属性时,获取的只是内部可变型的一个不可变版本,例如:

在公共API中:

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公开的不可变集合

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;

@end

在这里,我们将friends属性设置为不可变的set。然后,提供了来增加和删除这个set里的元素的公共接口。

在实现文件里:

@interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

@end

@implementation EOCPerson {
     NSMutableSet *_internalFriends;  //实现文件里的可变集合
}

- (NSSet*)friends {
     return [_internalFriends copy]; //get方法返回的永远是可变set的不可变型
}

- (void)addFriend:(EOCPerson*)person {
    [_internalFriends addObject:person]; //在外部增加集合元素的操作
    //do something when add element
}

- (void)removeFriend:(EOCPerson*)person {
    [_internalFriends removeObject:person]; //在外部移除元素的操作
    //do something when remove element
}

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName {

     if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
 return self;
}

我们可以看到,在实现文件里,保存一个可变set来记录外部的增删操作。

这里最重要的代码是:

- (NSSet*)friends {
 return [_internalFriends copy];
}

这个是friends属性的获取方法:它将当前保存的可变set复制了一不可变的set并返回。因此,外部读取到的set都将是不可变的版本。

等一下,有个疑问:

在公共接口设置不可变set 和 将增删的代码放在公共接口中是否矛盾的?

答案:并不矛盾!

因为如果将friends属性设置为可变的,那么外部就可以随便更改set集合里的数据,这里的更改,仅仅是底层数据的更改,并不伴随其他任何操作。 然而有时,我们需要在更改set数据的同时要执行隐秘在实现文件里的其他工作,那么如果在外部随意更改这个属性的话,显然是达不到这种需求的。

因此,我们需要提供给外界我们定制的增删的方法,并不让外部”自行“增删。

第19条:使用清晰而协调的命名方式

在给OC的方法取名字的时候要充分利用OC方法的命名优势,取一个语义清晰的方法名!什么叫语义清晰呢?就是说读起来像是一句话一样。

我们看一个例子:

先看名字取得不好的:

//方法定义
- (id)initWithSize:(float)width :(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithSize:5.0f :10.0f];

这里定义了Rectangle的初始化方法。虽然直观上可以知道这个方法通过传入的两个参数来组成矩形的size,但是我们并不知道哪个是矩形的宽,哪个是矩形的高。 来看一下正确的🌰 :

//方法定义
- (id)initWithWidth:(float)width height:(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithWidth:5.0f height:10.0f];

这个方法名就很好的诠释了该方法的意图:这个类的初始化是需要宽度和高度的。而且,哪个参数是高度,哪个参数是宽度,看得人一清二楚。永远要记得:代码是给人看的

笔者自己总结的方法命名规则:

每个冒号左边的方法部分最好与右边的参数名一致。

对于返回值是布尔值的方法,我们也要注意命名的规范:

  • 获取”是否“的布尔值,应该增加“is”前缀:

- isEqualToString:

获取“是否有”的布尔值,应该增加“has”前缀:

- hasPrefix:

第20条:为私有方法名加前缀

建议在实现文件里将非公开的方法都加上前缀,便于调试,而且这样一来也很容易区分哪些是公共方法,哪些是私有方法。因为往往公共方法是不便于任意修改的。

在这里,作者举了个例子:

#import <Foundation/Foundation.h>

@interface EOCObject : NSObject

- (void)publicMethod;

@end


@implementation EOCObject

- (void)publicMethod {
 /* ... */
}

- (void)p_privateMethod {
 /* ... */
}

@end

注意: 不要用下划线来区分私有方法和公共方法,因为会和苹果公司的API重复。

第23条:通过委托与数据源协议进行对象间通信

如果给委托对象发送消息,那么必须提前判断该委托对象是否实现了该消息:

NSData *data = /* data obtained from network */;

if ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)])
{
        [_delegate networkFetcher:self didReceiveData:data];
}

而且,最好再加上一个判断:判断委托对象是否存在


NSData *data = /* data obtained from network */;

if ( (_delegate) && ([_delegate respondsToSelector: @selector(networkFetcher:didReceiveData:)]))
{
        [_delegate networkFetcher:self didReceiveData:data];
}

对于代理模式,在iOS中分为两种:

  • 普通的委托模式:信息从类流向委托者
  • 信息源模式:信息从数据源流向类

普通的委托 | 信息源

就好比tableview告诉它的代理(delegate)“我被点击了”;而它的数据源(data Source)告诉它“你有这些数据”。仔细回味一下,这两个信息的传递方向是相反的。

第24条:将类的实现代码分散到便于管理的数个分类中

通常一个类会有很多方法,而这些方法往往可以用某种特有的逻辑来分组。我们可以利用OC的分类机制,将类的这些方法按一定的逻辑划入几个分区中。

例子:

无分类的类:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName lastName:(NSString*)lastName;

/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;


/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;


/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;


@end

分类之后:

#import <Foundation/Foundation.h>


@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;



- (id)initWithFirstName:(NSString*)firstName

lastName:(NSString*)lastName;

@end



@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end



@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end



@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

其中,FriendShip分类的实现代码可以这么写:


// EOCPerson+Friendship.h
#import "EOCPerson.h"


@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end


// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"


@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person {
 /* ... */
}

- (void)removeFriend:(EOCPerson*)person {
 /* ... */
}

- (BOOL)isFriendsWith:(EOCPerson*)person {
 /* ... */
}

@end

注意:在新建分类文件时,一定要引入被分类的类文件。

通过分类机制,可以把类代码分成很多个易于管理的功能区,同时也便于调试。因为分类的方法名称会包含分类的名称,可以马上看到该方法属于哪个分类中。

利用这一点,我们可以创建名为Private的分类,将所有私有方法都放在该类里。这样一来,我们就可以根据private一词的出现位置来判断调用的合理性,这也是一种编写“自我描述式代码(self-documenting)”的办法。

第25条:总是为第三方类的分类名称加前缀

分类机制虽然强大,但是如果分类里的方法与原来的方法名称一致,那么分类的方法就会覆盖掉原来的方法,而且总是以最后一次被覆盖为基准。

因此,我们应该以命名空间来区别各个分类的名称与其中定义的方法。在OC里的做法就是给这些方法加上某个共用的前缀。例如:

@interface NSString (ABC_HTTP)

// Encode a string with URL encoding
- (NSString*)abc_urlEncodedString;

// Decode a URL encoded string
- (NSString*)abc_urlDecodedString;

@end

因此,如果我们想给第三方库或者iOS框架里的类添加分类时,最好将分类名和方法名加上前缀。

第26条:勿在分类中声明属性

除了实现文件里的class-continuation分类中可以声明属性外,其他分类无法向类中新增实例变量。

因此,类所封装的全部数据都应该定义在主接口中,这里是唯一能够定义实例变量的地方。

关于分类,需要强调一点:

分类机制,目标在于扩展类的功能,而不是封装数据。

第27条:使用class-continuation分类 隐藏实现细节

通常,我们需要减少在公共接口中向外暴露的部分(包括属性和方法),而因此带给我们的局限性可以利用class-continuation分类的特性来补偿:

  • 可以在class-continuation分类中增加实例变量。
  • 可以在class-continuation分类中将公共接口的只读属性设置为读写。
  • 可以在class-continuation分类中遵循协议,使其不为人知。

第31条:在dealloc方法中只释放引用并解除监听

永远不要自己调用dealloc方法,运行期系统会在适当的时候调用它。根据性能需求我们有时需要在dealloc方法中做一些操作。那么我们可以在dealloc方法里做什么呢?

  • 释放对象所拥有的所有引用,不过ARC会自动添加这些释放代码,可以不必操心。
  • 而且对象拥有的其他非OC对象也要释放(CoreFoundation对象就必须手动释放)
  • 释放原来的观测行为:注销通知。如果没有及时注销,就会向其发送通知,使得程序崩溃。

举个简单的🌰 :


- (void)dealloc {

     CFRelease(coreFoundationObject);
    [[NSNotificationCenter defaultCenter] removeObserver:self];

}

尤其注意:在dealloc方法中不应该调用其他的方法,因为如果这些方法是异步的,并且回调中还要使用当前对象,那么很有可能当前对象已经被释放了,会导致崩溃。

并且在dealloc方法中也不能调用属性的存取方法,因为很有可能在这些方法里还有其他操作。而且这个属性还有可能处于键值观察状态,该属性的观察者可能会在属性改变时保留或者使用这个即将回收的对象。

第36条:不要使用retainCount

在非ARC得环境下使用retainCount可以返回当前对象的引用计数,但是在ARC环境下调用会报错,因为该方法已经被废弃了 。

它被废弃的原因是因为它所返回的引用计数只能反映对象某一时刻的引用计数,而无法“预知”对象将来引用计数的变化(比如对象当前处于自动释放池中,那么将来就会自动递减引用计数)。

第46条:不要使用dispatch_get_current_queue

我们无法用某个队列来描述“当前队列”这一属性,因为派发队列是按照层级来组织的。

那么什么是队列的层级呢?

队列的层及分布

安排在某条队列中的快,会在其上层队列中执行,而层级地位最高的那个队列总是全局并发队列。

在这里,B,C中的块会在A里执行。但是D中的块,可能与A里的块并行,因为A和D的目标队列是并发队列。

正因为有了这种层级关系,所以检查当前队列是并发的还是非并发的就不会总是很准确。

第48条:多用块枚举,少用for循环

当遍历集合元素时,建议使用块枚举,因为相对于传统的for循环,它更加高效,而且简洁,还能获取到用传统的for循环无法提供的值:

我们首先看一下传统的遍历:

1. 传统的for遍历

NSArray *anArray = /* ... */;
for (int i = 0; i < anArray.count; i++) {
   id object = anArray[i];
   // Do something with 'object'
}



// Dictionary
NSDictionary *aDictionary = /* ... */;
NSArray *keys = [aDictionary allKeys];
for (int i = 0; i < keys.count; i++) {
   id key = keys[i];
   id value = aDictionary[key];
   // Do something with 'key' and 'value'
}


// Set
NSSet *aSet = /* ... */;
NSArray *objects = [aSet allObjects];
for (int i = 0; i < objects.count; i++) {
   id object = objects[i];
   // Do something with 'object'

}

我们可以看到,在遍历NSDictionary,和NSet时,我们又新创建了一个数组。虽然遍历的目的达成了,但是却加大了系统的开销。

2. 利用快速遍历:

NSArray *anArray = /* ... */;
for (id object in anArray) {
 // Do something with 'object'
}

// Dictionary
NSDictionary *aDictionary = /* ... */;
for (id key in aDictionary) {
 id value = aDictionary[key];
 // Do something with 'key' and 'value'

}


NSSet *aSet = /* ... */;
for (id object in aSet) {
 // Do something with 'object'
}

这种快速遍历的方法要比传统的遍历方法更加简洁易懂,但是缺点是无法方便获取元素的下标。

3. 利用基于block的遍历:

NSArray *anArray = /* ... */;
[anArray enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop){

   // Do something with 'object'
   if (shouldStop) {
      *stop = YES; //使迭代停止
  }

}];


“// Dictionary
NSDictionary *aDictionary = /* ... */;
[aDictionary enumerateKeysAndObjectsUsingBlock:^(id key, id object, BOOL *stop){
     // Do something with 'key' and 'object'
     if (shouldStop) {
        *stop = YES;
    }
}];


// Set
NSSet *aSet = /* ... */;
[aSet enumerateObjectsUsingBlock:^(id object, BOOL *stop){
     // Do something with 'object'
     if (shouldStop) {
        *stop = YES;
    }
];

我们可以看到,在使用块进行快速枚举的时候,我们可以不创建临时数组。虽然语法上没有快速枚举简洁,但是我们可以获得数组元素对应的序号,字典元素对应的键值,而且,我们还可以随时令遍历终止。

利用快速枚举和块的枚举还有一个优点:能够修改块的方法签名

for (NSString *key in aDictionary) {
         NSString *object = (NSString*)aDictionary[key];
        // Do something with 'key' and 'object'
}

NSDictionary *aDictionary = /* ... */;

    [aDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop){

             // Do something with 'key' and 'obj'

}];

第50条:构建缓存时选用NSCache 而非NSDictionary

如果我们缓存使用得当,那么应用程序的响应速度就会提高。只有那种“重新计算起来很费事的数据,才值得放入缓存”,比如那些需要从网络获取或从磁盘读取的数据。

在构建缓存的时候很多人习惯用NSDictionary或者NSMutableDictionary,但是作者建议大家使用NSCache,它作为管理缓存的类,有很多特点要优于字典,因为它本来就是为了管理缓存而设计的。

1. NSCache优于NSDictionary的几点:

  • 当系统资源将要耗尽时,NSCache具备自动删减缓冲的功能。并且还会先删减“最久未使用”的对象。
  • NSCache不拷贝键,而是保留键。因为并不是所有的键都遵从拷贝协议(字典的键是必须要支持拷贝协议的,有局限性)。
  • NSCache是线程安全的:不编写加锁代码的前提下,多个线程可以同时访问NSCache。

2. 关于操控NSCache删减内容的时机

开发者可以通过两个尺度来调整这个时机:

  • 缓存中的对象总数.
  • 将对象加入缓存时,为其指定开销值。

对于开销值,只有在能很快计算出开销值的情况下,才应该考虑采用这个尺度,不然反而会加大系统的开销。

下面我们来看一下缓存的用法:缓存网络下载的数据

// Network fetcher class
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);

@interface EOCNetworkFetcher : NSObject

- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;

@end

// Class that uses the network fetcher and caches results
@interface EOCClass : NSObject
@end

@implementation EOCClass {
     NSCache *_cache;
}

- (id)init {

     if ((self = [super init])) {
    _cache = [NSCache new];

     // Cache a maximum of 100 URLs
    _cache.countLimit = 100;


     /**
     * The size in bytes of data is used as the cost,
     * so this sets a cost limit of 5MB.
     */
    _cache.totalCostLimit = 5 * 1024 * 1024;
    }
 return self;
}



- (void)downloadDataForURL:(NSURL*)url { 

     NSData *cachedData = [_cache objectForKey:url];

     if (cachedData) {

         // Cache hit:存在缓存,读取
        [self useData:cachedData];

    } else {

         // Cache miss:没有缓存,下载
         EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];      

        [fetcher startWithCompletionHandler:^(NSData *data){
         [_cache setObject:data forKey:url cost:data.length];    
        [self useData:data];
        }];
    }
}
@end

在这里,我们使用URL作为缓存的key,将总对象数目设置为100,将开销值设置为5MB。

3. NSPurgeableData

NSPurgeableData是NSMutableData的子类,把它和NSCache配合使用效果很好。

因为当系统资源紧张时,可以把保存NSPurgeableData的那块内存释放掉。

如果需要访问某个NSPurgeableData对象,可以调用beginContentAccess方发,告诉它现在还不应该丢弃自己所占据的内存。

在使用完之后,调用endContentAccess方法,告诉系统在必要时可以丢弃自己所占据的内存。

上面这两个方法类似于“引用计数”递增递减的操作,也就是说,只有当“引用计数”为0的时候,才可以在将来删去它所占的内存。


- (void)downloadDataForURL:(NSURL*)url { 

      NSPurgeableData *cachedData = [_cache objectForKey:url];

      if (cachedData) {         

            // 如果存在缓存,需要调用beginContentAccess方法
            [cacheData beginContentAccess];

             // Use the cached data
            [self useData:cachedData];

             // 使用后,调用endContentAccess
            [cacheData endContentAccess];


        } else {

                 //没有缓存
                 EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];    

                  [fetcher startWithCompletionHandler:^(NSData *data){                         NSPurgeableData *purgeableData = [NSPurgeableData dataWithData:data];
                         [_cache setObject:purgeableData forKey:url cost:purgeableData.length];

                          // Don't need to beginContentAccess as it begins            
                          // with access already marked
                           // Use the retrieved data
                            [self useData:data];

                             // Mark that the data may be purged now
                            [purgeableData endContentAccess];

            }];
      }
}
复制代码

注意:

在我们可以直接拿到purgeableData的情况下需要执行beginContentAccess方法。然而,在创建purgeableData的情况下,是不需要执行beginContentAccess,因为在创建了purgeableData之后,其引用计数会自动+1;

第51条: 精简initialize 与 load的实现代码

1. load方法

+(void)load;

每个类和分类在加入运行期系统时,都会调用load方法,而且仅仅调用一次,可能有些小伙伴习惯在这里调用一些方法,但是作者建议尽量不要在这个方法里调用其他方法,尤其是使用其他的类。原因是每个类载入程序库的时机是不同的,如果该类调用了还未载入程序库的类,就会很危险。

2. initialize方法

+(void)initialize;

这个方法与load方法类似,区别是这个方法会在程序首次调用这个类的时候调用(惰性调用),而且只调用一次(绝对不能主动使用代码调用)。

值得注意的一点是,如果子类没有实现它,它的超类却实现了,那么就会运行超类的代码:这个情况往往很容易让人忽视。

看一下🌰 :

#import <Foundation/Foundation.h>

@interface EOCBaseClass : NSObject
@end

@implementation EOCBaseClass
+ (void)initialize {
 NSLog(@"%@ initialize", self);
}
@end

@interface EOCSubClass : EOCBaseClass
@end

@implementation EOCSubClass
@end

当使用EOCSubClass类时,控制台会输出两次打印方法:

EOCBaseClass initialize
EOCSubClass initialize

因为子类EOCSubClass并没有覆写initialize方法,那么自然会调用其父类EOCBaseClass的方法。 解决方案是通过检测类的类型的方法:

+ (void)initialize {
   if (self == [EOCBaseClass class]) {
       NSLog(@"%@ initialized", self);
    }
}

这样一来,EOCBaseClass的子类EOCSubClass就无法再调用initialize方法了。 我们可以察觉到,如果在这个方法里执行过多的操作的话,会使得程序难以维护,也可能引起其他的bug。因此,在initialize方法里,最好只是设置内部的数据,不要调用其他的方法,因为将来可能会给这些方法添加其它的功能,那么会可能会引起难以排查的bug。

第52条: 别忘了NSTimer会保留其目标对象

在使用NSTimer的时候,NSTimer会生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那么就会生成保留环。

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end


@implementation EOCClass {
     NSTimer *_pollTimer;
}


- (id)init {
     return [super init];
}


- (void)dealloc {
    [_pollTimer invalidate];
}


- (void)stopPolling {

    [_pollTimer invalidate];
    _pollTimer = nil;
}


- (void)startPolling {
   _pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                                 target:self
                                               selector:@selector(p_doPoll)
                                               userInfo:nil
                                                repeats:YES];
}

- (void)p_doPoll {
    // Poll the resource
}

@end

在这里,在EOCClass和_pollTimer之间形成了保留环,如果不主动调用stopPolling方法就无法打破这个保留环。像这种通过主动调用方法来打破保留环的设计显然是不好的。

而且,如果通过回收该类的方法来打破此保留环也是行不通的,因为会将该类和NSTimer孤立出来,形成“孤岛”:

孤立了类和它的NSTimer

这可能是一个极其危险的情况,因为NSTimer没有消失,它还有可能持续执行一些任务,不断消耗系统资源。而且,如果任务涉及到下载,那么可能会更糟。。

那么如何解决呢? 通过“块”来解决!

通过给NSTimer增加一个分类就可以解决:

#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                         repeats:(BOOL)repeats;
@end



@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                         block:(void(^)())block
                                        repeats:(BOOL)repeats
{
             return [self scheduledTimerWithTimeInterval:interval
                                                  target:self
                                                selector:@selector(eoc_blockInvoke:)
                                                userInfo:[block copy]
                                                 repeats:repeats];

}


+ (void)eoc_blockInvoke:(NSTimer*)timer {
     void (^block)() = timer.userInfo;
         if (block) {
             block();
        }
}
@end

我们在NSTimer类里添加了方法,我们来看一下如何使用它:

- (void)startPolling {

         __weak EOCClass *weakSelf = self;    
         _pollTimer = [NSTimer eoc_scheduledTimerWithTimeInterval:5.0 block:^{

               EOCClass *strongSelf = weakSelf;
               [strongSelf p_doPoll];
          }

                                                          repeats:YES];
}

在这里,创建了一个self的弱引用,然后让块捕获了这个self变量,让其在执行期间存活。

一旦外界指向EOC类的最后一个引用消失,该类就会被释放,被释放的同时,也会向NSTimer发送invalidate消息(因为在该类的dealloc方法中向NSTimer发送了invalidate消息)。

而且,即使在dealloc方法里没有发送invalidate消息,因为块里的weakSelf会变成nil,所以NSTimer同样会失效。

如果我们可以知道集合里的元素类型,就可以修改签名。这样做的好处是:可以让编译期检查该元素是否可以实现我们想调用的方法,如果不能实现,就做另外的处理。这样一来,程序就能变得更加安全。

九、iOS底层原理精华

书中其它部分和之前研究底层原理的内容有交叉,因此,可以参照 底层原理的精华篇幅和文章:

9.1 前知识

9.2 基于OC语言探索iOS底层原理

9.3 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

9.4底层原理相关专题

9.4 iOS相关专题

9.5 webApp相关专题

9.6 跨平台开发方案相关专题

9.7 阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

9.8 Android、HarmonyOS页面渲染专题

9.9 小程序页面渲染专题


延伸阅读(掘金三部曲)

以下为同一作者(J_Knight_)对《Effective Objective-C》的概念 / 规范 / 技巧三分法总结,与本书 52 条一一对应,配有大量示例代码与图示,可作为按条目深挖的补充阅读。

标题链接内容概要
概念篇《Effective Objective-C》干货三部曲(一):概念篇掘金 - 概念篇第 1 条(起源、运行期组件、堆栈)、第 6 条(属性、存取方法、关键字)、第 8 条(等同性、hash)、第 11 条(objc_msgSend)、第 12 条(消息转发、EOCAutoDictionary)、第 14 条(类对象、objc_class、isKindOfClass)、第 21 条(NSError)、第 22 条(NSCopying、浅/深拷贝)、第 29–30 条(引用计数、ARC)、第 37 条(Block 栈/堆/全局)、第 47 条(系统框架)
规范篇《Effective Objective-C》干货三部曲(二):规范篇掘金 - 规范篇第 2 条(向前声明)、第 3–5 条(字面量、类型常量、枚举)、第 7 条(直接访问实例变量)、第 15 条(前缀)、第 18 条(不可变对象、内部可变集合)、第 19–20 条(命名、私有方法前缀)、第 23–27 条(委托、分类分散、分类前缀、勿在分类声明属性、class-continuation)、第 31 条(dealloc)、第 36 条(retainCount)、第 46 条(dispatch_get_current_queue)、第 48 条(块枚举)、第 50 条(NSCache、NSPurgeableData)、第 51 条(load/initialize)、第 52 条(NSTimer 保留环与 block 方案)
技巧篇《Effective Objective-C》干货三部曲(三):技巧篇掘金 - 技巧篇第 9 条(类族模式)、第 10 条(关联对象、UIAlertView+block)、第 13 条(方法调配、lowercaseString 示例)、第 16 条(全能初始化、子类覆写、initWithCoder)、第 17 条(description)、第 28 条(匿名对象)、第 32–35 条(异常安全、弱引用、自动释放池块、僵尸对象)、第 38–45 条(block typedef、handler 块、保留环、串行队列/barrier、GCD vs performSelector、NSOperation、dispatch group、dispatch_once)、第 49 条(无缝桥接)

参考文献

[1] Galloway, M. Effective Objective-C 2.0: 52 Specific Ways to Write Better iOS and OS X Programs. Addison-Wesley Professional, 2013.
[2] O'Reilly. Effective Objective-C 2.0 — Table of Contents and Chapter Summaries. www.oreilly.com/library/vie…
[3] Apple. Objective-C Runtime Programming Guide. Developer Documentation Archive.
[4] Apple. The Objective-C Programming Language (Legacy).
[5] Apple. Advanced Memory Management Programming Guide. developer.apple.com/library/arc…
[6] Apple. About Memory Management. developer.apple.com/library/arc…
[7] Apple. Transitioning to ARC Release Notes. developer.apple.com/library/arc…
[8] Clang. Automatic Reference Counting (ARC). clang.llvm.org/docs/Automa…
[9] Stack Overflow. Block retain cycle, weak-strong dance.
[10] Apple. Working with Blocks. Programming Guide.
[11] Apple. Dispatch (GCD). Concurrency Programming Guide.
[12] Apple. Message Forwarding. Objective-C Runtime Guide. developer.apple.com/library/arc…
[13] J_Knight_. 《Effective Objective-C》干货三部曲(一):概念篇. 掘金,2018-01-08. juejin.cn/post/684490…
[14] J_Knight_. 《Effective Objective-C》干货三部曲(二):规范篇. 掘金,2018-01-10. juejin.cn/post/684490…
[15] J_Knight_. 《Effective Objective-C》干货三部曲(三):技巧篇. 掘金,2018-01-12. juejin.cn/post/684490…