前言
@interface AViewController : <UITableViewDelegate, UITableViewDataSource>
@end
...
self.table.delegate = self;
self.table.datasource = self;
是否经常在开发过程中使用到列表?是否经常为控制器设置遵循的协议,并在相应的方法中提供数据或提供数据呢?实际上列表的这种设计采用得就是协议委托模式。上述代码中,协议 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;
}
@end
FruitShop
类删除购买方法的声明,直接实现 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]; //操作协议中的方法或属性