Effective Objective-C 读书笔记-中
通过协议进行对象间通信
在委托方实现协议时,如果协议中某方法时可选的(optional),那么就会写出一大批下面这样的模版代码:
if ([_delegate respondsToSelector:@selector(someClassDidSomething)]) {
[_delegate someClassDidSomething];
}
很容易用代码查处delegate是否能响应对应的选择子(selector),可是如果频繁执行此操作,除了第一次检测的结果有用之外,后面的可能都是多余的。因为如果delegate本身没变的话,那么它原来响应的某个选择子突然不能响应了是不太可能的。鉴于此,通常把delegate能否响应某个协议方法这一信息缓存起来,以优化程序效率。最佳的实现途径是使用位段(bitfield)数据类型。这是一项乏人问津的C语言特性,此处却正合适。将结构体某个字段所占用的二进制位个数设为特定的值,比如这样:
struct data {
unsigned int fieldA : 8;
unsigned int fieldA : 4;
unsigned int fieldA : 2;
unsigned int fieldA : 1;
}
结构体中,fieldA位段将占用8个二进制位,fieldB将占用4个,fieldC将占用2个,fieldD将占用1个。于是,filedA可以表示0到255之间的值,fieldD则可以表示0或1。可以像filedD这样,把delegate是否实现了协议中相关方法这一信息缓存起来。如果创建的结构体中只有大小为1的位段,那么就能把许多Boolean值塞入一小块数据里:
@interface EOCNetworkFetcher () {
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags;
}
@end
// Set flag
_delegateFlags.didReceiveData = 1;
// Get flag
if (didReceiveData.didReceiveData) {
...
}
将这个结构体涌来缓存delegate是否能响应特定的选择子。实现缓存功能所用的代码可以写在delegate的设置方法里:
- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate {
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
这样,每次调用delegate相关方法前,就不用再监测delegate是否能响应给定的选择子了,而是直接查询结构体的标志:
if (_delegateFlags.didReceiveData) {
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
对于那些频繁通过协议交换信息的情况,折线优化技术极有可能会提高程序效率。
将类的实现代码分散到便于管理的多个分类中
在实现类的时候,往往是所有方法的代码全部堆在一个巨大的实现文件里。如果还向类中继续添加方法的话,实现文件就会越来越大,变得难于管理。
// EOCPerson.h
@interface EOCPerson : NSObject
- (instance)initWithFirstName:(NSSting *)firstName lastName:(NSString *)lastName;
- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
- (BOOL)isFriend:(EOCPerson *)person;
- (void)performDayWork;
- (void)takeVacationFromWork;
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
// EOCPerson.m
...
这种情况下,可以通过Objc的分类(category)机制,把类代码按逻辑划入多个的易于管理的小部分。如果依然把整个类的定义都放在一个头文件中,实现都放在一个源文件中,随着功能的添加依然会膨胀得无法管理。其实可以把分类的声明和实现都单独分离出去:
// EOCPerson.h
@interface EOCPerson : NSObject
- (instance)initWithFirstName:(NSSting *)firstName lastName:(NSString *)lastName;
@end
// EOCPerson.m
...
// EOCPerson+Friendship.h
@interface EOCPerson (Friendship)
- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
- (BOOL)isFriend:(EOCPerson *)person;
@end
// EOCPerson+Friendship.m
...
// EOCPerson+Wrok.h
@interface EOCPerson (Wrok)
- (void)performDayWork;
- (void)takeVacationFromWork;
@end
// EOCPerson+Wrok.m
...
// EOCPerson+Play.h
@interface EOCPerson (Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
// EOCPerson+Play.m
...
将代码打散到分类还有个好处,就是便于调试:对于某个分类的所有方法来说,分类名称都会出现在其符号中:
-[EOCPerson(Friendship) addFriend:]
在调试器的回溯信息中,会看到下面这样的内容:
frame #2: 0x00001c50 Test '-[EOCPerson(Friendship) addFriend:]'
+ 32 at main.m:46
根据回溯信息中的分类名称,很容易就能精确定位到方法所属的功能分类,这对于某些应该视为私有的方法来说是极为有用的。通过创建Private的分类并放入所有私有方法,这样只要使用者有时在回溯信息中发现private一词就知道不应该直接调用此方法。
勿在分类中声明属性
属性时封装数据的方式,尽管技术上分类也可以声明属性,但是还是要尽量避免。因为只有扩展分类(class-continuation)能够增加实例变量以外,其他的分类无法合成属性对应的实例变量。
@interface EOCPerson (Friendship)
@property (nonatomic,readwrite,assign) NSUInteger count;
@end
正确的做法是把所有的公开属性都定义在主接口里,所有的私有属性都定义在扩展分类里,这是唯一能够定义实例变量的地方。而属性只是定义实例变量和相关存取方法所用的语法糖,所以也应当遵循和定义实例变量一样的规则。分类应该理解为类扩展功能而非数据的一种方法。虽然只读方法可以不依赖实例变量,不需要自动合成实例变量,但是最好还是直接声明一个方法代替。
// Wrong
@interface EOCPerson (Friendship)
@property (nonatomic,readonly,strong) NSArray *friends;
@end
// Right
@interface EOCPerson (Friendship)
- (NSArray *)friends;
@end
使用class-continuation隐藏实现细节
类中经常会包含一些无须对外公开的方法和实例变量,虽然也可以对外公布并注明@private,但是无论最少知识原则还是机密类型信息,都应该把不需要公开的隐藏在实现文件中。扩展分类(class-continuation)和普通分类不同,它必须定义在类的实现文件里,因为这是唯一能定义实例变量的分类。由于有稳固的ABI这一机制,使得无须知道对象的内存布局、内存大小就能使用它:
// EOCPerson.h
@interface EOCPerson : NSObject {
NSString *_publicStr;
}
@end
// EOCPerson.m
@interface EOCPerson () {
NSString *_privateStr1;
}
@end
@implementation EOCPerson {
NSString *_privateStr2;
}
@end
虽然机密类型可以放在公开类定义中并注明private,但是外界就知道了内部有一个机密的类型。如果使用id隐藏其类型信息,那么在内部实现中使用此实例时,没有类型信息就得不到编译器的帮助。没有必要为了隐藏某个内容而放弃编译器的类型检查功能。同理,对于没必要公开在外面造成类型污染的都应该如此考虑,比如编写Objc++代码时尤其有用。如果C++信息暴露在头文件,那么对应的实现文件的后缀名就要改成**.mm**。所有引用到此类的其它文件都会涉及到C++影响从而都需要将后缀改成.mm,这明显是我们不希望看到的。将对应的C++类型放到实现文件中的就可以很好的解决这个问题。同理,对于没有暴露在外部的本类遵循协议类型,因为编译器必须知道协议的具体内容,所以需要包含定义协议的头文件。如果放倒实现文件中,也可以避免所有包含了本类的文件都知道协议类型。
扩展分类还有一种用法,可以将public接口中声明为readonly的属性在扩展分类变为readwrite,以便在内部设置其值。通常不直接设置实例变量,而是通过属性设置。这样能触发外界的健值观测(Key-Value Observing)通知:
// EOCPerson.h
@interface EOCPerson : NSObject
@property (nonatomic,readonly,copy) NSString *firstName;
@property (nonatomic,readonly,copy) NSString *lastName;
@end
// EOCPerson.m
@interface EOCPerson ()
@property (nonatomic,readwrite,copy) NSString *firstName;
@property (nonatomic,readwrite,copy) NSString *firstName;
@end
通过协议提供匿名对象
协议定义了一些方法,遵循协议的对象应该实现它们。有时候,我们需要的仅仅是能够响应协议方法的对象就行了,至于它具体是什么类型可以不用关心。这样id范型,就将实际类型隐藏起来,避免了不必要的类型暴露和污染。这个概念称为匿名对象(anonymous object),比如:
@property (nonatomic,readwrite,assign) id<EOCDelegaet> delegate;
任何对象都可以当delegate,只要它遵循了
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
有时候对象的类型并不重要,重要的是对象有没有实现某些方法,在此情况下,可以将协议也看作是一个匿名类型,它就代表了一些特定的类型。从而在类的设计中类型解耦。
在dealloc中只释放引用并解除监听
对象在经历其生命期后,最终回被系统回收,这时就要执行且只执行一次dealloc方法。一旦调用过dealoc之后,对象就不再有效。在dealloc中,通常就是释放对象所拥有的其它引用,ARC会通过自动生成的**.cxx_destruct**方法在dealloc中添加释放引用代码。除此之外,通常还需要将原来配置过的观测行为(observation behavior)都清理掉,例如NSNotificationCenter。
对于开销较大或系统内稀缺的资源,比如文件描述符(file descriptor)、套接字(socket)、大块内存等等,不应该指望等到执行dealloc时才释放,因为有一些无法预料的情况致使本对象比想象中释放得要晚很多。如果非要等到系统调用dealloc时才释放,那么保留这些稀缺资源的时间就会过长,这样并不合适。通常需要另一个方法,在对象使用完资源后就调用它立即释放,比如:
- (void)open:(NSString *)address;
- (void)close;
为了防止忘记清理必要的资源,可以在dealloc进行最后检查:
- (void)close {
// clean up resources
_closed = YES;
}
- (void)dealloc {
if (!_closed) {
NSLog(@"Error: close was not called before dealloc");
[self close];
}
}
需要资源时调用open,使用完毕后调用close。系统并不保证每个创建出来的对象都会执行dealloc。极个别情况下,当程序终止时,仍有对象处于存活状态。在OS X和iOS对应的程序代理方法中,都有一个会在程序终止时调用的方法。如果一定需要清理的对象,可以在这里进行清理:
- (void)applicationWillTerminate:(NSNotification *)notification;
- (void)applicationWillTerminate:(UIApplication *)application;
编写dealloc时还需要注意,不要再里面随便调用其他方法,因为对象此时已近尾声(in a winding-down state)。如果调用的方法里又调用了异步任务,那么等到那些任务执行完毕时,对象可能已经彻底销毁了。万一callback则会访问野指针,导致很多问题。另一个需要注意的问题是,调用dealloc可以在任何线程,只要那个线程令对象的计数器为0。如果dealloc中调用了某些必须在特定线程(比如主线程)中才能正确执行的方法,由于无法保证线程的正确从而也会导致很多问题。设置属性也不应该调用,因为可能设置方法里可能有一些无法在回收阶段安全执行的操作,另外也可能引起外界KVO执行。