这是一份《Effective Objective-C 2.0: 编写高质量 iOS 与 OS X 代码的 52 个有效方法》(作者:Matt Galloway)的核心内容总结文档。这本书是 Objective-C 开发者的经典读物,深入探讨了最佳实践和常见陷阱。
核心目标: 帮助你编写更清晰、更健壮、更高效、更易维护的 Objective-C 代码。
核心思想总结:
-
深入理解语言特性: Objective-C 基于 C,但核心是 Smalltalk 式的消息传递机制(非严格的方法调用)和动态运行时。理解
isa
指针、消息派发 (objc_msgSend
)、方法解析和转发机制是基础。 -
内存管理是核心: 虽然 ARC 极大地简化了内存管理,但理解引用计数(Retain, Release, Autorelease)的原理、所有权修饰符 (
__strong
,__weak
,__unsafe_unretained
,__autoreleasing
) 以及循环引用的形成与避免(特别是block
和delegate
)至关重要。ARC 不是万能的。 -
接口设计清晰化: 类、方法、变量的命名要清晰一致,使用前缀避免冲突,设计良好的
API
,利用初始化方法和存取方法保证对象状态的有效性。 -
拥抱协议和分类: 协议 (
@protocol
) 定义清晰的行为契约,支持多态和委托模式。分类 (@category
) 是向现有类添加方法的强大工具,但需谨慎避免命名冲突和覆盖原有方法。 -
善用 Block 和 GCD: Block 是强大的闭包机制,简化回调、异步和函数式编程,但要特别注意捕获语义和循环引用。Grand Central Dispatch (GCD) 是管理并发和异步任务的现代、高效方式,理解队列 (
dispatch_queue_t
)、同步/异步派发 (dispatch_sync
/async
) 和任务组 (dispatch_group_t
) 是关键。 -
熟悉系统框架: Foundation 和 Core Foundation 框架提供了强大的基础类 (
NSString
,NSArray
,NSDictionary
,NSSet
,NSNumber
等) 和机制 (如NSCopying
,NSFastEnumeration
)。理解它们的特性(可变性、类簇、toll-free bridging
)和高效使用方法是提升代码质量的基础。 - 性能考虑: 避免过早优化,但需了解常见性能瓶颈(如消息发送开销、对象创建销毁、集合操作、I/O、不合理的锁使用),并使用 Instruments 进行性能分析。优化应基于测量结果。
52 个有效方法章节与核心要点总结:
第 1 章:熟悉 Objective-C
-
了解 Objective-C 语言的起源: OC 是 C 的超集,核心是动态消息结构(非函数调用)。理解
id
类型和运行时的重要性。 -
在类的头文件中尽量少引入其他头文件: 使用
@class
前向声明减少编译依赖,提高编译速度,避免循环引用。在实现文件中#import
。 -
多用字面量语法,少用与之等价的方法: 使用
@"string"
,@42
,@[], @{}
等使代码更简洁、安全(nil
插入会抛出异常)。 -
多用类型常量,少用 #define 预处理指令: 使用
static const
声明类型明确的常量,优于无类型的#define
。全局常量应在实现文件中定义,头文件extern
声明。 -
用枚举表示状态、选项、状态码: 使用
NS_ENUM
(通用枚举)和NS_OPTIONS
(位掩码选项)明确枚举类型和基础类型,提高可读性和类型安全。
第 2 章:对象、消息、运行期
-
理解“属性”的概念: 属性 (
@property
) 自动合成存取方法 (getter/setter
)、实例变量 (_ivar
),指定语义 (strong
,weak
,copy
,assign
,atomic
/nonatomic
) 和存取控制。@synthesize
和@dynamic
的用法。 -
在对象内部尽量直接访问实例变量: 在
init
、dealloc
和自定义的存取方法内部直接访问_ivar
更安全高效(避免 KVO 和子类覆写的影响)。其他情况通常通过属性访问。 -
理解“对象等同性”的重要性: 重写
isEqual:
和hash
方法来判断对象内容的等价性(而非指针相等==
)。确保等价对象拥有相同hash
值。 -
以“类族模式”隐藏实现细节: 使用抽象基类(如
NSArray
,NSString
)和工厂方法返回具体的私有子类实例,简化公共接口,隐藏实现。 -
在既有类中使用关联对象存放自定义数据: 使用
objc_setAssociatedObject
和objc_getAssociatedObject
在运行时为现有类实例动态添加数据(谨慎使用,可能引入难以调试的问题)。 -
理解
objc_msgSend
的作用: 深入理解消息发送机制(查找方法实现 IMP 的过程)是理解 OC 动态性的基础。 -
理解消息转发机制: 当对象无法响应消息时,运行时提供
resolveInstanceMethod:
(动态添加方法)、forwardingTargetForSelector:
(备用接收者)、methodSignatureForSelector:
+forwardInvocation:
(完整转发) 三次补救机会。 -
用“方法调配技术”调试“黑盒方法”: 运行时使用
class_getInstanceMethod
,method_exchangeImplementations
等函数交换方法实现 (Method Swizzling
),用于调试或扩展,但极其危险,应谨慎并仅作最后手段。 -
理解“类对象”的用意:
Class
类型本身也是对象(元类实例),理解isa
指针链 (实例 -> 类 -> 元类 -> 根元类 -> 自身
),使用class
和superclass
方法。
第 3 章:接口与 API 设计
-
用前缀避免命名空间冲突: 为类名、分类名、全局函数/常量使用独特的三字母前缀(如
ABC
),防止与系统库或其他第三方库冲突。 - 提供“全能初始化方法”: 指定一个主要的初始化方法(通常参数最全),其他初始化方法最终调用它。确保继承链上的全能初始化方法一致。
-
实现
description
方法: 重写description
方法返回人类可读的对象描述,对调试非常有帮助。debugDescription
用于调试器 (LLDB
po
命令)。 -
尽量使用不可变对象: 设计类时,优先将属性声明为
readonly
,仅在必要时暴露为readwrite
(通常在类扩展中)。不可变对象更易理解、线程安全。 - 使用清晰而协调的命名方式: 方法名应清晰表达意图和参数作用(长命名是 OC 风格),遵循驼峰命名法,保持一致性。避免歧义。
-
为私有方法名加前缀: 使用前缀(如
p_
)标记私有方法,避免与父类或子类方法意外覆盖,提高可读性。 -
理解 Objective-C 错误模型: OC 偏向使用
NSError **
模式处理可恢复的错误(预期可能发生的错误)。Exceptions
(异常) 仅用于不可恢复的编程错误(如数组越界),不应用于常规错误处理流程。@throw
在 OC 中极少用。
第 4 章:协议与分类
-
理解
NSCopying
协议: 若自定义对象需要支持拷贝 (copy
方法),需实现NSCopying
协议中的copyWithZone:
方法。区分浅拷贝(新容器,元素引用不变)和深拷贝(新容器,新元素)。通常提供专门的深拷贝方法。 -
通过委托与数据源协议进行对象间通信: 使用委托模式 (
Delegate
) 向相关对象通知事件或请求数据。定义清晰的协议,属性用weak
避免循环引用。数据源模式 (DataSource
) 专门用于为对象提供数据。 -
将类的实现代码分散到便于管理的数个分类之中: 使用分类将大型类按功能模块拆分到不同文件中。创建名为
Private
的分类隐藏私有方法声明(在.m文件中实现)。 - 总是为第三方类的分类名称加前缀: 向系统类或第三方库的类添加分类时,务必在分类名和方法名前加前缀,避免命名冲突。
-
勿在分类中声明属性: 分类原则上不能添加实例变量(除非使用关联对象)。在分类中声明属性,只会生成
getter/setter
方法的声明,不会生成实例变量和实现,编译器会警告。如需“属性”,需手动实现存取方法(通常借助关联对象,但这破坏了封装性,非首选)。
第 5 章:内存管理
-
理解引用计数: 核心原理:
retain
(+1),release
(-1),autorelease
(延迟 -1)。对象计数为 0 时销毁。理解自动释放池 (@autoreleasepool
) 的作用。 -
理解 ARC 如何解决引用计数问题: ARC 在编译期自动插入
retain
,release
,autorelease
调用。理解 ARC 规则:变量默认__strong
,方法名约定(alloc/new/copy/mutableCopy
返回的对象调用者持有)。 -
在
dealloc
方法中只释放引用并解除监听:dealloc
中应调用[_resource release]
(MRC) 或置nil
(ARC),移除 KVO 观察者和通知监听。不要调用异步方法、属性存取器(可能触发 KVO)、或[self method]
(对象已部分销毁)。 -
编写“异常安全代码”时留意内存管理问题: MRC 下,
@try
块中创建的对象可能因异常跳过release
,需在@finally
释放。ARC 下默认开启-fobjc-arc-exceptions
,会生成额外代码处理异常路径的内存,但有开销。C++ 异常与 OC 异常交互复杂。 -
以弱引用避免循环引用: 相互强引用导致对象无法释放。使用
__weak
(ARC) 或weak
属性打破强引用环(如delegate
,block
内部捕获self
时)。 -
以“自动释放池块”降低内存峰值: 在循环中创建大量临时对象时,用
@autoreleasepool {}
包裹循环体,让临时对象在池排干时释放,降低内存峰值。 -
用“僵尸对象”调试内存管理问题: 开启
NSZombieEnabled
环境变量,使已释放对象变成“僵尸”,再次向其发送消息时会抛出明确异常(而非野指针崩溃),有助于定位过度释放问题。仅在调试时使用。
第 6 章:块与大中枢派发 (Blocks & Grand Central Dispatch)
-
理解“块”这一概念: Block 是闭包(函数 + 捕获的上下文变量)。语法:
^returnType (parameters) {...}
。内存布局:isa
指针、标志位、函数指针、捕获变量描述符/值。 -
为常用的块类型创建
typedef
: 使用typedef
定义复杂的块类型,提高代码可读性和可维护性。例如:typedef void (^CompletionHandler)(NSData *data, NSError *error);
-
用
Handler Block
降低代码分散程度: 使用 Block 代替委托 (Delegate
) 或回调函数 (Callback Function
),将业务逻辑集中在一起(如网络请求完成后的处理),避免状态分散。 -
用 Block 引用其所属对象时不要出现循环引用: Block 会强引用其捕获的所有对象(包括
self
)。若 Block 又被self
强引用(如self.blockProperty = aBlock;
),则形成循环引用。解决方案:在 Block 外使用__weak
引用self
,在 Block 内使用__strong
引用弱引用来保证执行期间self
存活。__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; // 避免 weakSelf 在 block 执行过程中被释放 [strongSelf doSomething]; };
-
多用 GCD,少用同步锁: 优先使用 GCD 队列 (
dispatch_queue_t
) 实现同步:-
串行队列同步访问: 将读写任务都派发到同一个串行队列 (
dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_SERIAL)
)。 -
并发队列与栅栏块 (
dispatch_barrier_async
): 对于读写频繁,读可并发,写需独占的场景。读操作使用dispatch_sync
(并发执行),写操作使用dispatch_barrier_async
(确保写时独占)。 - 避免低效的
@synchronized
或NSLock
(性能较差且易出错)。
-
串行队列同步访问: 将读写任务都派发到同一个串行队列 (
- 使用 GCD 队列及 Block 实现操作同步,而非锁: 强调同上(第 38 条)。用队列管理任务执行的顺序。
-
使用
dispatch_group
来执行并行任务,并在任务完成后得到通知: 使用dispatch_group_async
将多个并发任务关联到同一个组 (dispatch_group_t
),用dispatch_group_notify
在所有任务完成后执行回调。dispatch_group_enter
/leave
管理非 Block API 的任务。 -
使用
dispatch_once
来执行只需运行一次的线程安全代码: 实现单例模式或其他一次性初始化。标准、线程安全的单例模板:+ (instancetype)sharedInstance { static MyClass *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; }
-
掌握 GCD 及操作队列的使用时机: GCD 是轻量级的 C API,适合大多数并发和同步任务。
NSOperationQueue
基于 GCD 构建,提供更高级特性:操作依赖 (addDependency:
)、取消 (cancel
)、优先级 (queuePriority
)、KVO 监听 (isFinished
,isCancelled
)。需要这些特性时选用NSOperationQueue
。
第 7 章:系统框架
-
熟悉系统框架: 首要掌握 Foundation (
NS
前缀) 和 CoreFoundation (CF
前缀, C API),它们是 Cocoa/Cocoa Touch 的基石。了解 UIKit/AppKit, Core Animation, Core Graphics 等。 -
多用 Block 枚举,少用
for
循环: 使用容器类 (NSArray
,NSSet
,NSDictionary
) 的基于 Block 的枚举方法 (enumerateObjectsUsingBlock:
,enumerateKeysAndObjectsUsingBlock:
):- 可并行枚举 (
NSEnumerationConcurrent
) - 可反向枚举 (
NSEnumerationReverse
) - 可安全修改集合(某些情况下)
- 代码更集中。性能通常接近或优于
for
循环。
- 可并行枚举 (
-
对自定义其内存管理语义的 collection 使用无缝桥接: 理解 Toll-Free Bridging:某些 Foundation 对象 (
NSArray
,NSString
,NSDictionary
) 与对应的 CoreFoundation 对象 (CFArrayRef
,CFStringRef
,CFDictionaryRef
) 在内存层面等价,可强制类型转换 (__bridge
,__bridge_retained
,__bridge_transfer
)。 -
构建缓存时选用
NSCache
而非NSDictionary
:NSCache
是专为缓存设计的类:- 自动删除策略:内存不足时自动清除,可设置成本 (
countLimit
,totalCostLimit
)。 - 线程安全。
- 不会拷贝键 (
Key
),而是保留 (retain
)。
- 自动删除策略:内存不足时自动清除,可设置成本 (
-
精简
initialize
与load
的实现代码:-
+(void)load
: 类或分类被加载到运行时时调用(非常早)。方法应精简,避免调用其他可能未加载的类。分类的load
也会执行。 -
+(void)initialize
: 类在首次接收消息前(通常是首次使用)由运行时调用一次(线程安全)。父类先于子类调用。应精简,避免复杂逻辑或调用子类可能覆写的方法。
-
-
不要使用
dispatch_get_current_queue
: 此函数行为复杂且易导致死锁(尤其涉及队列层级时),已被废弃。调试可使用队列特有数据 (dispatch_queue_set_specific
/dispatch_get_specific
) 替代。
关键建议回顾:
- 命名清晰、一致、带前缀。
- 理解内存管理 (ARC/引用计数) 和循环引用。
- 优先使用不可变对象。
- 设计清晰的全能初始化方法和 API。
- 善用协议 (委托/数据源) 和分类 (模块化)。
- 深入理解 Block 的捕获语义和循环引用风险。
- 优先使用 GCD 队列 (
串行
、并发+栅栏
、组
、once
) 进行并发和同步,避免锁。 - 使用
NSCache
做缓存。 - 熟悉 Foundation/CoreFoundation 的核心类及其特性(字面量、Block 枚举、桥接)。
- 利用运行时特性 (
description
, 消息转发),但谨慎使用关联对象和方法调配。
这份总结提炼了书中的核心精髓和最佳实践。要真正掌握,强烈建议仔细阅读原书每个条款的详细解释、示例代码和背后的原理分析。Objective-C 虽然逐渐被 Swift 取代,但其设计思想和与 Cocoa/Cocoa Touch 框架的深度集成,对于理解 iOS/macOS 开发生态仍有重要价值。在维护旧代码或深入理解底层机制时,这些知识尤为重要。