設計模式--代理模式(iOS)

寫在前頭:
我們都知道代理模式是一種通用的設計模式,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類型的,而屬性本質上也是一個成員變量和setget方法構成的,strong類型的指針會造成強引用,必定會影響一個對象的生命周期,這也就會形成循環引用。

循環引用

上圖中,由于代理對象使用強引用指針,引用創建的委托方LoginVC對象,并且成為LoginVC的代理。這就會導致LoginVCdelegate屬性強引用代理對象,導致循環引用的問題,最終兩個對象都無法正常釋放。

弱引用

我們將LoginVC對象的delegate屬性,設置為弱引用屬性。這樣在代理對象生命周期存在時,可以正常為我們工作,如果代理對象被釋放,委托方和代理對象都不會因為內存釋放導致的Crash

但是,這樣還有點問題,真的不會崩潰嗎?

下面兩種方式都是弱引用代理對象,但是第一種在代理對象被釋放后不會導致崩潰,而第二種會導致崩潰。

@property (nonatomic, weak) id<LoginProtocol> delegate;
@property (nonatomic, assign) id<LoginProtocol> delegate;

weakassign是一種“非擁有關系”的指針,通過這兩種修飾符修飾的指針變量,都不會改變被引用對象的引用計數。但是在一個對象被釋放后,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;

在控制器中只需要創建一個代理對象類,并將UITableViewdelegatedataSource都交給代理對象去處理,讓代理對象成為UITableView的代理,解決了控制器臃腫以及和UITableView的解藕。

上面的代碼只是簡單的實現了點擊cell的功能,如果有其他需求大多也都可以在代理對象中進行處理。使用代理對象類還有一個好處,就是如果多個UITableView邏輯一樣或類似,代理對象是可以復用的。

提供一份Demo,希望對你有幫助

寫在最后:
希望這篇文章對您有幫助。當然如果您發現有可以優化的地方,希望您能慷慨的提出來。最后祝您工作愉快!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容