<簡書 — 劉小壯> http://www.jianshu.com/p/2113ffe54b30git
在項目中咱們常常會用到代理的設計模式,這是iOS中一種消息傳遞的方式,也能夠經過這種方式來傳遞一些參數。這篇文章會涵蓋代理的使用技巧和原理,以及代理的內存管理等方面的知識。我會經過這些方面的知識,帶你們真正領略代理的奧妙。寫的有點多,但都是乾貨,我能寫下去,不知道你有沒有耐心看下去。github
本人能力有限,若是文章中有什麼問題或沒有講到的點,請幫忙指出,十分感謝!設計模式
在iOS中有不少種消息傳遞方式,這裏先簡單介紹一下各類消息傳遞方式。安全
block
代碼塊中,看起來邏輯清晰代碼整齊。target
的方式,來調用該對象方法,從內存角度來講和代理相似。NSObject
的Category
-NSKeyValueObserving
,經過屬性監聽的方式來監測某個值的變化,當值發生變化時調用KVO
的回調方法。.....固然還有其餘回調方式,這裏只是簡單的列舉。網絡
代理是一種通用的設計模式,在iOS
中對代理設計模式支持的很好,有特定的語法來實現代理模式,__OC__語言能夠經過@Protocol
實現協議。session
代理主要由三部分組成:多線程
這裏用一張圖來闡述一下三方之間的關係:併發
從上圖中咱們能夠看到三方之間的關係,在實際應用中經過協議來規定代理雙方的行爲,協議中的內容通常都是方法列表,固然也能夠定義屬性,我會在後續文章中順帶講一下協議中定義屬性。app
協議是公共的定義,若是隻是某個類使用,咱們常作的就是寫在某個類中。若是是多個類都是用同一個協議,建議建立一個Protocol
文件,在這個文件中定義協議。遵循的協議能夠被繼承,例如咱們經常使用的UITableView
,因爲繼承自UIScrollView
的緣故,因此也將UIScrollViewDelegate
繼承了過來,咱們能夠經過代理方法獲取UITableView
偏移量等狀態參數。框架
協議只能定義公用的一套接口,相似於一個約束代理雙方的做用。但不能提供具體的實現方法,實現方法須要代理對象去實現。協議能夠繼承其餘協議,而且能夠繼承多個協議,在iOS
中對象是不支持多繼承的,而協議能夠多繼承。
// 當前協議繼承了三個協議,這樣其餘三個協議中的方法列表都會被繼承過來 @protocol LoginProtocol <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate> - (void)userLoginWithUsername:(NSString *)username password:(NSString *)password; @end
協議有兩個修飾符@optional
和@required
,建立一個協議若是沒有聲明,默認是@required
狀態的。這兩個修飾符只是約定代理是否強制須要遵照協議,若是@required
狀態的方法代理沒有遵照,會報一個黃色的警告,只是起一個約束的做用,沒有其餘功能。
不管是@optional
仍是@required
,在委託方調用代理方法時都須要作一個判斷,判斷代理是否實現當前方法,不然會致使崩潰。
示例:
// 判斷代理對象是否實現這個方法,沒有實現會致使崩潰 if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) { [self.delegate userLoginWithUsername:self.username.text password:self.password.text]; }
示例:假設我在公司正在敲代碼,敲的正開心呢,忽然口渴了,想喝一瓶紅茶。這時我就能夠拿起手機去外賣app上定一個紅茶,而後外賣app就會下單給店鋪並讓店鋪給我送過來。
這個過程當中,外賣app就是個人代理,我就是委託方,我買了一瓶紅茶並付給外賣app錢,這就是購買協議。我只須要從外賣app上購買就能夠,具體的操做都由外賣app去處理,我只須要最後接收這瓶紅茶就能夠。我付的錢就是參數,最後送過來的紅茶就是處理結果。
可是我買紅茶的同時,我還想吃一份必勝客披薩,我須要另外向必勝客app去訂餐,上面的外賣app並無這個功能。我又向必勝客購買了一份披薩,必勝客當作個人代理去爲我作這份披薩,並最後送到我手裏。這就是多個代理對象,我就是委託方。
在iOS
中一個代理能夠有多個委託方,而一個委託方也能夠有多個代理。我指定了外賣app和必勝客兩個代理,也能夠再指定麥當勞等多個代理,委託方也能夠爲多個代理服務。
代理對象在不少狀況下實際上是能夠複用的,能夠建立多個代理對象爲多個委託方服務,在下面將會經過一個小例子介紹一下控制器代理的複用。
首先定義一個協議類,來定義公共協議
#import <Foundation/Foundation.h> @protocol LoginProtocol <NSObject> @optional - (void)userLoginWithUsername:(NSString *)username password:(NSString *)password; @end
定義委託類,這裏簡單實現了一個用戶登陸功能,將用戶登陸後的帳號密碼傳遞出去,有代理來處理具體登陸細節。
#import <UIKit/UIKit.h> #import "LoginProtocol.h" /** * 當前類是委託類。用戶登陸後,讓代理對象去實現登陸的具體細節,委託類不須要知道其中實現的具體細節。 */ @interface LoginViewController : UIViewController // 經過屬性來設置代理對象 @property (nonatomic, weak) id<LoginProtocol> delegate; @end 實現部分: @implementation LoginViewController - (void)loginButtonClick:(UIButton *)button { // 判斷代理對象是否實現這個方法,沒有實現會致使崩潰 if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) { // 調用代理對象的登陸方法,代理對象去實現登陸方法 [self.delegate userLoginWithUsername:self.username.text password:self.password.text]; } }
代理方,實現具體的登陸流程,委託方不須要知道實現細節。
// 遵照登陸協議 @interface ViewController () <LoginProtocol> @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; 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); }
在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
的處理邏輯不少,這就是致使控制器臃腫的一個很大的緣由。對於這種問題,咱們能夠考慮給控制器瘦身,經過代理對象的方式給控制器瘦身。
這是日常控制器使用UITableView
(圖畫的難看,主要是意思理解就行)
這是咱們優化以後的控制器構成
從上面兩張圖能夠看出,咱們將UITableView
的delegate
和DataSource
單獨拿出來,由一個代理對象類進行控制,只將必須控制器處理的邏輯傳遞給控制器處理。
UITableView
的數據處理、展現邏輯和簡單的邏輯交互都由代理對象去處理,和控制器相關的邏輯處理傳遞出來,交由控制器來處理,這樣控制器的工做少了不少,並且耦合度也大大下降了。這樣一來,咱們只須要將須要處理的工做交由代理對象處理,並傳入一些參數便可。
代理對象.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
邏輯同樣或相似,代理對象是能夠複用的。
在iOS2.0
以前尚未引入@Protocol
正式協議以前,實現協議的功能主要是經過給NSObject
添加Category
的方式。這種經過Category
的方式,相對於iOS2.0
以後引入的@Protocol
,就叫作非正式協議。
正如上面所說的,非正式協議通常都是以NSObject
的Category
的方式存在的。因爲是對NSObject
進行的Category
,因此全部基於NSObject
的子類,都接受了所定義的非正式協議。對於@Protocol
來講編譯器會在編譯期檢查語法錯誤,而非正式協議則不會檢查是否實現。
非正式協議中沒有@Protocol
的@optional
和@required
之分,和@Protocol
同樣在調用的時候,須要進行判斷方法是否實現。
// 因爲是使用的Category,因此須要用self來判斷方法是否實現 if ([self respondsToSelector:@selector(userLoginWithUsername:password:)]) { [self userLoginWithUsername:self.username.text password:self.password.text]; }
在iOS
早期也使用了大量非正式協議,例如CALayerDelegate
就是非正式協議的一種實現,非正式協議本質上就是Category
。
@interface NSObject (CALayerDelegate) - (void)displayLayer:(CALayer *)layer; - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; - (void)layoutSublayersOfLayer:(CALayer *)layer; - (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event; @end
在iOS
中的回調方法有不少,而代理和block
功能更加類似,都是直接進行回調,那咱們應該用哪一個呢,或者說哪一個更好呢?
其實這兩種消息傳遞的方式,沒有哪一個更好、哪一個很差直說....咱們應該區分的是在什麼狀況下應該用什麼,用什麼更合適!下面我將會簡單的介紹一下在不一樣狀況下代理和block
的選擇:
delegate
。在有多個消息傳遞時,用delegate
實現更合適,看起來也更清晰。block
就不太好了,這個時候block
反而不便於維護,並且看起來很是臃腫,很彆扭。
例如UIKit
的UITableView
中有不少代理若是都換成block
實現,咱們腦海裏想一下這個場景,這裏就不用代碼寫例子了.....那簡直看起來不能忍受。
block
。(這裏主要是針對於對象內部屬性不會對block
進行引用的狀況下,不然再調用同一個方法也會形成從新賦值問題)上面圖中代理1能夠被設置,代理2和代理3設置的時候被劃了叉,是由於這個步驟是錯誤的操做。咱們上面說過,delegate
只是一個保存某個代理對象的地址,若是設置多個代理至關於從新賦值,只有最後一個設置的代理纔會被真正賦值。
這裏的block
不是應用於聲明在.h
文件中屬性回調的,主要是應用於方法回調的。例如如今有以下狀況須要回調,用block
能夠可是用設置delegate
的方式就不行了:「假設有block
回調對象downloadImage
類,同一個downloadImage
對象帶有block
回調的方法,在多個類或多個地方進行回調,這種狀況就更佳適合用block
的方式了。「每調用一次方法就能夠在block
回調代碼塊中,進行本身的操做,比代理的方式更佳強大。
delegate
。單例對象因爲始終都只是同一個對象,若是使用delegate
,就會形成咱們上面說的delegate
屬性被從新賦值的問題,最終只能有一個對象能夠正常響應代理方法。
這種狀況咱們可使用block
的方式,在主線程的多個對象中使用block
都是沒問題的,下面咱們將用一個循環暴力測試一下block
到底有沒有問題。
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; queue.maxConcurrentOperationCount = 10; for (int i = 0; i < 100; i++) { [queue addOperationWithBlock:^{ [[LoginViewController shareInstance] userLoginWithSuccess:^(NSString *username) { NSLog(@"TestTableViewController : %d", i); }]; }]; }
上面用NSOperationQueue
建立了一個新的隊列,而且將最大併發數設置爲10,而後建立一個100次的循環。咱們在多線程狀況下測試單例在block的狀況下可否正常使用,答案是能夠的。
可是咱們仍是須要注意一點,在多線程狀況下由於是單例對象,咱們對block
中必要的地方加鎖,防止資源搶奪的問題發生。
代理是可選的,而block在方法調用的時候只能經過將某個參數傳遞一個nil
進去,只不過這並非什麼大問題,沒有代碼潔癖的能夠忽略。
[self downloadTaskWithResumeData:resumeData
sessionManager:manager savePath:savePath progressBlock:nil successBlock:successBlock failureBlock:failureBlock];
從設計模式的角度來講,代理更佳面向過程,而block
更佳面向結果。例如咱們使用NSXMLParserDelegate
代理進行XML解析,NSXMLParserDelegate
中有不少代理方法,NSXMLParser
會不間斷調用這些方法將一些轉換的參數傳遞出來,這就是NSXMLParser
解析流程,這些經過代理來展示比較合適。而例如一個網絡請求回來,就經過success
、failure
代碼塊來展現就比較好。
block
的性能消耗要略大於delegate
,由於block
會涉及到棧區向堆區拷貝等操做,時間和空間上的消耗都大於代理。而代理只是定義了一個方法列表,在遵照協議對象的objc_protocol_list
中添加一個節點,在運行時向遵照協議的對象發送消息便可。這篇文章並非講block
的,因此不對此作過多敘述。唐巧有一篇文章介紹過block
,很是推薦這篇文章去深刻學習block
。文章地址
文章中舉了代理對象的例子,能夠經過建立代理對象將代碼封裝到一個類中,我對這個代理對象的例子寫了一個簡單的Demo
。
Demo
只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo
一塊兒學習,只看Demo
仍是不能理解更深層的原理。Demo
中代碼都會有註釋,各位能夠打斷點跟着Demo
執行流程走一遍,看看各個階段變量的值。
Demo地址:劉小壯的Github