iOS 開發(fā):『Blocks』詳盡總結(jié) (一)基本使用

本文用來介紹 iOS開發(fā)中 『Blocks』的基本使用。通過本文您將了解到:

  1. 什么是 Blocks
  2. Blocks 變量語法
  3. Blocks 變量的聲明與賦值
  4. Blocks 變量截獲局部變量值特性
  5. 使用 __block 說明符
  6. Blocks 變量的循環(huán)引用以及如何避免

文中 Demo 我已放在了 Github 上,Demo 鏈接:傳送門


1. 什么是 Blocks

一句話總結(jié):Blocks 是帶有 局部變量匿名函數(shù)(不帶名稱的函數(shù))。

Blocks 也被稱作 閉包、代碼塊。展開來講,Blocks 就是一個代碼塊,把你想要執(zhí)行的代碼封裝在這個代碼塊里,等到需要的時候再去調(diào)用。

下邊我們先來理解 局部變量、匿名函數(shù) 的含義。

1.1 局部變量

在 C 語言中,定義在函數(shù)內(nèi)部的變量稱為 局部變量。它的作用域僅限于函數(shù)內(nèi)部, 離開該函數(shù)后就是無效的,再使用就會報錯。

int x, y; // x,y 為全局變量

int fun(int a) {
    int b, c; //a,b,c 為局部變量
    return a+b+c;
}

int main() {
    int m, n; // m,n 為局部變量
    return 0;
}

從上邊的代碼中,我們可以看出:

  1. 我們在開始位置定義了變量 x 和 變量 y。 x 和 y 都是全局變量。它們的作用域默認(rèn)是整個程序,也就是所有的源文件,包括 .c 和 .h 文件。
  2. 而我們在 fun() 函數(shù)中定義了變量 a、變量 b、變量 c。它們的作用域是 fun() 函數(shù)。只能在 fun() 函數(shù)內(nèi)部使用,離開 fun() 函數(shù)就是無效的。
  3. 同理,main() 函數(shù)中的變量 m、變量 n 也只能在 main() 函數(shù)內(nèi)部使用。

1.2 匿名函數(shù)

匿名函數(shù)指的是不帶有名稱的函數(shù)。但是 C 語言中不允許存在這樣的函數(shù)。

在 C 語言中,一個普通的函數(shù)長這樣子:

int fun(int a);

fun 就是這個函數(shù)的名稱,在調(diào)用的時候必須要使用該函數(shù)的名稱 fun 來調(diào)用。

int result = fun(10);

在 C 語言中,我們還可以通過函數(shù)指針來直接調(diào)用函數(shù)。但是在給函數(shù)指針賦值的時候,同樣也是需要知道函數(shù)的名稱。

int (*funPtr)(int) = &fun;
int result = (*funPtr)(10);

而我們通過 Blocks,可以直接使用函數(shù),不用給函數(shù)命名。


2. Blocks 變量語法

我們使用 ^ 運算符來聲明 Blocks 變量,并將 Blocks 對象主體部分包含在 {} 中,同時,句尾加 ; 表示結(jié)尾。

下邊來看一個官方的示例:

int multiplier = 7;
int (^ myBlock)(int)= ^(int num) {
    return num * multiplier;
};

這個 Blocks 示例中,myBlock 是聲明的塊對象,返回類型是 整型值,myBlock 塊對象有一個 參數(shù),參數(shù)類型為整型值,參數(shù)名稱為 num。myBlock 塊對象的 主體部分return num * multiplier;,包含在 {} 中。

參考上面的示例,我們可以將 Blocks 表達(dá)式語法表述為:

^ 返回值類型 (參數(shù)列表) { 表達(dá)式 };

例如,我們可以寫出這樣的 Block 語法:

^ int (int count) { return count + 1; };

Blocks 規(guī)定可以省略好多項目。例如:返回值類型、參數(shù)列表。如果用不到,都可以省略。

2.1 省略返回值類型:^ (參數(shù)列表) { 表達(dá)式 };

上邊的 Blocks 語法就可以寫為:

^ (int count) { return count + 1; };

表達(dá)式中,return 語句使用的是 count + 1 語句的返回類型。如果表達(dá)式中有多個 return 語句,則所有 return 語句的返回值類型必須一致。

如果表達(dá)式中沒有 return 語句,則可以用 void 表示,或者也省略不寫。代碼如下:。

^ void (int count)  { printf("%d\n", count); };    // 返回值類型使用 void
^ (int count) { printf("%d\n", count); };    // 省略返回值類型

2.2 省略參數(shù)列表 ^ 返回值類型 (void) { 表達(dá)式 };

如果表達(dá)式中,沒有使用參數(shù),則用 void 表示,也可以省略 void。

^ int (void) { return 1; };    // 參數(shù)列表使用 void
^ int { return 1; };    // 省略參數(shù)列表類型

2.3 省略返回值類型、參數(shù)列表:^ { 表達(dá)式 };

從上邊 2.1 中可以看出,無論有無返回值,都可以省略返回值類型。并且,從 2.2 中可以看出,如果不需要參數(shù)列表的話,也可以省略參數(shù)列表。則代碼可以簡化為:

^ { printf("Blocks"); }; 

3. Blocks 變量的聲明與賦值

3.1 Blocks 變量的聲明與賦值語法

Blocks 變量的聲明與賦值語法可以總結(jié)為:

返回值類型 (^變量名) (參數(shù)列表) = Blocks 表達(dá)式

注意:此處返回值類型不可以省略,若無返回值,則使用 void 作為返回值類型。

例如,定義一個變量名為 blk 的 Blocks 變量:

int (^blk) (int)  = ^(int count) { return count + 1; };
int (^blk1) (int);    // 聲明變量名為 blk1 的 Blocks 變量
blk1 = blk;        // 將 blk 賦值給 blk1

Blocks 變量的聲明語法有點復(fù)雜,其實我們可以和 C 語言函數(shù)指針的聲明類比著來記。

Blocks 變量的聲明就是把聲明函數(shù)指針類型的變量 * 變?yōu)?^。

//  C 語言函數(shù)指針聲明與賦值
int func (int count) {
    return count + 1;
}
int (*funcptr)(int) = &func;

// Blocks 變量聲明與賦值
int (^blk) (int)  = ^(int count) { return count + 1; };

3.2 Blocks 變量的聲明與賦值的使用

3.2.1 作為局部變量:返回值類型 (^變量名) (參數(shù)列表) = 返回值類型 (參數(shù)列表) { 表達(dá)式 };

我們可以把 Blocks 變量作為局部變量,在一定范圍內(nèi)(函數(shù)、方法內(nèi)部)使用。

// Blocks 變量作為本地變量
- (void)useBlockAsLocalVariable {
    void (^myLocalBlock)(void) = ^{
        NSLog(@"useBlockAsLocalVariable");
    };

    myLocalBlock();
}

3.2.2 作為帶有 property 聲明的成員變量:@property (nonatomic, copy) 返回值類型 (^變量名) (參數(shù)列表);

作用類似于 delegate,實現(xiàn) Blocks 回調(diào)。

/* Blocks 變量作為帶有 property 聲明的成員變量 */
@property (nonatomic, copy) void (^myPropertyBlock) (void);

// Blocks 變量作為帶有 property 聲明的成員變量
- (void)useBlockAsProperty {
    self.myPropertyBlock = ^{
        NSLog(@"useBlockAsProperty");
    };

    self.myPropertyBlock();
}

3.2.3 作為 OC 方法參數(shù):- (void)someMethodThatTaksesABlock:(返回值類型 (^)(參數(shù)列表)) 變量名;

可以把 Blocks 變量作為 OC 方法中的一個參數(shù)來使用,通常 blocks 變量寫在方法名的最后。

// Blocks 變量作為 OC 方法參數(shù)
- (void)someMethodThatTakesABlock:(void (^)(NSString *)) block {
    block(@"someMethodThatTakesABlock:");
}

3.2.4 調(diào)用含有 Block 參數(shù)的 OC方法:[someObject someMethodThatTakesABlock:^返回值類型 (參數(shù)列表) { 表達(dá)式}];

// 調(diào)用含有 Block 參數(shù)的 OC方法
- (void)useBlockAsMethodParameter {
    [self someMethodThatTakesABlock:^(NSString *str) {
        NSLog(@"%@",str);
    }];
}

通過 3.2.3 和 3.2.4 中,Blocks 變量作為 OC 方法參數(shù)的調(diào)用,我們同樣可以實現(xiàn)類似于 delegate 的作用,即 Blocks 回調(diào)(后邊應(yīng)用場景中會講)。

3.2.5 作為 typedef 聲明類型:

typedef 返回值類型 (^聲明名稱)(參數(shù)列表);
聲明名稱 變量名 = ^返回值類型(參數(shù)列表) { 表達(dá)式 };
// Blocks 變量作為 typedef 聲明類型
- (void)useBlockAsATypedef {
    typedef void (^TypeName)(void);
    
    // 之后就可以使用 TypeName 來定義無返回類型、無參數(shù)列表的 block 了。
    TypeName myTypedefBlock = ^{
        NSLog(@"useBlockAsATypedef");
    };

    myTypedefBlock();
}

4. Blocks 變量截獲局部變量值特性

先來看一個例子。

// 使用 Blocks 截獲局部變量值
- (void)useBlockInterceptLocalVariables {
    int a = 10, b = 20;

    void (^myLocalBlock)(void) = ^{
        printf("a = %d, b = %d\n",a, b);
    };

    myLocalBlock();    // 打印結(jié)果:a = 10, b = 20

    a = 20;
    b = 30;

    myLocalBlock();    // 打印結(jié)果:a = 10, b = 20
}

為什么兩次打印結(jié)果都是 a = 10, b = 20?

明明在第一次調(diào)用 myLocalBlock(); 之后已經(jīng)重新給變量 a、變量 b 賦值了,為什么第二次調(diào)用 myLocalBlock(); 的時候,使用的還是之前對應(yīng)變量的值?

因為 Block 語法的表達(dá)式使用的是它之前聲明的局部變量 a、變量 b。Blocks 中,Block 表達(dá)式截獲所使用的局部變量的值,保存了該變量的瞬時值。所以在第二次執(zhí)行 Block 表達(dá)式時,即使已經(jīng)改變了局部變量 a 和 b 的值,也不會影響 Block 表達(dá)式在執(zhí)行時所保存的局部變量的瞬時值。

這就是 Blocks 變量截獲局部變量值的特性。


5. 使用 __block 說明符

實際上,在使用 Block 表達(dá)式的時候,只能使用保存的局部變量的瞬時值,并不能直接對其進(jìn)行改寫。直接修改編譯器會直接報錯,如下圖所示。

那么如果,我們想要該寫 Block 表達(dá)式中截獲的局部變量的值,該怎么辦呢?

如果,我們想在 Block 表達(dá)式中,改寫 Block 表達(dá)式之外聲明的局部變量,需要在該局部變量前加上 __block 的修飾符。

這樣我們就能實現(xiàn):在 Block 表達(dá)式中,為表達(dá)式外的局部變量賦值。

// 使用 __block 說明符修飾,更改局部變量值
- (void)useBlockQualifierChangeLocalVariables {
    __block int a = 10, b = 20;
    void (^myLocalBlock)(void) = ^{
        a = 20;
        b = 30;
        
        printf("a = %d, b = %d\n",a, b);  // 打印結(jié)果:a = 20, b = 30
    };
    
    myLocalBlock();
}

可以看到,使用 __block 說明符修飾之后,我們在 Block表達(dá)式中,成功的修改了局部變量值。

根據(jù)評論區(qū)增補一點:如果 Blocks 截獲的是 Objective-C 對象,例如 NSMutablearray 類對象,對該對象調(diào)用變更的方法是不會編譯報錯的(例如調(diào)用 addObject: 方法)。但是如果對其調(diào)用賦值的方法,則會編譯報錯,就必須要加上 __block 說明符進(jìn)行修飾了。


6. Blocks 變量的循環(huán)引用以及如何避免

從上文中我們知道 Block 會對引用的局部變量進(jìn)行持有。同樣,如果 Block 也會對引用的對象進(jìn)行持有,從而會導(dǎo)致相互持有,引起循環(huán)引用。

/* —————— retainCycleBlcok.m —————— */   
#import <Foundation/Foundation.h>
#import "Person.h"
int main() {
    Person *person = [[Person alloc] init];
    person.blk = ^{
        NSLog(@"%@",person);
    };

    return 0;
}


/* —————— Person.h —————— */ 
#import <Foundation/Foundation.h>

typedef void(^myBlock)(void);

@interface Person : NSObject
@property (nonatomic, copy) myBlock blk;
@end


/* —————— Person.m —————— */ 
#import "Person.h"

@implementation Person    

@end

上面 retainCycleBlcok.mmain() 函數(shù)的代碼會導(dǎo)致一個問題:person 持有成員變量 myBlock blk,而 blk 也同時持有成員變量 person,兩者互相引用,永遠(yuǎn)無法釋放。就造成了循環(huán)引用問題。

那么,如何來解決這個問題呢?

6.1 ARC 下,通過 __weak 修飾符來消除循環(huán)引用

在 ARC 下,可聲明附有 __weak 修飾符的變量,并將對象賦值使用。

int main() {
    Person *person = [[Person alloc] init];
    __weak typeof(person) weakPerson = person;

    person.blk = ^{
        NSLog(@"%@",weakPerson);
    };

    return 0;
}

這樣,通過 __weak,person 持有成員變量 myBlock blk,而 blk 對 person 進(jìn)行弱引用,從而就消除了循環(huán)引用。

6.2 MRC 下,通過 __block 修飾符來消除循環(huán)引用

MRC 下,是不支持 __weak 修飾符的。但是我們可以通過 __block 來消除循環(huán)引用。

int main() {
    Person *person = [[Person alloc] init];
    __block typeof(person) blockPerson = person;

    person.blk = ^{
        NSLog(@"%@", blockPerson);
    };

    return 0;
}

通過 __block 引用的 blockPerson,是通過指針的方式來訪問 person,而沒有對 person 進(jìn)行強引用,所以不會造成循環(huán)引用。


參考資料


以上是 iOS 開發(fā):『Blocks』詳盡總結(jié) (一)基本使用 的全部內(nèi)容,可以用來了解 Block,入門使用。下一篇我們通過 Block 由 OC 代碼轉(zhuǎn)變的 C++ 源碼來抽絲剝繭的講一下 Block 的底層原理。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Blocks Blocks Blocks 是帶有局部變量的匿名函數(shù) 截取自動變量值 int main(){ ...
    南京小伙閱讀 1,077評論 1 3
  • Blocks編程要點 目錄 簡介............................................
    xuejunjun閱讀 1,453評論 0 5
  • 前言 上篇文章整理了關(guān)于“內(nèi)存管理”的知識點,這篇主要整理“Blocks”的內(nèi)容,在學(xué)習(xí)Blocks的內(nèi)容時,我一...
    丁寶2012閱讀 537評論 0 2
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,674評論 1 32
  • 火車站的人潮在我身邊匆匆而過,光陰在此化作一趟又一趟的列車,駛進(jìn)站臺,又匆忙離開,在嘈雜的輪回之中,亦如此安寧。我...
    為了552的沭沭閱讀 284評論 0 0

友情鏈接更多精彩內(nèi)容