寫在前頭:
我們都知道代理模式是一種通用的設計模式,iOS中對代理支持的很好,由代理對象、委托者、協議三部分組成。但你真的一眼就能分清誰是代理對象,誰是委托者嗎?舉個栗子:我們常用的UITableViewController,這里面有UITableViewController、UITableView、UITableViewDelegate,誰是代理對象,誰是委托者呢?
如果您不能一眼就分清,請靜下心來,一起漫步在這白雪皚皚(ai)的世界里。
代理的基本使用
代理是一種通用的設計模式,在iOS中對代理設計模式支持的很好,有特定的語法來實現代理模式,OC語言可以通過@Protocol實現協議。
代理主要由三部分組成:
協議:用來指定代理雙方可以做什么,必須做什么。
代理:根據指定的協議,完成委托方需要實現的功能。
委托:根據指定的協議,指定代理去完成什么功能。
這里用一張圖來闡述一下三方之間的關系:
在iOS中一個代理可以有多個委托方,而一個委托方也可以有多個代理。我指定了外賣app和必勝客兩個代理,也可以再指定麥當勞等多個代理,委托方也可以為多個代理服務。
代理對象在很多情況下其實是可以復用的,可以創建多個代理對象為多個委托方服務,在下面將會通過一個小例子介紹一下控制器代理的復用。
下面是一個簡單的代理:
//首先定義一個協議類,來定義公共協議
#import <Foundation/Foundation.h>
@protocol LoginProtocol
@optional
- (void)userLoginWithUsername:(NSString*)username password:(NSString*)password;
@end
//定義委托類,這里簡單實現了一個用戶登錄功能,將用戶登錄后的賬號密碼傳遞出去,有代理來處理具體登錄細節。
#import "LoginProtocol.h"
// 當前類是委托類。用戶登錄后,讓代理對象去實現登錄的具體細節,委托類不需要知道其中實現的具體細節。
@interface LoginViewController:UIViewController
// 通過屬性來設置代理對象
@property(nonatomic,weak)id delegate;
@end
//實現部分:
@ import LoginViewController
- (void)loginButtonClick:(UIButton*)button
{
// 判斷代理對象是否實現這個方法,沒有實現會導致崩潰
if([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {
// 調用代理對象的登錄方法,代理對象去實現登錄方法
[self.delegate userLoginWithUsername:self.username.text password:self.password.text];
}
}
//代理方,實現具體的登錄流程,委托方不需要知道實現細節。
// 遵守登錄協議
@interface ViewController()
@end
@ import ViewController
- (void)viewDidLoad {
[superviewDidLoad];
LoginViewController *loginVC = [[LoginViewController alloc] init];
loginVC.delegate =self;
[self.navigationController pushViewController:loginVC animated:YES];
}
//* 代理方實現具體登錄細節
*/- (void)userLoginWithUsername:(NSString*)username password:(NSString*)password {
NSLog(@"username : %@, password : %@", username, password);
}
一個簡單的代理就實現,回顧上面代理的三部分,協議是LoginProtocol,實現功能的代理是ViewController,委托是LoginViewController
A.delegate = B ;那么A就是委托,B就是被委托的代理
簡單來說,誰實現了代理的方法,誰就是代理。
回顧文章開頭的問題:UITableViewController、UITableView、UITableViewDelegate,誰是代理對象,誰是委托者。是不是一眼就知道了UITableViewController是UITableView的代理,UITableView是委托
通過以上的解讀,你是否弄懂了這個問題呢?下面來點原理性的內容
代理使用原理
代理實現流程
在iOS
中代理的本質就是代理對象內存的傳遞和操作,我們在委托類設置代理對象后,實際上只是用一個id
類型的指針將代理對象進行了一個弱引用。委托方讓代理方執行操作,實際上是在委托類中向這個id
類型指針指向的對象發送消息,而這個id
類型指針指向的對象,就是代理對象。
通過上面這張圖我們發現,其實委托方的代理屬性本質上就是代理對象自身,設置委托代理就是代理屬性指針指向代理對象,相當于代理對象只是在委托方中調用自己的方法,如果方法沒有實現就會導致崩潰。從崩潰的信息上來看,就可以看出來是代理方沒有實現協議中的方法導致的崩潰。
而協議只是一種語法,是聲明委托方中的代理屬性可以調用協議中聲明的方法,而協議中方法的實現還是由代理方完成,而協議方和委托方都不知道代理方有沒有完成,也不需要知道怎么完成。
代理內存管理
為什么我們設置代理屬性都使用weak呢?
我們定義的指針默認都是__strong
類型的,而屬性本質上也是一個成員變量和set
、get
方法構成的,strong
類型的指針會造成強引用,必定會影響一個對象的生命周期,這也就會形成循環引用。
上圖中,由于代理對象使用強引用指針,引用創建的委托方LoginVC
對象,并且成為LoginVC
的代理。這就會導致LoginVC
的delegate
屬性強引用代理對象,導致循環引用的問題,最終兩個對象都無法正常釋放。
我們將LoginVC
對象的delegate
屬性,設置為弱引用屬性。這樣在代理對象生命周期存在時,可以正常為我們工作,如果代理對象被釋放,委托方和代理對象都不會因為內存釋放導致的Crash。
但是,這樣還有點問題,真的不會崩潰嗎?
下面兩種方式都是弱引用代理對象,但是第一種在代理對象被釋放后不會導致崩潰,而第二種會導致崩潰。
@property (nonatomic, weak) id<LoginProtocol> delegate;
@property (nonatomic, assign) id<LoginProtocol> delegate;
weak
和assign
是一種“非擁有關系”的指針,通過這兩種修飾符修飾的指針變量,都不會改變被引用對象的引用計數。但是在一個對象被釋放后,weak
會自動將指針指向nil
,而assign
則不會。在iOS
中,向nil
發送消息時不會導致崩潰的,所以assign
就會導致野指針的錯誤unrecognized selector sent to instance
。
所以我們如果修飾代理屬性,還是用weak
修飾吧,比較安全。
控制器瘦身-代理對象
為什么要使用代理對象?
隨著項目越來越復雜,控制器也隨著業務的增加而變得越來越臃腫。對于這種情況,很多人都想到了最近比較火的MVVM設計模式。但是這種模式學習曲線很大不好掌握,對于新項目來說可以使用,對于一個已經很復雜的大中型項目,就不太好動框架這層的東西了。
在項目中用到比較多的控件應該就有UITableView
了,有的頁面往往UITableView
的處理邏輯很多,這就是導致控制器臃腫的一個很大的原因。對于這種問題,我們可以考慮給控制器瘦身,通過代理對象的方式給控制器瘦身。
什么是代理對象
我們平常使用的UIViewController+UITableView都是
UITableView.delegate = UIViewController模式,UIViewController是代理,實現了代理方法。用法簡單但有一個問題,導致了UIViewController的臃腫。一個tableView還好,要是用幾個tableView呢?
那有什么辦法換個代理對象嗎,將代理方法從UIViewController中移除?
下面我們用一段代碼來實現一個簡單的代理對象
代理對象.h文件的聲明
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef void (^selectCell) (NSIndexPath *indexPath);
// 代理對象(UITableView的協議需要聲明在.h文件中,不然外界在使用的時候會報黃色警告,看起來不太舒服)
@interface TableViewDelegateObj : NSObject <UITableViewDelegate, UITableViewDataSource>
/**
* 創建代理對象實例,并將數據列表傳進去
* 代理對象將消息傳遞出去,是通過block的方式向外傳遞消息的
* @return 返回實例對象
*/
+ (instancetype)createTableViewDelegateWithDataList:(NSArray *)dataList
selectBlock:(selectCell)selectBlock;
@end
代理對象.m文件中的實現
import "TableViewDelegateObj.h"
@interface TableViewDelegateObj ()
@property (nonatomic, strong) NSArray *dataList;
@property (nonatomic, copy) selectCell selectBlock;
@end
@implementation TableViewDelegateObj
+ (instancetype)createTableViewDelegateWithDataList:(NSArray *)dataList
selectBlock:(selectCell)selectBlock {
return [[[self class] alloc] initTableViewDelegateWithDataList:dataList
selectBlock:selectBlock];
}
- (instancetype)initTableViewDelegateWithDataList:(NSArray *)dataList selectBlock:(selectCell)selectBlock {
self = [super init];
if (self) {
self.dataList = dataList;
self.selectBlock = selectBlock;
}
return self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
cell.textLabel.text = self.dataList[indexPath.row];
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataList.count;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
// 將點擊事件通過block的方式傳遞出去
self.selectBlock(indexPath);
}
@end
外界控制器的調用非常簡單,幾行代碼就搞定了。
self.tableDelegate = [TableViewDelegateObj createTableViewDelegateWithDataList:self.dataList
selectBlock:^(NSIndexPath *indexPath) {
NSLog(@"點擊了%ld行cell", (long)indexPath.row);
}];
self.tableView.delegate = self.tableDelegate;
self.tableView.dataSource = self.tableDelegate;
在控制器中只需要創建一個代理對象類,并將UITableView
的delegate
和dataSource
都交給代理對象去處理,讓代理對象成為UITableView
的代理,解決了控制器臃腫以及和UITableView
的解藕。
上面的代碼只是簡單的實現了點擊cell
的功能,如果有其他需求大多也都可以在代理對象中進行處理。使用代理對象類還有一個好處,就是如果多個UITableView
邏輯一樣或類似,代理對象是可以復用的。
提供一份Demo,希望對你有幫助
寫在最后:
希望這篇文章對您有幫助。當然如果您發現有可以優化的地方,希望您能慷慨的提出來。最后祝您工作愉快!