第四章 协议与分类
协议和分类都是OC中非常好的特性,善用这些功能会大大增加我们代码的效率。
23. 通过委托与数据源协议进行对象间通信
OC中的对象之间进行通信,其中最终要的一种方法就是协议,整个Cocoa系统框架都是这么做的。
协议的写法这里不做过多介绍,这里只说一些可能忽略的点
1.委托协议(Delegate Pattern)名通常是在相关类名后面加上Delegate一词,整个类名采用驼峰命名来写,虽然这不是必须的,但是基本所有的OC代码都遵循这个规则。
2.用属性来存放委托对象的时候一定不能将属性定义成strong,这样做会出现“保留环”(也就是循环引用,导致无法释放),类中存放委托对象的属性要么定义成weak(相关对象销毁时自动清空),要么定义成unsafe_unretained(相关对象销毁时不制动清空)。
3.一个类遵从委托协议,可以在接口中声明,也可以在分类中声明,看具体情况需要。
4.注意委托协议中的关键词,@required修饰的方法必须实现,@optional修饰的方法可选择是否实现。
5.在委托对象上调用可选方法,必须提前使用类型信息查询方法判断委托对象能否响应相关方法。
6.在调用delegate对象中的方法是,总应该把发起委托的实例也一并传入方法中,并且方法名要起的恰当,让使用者一目了然。
7.可以用协议定义一套接口,令某类经由该接口获取所需的数据,这种形式又叫“数据源模式”(Data Source Pattern),这种模式和代理模式最大的不同是,数据源模式中信息是从数据源流向类,在代理模式中,信息则从类流向受委托者。
8.一般情况下我们会把委托协议与数据源模式分开(委托协议和数据源模式可以参考UITableViewDelegate,UITableViewDataSource)。
前面提到,在委托对象上调用可选方法,必须提前使用类型信息查询方法判断委托对象能否响应相关方法,如下:
if ([_delegate respondsToSelector:@selector(someClassDidSomething)]) {
[_delegate someClassDidSomething];
}
但是我们考虑一种情况,如果这个代理方法被频繁调用的时候,例如UIScrollViewDelegate的scrollViewDidScroll:
方法,这个时候当第一次判断完响应方法之后,后面再进行响应方法的判断显然是多余的,这时通常把委托对象能否响应某个协议方法这一信息缓存起来,以此来优化程序,下面举例说明。
我们可以采用“位段”数据类型来实现方法响应的缓存(“位段是一项C语言特性”),首先定义一个协议,以网络获取数据为例,协议中有三个方法:
@protocol EOCNetworkFetcherDelegate<NSObject>
@optional
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiverData:(NSData *)data; // 抓取数据成功
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error; // 抓取数据失败
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float)progress; // 获取加载进度
@end
为了缓存响应方法,在分类中定义一个结构体:
@interface EOCNetworkFetcher (){
struct {
unsigned int didReceiveData : 1;
unsigned int didFailWithError : 1;
unsigned int didUpDateProgressTo : 1;
} _delegateFlags;
}
@end
结构体中的1,表示占用一个二进制位,正好可以用来表示一个BOOL值
然后我们可以在delegate属性的设置方法中设置值:
- (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate{
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiverData:)];
_delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpDateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
这样在调用代理相关方法的时候,就不用检测响应方法,直接查询结构体里面的标志就行:
if (_delegateFlags.didUpDateProgressTo) {
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
具体的是不是需要做这样的代码优化,还是看具体的需求。
24. 将类的实现代码分散到便于管理的数个分类之中
一个类中通常会有大量的方法,当方法多的时候不便于查找也不便于管理,这时候我们就可以考虑OC的“分类机制”,把类代码划分到几个分区中,便于开发调试,分类的具体写法不再过多介绍,只说重点:
1.可以把整个类和其分类都定义在一个文件中,实现代码都写在一个文件中,但是不建议这么做,通常都将分类写到单独的文件中。
2.写分类的一个主要原因就是便于调试,在调试器的回溯信息中能够看到分类的名称,根据名称我们很容易找到该分类,解决问题。
3.可以创建名为Private的分类,将一些只在类或框架内部才会使用的方法放在里面,这些方法不需要对外公布,并且该分类的头文件也不随程序库公开,于是就可以隐藏这些方法,结合第二条,当回溯信息出现Private的分类名的时候,开发者就会知道这是私有方法,不该调用。
4.向某个类的分类中加入方法,那么在程序中,该类的每个实例都可调用这些方法。
25. 总是为第三方类的分类名称加前缀
前面也有专门提到加前缀的小节,都是为了防止引入第三方类的时候冲突,我们要谨记,向第三方类中添加分类时,总应给其名称加上你专用的前缀,在向第三方添加分类时,总应给其中的方法名加上你专用的前缀,至于前缀这里不再过多介绍。
26. 勿在分类中声明属性
属性是封装数据的方式,尽管从技术上说分类可以声明属性,但是还是应该尽量避免这种做法。原因在于除了“class-continuation分类”(后面会专门介绍这个特殊的分类)之外,其他分类无法向类中新增实力变量,因此,他们无法把实现属性所需要的实例变量合成出来。如果直接在分类中声明属性,编译器会报警告,提示无法生成存取方法,那么从技术上怎么解决这个问题呢?开发者可以在需要的分类中自己为该属性实行存取方法,此时可以利用@dynamic(参见12条),等到运行期在提供这些方法。
我们也可以通过关联对象解决这个问题(参见10条)例如我们前面用过的EOCPerson类,将其中的friends属性在Friendship分类中定义,并且该属性是NSArray类型,这时我们通过关联对象解决,如下:
#import <objc/runtime.h>
static const char *kFriendshipPropertyKey = "kFriendshipPropertyKey";
@implementation EOCPerson (Friendship)
- (NSArray *)friends{
return objc_getAssociatedObject(self, kFriendshipPropertyKey);
}
- (void)setfriends:(NSArray *)friends{
objc_setAssociatedObject(self, kFriendshipPropertyKey, friends, OBJC_ASSOCIATION_RETAIN);
}
@end
这样做也是可行的,但是不理想,每有一个属性,我们都会写一些重复的代码,并且在内存管理上容易出错,因为我们在为属性实现存取方法时,经常忘记遵从内存管理语义,另外当数组是可变数组的时候,这又增加了拷贝时候可能出错的概率。
总体来说,分类的功能是扩展类的功能,而非封装数据。
27. 使用“class-continuation分类”隐藏实现细节
“class-continuation分类”和普通分类不同,它必须定义在类的实现文件里面,这个分类没有分类名,并且这个分类没有特定的实现文件,其中的方法都定义在类的主实现文件中,最重要的是,这个分类可以声明实例变量。
“class-continuation分类”最大的好处就是我们可以把不想给别人看见的东西隐藏起来,只供本类使用,即使我们可以通过在接口文件中用private声明或者通过id来隐藏引用的类名,但是这样做还是暴露了一些信息,“class-continuation分类”可以很好的解决这个问题,把想隐藏的东西一点都不暴露出来。
另外说几点和“class-continuation分类”有关的用法,实例变量也可以定义在实现块里,在语法上和定义在“class-continuation分类”中等效。
编写Objective-C++代码时“class-continuation分类”尤为重要(Objective-C++是OC与C++的混合体,其代码可以用两种语言写),由于兼容性问题游戏后端一般用C++写,有时使用第三方库时可能只有C++绑定,此时也必须用C++写,在这些情况下使用分类会很方便。
举例,假设一个类可能这样写EOCClass.h:
#import <Foundation/Foundation.h>
#include "SomeCppClass.h"
@interface EOCClass : NSObject{
@private
SomeCppClass _cppClass;
}
@end
该类的实现文件是一个叫做EOCClass.mm的文件(编译器遇到.mm扩展名可将此文件按Objective-C++来编译,否则就无法引入SomeCppClass.h),但是这么写的话只要包含EOCClass.h的类,都必须编译为Objective-C++才行,因为他们都引入了SomeCppClass.h,这样做的别人在使用这个类的时候都必须把源文件的扩展名改为.mm,这个时候“class-continuation分类”的作用就提现出来了,我们无需再接口中就引入SomeCppClass.h,只要在实现文件中引入即可,然后在“class-continuation分类”中声明,如下:
#import "EOCClass.h"
#include "SomeCppClass.h"
@interface EOCClass (){
SpmeCppClass _cppClass;
}
@end
@implementation EOCClass
@end
注意此时的文件扩展名是.mm
这样写完的EOCClass.h类,在隐藏了实现细节后,调用者甚至不知道我们用了C++语法,WebKit框架,CoreAnimation框架中许多后端代码都是C++实现的,但是提供给我们的是纯OC的接口。
“class-continuation分类”可以将接口中声明的只读属性再次声明为可读写,这个在前面已经说过,不在赘述。
“class-continuation分类”另外一种用法,若对象遵循的协议只应视为私有,则可在“class-continuation分类”中声明,因为在公共接口中我们可能不希望暴露这些信息。
最后提到的一点就是,我们应该将私有方法在“class-continuation分类”中声明,虽然现在的编译器不要求这么做,但是在“class-continuation分类”中声明这些私有方法后,方便我们查找代码。
28. 通过协议提供匿名对象
首先看一下我们在声明代理对象属性的形式
@property (nonatomic, unsafe_unretained) id<EOCNetworkFetcherDelegate> delegate
通过这个形式可以发现,在声明代理对象属性的时候,我们不在乎真正的是哪个类,我们要求的是只要这个类遵循需要的代理就可以,类名没有显现出来而是用的id代替,这就是我们说的通过协议提供匿名对象。
为什么要这么写呢?因为具体是哪个类我们不关心,我们关心的是这个类是不是遵循了指定的代理协议,例如系统中的NSDictionary,在字典中,键的标准内存管理语义是“设置时拷贝”,值的内存管理语义是“设置时保留”,这个通过可变字典的一个设置键值对的方法看出来,该方法如下:
- (void)setObject:(ObjectType)anObject forKey:(KeyType <NSCopying>)aKey;
从这个设置方法可以看出,表示键的参数可以是任何类型,只要遵循NSCopying协议即可,这个aKey参数就是一个匿名对象,和代理属性一样不关心具体类型。
在设计代码结构的时候,有时我们自己写的代码可能也有这样的需求,对对象类型并不关心,重要的是对象有没有实现某些方法,这个时候我们就可以采用协议的方式,来达到我们要的目的,并且这样的代码,在修改的时候,不必修改外部接口,只要在后端修改对应的方法实现即可。