1. 程式人生 > >iOS開發之基礎篇(14)—— Block

iOS開發之基礎篇(14)—— Block

版本

Xcode 9.1

block簡介

block是一個OC物件,於iOS4開始引入。其本身封裝了一段程式碼,可被當作變數、當作引數或作為返回值。block常用於GCD、動畫、排序及各類回撥傳值中。


block程式碼結構圖
block程式碼結構圖
注:圖片來自這裡

示例1

    // 建立一個block
    int(^myBlock)(int) = ^(int num) {
        return num * 3;
    };

    // 呼叫block
    NSLog(@"%d",myBlock(3));        // 結果輸出9

示例2
如果需要重複宣告多個相同引數和返回值的block,我們可以用typedef來定義block型別。

    // 宣告一個block
    typedef int(^myBlock)(int);

    // 例項化第一個block
    myBlock block1 = ^(int num) {
        return num * 3;
    };

    // 例項化第二個block
    myBlock block2 = ^(int num) {
        return num * 4;
    };

    // 呼叫block
    NSLog(@"%d, %d",block1(3),block2(3));       // 結果為9, 12

block分類

按照儲存區域可分為三類:

  • NSGlobalBlock
  • NSStackBlock
  • NSMallocBlock

1. NSGlobalBlock

全域性block——沒有用到外界變數,或者只用到全域性變數、靜態(static)變數。生命週期從建立到應用程式結束。

示例:

    /* 情況1:不使用外部變數 */
    // 宣告一個沒有用到外部變數的block
    void (^globalBlock1)(void) = ^{
    };
    // 呼叫block
    globalBlock1();
    // 檢視block型別
    NSLog(@"globalBlock1:%@",globalBlock1);

    /* 情況2:使用全域性變數和靜態變數 */
// 新建靜態變數 static NSString *staticStr = @"我是靜態變數"; // 宣告一個使用全域性變數和靜態變數的block void (^globalBlock2)(void) = ^{ NSLog(@"%@, %@",globalStr,staticStr); }; // 呼叫block globalBlock2(); // 檢視block型別 NSLog(@"globalBlock2:%@",globalBlock2);

結果如下:


2. NSStackBlock

棧block——用到區域性變數、成員屬性/變數,且沒有強指標引用。生命週期由系統管理,超出作用域(函式返回時)馬上被銷燬。

    /* 情況1:用到區域性變數 */
    // 新建一個區域性變數
    NSString *localStr = @"我是區域性變數";
    // 檢視block型別, 直接在NSLog裡面呼叫使用了局部變數的block,這樣就沒有強指標引用了
    NSLog(@"stackBlock1:%@",^{NSLog(@"%@",localStr);});

    /* 情況2:用到成員屬性/變數 */
    // 給成員變數、成員屬性賦值
    _variateStr = @"我是成員變數";
    self.propertyStr = @"我是成員屬性";
    // 檢視block型別, 直接在NSLog裡面呼叫使用了成員屬性/變數的block,這樣就沒有強指標引用了
    NSLog(@"stackBlock2:%@",^{NSLog(@"%@, %@",_variateStr,self.propertyStr);});

結果:


3. NSMallocBlock

堆block——有強指標引用或使用有copy修飾的成員屬性/變數。沒有強指標引用即銷燬,生命週期需手動管理。

    /* 情況1:用到強指標引用 */
    // 新建一個區域性變數
    NSString *localStr = @"我是區域性變數";
    // 宣告一個使用強指標引用的block
    void (^mallocBlock1)(void) = ^{
        NSLog(@"%@",localStr);
    };
    // 呼叫block
    mallocBlock1();
    // 檢視block型別
    NSLog(@"mallocBlock1:%@",mallocBlock1);

    /* 情況2:用到有copy修飾的成員屬性/變數 */
    // 給成員變數、成員屬性賦值
    _variateStr = @"我是成員變數";
    self.propertyStr = @"我是成員屬性";
    // 宣告一個使用有copy修飾的成員屬性的block
    void (^mallocBlock2)(void) = ^{
        NSLog(@"%@, %@",_variateStr, self.propertyStr);
    };
    // 呼叫block
    mallocBlock2();
    // 檢視block型別
    NSLog(@"mallocBlock2:%@",mallocBlock2);

結果:


block訪問外部變數

1. 訪問區域性變數

在block中可以訪問區域性變數,但不能直接修改區域性變數。
示例1:

    // 宣告區域性變數local
    int local = 100;
    // 宣告block
    void(^myBlock)(void) = ^{
        // local++;    // 修改local值會報錯
        NSLog(@"local1 = %d", local);
    };
    // 在呼叫block之前改變local的值
    local = 101;
    // 呼叫block
    myBlock();
    // 檢視local改變後的值
    NSLog(@"local2 = %d", local);

列印結果:


原理分析:

在block定義時是將區域性變數的值傳給block變數所指向的結構體,因此在呼叫block之前對區域性變數進行修改並不會影響block內部的值。同時傳進來的內部值也是不可修改的。

但是,我們有時候又想在block內部修改block外的區域性變數,應該怎麼辦呢?

使用__block修飾的區域性變數,在block中可以直接修改
示例2:

    // 宣告區域性變數local (用__block修飾)
    __block int local = 100;
    // 宣告block
    void(^myBlock)(void) = ^{
        NSLog(@"修改前local1 = %d", local);
        local++;    // 不會報錯
        NSLog(@"修改後local1 = %d", local);
    };
    // 在呼叫block前修改local值
    local = 200;
    // 呼叫block
    myBlock();
    // 檢視local改變後的值
    NSLog(@"local2 = %d", local);

列印結果:


原理分析:

在區域性變數前使用__block修飾,block定義時是將區域性變數的指標傳給block變數所指向的結構體,因此在呼叫block之前對區域性變數進行修改會影響block內部的值。同時內部的值也是可以修改的。

2. 訪問靜態變數

在block中可以訪問靜態變數,也可以直接修改靜態變數。

示例:

    // 宣告靜態變數staticInt
    static int staticInt = 100;
    // 宣告block
    void(^myBlock)(void) = ^{
        NSLog(@"修改前staticInt1 = %d", staticInt);
        staticInt++;    // 不會報錯
        NSLog(@"修改後staticInt1 = %d", staticInt);
    };
    // 在呼叫block前修改staticInt值
    staticInt = 200;
    // 呼叫block
    myBlock();
    // 檢視staticInt改變後的值
    NSLog(@"staticInt2 = %d", staticInt);

列印結果:


原理分析:

block定義時是將靜態變數的指標傳給Block變數所指向的結構體,因此在呼叫block之前對靜態變數進行修改會影響block內部的值。同時內部的值也是可以修改的。

3. 訪問全域性變數

在block中可以訪問全域性變數,也可以直接修改全域性變數。
示例:

    // 先在@implementation前定義一個全域性變數globalInt

    // 宣告block
    void(^myBlock)(void) = ^{
        NSLog(@"修改前globalInt1 = %d", globalInt);
        globalInt++;    // 不會報錯
        NSLog(@"修改後globalInt1 = %d", globalInt);
    };
    // 在呼叫block前修改globalInt值
    globalInt = 200;
    // 呼叫block
    myBlock();
    // 檢視globalInt改變後的值
    NSLog(@"globalInt2 = %d", globalInt);

列印結果:


原理分析:

全域性變數所佔用的記憶體只有一份,供所有函式(方法)共同呼叫,在block定義時並未將全域性變數的值或者指標傳給block變數所指向的結構體,因此在呼叫Block之前對區域性變數進行修改會影響block內部的值。同時內部的值也是可以修改的。

block在ARC下的記憶體管理

如果物件內部有一個block屬性,而在block內部又訪問了該物件,那麼會造成迴圈引用
錯誤示例:

// 宣告block成員屬性
@property (nonatomic, copy) void(^myBlock)(void);

    // 定義block
    self.myBlock = ^{
        NSLog(@"%@", self);    // 此句造成迴圈引用,編譯報錯
    };

    // 執行block
    self.myBlock();

解決辦法:使用一個弱引用的指標指向該物件,然後在Block內部使用該弱引用指標來進行操作。
正確示例:

// 宣告block成員屬性
@property (nonatomic, copy) void(^myBlock)(void);

    // 宣告一個弱引用指標物件
    __weak typeof(self) weakSelf = self;

    // 定義block
    self.myBlock = ^{
        NSLog(@"%@", weakSelf);        // 使用弱引用指標物件
    };

    // 執行block
    self.myBlock();

如果擔心在呼叫Block之前引用的物件已經被釋放,那麼我們需要在Block內部再定義一個強指標來指向該物件
官方示例:

// 宣告block成員屬性
@property (nonatomic, copy) void(^myBlock)(void);

    // 宣告一個弱引用指標物件
    __weak typeof(self) weakSelf = self;

    // 定義block
    self.myBlock = ^{
        typeof(self) strongSelf = weakSelf;     // 宣告一個強引用指標物件
        NSLog(@"%@", strongSelf);               // 使用強引用指標物件
    };

    // 執行block
    self.myBlock();

block的傳值應用

先看看效果:


從介面1載入介面2,然後在介面2返回時回傳textField裡的text。方法步驟:

  • 介面1(接收方):
    1、定義(實現)一個block並傳給介面2(的block屬性)
    2、在block中處理傳遞過來的值
  • 介面2(傳值方):
    1、宣告一個block屬性,其具體實現由接收方來完成
    2、在需要傳值的地方呼叫block

介面1 .m檔案:

// 跳轉到介面2
- (IBAction)btnShowViewController2:(UIButton *)sender {

    // 從storyboard例項化介面2
    ViewController2 *vc2 = [self.storyboard instantiateViewControllerWithIdentifier:@"ViewController2"];

    // 定義(實現)一個block並傳給介面2(的block屬性)
    vc2.textBlock = ^(NSString *str) {
        self.label.text = str;      // 處理傳回來的值
    };

    // 載入介面2
    [self presentViewController:vc2 animated:YES completion:nil];
}

介面2 .h檔案

@interface ViewController2 : UIViewController

// 宣告block屬性
@property (nonatomic, copy) void(^textBlock)(NSString *);

@end

介面2 .m檔案:

// 返回介面1
- (IBAction)btnBack:(UIButton *)sender {

    // 先判斷block是否為空,為空則報錯
    if (self.textBlock) {
        // 返回textField裡的text
        self.textBlock(self.textField.text);
    }

    // 退出介面2
    [self dismissViewControllerAnimated:YES completion:nil];
}