iOS 深入理解 Block 使用及原理

探究系列已發(fā)布文章列表,有興趣的同學(xué)可以翻閱一下:

第一篇 | iOS 屬性 @property 詳細(xì)探究

第二篇 | iOS 深入理解 Block 使用及原理

第三篇 | iOS 類別 Category 和擴(kuò)展 Extension 及關(guān)聯(lián)對(duì)象詳解

第四篇 | iOS 常用鎖 NSLock ,@synchronized 等的底層實(shí)現(xiàn)詳解

第五篇 | iOS 全面理解 Nullability

------- 正文開始 -------

引言

在 iOS 日常開發(fā)中,Block 的使用頻率是比較多的,我們不會(huì)每天都做啟動(dòng)優(yōu)化,也不會(huì)每天都做性能優(yōu)化,但有可能每天都會(huì)用到 Block 。本文就著重介紹一下 Block 在日常開發(fā)中值得我們關(guān)注的技術(shù)點(diǎn),大家一起學(xué)習(xí)。


代碼規(guī)范

// 定義一個(gè) Block
typedef returnType (^BlockName)(parameterA, parameterB, ...);

eg: typedef void (^RequestResult)(BOOL result);

// 實(shí)例
^{
    NSLog(@"This is a block");
 }

本質(zhì)

Block 本質(zhì)上是一個(gè) Objective-C 的對(duì)象,它內(nèi)部也有一個(gè) isa 指針,它是一個(gè)封裝了函數(shù)及函數(shù)調(diào)用環(huán)境的 Objective-C 對(duì)象,可以添加到 NSArrayNSDictionary 等集合中,它是基于 C 語言及運(yùn)行時(shí)特性,有點(diǎn)類似標(biāo)準(zhǔn)的 C 函數(shù)。但除了可執(zhí)行代碼以外,另外包含了變量同堆或棧的自動(dòng)綁定。


常用介紹

  • Block 的類型:
  1. NSGlobalBlock
void (^exampleBlock)(void) = ^{
    // block
};
NSLog(@"exampleBlock is: %@",[exampleBlock class]); 

打印日志:exampleBlock is: __NSGlobalBlock__

如果一個(gè) block 沒有訪問外部局部變量,或者訪問的是全局變量,或者靜態(tài)局部變量,此時(shí)的 block 就是一個(gè)全局 block ,并且數(shù)據(jù)存儲(chǔ)在全局區(qū)。

  1. NSStackBlock
int temp = 100;
void (^exampleBlock)(void) = ^{
    // block
    NSLog(@"exampleBlock is: %d", temp);
};

NSLog(@"exampleBlock is: %@",[exampleBlock class]);

打印日志:exampleBlock is: __NSMallocBlock__???
不是說好的 __NSStackBlock__ 的嗎?為什么打印的是__NSMallocBlock__ 呢?這里是因?yàn)槲覀兪褂昧?ARC ,Xcode 默認(rèn)幫我們做了很多事情。

我們可以去 Build Settings 里面,找到 Objective-C Automatic Reference Counting ,并將其設(shè)置為 No ,然后再 Run 一次代碼。你會(huì)看到打印日志是:exampleBlock is: __NSStackBlock__

如果 block 訪問了外部局部變量,此時(shí)的 block 就是一個(gè)棧 block ,并且存儲(chǔ)在棧區(qū)。由于棧區(qū)的釋放是由系統(tǒng)控制,因此棧中的代碼在作用域結(jié)束之后內(nèi)存就會(huì)銷毀,如果此時(shí)再調(diào)用 block 就會(huì)發(fā)生問題,( 注: 此代碼運(yùn)行在 MRC 下)如:

void (^simpleBlock)(void);
void callFunc() {
    int age = 10;
    simpleBlock = ^{
        NSLog(@"simpleBlock-----%d", age);
    };
}

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        callFunc();
        simpleBlock();
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return 0;
}

打印日志:simpleBlock--------41044160

  1. NSMallocBlock

當(dāng)一個(gè) __NSStackBlock__ 類型 Block 做 copy 操作后就會(huì)將這個(gè) Block 從棧上復(fù)制到堆上,而堆上的這個(gè) Block 類型就是 __NSMallocBlock__ 類型。在 ARC 環(huán)境下,編譯器會(huì)根據(jù)情況,自動(dòng)將 Block 從棧上 copy 到堆上。具體會(huì)進(jìn)行 copy 的情況有如下 4 種:

  • block 作為函數(shù)的返回值時(shí);
  • block 賦值給 __strong 指針,或者賦值給 block 類型的成員變量時(shí);
  • block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數(shù)時(shí);
  • block 作為 GCD API 的方法參數(shù)時(shí);

  • __block 的作用

簡(jiǎn)單來說,__block 作用是允許 block 內(nèi)部訪問和修改外部變量,在 ARC 環(huán)境下還可以用來防止循環(huán)引用;

__block int age = 10;
void (^exampleBlock)(void) = ^{
    // block
    NSLog(@"1.age is: %d", age);
    age = 16;
    NSLog(@"2.age is: %d", age);
};
exampleBlock();
NSLog(@"3.age is: %d", age);

__block 主要用來解決 block 內(nèi)部無法修改 auto 變量值的問題,為什么加上 __block 修飾之后,auto 變量值就能修改了呢?

這是因?yàn)椋由?__block 修飾之后,編譯器會(huì)將 __block 變量包裝成一個(gè)結(jié)構(gòu)體 __Block_byref_age_0 ,結(jié)構(gòu)體內(nèi)部 *__forwarding 是指向自身的指針,并且結(jié)構(gòu)體內(nèi)部還存儲(chǔ)著外部 auto 變量。

struct __Block_byref_val_0 {
    void *__isa; // isa指針
    __Block_byref_val_0 *__forwarding; 
    int __flags;
    int __size; // Block結(jié)構(gòu)體大小
    int age; // 捕獲到的變量
}
block_copy.png

從上圖可以看到,如果 block 是在棧上,那么這個(gè) __forwarding 指針就是指向它自己,當(dāng)這個(gè) block 從棧上復(fù)制到堆上后,棧上的 __forwarding 指針指向的是復(fù)制到堆上的 __block 結(jié)構(gòu)體。堆上的 __block 結(jié)構(gòu)體中的 __forwarding 指向的還是它自己,即 age->__forwarding 獲取到堆上的 __block 結(jié)構(gòu)體,age->__forwarding->age 會(huì)把堆上的 age 賦值為 16 。因此不管是棧上還是堆上的 __block 結(jié)構(gòu)體,最終使用到的都是堆上的 __block 結(jié)構(gòu)體里面的數(shù)據(jù)。


  • __weak 的作用

簡(jiǎn)單來說是為了防止循環(huán)引用。

self_block.png

self 本身會(huì)對(duì) block 進(jìn)行強(qiáng)引用,block 也會(huì)對(duì) self 形成強(qiáng)引用,這樣就會(huì)造成循環(huán)引用的問題。我們可以通過使用 __weak 打破循環(huán),使 block 對(duì)象對(duì) self 弱引用。

此時(shí)我們注意,由于 block 對(duì) self 的引用為 weak 引用,因此有可能在執(zhí)行 block 時(shí),self 對(duì)象本身已經(jīng)釋放,那么我們?nèi)绾伪WC self 對(duì)象不在 block 內(nèi)部釋放呢?這就引出了下面__strong 的作用。


  • __strong 的作用
    簡(jiǎn)單來說,是防止 Block 內(nèi)部引用的外部 weak 變量被提前釋放,進(jìn)而在 Block 內(nèi)部無法獲取 weak 變量以繼續(xù)使用的情況;
__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf exampleFunc];
};

這樣就保證了在 block 作用域結(jié)束之前,block 內(nèi)部都持有一個(gè) strongSelf 對(duì)象可供使用。

但是,即便如此,依然有一個(gè)場(chǎng)景,就是執(zhí)行 __strong __typeof(weakSelf) strongSelf = weakSelf; 之前,weakSelf 對(duì)象已經(jīng)釋放,這時(shí)如果給 self 對(duì)象發(fā)送消息,這沒有問題,Objective-C 的消息發(fā)送機(jī)制允許我們給一個(gè) nil 對(duì)象發(fā)送消息,這不會(huì)出現(xiàn)問題。

但如果有額外的一些操作,比如說將 self 添加到數(shù)組,這時(shí)因?yàn)?selfnil,程序就會(huì) Crash。

我們可以增加一層安全保護(hù)來解決這個(gè)問題,如:

__weak __typeof(self) weakSelf = self;
void (^exampleBlock)(void) = ^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        // Add operation here
    }
};

拓展知識(shí)

  • 思考題

Block 內(nèi)修改外部 NSMutableStringNSMutableArrayNSMutableDictionary 對(duì)象,是否需要添加 __block 修飾?

NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
[mutableArray addObject:@"1"];
void (^exampleBlock)(void) = ^{
    // block
    [mutableArray addObject:@"2"];
};
exampleBlock();
NSLog(@"mutableArray: %@", mutableArray);

打印日志:

mutableArray: (
1,
2
)

答案是:不需要。因?yàn)樵?block 內(nèi)部,我們只是使用了對(duì)象 mutableArray 的內(nèi)存地址,往其中添加內(nèi)容。并沒有修改其內(nèi)存地址,因此不需要使用 __block 也可以正確執(zhí)行。當(dāng)我們只是使用局部變量的內(nèi)存地址,而不是對(duì)其內(nèi)存地址進(jìn)行修改時(shí),我們無需對(duì)其添加 __block ,如果添加了 __block 系統(tǒng)會(huì)自動(dòng)創(chuàng)建相應(yīng)的結(jié)構(gòu)體,這種情況冗余且低效。

  • Block 數(shù)據(jù)結(jié)構(gòu)

Block 內(nèi)部數(shù)據(jù)結(jié)構(gòu)圖如下:

block_layout.jpg
struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

Block_layout 結(jié)構(gòu)體成員含義如下:

isa: 指向所屬類的指針,也就是 block 的類型

flags: 按 bit 位表示一些 block 的附加信息,比如判斷 block 類型、判斷 block 引用計(jì)數(shù)、判斷 block 是否需要執(zhí)行輔助函數(shù)等;

reserved: 保留變量;

invoke: block 函數(shù)指針,指向具體的 block 實(shí)現(xiàn)的函數(shù)調(diào)用地址,block 內(nèi)部的執(zhí)行代碼都在這個(gè)函數(shù)中;

descriptor: 結(jié)構(gòu)體 Block_descriptor,block 的附加描述信息,包含 copy/dispose 函數(shù),block 的大小,保留變量;

variables: 因?yàn)?block 有閉包性,所以可以訪問 block 外部的局部變量。這些 variables 就是復(fù)制到結(jié)構(gòu)體中的外部局部變量或變量的地址;

Block_descriptor 結(jié)構(gòu)體成員含義如下:

reserved: 保留變量;

size: block 的大小;

copy: 函數(shù)用于捕獲變量并持有引用;

dispose: 析構(gòu)函數(shù),用來釋放捕獲的資源;


總結(jié)

使用 Block 過程中需要我們關(guān)注的重點(diǎn)有 4 個(gè):

  1. block 的三種類型;
  2. block 避免引起循環(huán)引用;
  3. block 對(duì) auto 變量的 copy 操作;
  4. __block、__weak、__strong 的作用;

以上就是本文對(duì) Block 的相關(guān)知識(shí)點(diǎn)的介紹,感謝閱讀。


參考資料:

Working with Blocks


關(guān)于技術(shù)組

iOS 技術(shù)組主要用來學(xué)習(xí)、分享日常開發(fā)中使用到的技術(shù),一起保持學(xué)習(xí),保持進(jìn)步。文章倉庫在這里:https://github.com/minhechen/iOSTechTeam
微信公眾號(hào):iOS技術(shù)組,歡迎聯(lián)系交流,感謝閱讀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 本文主要根據(jù)《Objective-C高級(jí)編程》這本書中的第二章來進(jìn)行的一個(gè)總結(jié),其中包含了查看其它文章后的總結(jié)和自...
    AnICoo1閱讀 1,064評(píng)論 0 2
  • 0.前言 日常開發(fā)中經(jīng)常會(huì)用到 Block,但如果對(duì)它的底層實(shí)現(xiàn)沒有深入地挖掘過,就不能算是真正掌握,本篇就來探究...
    RiverSea閱讀 743評(píng)論 0 13
  • block是將函數(shù)及其執(zhí)行上下文封裝起來的對(duì)象。Objective-C的函數(shù)式編程也是通過Block實(shí)現(xiàn)的,Blo...
    蘭帕德閱讀 1,170評(píng)論 0 3
  • Block源碼解析和深入理解 Block的本質(zhì) Block是"帶有自動(dòng)變量值的匿名函數(shù)". 我們通過Clang(L...
    十三億少女夢(mèng)丶閱讀 3,194評(píng)論 1 18
  • 對(duì)象類型的auto變量 例子一 首先看一個(gè)簡(jiǎn)單的例子定義一個(gè)類 YZPerson,里面只有一個(gè)dealloc方法 ...
    霧中探雪閱讀 328評(píng)論 0 1