2017/12/31
第一章 熟悉Objective-C
第1条 了解Objective-C语言的起源
- Objective-C为C语言添加了面向对象的特性,是其超急。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息后,究竟应执行何种代码,由运行环境而非编译器来决定。
- 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型。
第2条 在类的头文件中尽量少引入其他头文件
- 除非确有必要,否则不要进入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类要遵守某个协议。这种情况下,尽量把“该类遵守某协议”的这条声明移至“分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
第3条 多用字面量语法
- 应该使用字面量语法, 以使代码更简明扼要。
- 字符串 @“str” ✔️
- 数值 @10 ✔️
- 数组 @[] ✔️
- 字典 @{} ✔️
- 应使用下标来访问数组,键访问字典。
- arr[10] ✔️
- dict[@"name"] ✔️
- 字面量创建数组或字典时,若值中有
nil
,会抛出异常。请确保值不含nil
。- @[nil] ❌
- @{nil} ❌
第4条 多用类型常量,少用#define预处理指令
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只会在编译时做替换操作。即使有人重新定义了常量值,编译器也不会发出警告。还有个弊端是会产生N多份临时变量。
- 在实现文件中使用
static const
来定义只在当前文件可见的常量,例如static NSString *const kAnimationName = @“greatWave”
- 在头文件中使用
extern
来声明全局常量,并在相关实现文件中定义值。这种常量会出现在全局符号中,所以其名称应以类名做前缀,如UIAplicationDidBecomeActiveNotification
第5条 用枚举表示状态、选项、状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果值为可选项类型(可多选),那就定义值为
2^N
,以便可以使用按位或操作。 - 用
NS_ENUM
与NS_OPTIONS
宏来定义枚举类型,并指明底层数据类型例如NS_ENUM(NSUInteger, VLCPlayerState)
。 - 在用
switch
处理枚举时不要有default
分支,这样在加入新枚举后编译器会提示有未处理的枚举。
第二章 对象、消息、运行期
第6条 理解“属性”这一概念
- 用
@property
来定义对象的属性。 - 通过“特质”来指定存储数据所需的正确语义。
- iOS开发应该用
nonatomic
,因为atomic
会严重影响性能。
第7条 在对象内部尽量直接访问实例变量
- 在对象内部读取数据时,应该直接通过追李变量来读,而写入数据时,则应该通过属性来写。
- 在初始化方法及
dealloc
方法总,总是应该直接通过实例变量来读写数据。 - 懒加载时,通过属性来读取数据。
第8条 理解“对象等同性”这一概念
- 检测对象等同性应提供
isEqual
与hash
方法。 -
==
只会对比两个指针所指对象的地址。 - 相同对象必须具有相同
hash
,但hash
相同未必对象相同。 - 编写
hash
方法时,应使用计算速度快而且hash
碰撞率低的算法。
第9条 以“类族模式”隐藏实现细节
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架里的大部分collection类都是类族,例如NSArray、NSMutableArray。
第10条 在既有类中使用关联对象存放自定义数据
- 只有在其他做法不可行时采用,否则可能引入难以查找的bug。
- 使用方法:
void setAssociatedObject(id object, void\*key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, void\*key);
void objc_removeAssociatedObjects(id object)
第11条 理解objc_msgSend的作用
void objc_msgSend(id self, SEL cmd, ...)
- 举例
id return = [git commit:parameter];
上面的Objective-C方法在运行时会转换成如下函数:
id return = objc_msgSend(git, @selector(commit), parameter);
第12条 理解消息转发机制
第13条 用“方法调配技术”调试“黑盒方法”
第14条 理解“类对象”的用意
第三章 接口与API
第15条 用前缀避免命名冲突
第16条 提供“全能初始化方法”
- 所有初始化方法最终都会调用到这个方法,便于统一管理
第17条 实现description方法
- debugDescription方法是在开发者在调试器中以控制台命令打印对象时调用的,如po
第18条 尽量使用不可变对象
- 尽量把对外开放的属性设为readonly,而且可能的话尽量不要对外开放(隐藏细节)。
- 不想被外部改变的属性,对外声明时应该使用readonly,而在内部分类中重新声明为readwrite。
- 不要把可变的collection作为属性公开,而应该提供相关方法,以此修改对象中的可变collection。
- 例如:
// Person.h
@interface Person:NSObject
@property (nonatomic, strong, readonly) NSSet *friends;
@end
// Person.m
@implementation Person {
NSMutableSet *_internalFriends;
}
- (NSSet *)friends {
return [_internalFriends copy];
}
- (void)addFriend:(Person *)person {
[_internalFriends addObject:person];
}
@end
第19条 使用清晰而协调的命名方式
- 尽量不要使用缩略词。
- 如果方法返回值是新创建的,那么方法名的首个词应是返回值的类型(如-intValue;+string),除非另有修饰词(如:- lowercaseString;)。
- Boolean属性应选用has、is当前缀。
- 将get这个前缀留给借由“输出参数”来保存返回值的方法,如
-(void)getBytes:(uchar \*)buffer range:(Range)aRange;
第20条 为私有方法名加前缀
- 给私有方法名加上前缀,建议使用
p_
,便于与公开方法区分。 - 不要使用单个下划线
_
作为私有方法名的前缀,因为这是预留给苹果公司用的,这可能会覆盖苹果的私有方法。
第21条 理解Objective-C错误模型
- NSError封装了三个属性 {domain code info}
- 只有发生了严重错误时才使用异常NSException
- 小错误可以返回NSError给调用者处理
第22条 理解NSCopying协议
- 要实现
copy
就要重写- (id)copyWithZone:(NSZone \*)zone;
- 要实现
mutableCopy
就要重写- (id)mutableCopyWithZone:(NSZone \*)zone;
(以前会把内存分成不同的zone,现在只有一个zone,所以不必担心这个参数) - 复制对象时需决定深拷贝还是浅拷贝,尽量用浅拷贝。
第四章 协议与分类
第23条 通过代理与数据源协议进行对象间通信
- delegate属性一定要是weak,否则会引入“保留环”
// 如果频繁调用如下代码:
if ([_delegate respondsToSelecter:@selecter(someClassDidSomething:)]) {
[_delegate someClassDidSomething];
}
// 可考虑将检查加过缓存到本地标记:
_delegateFlags.didUpdateProgressTo = [_delegate respondsToSelecter:@selecter(someClassDidSomething:)]
...
if (_delegateFlags.didUpdateProgressTo) {
[_delegate didUpdateProgressTo:progress]
}
第24条 将类的实现代码分散到便于管理的数个分类之中
- 使用分类机制把类的实现代码划分成易于管理的小块。
- 将应该视为“私有”的方法归入名为Private的分类中,以隐藏实现细节。
第25条 总是为第三方类的分类名称加入前缀
- 添加分类的时候要注意是否会覆写已有方法,因加载时机不确定很可能发生覆盖或被覆盖,这种bug很难查。
- 向第三方类中添加分类时,应该给其名称及方法加上你的专用前缀,以减少覆写情况。
第26条 勿在分类中声明属性
- 为分类添加属性技术上可行(见第6条),但尽量避免,因为这种方法无法把实现属性所需的实例变量合成出来。
- 把封装数据所用的全部属性都定义在主接口里。
- 在分类中可以定义存取方法,但尽量不要定义属性。
第27条 使用匿名扩展隐藏实现细节
- 通过扩展向类中增加实例变量。
- 如果某属性在主接口中声明为readonly,那可以在扩展中重新声明为readwrite。
若想使类遵循的下一不为人知,可在扩展中声明。
第28条 通过协议提供匿名对象
第5章 内存管理
第29条 理解引用计数
第30条 以ARC简化引用计数
- ARC只负责管理OC对象的内存,CF开头的对象要开发者管理。
第31条 在dealloc方法中只释放引用并解除监听
- dealloc里应该做的事是释放指向其他对象的引用,并取消订阅KVO或Notification等通知,不要做其他事。
- 如果持有文件秒速符等系统资源,就应该编写一个方法来释放这种资源。
- 不要在dealloc里做异步任务,因为此时对象已经在回收状态了。
第32条 编写“异常安全代码”时留意内存管理问题
- OC中抛出异常的时候可能会引起内存泄漏
- 在@try捕获异常中清理干净
第33条 以弱引用避免保留环
- 以弱引用避免“保留环(Retain Cycle)”
第34条 以“自动释放池”降低内存峰值
- 自动释放池排布在栈中。
- 自动释放池一般放在可能引起内存峰值比较高的地方,如在for循环创建大量临时变量。
- 写法:
@autoreleasepool {
// ...
}
第35条 用“僵尸对象”调试内存管理问题
- 在Scheme->Run->Diagnostics->“Enable Zombie Objects”,勾选即可开启此功能。
- 开启此功能后,系统回收对象时不是释放对象,而是转为僵尸对象,僵尸对象能响应所有选择子,响应方式为打印相关信息然后终止程序。
第36条 不要使用retainCount
- 对象的保留计数并不能绝对准确反映对象的生命期。
- 在苹果引入ARC之后retainCount已经正式废弃,任何时候都不要调用这个retainCount方法来查看引用计数了,因为这个值实际上已经没有准确性了。但是在MRC下还是可以正常使用
块与大中枢派发
第37条 理解“块”这一概念
- 块是闭包。
- 块可以分配在栈块、堆块或全局块上,要注意块的作用域
// 不安全的块,因为A块、B块都是分配在栈上的,离开了if语句可能会被释放
void (^block)();
if (condition_1) {
block = ^{ print("A") };
} else {
block = ^{ print("B") };
}
block();
// 若将块拷贝copy就会搬到堆上,就会变成对象,就会安全
block = [^{ print("A") } copy];
block = [^{ print("B") } copy];
第38条 为常见的块类型创建typedef
- 主要是为了考虑代码可读性。
第39条 用handler块降低代码分散程度
- 异步操作中使用block的方式设计,这样业务相关的代码会比较紧凑,不会显得那么凌乱。
第40条 用块引用其所属对象时不要出现保留环
- 不是一定得在block中使用weakself,block 不是被self所持有的,在block中就可以直接使用self
第41条 多用派发队列,少用同步锁
- 这种写法效率很低,也不能保证线程中绝对安全:
// 1. synchronized
@synchronized(self) {
return _someString;
}
// 2. nslock
NSLock *_locker = [[NSLock alloc] init];
[_locker lock];
// safe code
[_locker unlock];
- 应该用GCD来替换:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//读取字符串
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString*)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
第42条 多用GCD,少用performSelector系列方法
- performSelector有很多缺点:
- 内存管理问题:在ARC下我们经常会看到编译器发出如下警告:warning: performSelector may cause a leak because its selector is unknown [-Warc-performSelector-leaks];
- 返回值只能是void或对象类型;
- 无法处理带有多个参数的选择子,最多只能处理两个参数
- 用dispatch系列函数代替
第43条 掌握GCD以及操作队列的使用时机
- 需要的时候可以用NSOperation和NSOperationQueue代替GCD
- 使用NSOperation和NSOperationQueue的优点:
- 支持取消某个操作
- 支持指定操作间的依赖关系
- 支持指定操作的优先级
- 重用NSOperation对象
第44条 通过Dispatch Group机制,根据系统资源状况来执行任务
- DispatchGroup可将任务分组,然后等待这组任务执行完毕时会有通知,开发者可以拿到结果然后继续下一步操作:
// A B C将会同步执行
dispatch_group_async({...A})
dispatch_group_async({...B})
dispatch_group_async({...C})
// 以上都执行完再执行notify
dispatch_group_notify({...})
- 通过dispatch group在并发队列上同时执行多项任务的时候,GCD会根据系统资源状态来帮忙调度这些并发执行的任务。
第45条 使用dispatch_once来执行只需要运行一次的线程安全代码
- dispatch_once比较高效,没有重量级的同步机制, 常用于单例模式:
+ (instancetype)sharedInstance {
static id _instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_instance = [[super allocWithZone: NULL] init];
});
return _instance;
}
+ (id)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
- (id)copyWithZone:(struct _NSZone *)zone {
return [[self class] sharedInstance];
}
- (instancetype)init {
if (self = [super init]) {
<#code#>
}
return self;
}
第46条 不要使用dispatch_get_current_queue
- dispatch_get_current_queue 函数的行为常常与开发者所预期的不同,iOS 6.0起此函数已经废弃,只应做调试之用。
第7章 系统框架
第47条 熟悉系统框架
- Foundation OC基础框架,其中的类以NS为前缀
- CoreFoundation C语言框架,CF前缀,可与Foundation无缝桥街
- AVFoundation OC对象可用来回访并录制音频及视频。
- CFNetwork C语言级别的网络通信能力。
- CoreAudio C语言API可以用来操作设备上的音频硬件。
- CoreData OC接口可以将对象放入数据库,将数据持久化。
- CoreText C语言接口可以高效执行文字排版以及渲染操作。
- SpriteKit 游戏框架
- CoreLocation、MapKit 定位地图相关框架
- Social 社交网络框架
- AddressBook 需要使用通讯录时才使用该框架
- MusicLibraries 音乐库相关框架
- HealthKit 健康相关框架
- HomeKit 为智能化硬件提供的框架
- CloudKit iCloud相关的框架
- NSLinguisticTagger 解析自然语言,如动词,名词等
- UIKit 构建在基础框架之上,包含UI元素和粘合机制
- CoreAnimation 是QuartzCore框架的一部分,OC级别的核心动画库,用以图形渲染
- CoreGraphics C语言级别2D渲染所必备的数据结构与函数,如CGRect、CGPoint
第48条 多用块枚举,少用for循环
- 遍历有4方法:for、NSEnumerator、for—in、块枚举
- 块枚举法是通过GCD来并发执行遍历操作,当属最高效方法。
- 若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。
- 例如字典遍历:
[dict enumeratKeysAndObjectsUsingBlock: ^(NSString \*key, NSString \*obj){
// ...
}];
第49条 对自定义其内存管理语义的collecion使用无缝桥接
- 通过无缝桥接技术,可以在定义于Foundation框架中的类和CoreFoundation框架中的C语言数据结构之间来回转换。
下面代码展示了简单的无缝桥接:
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
//Output: Size of array = 5
转换操作中的__bridge告诉ARC如何处理转换所涉及的OC对象,也就是ARC仍然具备这个OC对象的所有权。__bridge_retained
则意味着ARC将交出所有权。
- 使用
CFRelease(aCFArray)
释放对象。
第50条 构建缓存时选用NSCache而非NSDictionary
- NSCache会在系统资源紧急时自动释放缓存。
- NSCache是线程安全的。
- NSCache会先行释放最久未使用的对象。
- 只有重新计算、获取开销很大的数据才值得缓存,如网络获取,磁盘读取。
第51条 精简initialize与load的实现代码
- oad与initialize 方法都应该实现的精简一点,这样有助于保持应用程序的响应能力,也可以减少引入依赖环的几率
第52条 别忘了NSTimer会保留其目标对象
- 在iOS开发中经常会用到定时器:NSTimer,由于NSTimer会生成指向其使用者的引用,而其使用者如果也引用了NSTimer,那就形成了该死的循环引用。