淺析Block閉包

来源:https://www.cnblogs.com/Solist/archive/2020/05/20/12923264.html
-Advertisement-
Play Games

淺析Block閉包 簡單來說,block就是將函數及其上下文封裝起來的對象,從功能上可以把它看作是C++中的匿名函數,也可稱之為塊。 Block類型寫法: 返回值+(^塊名)+(參數)= ^(參數){ 內容 } 如下所示: Block結構 Block存儲區域 Block本質上也是OC對象,所以每個B ...


淺析Block閉包

簡單來說,block就是將函數及其上下文封裝起來的對象,從功能上可以把它看作是C++中的匿名函數,也可稱之為塊。

Block類型寫法:

返回值+(^塊名)+(參數)= ^(參數){ 內容 }

如下所示:

int (^myBlock)(int a, int b) = ^(int a, int b){
    return a + b;
};

Block結構

Block存儲區域

Block本質上也是OC對象,所以每個Block對象也有isa指針指向它們的類對象。根據Block類對象存儲的記憶體空間的不同可分為三種不同的類,分別是:

位於全局區的Block類:__NSGlobalBlock__

位於棧區的Block類:__NSStackBlock__

位於堆區的Block類:__NSMallocBlock__

  • 全局區Block:當Block不捕獲外部變數時,會被編譯器分配到全局區。因為無外部變數,所以運行時不會在Block內部進行copy或dispose操作,為了削減開銷,所以在編譯時就確定了大小,即存儲在全局區。如下:
void (^myBlock)(void)=^(void){
    NSLog(@"global");
};
NSLog(@"%@",[myBlock class]);

//輸出:
//__NSGlobalBlock__
  • 棧區Block:當Block捕獲了外部變數後,會被分配到棧區。但是在ARC環境下,系統會自動為生成的棧區Block進行copy操作,所以為了驗證是否是在棧區,需要採用MRC環境,在main.m文件的編譯選項設置為: -fno-objc-arc後運行如下代碼:

    NSString* flag=@"yes";
    void (^myBlock)(void)=^(void){
        NSLog(@"stack:%@",flag);
    };
    NSLog(@"%@",[myBlock class]);
    
    //輸出:
    //__NSStackBlock__
    
  • 堆區Block:在MRC模式下,用copy後,會將棧區block複製到堆區。在ARC模式下,系統自動將初始化的Block複製到堆區。

    //MRC環境下:
    NSString* flag=@"yes";
    void (^myBlock)(void)=[^(void){
        NSLog(@"stack:%@",flag);
    } copy];
    NSLog(@"%@",[myBlock class]);
    
    //輸出:
    //__NSMallocBlock__
    

Block內部結構

官方的Block定義在 Block_private.h中,具體的源碼:Block_private.h

#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

//Block結構
struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
  • isa指針:指向類對象的指針,即根據不同分區指向: __NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__,但是這裡的底層isa實際上指向的是父類的結構體(C語言)即:_NSConcreteGlobalBlock _NSConcreteStackBlock _NSConcreteMallocBlock結構體,但意義是一樣的。
  • flags:類型為枚舉,主要用來保存Block的狀態信息。
  • reserved:為之後開發準備的保留信息,暫時無用。
  • invoke:函數指針,指向的是實際的功能運行函數。在invoke函數的參數中還包含了Block結構體本身,這麼做的目的是在執行時,可以從記憶體中獲取block中捕獲的變數。
  • descriptor:主要存儲Block的附加信息,其中包括占址大小、簽名等。預設指向Block_descriptor_1結構體,當Block被copy到堆上時,則會添加Block_descriptor_2和Block_descriptor_3,新增copydispose方法用來拷貝和銷毀捕獲的變數。

Block內部結構圖(來自於Effective-OC):

Block作用

在日常的開發中,使用Block的主要用處在以下兩個方面:

  1. 作為回調的方式之一,對比於代理模式,Block可將將分散的代碼塊集中寫在一處編寫。因為有捕獲變數的機制,所以可以很輕鬆的訪問上下文,並且Block的代碼是內聯的,運行效率會更高。

  2. 正是因為有了以上的優勢,所以在編寫非同步代碼,作為非同步處理回調時,在封裝時往往會採用handler塊的方式來編寫相關代碼。

    在編寫handler塊時有兩種策略,一種是在一個方法中提供提供兩個Block塊分別處理CompletionHandler和errorHandler,另外一種是只提供一個Block塊,在Block塊中提供error參數,用戶自己來對error值進行判斷。一般我們更傾向於後者的方式,因為這樣處理數據會更加靈活

兩種Handler風格如下:

Downloader *myDownloader = [[Downloader alloc] initWithURL:url];
[myDownloader downloadWithCompletionHandler:^(NSData *onlineData){
  //download success
}
failureHandler:^(NSError *error){
  //handle error
}];
Downloader *myDownloader = [[Downloader alloc] initWithURL:url];
[myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
  if(succeeded){
    //download success
  }
  else{
    //handle error
  }
}];

Block記憶體泄漏

當幾個oc對象互相強引用成環時,就會導致對象永遠都不會被釋放,當這些對象的數量很大時,就會造成記憶體泄漏,從而導致整個系統crash的風險。

舉個例子:

當A類對象強引用了B類對象,B類對象強引用了C類對象,而C對象又強引用了A類對象。假設它們都在一個代碼段中。如下圖所示:

因為a、b、c都被該代碼段所強引用,所以retainCount初始化都為1,又因為它們互相強引用,所以在連成環的時候retainCount都變為了2。這時候在代碼段中,無論是哪一個對象先從代碼段中釋放,即retainCount--,都仍然還剩1。當整個代碼段執行完後,三個類對象a、b、c的retainCount都從2減為了1,在整個系統中,再也沒有其他影響因素會讓它們的retainCount減少為0,這樣就會導致這三個對象在運行中永不釋放,從而造成記憶體泄漏。

在使用Block時也會很容易造成這個現象,當在網路非同步的handler塊中,我們通常會將當前ViewController中的某個網路數據屬性捕獲到handler中,在網路連接成功後將其進行賦值,這樣就相當於Block塊間接地強引用了當前VC,而通常來說,VC肯定會強引用下載器,而下載器中的Block塊一般也會做為其屬性進行強引用。如下圖所示:

為瞭解決強引用環的問題,可以通過將任意一個連接處斷開即可。

  • 斷開1:基本不可能,在開發中在ViewController或者時ViewModel中都會將下載器作為屬性而非臨時變數,因為在調取過程中會一般會根據當前下載狀態來進行下一步操作。

  • 斷開2:

    方法一:不將_downloadHandler作為屬性,而是使用臨時Block變數,通常這麼做的情況是因為下載器類不需要多次使用該block,對於複雜的下載器,這種策略很難得以保證。

    方法二:(推薦)在下載操作結束後調用的方法中令 self.downloadHandler = nil,只要下載請求執行完畢,_downloadHandler屬性就不再強引用該block,就打破了強引用環。

  • 斷開3:

    方法一:因為Block強引用了VC的data屬性,實際上也就強引用了VC(self),所以我們可以通過: __weak typeof(self) weakSelf=self將當前VC,即self弱引用化,生成一個名為weakSelf的當前vc對象,然後在block中使用 weakSelf.data=_data來進行調用。

    方法二:方法一中大部分情況不會出現問題,但是當block塊中有延時操作,而對_data的處理也在延時操作當中時,就會出現問題了,例如:

    [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
      if(succeeded){
        //download success
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //延遲2s獲取data數據
            weakSelf.data = onlineData;
            NSLog(@"%@",weakSelf.data);
        });
      }
      else{
        //handle error
      }
    }];
    
    //假設成功從網路上獲取到data
    //列印為空
    

    這時候就會發現,無論是weakSelf還是self的data屬性都為空。這就是因為在block執行完後(延時函數還未執行完),weakSelf所在的弱引用表已經被除名了,雖然延時函數還在執行。這時候當2s過後,weakSelf已經變為了nil,對nil發送getter消息也不會報錯,所以這裡就會出現取值為空的情況。

    為瞭解決這一問題,只需要在block內再將weakSelf在代碼段內部強引用化(該強引用僅限於Block內部)。例如:

    [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
      if(succeeded){
        //download success
        //將weakSelf強引用化生成該代碼段的strong變數
        __strong typeof(self) strongSelf=weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //延遲2s獲取data數據
            //這裡使用strongSelf臨時變數
            strongSelf.data = onlineData;
            NSLog(@"%@",strongSelf.data);
        });
      }
      else{
        //handle error
      }
    }];
    

    這裡的strongSelf屬於臨時變數,會加到該代碼段(Block內)的autoreleasepool當中,當該處代碼段結束時會自動釋放掉,所以也就不會出現強引用情況。

    方法三:使用臨時變數充噹噹前VC(self),如下:

    __block XXXViewController* vc = self;  //這裡self的retainCount會+1
    [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
      if(succeeded){
        //download success
        vc.data = onlineData;
        //這裡需要註意將該臨時變數置為nil,即將retainCount重新減為1
        vc=nil;
      }
      else{
        //handle error
      }
    }];
    

    這裡需要註意在賦完值後必須將該臨時變數重新置為nil,即將retainCount減1,否則仍會出現強引用的問題。

    方法四:將當前self作為block參數傳入,例如:

    [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded, XXXViewController* vc){
      if(succeeded){
        //download success
        vc.data = onlineData;
      }
      else{
        //handle error
      }
    }];
    

    這種情況一般很少出現,因為下載器通常作為第三方提供的API,通常參數不會有當前控制類。所以這種情況只能用在自定義block當中使用。

總結

  • 在ARC環境下開發,我們用到的一般都是堆Block或全局Block,當捕獲外界變數時為堆Block,否則為全局Block
  • Block主要用於代碼回調以及非同步操作以降低代碼分散程度。
  • Block在捕獲變數時很容易造成迴圈引用,導致記憶體泄漏。在不確定調用第三方API是否在最後將block屬性置為空,或者沒有使用屬性而是臨時變數作為調用block,所以在不破環封裝性的原則下,將其視為未處理,然後在自己的代碼中使用waekSelf和strongSelf方式來進行當前self的屬性進行操作,這樣就實現了在環節[3]中打破強引用環。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [TOC] 1.Xtrabackup介紹 Xtrabackup是Percona公司專門針對MySQL資料庫開發的一款開源免費的物理備份(熱備)工具,可以對InnoDB和XtraDB等事務引擎的資料庫實現非阻塞(即不鎖表)方式的備份,也可以針對MyISAM等非事務引擎實現鎖表方式備份。 Xtrabac ...
  • 一、什麼是PL/SQL? PL/SQL(Procedure Language/SQL)是oracle在標準的sql語言上的擴展。ql/sql不僅允許嵌入sql語言,還可以定義變數和常量,允許私用條件語句和迴圈語句,允許使用例外處理各種錯誤,這使得它的功能變得更加強大。 PL/SQL開發工具主要有: ...
  • [TOC] 1.資料庫管理員的兩大工作核心 1.1.能夠讓數據安全得到保護 所謂的數據安全,最容易被人誤以為是只有數據丟失,其實還包括數據被脫庫、泄密等方面。 1.2.能7 24小時提供服務 資料庫具備7 24小時提供服務的能力,是資料庫管理員的重要職責。 2.全量備份和增量備份 2.1.全量備份的 ...
  • 前言: 本文詳細介紹了 HBase DependentColumnFilter 過濾器 Java&Shell API 的使用,並貼出了相關示例代碼以供參考。DependentColumnFilter 也稱參考列過濾器,是一種允許用戶指定一個參考列或引用列來過濾其他列的過濾器,過濾的原則是基於參考列的 ...
  • 前言: 本文詳細介紹了 HBase ValueFilter 過濾器 Java&Shell API 的使用,並貼出了相關示例代碼以供參考。ValueFilter 基於列值進行過濾,在工作中涉及到需要通過HBase 列值進行數據過濾時可以考慮使用它。比較器細節及原理請參照之前的更文: "HBase Fi ...
  • A.前言:為瞭解決安卓端向伺服器上傳照片的問題 1.獲得相冊許可權,選取照片,取到照片的url 2.使用okhttp訪問伺服器並向伺服器傳照片 3.配置springmvc文件解析器 4.搭建伺服器,獲取數據保存照片 B.Android添加一個按鈕和一個ImageView,設置它的點擊事件,打開相冊選擇 ...
  • 1.這裡編寫一個類用於開啟活動,首先在onCreateView()方法中載入了我們剛剛創建的news_content_frag佈局,這個沒什麼好解釋的,接下來又提供了一個refresh()方法,這個方法就是用於將新聞的標題和內容顯示在界面上的。可以看到,這裡通過findViewById()方法分別獲 ...
  • 插件 1. "get" Navigate between screens, display snackbars, dialogs and bottomSheets, from anywhere in your code without context with Get. 1. "FlutterVis ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...