【iOS】Effective Objective-C第四章
协议与分类
协议和分类都是OC的一项重要语言特性。
- 协议:OC不支持多重继承,因而我们把某个类该实现的一系列方法定义在协议里面。协议最为常见的用途是实现委托模式。
- 分类:利用分类,我们无须继承子类即可直接为当前类添加方法。
通过委托与数据源协议进行对象间通信
OC使用一种名叫"委托模式"的编程设计模式来实现对象间的通信,其主旨是:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其委托对象。而另一个对象则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。此模式可将数据与业务逻辑解耦,OC中一般通过协议来实现此模式。
具体示例:获取网络数据的类(EOCNetworkFetcher)含有一个委托对象(EOCDataModel),在获取完数据之后回调这个委托对象。

objc
#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate <NSObject>
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
@end
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
@end
一定要注意这个属性必须是weak,因为两者之间必须为"非拥有关系"。通常情况下,扮演delegate的那个对象也要持有本对象,直到用完本对象之后,才会释放。
如果声明属性时用strong了,那么就会引入"保留环"。
保留环实则就是强引用,使两个或多个对象互相强引用对方,导致它们都无法被系统释放,造成内存泄漏。

委托协议中的方法一般都是可选的,用关键字@optional标注。
如果要在委托对象上调用可选方法,必须提前判断该委托对线是否相应相关选择子。例如:

使用respondsToSelector:方法来判断委托对象是否实现相关方法。实现就调用,反之不执行任何操作。
委托模式的另一用法是:用协议定义一套接口,令某类经由该接口获取其所需的数据。旨在向类提供数据,因此又称"数据源模式"。常规委托模式中,信息从类流向受委托着,而该模式中信息从数据源流向类。

由上可知,无论是实现委托模式还是数据源模式,如果协议中方法是可选的,那么就要用上面提到的代码检查委托对象是否能响应特定选择子。可是如果频繁执行该操作,除了第一次检测结果有效之外,后续检测可能都是多余的。如果委托对象本身没变,那么不太可能会突然响应某个原来不能响应的选择子,同样也不会突然无法响应某个原来可以响应的选择子。因此,我们通常把委托对象能否响应某个协议方法这一信息缓存起来以优化程序效率。
缓存的最佳途径是使用C语言特性------"位段"数据类型。
用结构体来缓存委托对象是否能响应特定选择子。实现缓存功能所用代码写在delegate属性所对应的设置方法里:
这样每次调用delegate相关方法之前就不用检测委托对象是否能响应给定选择子,而是直接查询结构体里的标志。
将类的实现代码分散到便于管理的数个分类之中
实现类方法代码可能会写在一个大文件里,源代码文件过大,难以管理。可以写在分类里:
objc
#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 andLastName:(NSString *)lastName;
@end
objc
#import "EOCPerson.h"
@interface EOCPerson (Friendaship)
- (void)addFriend:(EOCPerson *)person;
- (void)removeFriend:(EOCPerson *)person;
@end
#import "EOCPerson.h"
@interface EOCPerson (Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end
#import "EOCPerson.h"
@interface EOCPerson (Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
勿在分类中声明属性
属性是封装数据的方式。尽管分类里可以声明属性,但也尽量避免。这是因为除了"class-continuation分类"("私有扩展",写在.m文件里,没有名字)之外,其他分类都无法向类中新增实例变量,也就是无法把实现属性所需的实例变量合成出来。
如果写了会出现警告:
objc
#import "EOCPerson.h"
@interface EOCPerson (Friendaship)
@property (nonatomic, strong) NSArray *friends;
- (BOOL)isFriendsWith:(EOCPerson *)person;
@end

这段警告是说此分类无法合成与friends属性相关的实例变量,此首需要开发者把存取方法声明为@dynamic,然后手动实现在分类中。
虽说如此,但只读属性还是可以在分类中使用的。这是因为该获取方法并不访问数据,而且属性也不需要由实例变量来实现:
objc
#import "EOCPerson.h"
@interface EOCPerson (Friendaship)
@property (nonatomic, strong, readonly) NSArray *friendship;
@end
#import "EOCPerson+Friendaship.h"
@implementation EOCPerson (Friendaship)
- (NSArray *)friendship {
return @[@"1", @"2"];
}
@end
这样编译器也不会发出警告了。
使用"class-continuation分类"隐藏实现细节
class-continuation分类"有以下几种用法:
- 我们常把不需对外公布但却应该具有的办法及实例变量写在"class-continuation分类"里。"class-continuation分类"必须定义在其所接续的那个类的实现文件里,它是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。该分类没有名字。
写法如下:

这种分类之所以能在其中定义方法和实例变量,是因为"稳固的ABI"机制,这使得我们无须知道对象大小即可使用它。
"稳固的ABI"机制:
ABI是应用程序和操作系统、编译器、库之间的二进制接口。稳固的ABI使得对象内存布局固定、方法调用规则固定。
- 对象内存布局固定:没有稳固 ABI,类扩展新增 ivar 就可能改变对象内存大小,导致原对象访问 ivar 越界。
- 方法调用规则固定:没有稳固 ABI,方法表布局可能随编译器版本变化,class-continuation 方法调用可能出错
即便在公共接口将其标注为private,也还是会会泄漏实现细节,这是因为这些标注只是编译器告诉开发者的,并不会从二进制上真正隐藏。而定义在class-continuation分类里可以将其真正隐藏起来,供本类使用。
- 编写OC++的.mm文件时,class-continuation分类也尤为有用。、
- class-continuation分类也可将public接口中声明为"只读"的属性扩展为"可读写",以便在类的内部设置其值。
objc
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@end
objc
#import "EOCPerson.h"
@interface EOCPerson()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson
@end
这样就可以使得既能令外界无法修改对象,又能在其内部按照需要管理其数据。
- 若对象所遵从的协议只应视为私有,则在"class-continuation分类"中声明。
通过协议提供匿名对象
协议可在某种程度上提供匿名对象。具体的对象类型可淡化为遵从某协议的id类型,协议里规定了对象所应实现的方法。用法有:
- 可使用匿名对象来隐藏类型名称。
- 如果具体类型不重要,重要的是对象能够响应特定方法,那么可以使用匿名对象来表示。
