是否经常在开发过程中使用到列表?是否经常为控制器设置遵循的协议,并在相应的方法中提供数据或提供数据呢?实际上列表的这种设计采用得就是协议委托模式。上述代码中,协议 UITableViewDelegate 和 UITableViewDataSource 是列表所需要的关于cell的数量、样式、高度等等,这些功能不应由内部实现,只能由外部使用者提供,因此将其抽出来形成协议,交由那些委托对象来实现。这里的委托者就是 self即当前的控制器。
在开发过程中对某些功能进行封装时,经常要考虑的就是如何和外部进行信息的交互。常用的手段很多,如 匿名函数block,这种方式保留了 C 的风格,OC 中与之相似的就是协议委托模式。
接下来我们一步一步的深入了解什么是协议委托模式。为了能够通俗的理解,我下面会生活中的例子来进行讲解。
有一天,我想吃些水果,该如何呢?那么我可能需要找到一个水果店进行水果的挑选和购买。
我们分别创建人和水果店的类 Person 和 FruitShop,并创建实例表示我和水果店这两个对象。
// 水果店定义 @interface FruitShop : NSObject /// 购买苹果 -(CGFloat)buySomeAppleWithMoney:(CGFloat)money; @end @implementation FruitShop /// 购买苹果 -(CGFloat)buySomeAppleWithMoney:(CGFloat)money{ return money / 10.98; } @end // 人个体 #import "FruitShop.h" @interface Person : NSObject @property (nonatomic, strong) FruitShop* shop; //水果店 @end @implementation Person @end水果店具有贩卖水果的功能。 我需要水果店购买苹果,所以要对水果店的进行引用操作,为了能够进行水果的购买操作。
self.me = Person.new; self.shop = FruitShop.new; // 对水果店进行引用 self.me.shop = self.shop;现在我们需要触发购买操作,我们给 Person 添加购买方法,以便触发购买。
@implementation Person -(void)buyApple{ // 如果找到水果店,并且该水果店能相应购买需求 if (self.shop && [self.shop respondsToSelector:@selector(buySomeAppleWithMoney:)]) { // 购买水果 CGFloat number = [self.shop buySomeAppleWithMoney:100]; NSLog(@"买到了 %.2f 斤苹果", number); } } @end外部触发购买的事件。
[self.me buyApple]; 买到了 9.11 斤苹果我们成功通过水果店购买到了 9.11 斤苹果。
当自身不具备某种功能时,可以通过引入另一种类型来提供相关的能力,新引入的对象就是被委托者,换句话说,我没有能提供水果的能力,但是我又需要水果,那么我通过属性注入的方式引入一个具有提供水果的能力水果店,通过水果店来为我产生水果,而这个水果店对象就是被委托对象。
问题一:循环引用
一般来说上述例子没有什么问题,但是在一些特定的应用场景下就会出现问题。学 OC 的人想必都知道 OC 的内存管理机制,其中有一个需要特别注意的问题就是循环引用,无论是相互持有还是单向循环持有都会无法释放。
在本类中,me 和 shop 只是单向持有,因此不用担心无法释放的问题,但是如果在 me 持有 shop 的之前,shop 已经持有了 me 呢?但是似乎没有什么理由让 shop 持有 me ,但是我们回到 UITableView 示例中,通常情况下,控制器类会先持有一个列表视图,而列表视图的 delegate、datasource 又引用到了 self 即当前类,那么就会出现循环引用的问题,可是为什么没有出现内存泄漏的问题呢?这是 OC 中 weak 修饰符 所起到的作用,该修饰符引用对象但是不会让对象计数器加一,在可能存在的循环链中如果有一方采用了该修饰符,那么循环链就不存在了。
将 delegate、datasource 指向其他对象而非 self 同样可以避免循环引用问题,只不过系统在设计时已经考虑到这些错误的使用情况,因此采用了 weak 修饰符 解决很大可能性的循环引用问题。
注:无论何种方式,只要避免单向循环引用链的出现就能避免这样的内存泄漏问题。
问题二:硬编码
水果店的例子中还会存在一个问题,并不是只有水果店才能卖给我水果,如果我遇到了一家超市,该超市同样能提供水果的能力,那我还需要继续寻找水果店吗?当然不需要了,但是会有新的问题出现。
#import "FruitShop.h" @interface Person : NSObject @property (nonatomic, strong) FruitShop* shop; -(void)buyApple; @end我们发现,在 Person 类中,我们指定了 FruitShop 类,即水果店,这就意味着引用过来的对象应该是 FruitShop 类或者其子类,否则我们可能无法购买,但是超市很大可能并不是水果店的子类,或者购买方法并不是 buySomeAppleWithMoney:。那我们再引入一个新的 Supermarket 超市类? 继续引入新类型无法解决根本问题。
我们知道,OC 中有一个 id 类型,该类型表示任何类型。我们可以使用它来代替,但是下一个问题又随之而来,如果提供对象之间针对同一种功能有着不同的方法声明,那么这个 id 对象如何调用正确的方法呢?解决方案就是协议。
协议就是一组方法的声明。类似 Java 中接口,只有方法的声明,没有相关的实现。协议的作用是用来约束类型的,当某个类声称遵循了协议,那么协议中标记@required 的方法就需要进行实现。 换个角度来看,协议可以为类添加更多的功能:当自定义对象遵循并实现了 NSCopying 协议,那么这个对象就能够通过 [obj copy] 方法复制一个新对象;当自定义对象遵循并实现了 NSCoding 协议,那么这个对象就能被归档和解档。
回到水果店问题,我们通过协议来约定 Person 类购买水果的相关能力。
在协议中声明购买水果的方法。
@protocol MyBuy <NSObject> @required /// 购买苹果 -(CGFloat)buySomeAppleWithMoney:(CGFloat)money; @end我们对 Person 进行改造。
#import "MyBuy.h" @interface Person : NSObject @property (nonatomic, weak) id <MyBuy> shop; //遵循协议的任意对象 -(void)buyApple; @end @implementation Person -(void)buyApple{ // 如果找到水果店,并且该水果店能相应购买需求 if (self.shop && [self.shop respondsToSelector:@selector(buySomeAppleWithMoney:)]) { // 购买水果 CGFloat number = [self.shop buySomeAppleWithMoney:100]; NSLog(@"买到了 %.2f 斤苹果", number); } } @end这样,只要 Person 遵循实现协议 MyBuy 中购买水果的能力,就能够完成买水果的操作。
我们接着让水果店遵循并实现该协议。
#import "MyBuy.h" @interface FruitShop : NSObject <MyBuy> @end @implementation FruitShop /// 购买苹果 -(CGFloat)buySomeAppleWithMoney:(CGFloat)money{ return money / 10.98; } @endFruitShop 类删除购买方法的声明,直接实现 MyBuy 协议中的购买方法即可。
// 对水果店进行引用 self.me.shop = self.shop; [self.me buyApple];外部虽然没有什么变化,但是性质完全不一样,现在只要其他的类,如 Supermarket,同样遵循并实现协议 MyBuy 就可以直接替换掉 shop。
self.me.shop = self.supermarket; [self.me buyApple];这样就解决了硬编码问题,同时,当我遇到一家超市而不是水果店时,我依然可以获取到水果啦。
协议委托模式是 OC 中经典的设计模式,该模式在一定程度上降低了代码的耦合性。同时协议委托模式提高了代码的灵活性,也解决了委托者和被委托者的通信问题。
前文提到,协议是一种类型约束。在 iOS 开发过程中,你会经常接触。这里我列举了一些常用到的情景。
添加功能协议可以被看作为类添加功能的一种方式,通过协议我们能清楚的知道类具有哪些能力。协议也方便我们管理类,当我们想移除类遵循的协议时,可以通过查询协议找出对应的方法进行移除即可。
约束类、类型协议可以用来约束类和类型,常见情况如下。
// 约束类 // 该类需要实现 NSCoding 协议的 @required 方法 @interface Person : NSObject <NSCoding> @end // 约束对象 // 虽说是id类型,但前提条件是必须遵循协议 @property (nonatomic, weak) id <MyBuy> shop; // 必须是字符串数组 @property (nonatomic, strong) NSArray <NSString*>* list; // value 必须是 Student @property (nonatomic, strong) NSDictionary <NSString*,Student*>* list; 添加属性协议中是可以添加属性的,但是需要使用 synthesize 关联成员变量。
@protocol MyBuy <NSObject> @property (nonatomic, assign) CGFloat price; @end @interface FruitShop : NSObject <MyBuy> @end @implementation FruitShop @synthesize price = _price; //关联成员变量 @end 面向接口编程也就是面向协议编程,对外声明协议,将必要的属性、方法声明在协议中,外部通过某种手段通过协议取得该对象。
NSObject <MyBuy>* object = 获取的id对象; [object protocolMethod]; //操作协议中的方法或属性