你真的瞭解iOS代理設計模式嗎?

來源:互聯網
上載者:User

你真的瞭解iOS代理設計模式嗎?

在項目中我們經常會用到代理的設計模式,這是iOS中一種訊息傳遞的方式,也可以通過這種方式來傳遞一些參數。

這篇文章會涵蓋代理的提示和原理,以及代理的記憶體管理等方面的知識。我會通過這些方面的知識,帶大家真正領略代理的奧妙。寫的有點多,但都是乾貨,我能寫下去,不知道你有沒有耐心看下去。

本人能力有限,如果文章中有什麼問題或沒有講到的點,請幫忙指出,十分感謝!

iOS中訊息傳遞方式

在iOS中有很多種訊息傳遞方式,這裡先簡單介紹一下各種訊息傳遞方式。

 

  • 通知:在iOS中由通知中樞進行訊息接收和訊息廣播,是一種一對多的訊息傳遞方式。
     

  • 代理:是一種通用的設計模式,iOS中對代理支援的很好,由代理對象、委託者、協議三部分組成。
     

  • block:iOS4.0中引入的一種回調方法,可以將回調處理代碼直接寫在block代碼塊中,看起來邏輯清晰代碼整齊。
     

  • target action:通過將對象傳遞到另一個類中,在另一個類中將該對象當做target的方式,來調用該對象方法,從記憶體角度來說和代理類似。
     

  • KVO:NSObject的Category-NSKeyValueObserving,通過屬性監聽的方式來監測某個值的變化,當值發生變化時調用KVO的回調方法。
     

  • …..當然還有其他回調方式,這裡只是簡單的列舉。

     

    代理的基本使用

     

    代理是一種通用的設計模式,在iOS中對代理設計模式支援的很好,有特定的文法來實現代理模式,OC語言可以通過@Protocol實現協議。

    代理主要由三部分組成:

    • 協議:用來指定代理雙方可以做什麼,必須做什麼。

    • 代理:根據指定的協議,完成委託方需要實現的功能。

    • 委託:根據指定的協議,指定代理去完成什麼功能。

      這裡用一張圖來闡述一下三方之間的關係:


       

       

      Protocol-協議的概念

       

      從中我們可以看到三方之間的關係,在實際應用中通過協議來規定代理雙方的行為,協議中的內容一般都是方法列表,當然也可以定義屬性,我會在後續文章中順帶講一下協議中定義屬性。

       

      協議是公用的定義,如果只是某個類使用,我們常做的就是寫在某個類中。如果是多個類都是用同一個協議,建議建立一個Protocol檔案,在這個檔案中定義協議。遵循的協議可以被繼承,例如我們常用的UITableView,由於繼承自UIScrollView的緣故,所以也將UIScrollViewDelegate繼承了過來,我們可以通過代理方法擷取UITableView位移量等狀態參數。

       

      協議只能定義公用的一套介面,類似於一個約束代理雙方的作用。但不能提供具體的實現方法,實現方法需要代理對象去實現。協議可以繼承其他協議,並且可以繼承多個協議,在iOS中對象是不支援多繼承的,而協議可以多繼承。

       

      // 當前協議繼承了三個協議,這樣其他三個協議中的方法列表都會被繼承過來

      @protocol LoginProtocol

      - (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

      @protocol LoginProtocol

      @optional

      - (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;

      @end

       

      定義委託類,這裡簡單實現了一個使用者登入功能,將使用者登入後的帳號密碼傳遞出去,有代理來處理具體登入細節。

       

      #import

      #import "LoginProtocol.h"

      /**

      *當前類是委託類。使用者登入後,讓代理對象去實現登入的具體細節,委託類不需要知道其中實現的具體細節。

      */

      @interface LoginViewController : UIViewController

      // 通過屬性來設定代理對象

      @property (nonatomic, weak) id 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 ()

      @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 delegate;

      @property (nonatomic, assign) id 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

      #import

       

      typedef void (^selectCell) (NSIndexPath *indexPath);

      //代理對象(UITableView的協議需要聲明在.h檔案中,不然外界在使用的時候會報黃色警告,看起來不太舒服)

      @interface TableViewDelegateObj : NSObject

       

      /**

      *建立代理對象執行個體,並將資料列表傳進去

      *代理對象將訊息傳遞出去,是通過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)actionForLayer:(CALayer *)layer forKey:(NSString *)event;

      @end

       

      代理和block的選擇

      在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

        上面用NSOperationQueue建立了一個新的隊列,並且將最大並發數設定為10,然後建立一個100次的迴圈。我們在多線程情況下測試單例在block的情況下能否正常使用,答案是可以的。

        但是我們還是需要注意一點,在多線程情況下因為是單例對象,我們對block中必要的地方加鎖,防止資源搶奪的問題發生。

        代理是可選的,而block在方法調用的時候只能通過將某個參數傳遞一個nil進去,只不過這並不是什麼大問題,沒有代碼潔癖的可以忽略。

         

        [self downloadTaskWithResumeData:resumeData

        sessionManager:manager

        savePath:savePath

        progressBlock:nil

        successBlock:successBlock

        failureBlock:failureBlock];

         

        代理更加面相過程,block則更面向結果。

        從設計模式的角度來說,代理更佳面向過程,而block更佳面向結果。例如我們使用NSXMLParserDelegate代理進行XML解析,NSXMLParserDelegate中有很多代理方法,NSXMLParser會不間斷調用這些方法將一些轉換的參數傳遞出來,這就是NSXMLParser解析流程,這些通過代理來展現比較合適。而例如一個網路請求回來,就通過success、failure代碼塊來展示就比較好。

        從效能上來說,block的效能消耗要略大於delegate,因為block會涉及到棧區向堆區拷貝等操作,時間和空間上的消耗都大於代理。而代理只是定義了一個方法列表,在遵守協議對象的objc_protocol_list中添加一個節點,在運行時向遵守協議的對象發送訊息即可。

        這篇文章並不是講block的,所以不對此做過多敘述。唐巧有一篇文章介紹過block,非常推薦這篇文章去深入學習block。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.