iOS中的Block

1 什么是Block

我們先來看一段代碼:

[UIView animateWithDuration:0.2
                 animations:^{view.alpha = 0.0;}
                 completion:^(BOOL finished){ [view removeFromSuperview]; }];

上面的代碼來自UIViewanimateWithDuration:animations:completion:方法,用于執行在視圖的動畫過程中淡化視圖直到完全透明,然后從視圖層次結構中刪除視圖的操作。該方法一共有三個參數,其中animationscompletion都是以脫字符^開頭的block對象,這是框架方法中使用block的典型示例之一。

塊(Block)是蘋果在iOS4開始引入的對C語言的一種擴展,它允許我們創建不同的代碼段,與函數類似,不同的是,塊定義在函數或方法內部,并能夠訪問在函數或方法范圍內而塊之外的任何變量。一般來說,這些變量能夠訪問但是它們的值并不能被改變。只有通過在變量前添加一個特殊的塊修改器__block(在block前面添加兩個下劃線的字符組成)才能修改該變量的值,后面我們會有詳細介紹。同時,塊也是Objective-C對象,因此它們可以存儲到NSArray或NSDictionary數據結構中,并且作為返回值從方法返回,甚至被分配給變量。

在這里我們主要介紹block的聲明、創建、使用,以及在block內部訪問變量等相關知識。

2 聲明并創建Block

block.jpg

2.1 Block的聲明

返回值類型 (^block名稱) (參數列表);

下面是幾種有效的block聲明:

void (^blockReturningVoidWithVoidArgument)(void);
void (^blockReturningVoidWithIntAndCharArguments)(int, char);
int (^blockReturningIntWithVoidArgument)(void);
NSString *(^blockReturningNSStringWithNSStringArgument)(NSString *);
NSString *(^blockReturningNSStringWithNSStringArgument)(NSString *string);
void (^arrayOfTenBlocksReturningVoidWithIntArgument[10])(int);

有幾點值得注意的地方:

  • block的聲明和函數類似,如果不返回任何內容,則返回類型為void,且不可省略。
  • block名稱前面的脫字符^不能省略,塊名稱的命名規則與其它變量名或方法名的命名規則一致,并且脫字符和塊名稱都在圓括號內。
  • 參數列表必須在括號內,如果有多個參數,參數間用逗號隔開。如果不傳遞參數,設置為void。如果有參數,必須標明參數類型,參數名稱僅助于開發人員記住其功能,可以忽略。
  • 最后,不要忘記寫分號;

2.2 Block的創建與調用

定義一個Block:
^(參數列表){
... 塊主體 ...
};

與聲明不同的是,在block定義中,如果有參數,參數名稱不能省略;如果沒有參數,則可以省略(void)參數列表。

調用一個Block:
// 有參數:
block名稱(參數); 

// 無參數:
block名稱();
代碼示例:
  • 無返回內容,無參數:
    // 1 聲明一個block
    void (^block)(void);
    
    // 2 定義block(為block賦值)
    block = ^{
        NSLog(@"該block沒有參數,不返回任何內容。");
    };
    
    // 3 調用block
    block();

也可以將block的聲明和定義合并在一起,如下:

    // 1 創建block
    void (^block)(void) = ^{
        NSLog(@"該Block沒有參數,不返回任何內容。");
    };
    
    // 2 調用block
    block();
  • 無返回內容,有參數:
    void (^block)(int);
    
    block = ^(int value){
        NSLog(@"The value is %i", value);
    };
    
    block(3);

輸出結果:

The value is 3
  • 有返回內容,無參數:
    int (^block)(void);
    
    block = ^{
        int value = 3;
        return value;
    };
    
    NSLog(@"The value is %i", block());

輸出結果:

The value is 3
  • 有返回內容,有參數:
    int (^block)(int, int);
    
    block = ^(int a, int b){
        int value = a + b;
        return value;
    };
    
    NSLog(@"The value is %i", block(3, 5));

輸出結果:

The value is 8

2.3 使用typedef定義Block

如果需要重復聲明多個相同類型(即返回值類型相同,參數列表相同)的block,我們可以使用typedef來定義block類型,以此來避免編寫重復的代碼,示例如下:

    // 1 聲明一種返回值為int,并且有兩個參數均為int的block類型
    typedef int (^Block)(int, int);
    
    // 2 使用該block類型聲明并定義兩個變量block1和block2
    Block block1 = ^(int a, int b){
        return a + b;
    };
    
    Block block2 = ^(int a, int b){
        return a * b;
    };
    
    // 3 調用
    NSLog(@"The value of block1 is %i", block1(3, 5));
    NSLog(@"The value of block2 is %i", block2(3, 5));

輸出結果:

The value of block1 is 8
The value of block2 is 15

2.4 將Block聲明為全局變量

Block與其它變量一樣,也可以被聲明為全局變量,示例如下:

#import "ViewController.h"

// 聲明并定義一個block為全局變量
NSString *(^globalBlock)(void) = ^{
    return @"This is a global block";
};

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 調用
    NSLog(@"%@", globalBlock());
}

3 Block與變量

Block與函數類似,能夠訪問在函數或方法范圍內而在塊之外定義的任何變量。

3.1 Block內訪問全局變量,包括靜態變量

#import "ViewController.h"

// 聲明一個全局變量
int gGlobalVar = 5;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    void (^block)(void) = ^{
        // 1 訪問全局變量
        NSLog(@"gGlobalVar = %i", gGlobalVar);
        
        // 2 直接修改全局變量的值
        gGlobalVar = 8;
        NSLog(@"gGlobalVar = %i", gGlobalVar);
    };
    
    // 調用
    block();
    
    // 3 調用前修改全局變量的值
    gGlobalVar = 6;
    block();
}

輸出結果:

gGlobalVar = 5
gGlobalVar = 8
gGlobalVar = 6
gGlobalVar = 8

從上面代碼及輸出結果,我們不難發現:

  1. 在block內可以訪問全局變量的值。
  2. 在block中可以直接修改全局變量的值。
  3. 在block定義后、調用前修改全局變量的值,會影響輸出結果。當調用block時,訪問的全局變量的值是修改后的新值。這是因為全局變量(或靜態變量)在內存中的地址是固定的,block在讀取該變量值的時候是直接從其所在的內存中讀取,因此得到的是最新值,而不是在定義時copy的常量。

block也可以訪問靜態變量、修改靜態變量的值等,與全局變量類似,這里不再贅述。

<a id = "3.2">

3.2 Block訪問局部變量

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 定義一個局部變量
    int var = 5;
    
    void (^block)(void) = ^{
        // 1 訪問局部變量
        NSLog(@"var = %i", var);
        
        // 2 修改局部變量的值
        var = 8;     //編譯時會報錯
        NSLog(@"var = %i", var);
    };
    
    // 調用
    block();
    
    // 3 調用前修改局部變量的值
    var = 6;
    block();
}

上面var = 8;所在的代碼行會出現錯誤提示:Variable is not assignable(missing __block type specifier),這是因為在block內部不能直接修改局部變量的值,如果要修改,必須使用__block修飾符重新聲明局部變量。這里我們先注釋掉2中的代碼,看一下運行結果:

var = 5
var = 5

由此可見:

  1. 在block內可以訪問局部變量的值。
  2. 在block內不能直接修改局部變量的值,如果要修改必須使用__block修飾符聲明局部變量。
  3. 在block定義后、調用前修改局部變量的值,不影響輸出結果,當調用block時,訪問的局部變量的值是修改前的舊值。這是因為block在定義時會copy變量的值作為常量使用,因此局部變量在block中是只讀的,即使變量的值在block外改變,也不影響其在block中的值。

</a>

3.3 Block訪問__block修飾的局部變量

在這里我們使用__block存儲類型修飾符(由下劃線下劃線block組成)重新定義局部變量,以解決3.2中遇到的錯誤提示:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 使用__block重新定義局部變量
    __block int var = 5;
    
    void (^block)(void) = ^{
        // 1 訪問局部變量
        NSLog(@"var = %i", var);
        
        // 2 修改局部變量的值
        var = 8;
        NSLog(@"var = %i", var);
    };
    
    // 調用
    block();
    
    // 3 調用前修改局部變量的值
    var = 6;
    block();
}

這次沒有任何錯誤提示,并且輸出結果如下:

var = 5
var = 8
var = 6
var = 8

由此我們發現:

  1. __block修飾的局部變量,在block內可以正常訪問該變量的值。
  2. __block修飾的局部變量,在block內可以修改該變量的值。
  3. __block修飾的局部變量,在block定義后、調用前修改該變量的值會影響輸出結果,調用block時,訪問的該變量的值是修改后的新值。

4 Block的使用

我們已經知道block被廣泛地用在很多框架方法中,除此之外,它還常用于GCD、動畫、排序及各類回調中,在這里我們主要介紹block作為參數和屬性的用法。

4.1 Block作為方法或函數的參數

我們將block作為參數傳遞給方法或函數的過程與其它參數類似,需要注意的是,一般情況下,我們只對方法使用一個block參數,如果該方法還需要其它非block的參數,則該block參數應該是方法的最后一個參數。

// 定義一個含有block參數的方法:
- (void)addNumber:(int)number1 withNumber:(int)number2 withBlockArgument:(void (^)(int))block
{
    int result = number1 + number2;
    block(result);
}

// 在viewDidLoad中調用該方法:
- (void)viewDidLoad {
    [super viewDidLoad];
   
    [self addNumber:3 withNumber:5 withBlockArgument:^(int result) {
        NSLog(@"The result is %i", result);
    }];
}

輸出結果:

The result is 8

4.2 Block被用作屬性

Block作為屬性時用法與其它屬性類似,不同的是,使用block作為屬性時,在MRC環境下只能用copy修飾。這是因為在使用block訪問外部變量時,block類型是堆block,當堆中的block引用計數為0被銷毀時,如果想繼續在外部訪問和調用block,就需要將block從棧中復制一份移動到堆中,因此需要用copy修飾。如果是在ARC環境下這些會自動發生,我們不需要擔心,可以用strong修飾。

下面是一段簡單的代碼示例:

#import "ViewController.h"

@interface ViewController ()

// 聲明一個block屬性
@property (nonatomic, copy) void (^blockAsProperty)(void);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 設置該block屬性
    self.blockAsProperty = ^{
        NSLog(@"Using block as a property.");
    };
    
    // 調用
    self.blockAsProperty();
}

也可以使用typedef來聲明block屬性:

typedef void (^blockAsProperty)(void);

@interface ViewController ()

@property (nonatomic, copy) blockAsProperty blockProperty;
@property (nonatomic, copy) blockAsProperty anotherBlockProperty;

@end

另外,block作為屬性多被用于逆向傳值,如下圖所示,我們將第二個視圖控制器中設置的顏色通過block屬性傳遞給第一個視圖控制器做背景顏色。在這里我們不詳細介紹實現過程,如果需要可以下載BlockDemo查看。

BlockDemo.gif

4.3 Block在框架方法中的使用

文章的開頭我們就展示了在框架方法中使用block的示例,在這里只列舉一下block在系統框架中的幾種用法,詳細內容可以參閱文檔中關于Blocks in the System Framework APIs的講解。

Block在系統框架方法中主要被用作以下幾種形式:

  • 完成處理程序 (Completion handlers)
  • 通知處理程序 (Notification handlers)
  • 錯誤處理程序 (Error handlers)
  • 枚舉 (Enumeration)
  • 視圖動畫和轉場 (View animation and transitions)
  • 排序 (Sorting)

5 Block的內存管理

Block在ARC和MRC下的內存管理是不同的,在這里我們只介紹block在ARC下的內存管理,在MRC下的內存管理可以參考這里

在ARC默認情況下,block是存儲在堆中,ARC會自動進行內存管理,我們只需要避免循環引用即可。下面我們就介紹下在什么情況會造成循環引用以及如何去避免。

5.1 Block中的循環引用

Block會對包含self在內的所引用的對象進行強引用,如果對象內部有一個block屬性,而在block內又訪問了該對象,這時就會造成循環引用。示例如下:

@interface ViewController ()

// 聲明block變量
@property (nonatomic, copy) void (^block)(void);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.block = ^{
        // 在block內訪問self
        [self testMethod];    //編譯時會出現警告提示
        NSLog(@"Block");
    };
    
    self.block();
}

- (void)testMethod
{
    NSLog(@"Test method");
}

上面[self testMethod];所在的代碼行會出現警告提示:Capturing 'self' strongly in this block is likely to lead to a retain cycle,這是因為在block內部對self進行了一次強引用,會導致循環引用無法釋放。

5.2 避免循環引用

為了避免循環引用,最好的是對block內部引用的self對象進行弱引用。一般使用一個弱指針來指向該對象,然后在block內使用該弱引用指針來進行操作,這樣就避免了block對該對象的強引用。

對于上面的代碼,我們使用一個弱指針對self進行弱引用,這樣警告就會消失,修改viewDidLoad方法中的代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak ViewController *weakSelf = self;
    
    self.block = ^{
        [weakSelf testMethod];
        NSLog(@"Block");
    };
    
    self.block();
}

注意:這里的__weak是在weak前添加兩個下劃線字符組成的,而且只能用于iOS5之后的版本。

6 參考資料

  1. Blocks Programming Topics
  2. Working with Blocks
  3. A Short Practical Guide to Blocks
  4. Introduction to Objective-C Blocks
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容