Effective Objective 2.0
Effective Objective 2.0
(编写高质量iOS与OS X代码的52个有效方法)
1. runtime :
- 消息机制
- 消息转发
2. 在类的头文件中尽量少引入其他头文件
- 减少编译时间,降低耦合
- 使用import而非include指令不会导致循环引用,但两个类里有一个无法被正确编译,建议使用@class 向前声明
- 有些协议,例如委托协议(delegate protocol)不用单独写一个头文件
- 协议单独写.h.m文件,或者在委托协议里声明协议,
- 一般遵循协议,建议写在扩展分类后边(代理对象的扩展分类),向外界隐藏实现
3. 多用字面量语法,少用与之等价的方法
- 字面量语法创建数组或者字典时,若值中有nil,则会抛出异常
4. 多用类型常量,少用#define预处理指令
不要用预处理指令代替常量,不含类型信息,并且不安全
用
static const NSTimeinterval kAnimationDuration = 0.3;
代替_#define ANIMATION_DURATION 0.3
OC 没有“名称空间”(namespace)这一概念
若常量局限于实现文件之内,则在前面加字母k,若常量在类之外可见,则通常以类名为前缀
定义常量的位置很重要。
在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值,这种常量为全局常量,会出现在全局符号表中,其名称应添加类名前缀
也可以单独定义.h.m文件用来声明所有的全局常量。
5. 用枚举表示状态、选项、状态码
<< 左移运算符
宏为完全替代操作
_#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
typedef enum EOCConnectionState : NSUInteger EOCConnectionState;
enum EOCConnectionState : NSUInterger {
};
6. 理解”属性”这一概念
属性是封装数据的方式。
OC 把实例变量当做一种存储偏移量所用的“特殊变量”,交由类对象(class object)保管
autosynthesis 自动合成这个过程有编译器在编译器执行
@porperty 关键字告诉编译器需要自动合成setter和getter方法,并生成相应的实例变量
@synthesize 关键字一般不用写,如果重写了setter和getter方法,需要指明需要合成的实例变量,单个重写setter 或 getter方法不需要写
@dynamic 关键字,它会告诉编译器: 不要自动创建实例变量,也不要创建setter和getter方法
@dynamic 需要和预先准备的实例变量进行绑定(赋值),需要手动合成setter和getter方法,运行时如果没有存取方法,直接报错
内存管理语义
- assign: 简单赋值操作
- strong: 设置方法会先保留新值,并释放旧值,然后再设置新值
- weak : 设置方法既不保留新值,也不释放旧值,特质同assign,在属性所指向的对象被废弃时,属性值清空
- unsafe_unretained : 特质同assign,但是用于对象类型(object type)
- copy : 类似于strong特质,但设置方法并不保留新值,保留的是新值的copy
MRC
** strong 修饰: **
-(void)setObject:(NSObject *)obj {
if(_obj != obj) {
[obj retain]; // 保留新值
[_obj release]; // 释放旧值
_obj = obj; // 设置新值
}
}
** weak 修饰: **
-(void)setObject:(NSObject *)obj {
if(_obj != obj) {
_obj = obj; // 不保留新值,不释放旧值,直接设置
}
}
** copy 修饰: **
- (void)setObject:(NSObject *)obj {
if(_obj != obj) {
[_obj release]; // 释放旧值
_obj = [obj copy]; // 设置新值copy
}
}ARC
** strong 修饰: **
- (void)setObject:(NSObject *)obj {
if(_obj != obj) {
_obj = obj; // 设置新值
}
}
** weak 修饰: **
- (void)setObject:(NSObject *)obj {
if(_obj != obj) {
_obj = obj; // 不保留新值,不释放旧值,直接设置新值
}
}
** copy 修饰: **
- (void)setObject:(NSObject *)obj {
if(_obj != obj) {
_obj = [obj copy]; // 设置新值copy
}
}
7. 在对象内部尽量直接访问实例变量
- 在写入实例变量时,通过其设置方法来做,而在读取实例变量时,则直接使用实例变量
- 在初始化方法及dealloc方法中应该直接访问实例变量
- 惰性初始化需要通过属性来读取数据
8. 理解“对象等同性”这一概念
- 相同的对象必须具有相同的哈希码,但两个哈希码相同的对象却未必相同
- NSString isEqualToString
- NSArray isqualToArray
- NSDictionary isEqualToDictionary
9. 以“类簇模式”隐藏实现细节
- 工厂模式(factory pattern)是创建类族的办法之一
- 系统框架中有许多类族,大部分collection类都是类族,NSArray,NSMutableArray
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面
- 判断类族不能用isMemberOfClass, 也不能能 = ,需要用isKindOfClass
10. 在既有类中使用关联对象存放自定义数据
- 设置关联对象值时,若想令两个键匹配到同一个值,则二者必须完全相同的指针才行
- 在设置关联对象的值时,必须使用静态全局变量做键
- 可以通过“关联对象”机制把两个对象连起来
- 定义关联对象时刻指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”和“非拥有关系”
- block绑定对象,使代码不分散 例如:alertView 代理 绑定block
11. 理解objc_msgSend的作用
- OC中,向某对象传递消息,使用动态绑定机制来决定需要调用的方法,在底层,所有方法都是普通的c语言函数
- 消息由接收者,选择子及参数构成
- 向对象发消息,由动态消息派发系统来处理,该系统会查出相应的方法,并执行
12. 理解消息转发机制
动态方法解析(dynamic method resolution) 先征询接收者所属的类,是否能动态添加方法,以处理当前
未知的选择子
+(BOOL) resolveInstanceMethod:(SEL)selector
or+(BOOL) resolveClassMethod:(SEL)selector
备援接收者
- (id)forwardingTargetForSelector:(SEL)selector
完整的消息转发
- (void)forwardInvocation:(NSInvocation *)invocation
如若还不能处理,抛出异常
doesNotRecognizeSelector
搭配@dynamic
13. 用“方法调配技术”调试“黑盒方法”
- 开发者可以为那些“完全不知道具体实现的”(完全不透明)黑盒方法增加日志记录功能
- 此方法在调试程序时非常有用,向原有实现中添加新功能
14. 理解“类对象”的用意
- 每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”
- 在运行期检查对象的类型,这一操作也叫“类型信息查询”(introspection,”内省”)
- 如果对象类型无法再编译期确定,那么就应该使用类型信息查询方法来探知
- isMemberOfClass 能够判断出对象是否某个特定类的实例
- isKindOfClass 能够判断出对象是某类或其派生类的实例
- 每个实例都有一个指向class对象的指针,用以表明其类型,而这些class对象构成了类的继承体系
15. 用前缀避免命名空间冲突
- OC 没有命名空间
- 不仅是类名,项目中所有名称都应加前缀,包括分类及分类方法也需要加前缀
- 实现文件中若存在c函数,也应该加前缀
16. 提供全能初始化方法
- 在类中提供一个全能初始化方法,并于文档里指明,其他初始化方法均应调用此方法
17. 实现description方法
- 可以在description中输出很多互不相同的信息,那就是借助NSDictionary类的sescription方法,直接把需要的数据包装成字典
18. 尽量使用不可变字符串
- 尽量创建不可变的对象
- 若某属性仅用于对象内部修改,则在扩展分类中将其readonly属性扩展为readwrite属性
- 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection
19. 使用清晰而协调的命名方式
- 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型
- 应该把表示参数类型的名词放在参数前边
- 不要使用str这种简称,应使用string这样的全称
20. 为私有方法名前加前缀
- 给私有方法的民称前加前缀,这样可以很容易将其同公共方法区分开
- 不要单用一个下划线做私有方法的前缀,因为这种方法是预留给苹果
21. 理解Objective-C 错误模型
- 只有发生了可使整个程序崩溃的严重错误时,才应使用异常
- 把错误信息放在NSError里,经由“输出参数”返回给调用者
22. 理解NSCopying协议
- 若想令自己缩写的对象具有拷贝功能,则需实现NSCopying协议
- (id)copyWithZone:(NSZone *)zone
- 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应尽量执行浅拷贝
- 深拷贝的意思是:在拷贝对象自身时,将其底层数据也一并复制过去。
- Foundation框架中的所有collection类在默认情况下都执行的浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中数据
23. 通过委托与数据源协议进行对象间通信
委托模式 可以将数据与业务逻辑解耦
注意使用weak属性,声明非拥有关系,因为代理对象一般会强引用委托者,否则容易形成保留环
协议中使用可选方法时,需要判断是否响应此方法
委托模式:数据源模式 (Data Source pattern) 和 常规委托模式 (delegate pattern)
数据源模式:信息从数据源流向类 Data Source —> Class 栗子:UITableViewDataSource
常规委托模式: 信息从类流向受委托者(delegate) class —> Delegate 栗子: UITableViewDelegate
非正式协议,给NSObject 类增加分类的方法
delegate 只是一个保存某个代理对象的地址,如果设置多个代理相当于重新赋值,只有最后一个设置的代理才会被真正赋值
单例最好不要使用delegate,单例对象始终都只有同一个对象
delegate 和 block 如何选择:
多条消息需要传递,选用delegate,
一个委托对象的代理属性只能有一个代理对象时
delegare 更加面向过程, block更加面向结果
性能上来说,block的性能消耗要略大于delegate,block会涉及栈区向堆区拷贝等操作,时间和空间上的消耗都大于代理
24. 将类的实现代码分散在便于管理的数个分类之中
- 使用分类机制把类的实现代码划分成易于管理的小块
- 将应该视为“私有”的方法归入名为Private的分类中,以隐藏实现细节
25. 总是为第三方类的分类名称加前缀
- 向第三方类中添加分类时,总应给其名称加上你专用的前缀
- 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀
26. 勿在分类中声明属性
- class-continuation 分类,也叫扩展分类可以直接添加属性,但常规分类不会生成实例变量及存取方法
- 使用关联对象可以给分类添加属性
- 把封装数据所用的全部属性都定义在主接口里
- 在扩展分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性
27. 使用“class-continuation分类”隐藏实现细节
- OC的动态消息系统的工作方式决定了其不可能实现真正的私有方法或私有私立变量
- 扩展分类时唯一能声明实例变量的分类
- 实现文件中定义协议,分类中遵循协议,仅自己用到时
- .h文件声明为readonly,.m扩展分类修改为readwrite
- 一般在扩展分类声明类遵循的协议,便于隐藏
- 协议分委托协议和普通协议,委托协议需要分情况,在需要使用的类前声明 or 提取出来(为更多类使用)
28. 通过协议提供匿名对象
- 协议可在某种程度上提供匿名类型,具体的对象类型可以淡化为遵从某协议的id类型,协议里规定了对象所应实现的方法
- 使用匿名对象来隐藏类型名称(或类名)
- 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示
29. 理解引用计数
属性存取方法中的内存管理
三个方法用于操作计数器:Retain 递增 release 递减 autorelease 带稍后清理“自动释放池”时,再递减保留计数
野指针,悬垂指针(oc中): 指针指向的对象已经被释放了
僵尸对象:一个已经被释放的对象,但是这个对象所占用的空间还没有分配给别人,这样的对象叫做僵尸对象
autorelease释放时机:当前线程的下一次事件循环。因为自动释放池中的释放操作要等到下一次事件循环时才会执行
30. 以ARC简化引用计数
- ARC管理对象生命期的办法基本上就是:在合适的地方插入”保留”及”释放”操作
- 在ARC环境中,变量的内存管理语义可以通过修饰符指明
- ARC只负责OC对象的内存,CoreFoundation框架是C语言接口,不归ARC管理,开发者必须适时调用CFReatin/CFRelease
31. 在dealloc方法中只释放引用并解除监听
- 在dealloc方法里,应该做得事情就是释放指向其他对象的引用,并取消原来订阅的“键值观察(KVO)”或NSNotificationCenter等通知,不要做其他事情
- 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法
- 执行异步任务的方法不应再dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了
32. 编写“异常安全代码”时留意内存管理问题
33. 以弱引用避免保留环
- 将某些引用设为weak,可避免出席那“保留环”
- weak引用可以自动清空
34. 以“自动释放池块”降低内存峰值
- 主线程和GCD线程,这些线程默认都有自动释放池,每次执行“事件循环(event loop)”时,就将其清空
- 自动释放池嵌套用的好处是,可以借此控制应用程序的内存峰值
- 自动释放池要等到线程执行下一次事件循环时才会清空
- 尽管自动释放池的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池
- @autoreleasepool这种写法能创建更为轻便的自动释放池
35. 用“僵尸对象”调试内存管理问题
- 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
- 系统会修改对象的isa指针,另其指向特殊的僵尸类,从而使该对象变为僵尸对象。
- 僵尸对象能够响应所有的选择子,响应方式为:打印一条包括消息内容及其接受者的消息,然后终止应用程序。
- dealloc方法调配(swizzle)成会检测僵尸对象的版本
- _NSZombie_类(以及所有从该类拷贝出来的类)并未实现任何方法。此类没有超类,因此和NSObject一样,也是个”根类”,该类只有一个实例变量,叫做isa
36. 不要使用retainCount
- ARC之后,retainCount方法就正式废止了
- 任何给定时间点上的“绝对保留计数”都无法反映对象生命期的全貌
- 单例对象保留计数很大
37. 理解“块”这一概念
- 块 极为有用,开发者可将代码像对象一样传递,并且在定义“块”的范围内,它可以访问到其中的全部变量
- 块的强大之处:在声明它的范围里,所有变量都可以为其所捕获
- 如果快所捕获的变量是对象类型,那么会自动保留它
- 快总能修改实例变量,所以在声明时无须加__block
- 块可以分配在栈上或堆上,也可以是全局的
- 块可接受参数,也可返回值
- 块是对象
38. 为常用的块类型创建typedef
- 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突
- 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef
39. 用handler块降低代码分散程度
- 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行
- 在创建对象时,可以使用内联的handler块将相关的业务逻辑一并声明
- 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,若改用handler块来实现,则可直接将块和相关对象放在一起
- 常见用法:分类里声明块属性,公共接口里作为参数,传值给属性,注意块的释放时机,一次性操作可在执行完成后释放,或可在对象释放时释放,避免循环引用
40. 用块引用其所属对象时不要出现保留环
如果块所捕获的对象直接或间接的保留了块本身,那么就得当心保留环的问题
一定要找个适当的时机解除保留环,而不能把责任推给API的调用者
常见错误用法:
A类把块作为属性,但在B类使用时,在某个方法中直接临时生成A对象并调用块,
虽然编译器会复制块到堆中,但在块内代码执行完成后会释放,
建议B类保留A对象,用weakself避免保留环,或者B类中创建个数组,把A对象保留中,在dealloc方法中,清理数组内容
B对象 –> block –> A对象 –>B对象
41. 多用派发队列,少用同步锁
派发队列可用来表述同步语义(synchronization semantic)(为代码加锁),这种做法要比使用@synchronized块或NSLock对象更简单
将同步与异步结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程
使用同步队列及栅栏块,可以令同步行为更高效
atomic修饰属性,无法保证访问该对象时绝对是线程安全的,使用属性时,必定能从中获取到有效值,然而在同一个线程上多次调用获取方法(getter)
每次获取到的结果未必相同,在两次访问操作之间,其他线程可能会写入新的属性值
// 在同一个队列上执行setter和getter方法
-(NSString *)someString {
__block NSStirng *localSomeString;
dispatch_sync(queue, ^{
localSomeString = _someString;
});
return localSomeString;
}
-(void)setSomeString:(NSString *)someString {
dispatch_barrier_async(queue, ^{
_someString = someString;
});
}
42. 多用GCD,少用performSelector系列方法
- performSelector系列方法在内存管理方面容易疏失,它无法确定将要执行的选择子具体是什么,因而ARC编译器无法插入适当的内存管理方法
- performSelector系列方法所能处理的选择子太过局限
43. 掌握GCD及操作队列的使用时机
操作队列在底层是用GCD来实现的
GCD是纯C的API,而操作队列则是OC对象
使用NSOperation 及 NSOperationQueue的好处如下:
取消某个操作
指定操作间的依赖关系
通过键值观察机制监控NSOperation对象的属性
指定操作的优先级
重用NSOperation对象
有一个API选用了操作队列而非派发队列,这就是NSNotifacationCenter,开发者可通过其中的方法来注册监听器,以便在发生相关事件时得到通知
44. 通过Dispatch Group机制,根据系统资源状况来执行任务
- 一系列任务可归入 dispatch group之中,开发者可以在这组任务执行完毕时获得通知
- 通过dispatch group,可以在并发式派发队列里同时执行多项任务,此时GCD会根据系统资源状况来调度这些并发执行的任务
45. 使用dispatch_once来执行只需运行一次的线程安全代码
- 标记应该声明在static或global作用域中,这样的话,在把块传给dispatch_once函数时,传进去的标记也是相同的
46. 不用使用dispatch_get_current_queue
47. 熟悉系统框架
- 在打算编写新的工具之前,最好在系统框架里搜索一下,通常都有写好的类可供直接使用
48. 多用块枚举,少用for循环
- 字典和set都是“无序的”(unordered)
- 数组反向遍历 [array reverseObjectEnumerator];
- 基于块的遍历方式,可以修改块签名
- 遍历collection有四种方式, for循环,NSEnumerator遍历法,快速枚举,块枚举
49. 对自定义其内存管理语义的collection使用无缝桥接
- Foundation <==> CoreFoundation
- CFArrayRef acfArray = (__bridge CFArrayRef) array;
- 反向转换可以使用 __bridge_transfer 实现
- Foundation框架中的字典,其键的内存管理语义为”拷贝”(copy),而值的语义却是”保留”
- 在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素,然后可运用无缝桥接技术,将其转换成具备特殊内存管理语义的collection
50. 构建缓存时选用NSCache而非NSDictionary
NSCache胜过NSDictionary之处
自动删减缓存
NSCache并不会拷贝键,而是会保留它
NSCache是线程安全的
NSPurgeableData对象搭配NSCache使用效果很好
51. 精简initialize 与 load 的实现代码
load方法:
先调用类里的,在调用分类里的
在执行子类的load方法之前,必定会先执行所有超类的load方法
load方法中使用其他类是不安全的
load方法并不会像普通的方法那样,它并不会遵从那套继承规则
整个应用程序在执行load方法时都会阻塞
真正用途仅在于调试程序,比如分类里编写此方法,用来判断该分类是否已正确载入系统中
load方法不参与覆写机制
initialize:
惰性调用的
可以安全使用
initialize方法同其他消息一样,如果某个类未实现它,而其超类实现了,就会运行超类的实现代码
无法再编译期设定的全局变量,可以在initialize方法里初始化
由于此方法遵从普通的覆写规律,所以通常应该在里边判断当前需要初始化的是哪个类
52. 别忘了NSTimer会保留其目标对象
NSTimer对象会保留其目标,直到计时器本身失效为止,如果计时器是用实例变量存放的,则实例也保留了计时器,形成了保留环
一次性计时器在触发完任务之后会失效
反复执行任务的计时器(repeating timer),很容易引入保留环,这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的
可以扩充NSTimer的功能,用块来打破保留环。(必须创建分类)
@interface NSTimer (EOCBlocksSupport)
+(NSTimer *)eoc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
blcok : (void (^)()) block
repeats:(BOOL) repeats;
@endtimer -> block -> self -> timer
类对象(class object)无须回收,因为类对象是单例