轉(zhuǎn)自 <簡書 — 劉小壯>
代理的基本使用
代理是一種通用的設(shè)計(jì)模式,在iOS中對代理設(shè)計(jì)模式支持的很好,有特定的語法來實(shí)現(xiàn)代理模式,OC語言可以通過@Protocol實(shí)現(xiàn)協(xié)議。
代理主要由三部分組成:
- 協(xié)議:用來指定代理雙方可以做什么,必須做什么。
- 代理:根據(jù)指定的協(xié)議,完成委托方需要實(shí)現(xiàn)的功能。
- 委托:根據(jù)指定的協(xié)議,指定代理去完成什么功能。
這里用一張圖來闡述一下三方之間的關(guān)系:
Protocol-協(xié)議的概念
從上圖中我們可以看到三方之間的關(guān)系,在實(shí)際應(yīng)用中通過協(xié)議來規(guī)定代理雙方的行為,協(xié)議中的內(nèi)容一般都是方法列表,當(dāng)然也可以定義屬性。
協(xié)議是公共的定義,如果只是某個(gè)類使用,我們常做的就是寫在某個(gè)類中。如果是多個(gè)類都是用同一個(gè)協(xié)議,建議創(chuàng)建一個(gè)Protocol文件,在這個(gè)文件中定義協(xié)議。遵循的協(xié)議可以被繼承,例如我們常用的UITableView,由于繼承自UIScrollView的緣故,所以也將UIScrollViewDelegate繼承了過來,我們可以通過代理方法獲取UITableView偏移量等狀態(tài)參數(shù)。
協(xié)議只能定義公用的一套接口,類似于一個(gè)約束代理雙方的作用。但不能提供具體的實(shí)現(xiàn)方法,實(shí)現(xiàn)方法需要代理對象去實(shí)現(xiàn)。協(xié)議可以繼承其他協(xié)議,并且可以繼承多個(gè)協(xié)議,在iOS中對象是不支持多繼承的,而協(xié)議可以多繼承。
// 當(dāng)前協(xié)議繼承了三個(gè)協(xié)議,這樣其他三個(gè)協(xié)議中的方法列表都會(huì)被繼承過來
@protocol LoginProtocol <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;
@end
協(xié)議有兩個(gè)修飾符@optional和@required,創(chuàng)建一個(gè)協(xié)議如果沒有聲明,默認(rèn)是@required狀態(tài)的。這兩個(gè)修飾符只是約定代理是否強(qiáng)制需要遵守協(xié)議,如果@required狀態(tài)的方法代理沒有遵守,會(huì)報(bào)一個(gè)黃色的警告,只是起一個(gè)約束的作用,沒有其他功能。
無論是@optional還是@required,在委托方調(diào)用代理方法時(shí)都需要做一個(gè)判斷,判斷代理是否實(shí)現(xiàn)當(dāng)前方法,否則會(huì)導(dǎo)致崩潰。
示例:
// 判斷代理對象是否實(shí)現(xiàn)這個(gè)方法,沒有實(shí)現(xiàn)會(huì)導(dǎo)致崩潰
if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {
[self.delegate userLoginWithUsername:self.username.text password:self.password.text];
}
舉例
假設(shè)我在公司正在敲代碼,敲的正開心呢,突然口渴了,想喝一瓶紅茶。這時(shí)我就可以拿起手機(jī)去外賣app上定一個(gè)紅茶,然后外賣app就會(huì)下單給店鋪并讓店鋪給我送過來。
這個(gè)過程中,外賣app就是我的代理,我就是委托方,我買了一瓶紅茶并付給外賣app錢,這就是購買協(xié)議。我只需要從外賣app上購買就可以,具體的操作都由外賣app去處理,我只需要最后接收這瓶紅茶就可以。我付的錢就是參數(shù),最后送過來的紅茶就是處理結(jié)果。
但是我買紅茶的同時(shí),我還想吃一份必勝客披薩,我需要另外向必勝客app去訂餐,上面的外賣app并沒有這個(gè)功能。我又向必勝客購買了一份披薩,必勝客當(dāng)做我的代理去為我做這份披薩,并最后送到我手里。這就是多個(gè)代理對象,我就是委托方。
在iOS中一個(gè)代理可以有多個(gè)委托方,而一個(gè)委托方也可以有多個(gè)代理。我指定了外賣app和必勝客兩個(gè)代理,也可以再指定麥當(dāng)勞等多個(gè)代理,委托方也可以為多個(gè)代理服務(wù)。
代理對象在很多情況下其實(shí)是可以復(fù)用的,可以創(chuàng)建多個(gè)代理對象為多個(gè)委托方服務(wù),在下面將會(huì)通過一個(gè)小例子介紹一下控制器代理的復(fù)用。
實(shí)現(xiàn)
首先定義一個(gè)協(xié)議類,來定義公共協(xié)議
#import <Foundation/Foundation.h>
@protocol LoginProtocol <NSObject>
@optional
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;
@end
定義委托類,這里簡單實(shí)現(xiàn)了一個(gè)用戶登錄功能,將用戶登錄后的賬號密碼傳遞出去,有代理來處理具體登錄細(xì)節(jié)。
#import <UIKit/UIKit.h>
#import "LoginProtocol.h"
/**
* 當(dāng)前類是委托類。用戶登錄后,讓代理對象去實(shí)現(xiàn)登錄的具體細(xì)節(jié),委托類不需要知道其中實(shí)現(xiàn)的具體細(xì)節(jié)。
*/
@interface LoginViewController : UIViewController
// 通過屬性來設(shè)置代理對象
@property (nonatomic, weak) id<LoginProtocol> delegate;
@end
實(shí)現(xiàn)部分:
@implementation LoginViewController
- (void)loginButtonClick:(UIButton *)button {
// 判斷代理對象是否實(shí)現(xiàn)這個(gè)方法,沒有實(shí)現(xiàn)會(huì)導(dǎo)致崩潰
if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {
// 調(diào)用代理對象的登錄方法,代理對象去實(shí)現(xiàn)登錄方法
[self.delegate userLoginWithUsername:self.username.text password:self.password.text];
}
}
代理方,實(shí)現(xiàn)具體的登錄流程,委托方不需要知道實(shí)現(xiàn)細(xì)節(jié)。
// 遵守登錄協(xié)議
@interface ViewController () <LoginProtocol>
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LoginViewController *loginVC = [[LoginViewController alloc] init];
loginVC.delegate = self;
[self.navigationController pushViewController:loginVC animated:YES];
}
/**
* 代理方實(shí)現(xiàn)具體登錄細(xì)節(jié)
*/
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password {
NSLog(@"username : %@, password : %@", username, password);
}
代理使用原理
代理實(shí)現(xiàn)流程
在iOS中代理的本質(zhì)就是代理對象內(nèi)存的傳遞和操作
我們在委托類設(shè)置代理對象后,實(shí)際上只是用一個(gè)id類型的指針將代理對象進(jìn)行了一個(gè)弱引用。委托方讓代理方執(zhí)行操作,實(shí)際上是在委托類中向這個(gè)id類型指針指向的對象發(fā)送消息,而這個(gè)id類型指針指向的對象,就是代理對象。
通過上面這張圖我們發(fā)現(xiàn),其實(shí)委托方的代理屬性本質(zhì)上就是代理對象自身,設(shè)置委托代理就是代理屬性指針指向代理對象,相當(dāng)于代理對象只是在委托方中調(diào)用自己的方法,如果方法沒有實(shí)現(xiàn)就會(huì)導(dǎo)致崩潰。從崩潰的信息上來看,就可以看出來是代理方?jīng)]有實(shí)現(xiàn)協(xié)議中的方法導(dǎo)致的崩潰。
而協(xié)議只是一種語法,是聲明委托方中的代理屬性可以調(diào)用協(xié)議中聲明的方法,而協(xié)議中方法的實(shí)現(xiàn)還是有代理方完成,而協(xié)議方和委托方都不知道代理方有沒有完成,也不需要知道怎么完成。
代理內(nèi)存管理
為什么我們設(shè)置代理屬性都使用weak呢?
我們定義的指針默認(rèn)都是__strong類型的,而屬性本質(zhì)上也是一個(gè)成員變量和set、get方法構(gòu)成的,strong類型的指針會(huì)造成強(qiáng)引用,必定會(huì)影響一個(gè)對象的生命周期,這也就會(huì)形成循環(huán)引用。
強(qiáng)引用
上圖中,由于代理對象使用強(qiáng)引用指針,引用創(chuàng)建的委托方LoginVC對象,并且成為LoginVC的代理。這就會(huì)導(dǎo)致LoginVC的delegate屬性強(qiáng)引用代理對象,導(dǎo)致循環(huán)引用的問題,最終兩個(gè)對象都無法正常釋放。
弱引用
我們將LoginVC對象的delegate屬性,設(shè)置為弱引用屬性。這樣在代理對象生命周期存在時(shí),可以正常為我們工作,如果代理對象被釋放,委托方和代理對象都不會(huì)因?yàn)閮?nèi)存釋放導(dǎo)致的Crash。
weak還是assign
下面兩種方式都是弱引用代理對象,但是第一種在代理對象被釋放后不會(huì)導(dǎo)致崩潰,而第二種會(huì)導(dǎo)致崩潰。
@property (nonatomic, weak) id<LoginProtocol> delegate;
@property (nonatomic, assign) id<LoginProtocol> delegate;
weak和assign是一種“非擁有關(guān)系”的指針,通過這兩種修飾符修飾的指針變量,都不會(huì)改變被引用對象的引用計(jì)數(shù)。但是在一個(gè)對象被釋放后,weak會(huì)自動(dòng)將指針指向nil,而assign則不會(huì)。在iOS中,向nil發(fā)送消息時(shí)不會(huì)導(dǎo)致崩潰的,所以assign就會(huì)導(dǎo)致野指針的錯(cuò)誤unrecognized selector sent to instance。
所以我們?nèi)绻揎棿韺傩裕€是用weak修飾吧,比較安全。
控制器瘦身-代理對象
為什么要使用代理對象?
隨著項(xiàng)目越來越復(fù)雜,控制器也隨著業(yè)務(wù)的增加而變得越來越臃腫。對于這種情況,很多人都想到了最近比較火的MVVM設(shè)計(jì)模式。但是這種模式學(xué)習(xí)曲線很大不好掌握,對于新項(xiàng)目來說可以使用,對于一個(gè)已經(jīng)很復(fù)雜的大中型項(xiàng)目,就不太好動(dòng)框架這層的東西了。
在項(xiàng)目中用到比較多的控件應(yīng)該就有UITableView了,有的頁面往往UITableView的處理邏輯很多,這就是導(dǎo)致控制器臃腫的一個(gè)很大的原因。對于這種問題,我們可以考慮給控制器瘦身,通過代理對象的方式給控制器瘦身。
什么是代理對象
這是平??刂破魇褂?code>UITableView(圖畫的難看,主要是意思理解就行)
這是我們優(yōu)化之后的控制器構(gòu)成
從上面兩張圖可以看出,我們將UITableView的delegate和DataSource單獨(dú)拿出來,由一個(gè)代理對象類進(jìn)行控制,只將必須控制器處理的邏輯傳遞給控制器處理。
UITableView的數(shù)據(jù)處理、展示邏輯和簡單的邏輯交互都由代理對象去處理,和控制器相關(guān)的邏輯處理傳遞出來,交由控制器來處理,這樣控制器的工作少了很多,而且耦合度也大大降低了。這樣一來,我們只需要將需要處理的工作交由代理對象處理,并傳入一些參數(shù)即可。