iOS Block學習筆記

即將成為型男的濤發表於2019-03-29

一、概述

閉包 = 一個函式「或指向函式的指標」+ 該函式執行的外部的上下文變數「也就是自由變數」;

Block實質是Objective-C對閉包的物件實現,簡單說來,Block就是物件。

二、Block的宣告

1.有引數有返回值

int (^CustomBlock1)(int) = ^int (int a) {
        return a + 1;
    };
複製程式碼

2.有引數無返回值

void (^CustomBlock)(int) = ^void (int a) {
    NSLog(@"-有引數無返回值--引數:%d", a);
};

// 也可以簡寫
void (^CustomBlock1)(int) = ^(int a) {
    NSLog(@"-有引數無返回值--引數:%d", a);
};
複製程式碼

3. 無引數有返回值

int (^CustomBlock)(void) = ^int(void) {
    return 1;
};
// 也可以簡寫
int (^CustomBlock1)(void) = ^int {
    return 1;
};
複製程式碼

4. 無引數無返回值

void (^CustomBlock)(void) = ^void (void) {
    NSLog(@"-無引數無返回值--");
};
// 也可以簡寫
void (^CustomBlock1)(void) = ^(void){
    NSLog(@"-無引數無返回值--");
};

複製程式碼

5. 利用 typedef 宣告block

// 利用 typedef 宣告block
typedef return_type (^BlockTypeName)(var_type);

// 例子1:作屬性
@property (nonatomic, copy) BlockTypeName blockName;

// 例子2:作方法引數
- (void)requestForSomething:(Model)model handle:(BlockTypeName)handle;
複製程式碼

三、Block捕獲變數及物件

1、變數的定義

  • 全域性變數
    • 函式外面宣告
    • 可以跨檔案訪問
    • 可以在宣告時賦上初始值。如果沒有賦初始值,系統自動賦值為0
    • 儲存位置:既非堆,也非棧,而是專門的【全域性(靜態)儲存區static】!
  • 靜態變數
    • 函式外面或內部宣告(即可修飾原全域性變數亦可修飾原區域性變數)
    • 僅宣告該變數的檔案可以訪問
    • 可以在宣告時賦上初始值。如果沒有賦初始值,系統自動賦值為0
    • 儲存位置:既非堆,也非棧,而是專門的【全域性(靜態)儲存區static】!
  • 區域性變數(自動變數)
    • 函式內部宣告
    • 僅當函式執行時存在
    • 僅在本檔案本函式內可訪問
    • 儲存位置:自動儲存在函式的每次執行的【棧幀】中,並隨著函式結束後自動釋放,另外,函式每次執行則儲存在【棧】中

2、Block捕獲變數

將Objective-C 轉 C++的方法

1、在OC原始檔block.m寫好程式碼。

2、開啟終端,cd到block.m所在資料夾。

3、輸入clang -rewrite-objc block.m,就會在當前資料夾內自動生成對應的block.cpp檔案。

OC程式碼:

int global_val = 10; // 全域性變數
static int static_global_val = 20; // 全域性靜態變數

int main() {
    typedef void (^MyBlock)(void);
    
    static int static_val = 30; // 靜態變數
    int val = 40; // 區域性變數
    int val_unuse = 50; // 未使用的區域性變數
    
    MyBlock block = ^{
        // 捕獲區域性變數
        NSLog(@"val------------------%d", val);
        // 修改區域性變數  -> 程式碼編譯不通過
        //val = 4000;  
        // 全域性變數
        global_val *= 10;
        // 全域性靜態變數
        static_global_val *= 10;
        // 靜態變數
        static_val *= 10;
    };
    val *= 10;
    block();
    NSLog(@"global_val-----------%d", global_val);
    NSLog(@"static_global_val----%d", static_global_val);
    NSLog(@"static_val-----------%d", static_val);
}

---輸出結果:---
區域性變數:     val------------------40
全域性變數:     global_val-----------100
全域性靜態變數: static_global_val----200
靜態變數:     static_val-----------300
複製程式碼

C++程式碼:

int global_val = 10;
static int static_global_val = 20;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *static_val;  // 靜態變數  --> 指標
  int val;          // 區域性變數  --> 值
  
  // 在建構函式中,也可以看到 static_val、val被傳入
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *static_val = __cself->static_val; // bound by copy
  int val = __cself->val; // bound by copy

        global_val *= 10;
        static_global_val *= 10;
        (*static_val) *= 10;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_8k_cgm28r0d0bz94xnnrr606rf40000gn_T_block_75d081_mi_0, val);
    }

// 紀錄了block結構體大小等資訊
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
    ...
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼

對於 block 外的變數引用,block預設是將其複製到其資料結構中來實現訪問的。也就是說block的自動變數截獲只針對block內部使用的自動變數,不使用則不截獲,因為截獲的自動變數會儲存於block的結構體內部, 會導致block體積變大(__main_block_desc_0)。特別要注意的是預設情況下block只能訪問不能修改區域性變數的值。

捕獲區域性變數

在Block內部使用了其外部變數,這些變數就會被Block儲存。自動變數val雖然被捕獲進來了,但是是用 __cself->val來訪問的。Block僅僅捕獲了val的值,並沒有捕獲val的記憶體地址。所以,我們在block外部修改了val的值,在block內部並沒有效果。

修改區域性變數

程式碼編譯不通過。預設情況下block只能訪問不能修改區域性變數的值。因為Block僅僅捕獲了val的值,並沒有捕獲val的記憶體地址,block內部修改值並不會對外部的val生效。可能基於此原因,O這種寫法直接編譯錯誤。

修改全域性變數&修改全域性靜態變數

可以直接訪問。全域性變數和全域性靜態變數沒有被截獲到block裡面,它們的訪問是不經過block的(見__main_block_func_0)

修改靜態變數

通過指標訪問。訪問靜態變數(static_val)時,將靜態變數的 指標 傳遞給__main_block_impl_0結構體的建構函式並儲存。修改靜態變數時,是指標操作,所以可以修改其值。

總結: 由上述Block的變數捕獲機制,可以總結出下圖:

變數型別 是否捕獲到Block內部 傳遞方式
區域性變數 值傳遞
區域性staic變數 指標傳遞
全域性變數 直接訪問

3、Block捕獲物件

OC程式碼:

int main() {
    typedef void (^MyBlock)(void);

    NSMutableArray *arr = [[NSMutableArray alloc]init];
    
    MyBlock block = ^{
        [arr addObject:@1];
    };
    
    block();
    NSLog(@"arr.count------------%d", (int)arr.count);
}
複製程式碼

C++程式碼:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *arr;    // 陣列物件  --> 指標
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_arr, int flags=0) : arr(_arr) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableArray *arr = __cself->arr; // bound by copy

    ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id _Nonnull)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1));
}
// 相當於retain操作,將物件賦值在物件型別的結構體成員變數中
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->arr, (void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

// 當於release操作
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main() {
    ...
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼

捕獲物件

在block中可以修改物件的值,因為捕獲物件時,在__main_block_impl_0中可以看到捕獲的是指標。

我們可以看到在捕獲物件的原始碼中多了兩個函式 __main_block_copy_0 和 __main_block_dispose_0。
這兩個函式涉及到Block的儲存域及copy操作,在下一節中會說明。
複製程式碼

四、三種不同型別的Block

  • 全域性Block(_NSConcreteGlobalBlock):存在於全域性記憶體中, 生命週期從建立到應用程式結束,相當於單例。
  • 棧Block(_NSConcreteStackBlock):存在於棧記憶體中, 超出其作用域則馬上被銷燬
  • 堆Block(_NSConcreteMallocBlock):存在於堆記憶體中, 是一個帶引用計數的物件, 需要自行管理其記憶體

1、怎麼確定Block的型別?

在上述的原始碼中,可以看到Block的建構函式__main_block_impl_0中的isa指標指向的是&_NSConcreteStackBlock,它表示當前的Block位於棧區中。

Block型別 是否捕獲到Block內部
_NSConcreteGlobalBlock 沒有用到外界變數或只用到全域性變數、靜態變數
_NSConcreteStackBlock 只用到外部區域性變數、成員屬性變數,且沒有強指標引用
_NSConcreteMallocBlock 有強指標引用或copy修飾的成員屬性引用的block會被複制一份到堆中成為堆Block

2、全域性Block(_NSConcreteGlobalBlock)

全域性Block的生成條件:

  • 定義全域性變數的地方有block語法時
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
}
複製程式碼
  • Block不截獲的自動變數時
int(^block)(int count) = ^(int count) {
        return count;
    };
block(2);
複製程式碼

3、棧Block(_NSConcreteStackBlock)

在生成Block以後,如果這個Block不是全域性Block,那麼它就是為_NSConcreteStackBlock物件,但是如果其所屬的變數作用域名結束,該block就被廢棄。在棧上的__block變數也是如此。

4、堆Block(_NSConcreteMallocBlock)

為了解決棧塊在其變數作用域結束之後被廢棄(釋放)的問題,我們需要把Block複製到堆中,延長其生命週期。開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要將Block從棧複製到堆。

Block的複製操作執行的是copy例項方法。不同型別的Block使用copy方法的效果如下表:

Block型別 儲存位置 複製效果
_NSConcreteGlobalBlock 程式的資料區域 什麼也不做
_NSConcreteStackBlock 棧區 從棧區複製到堆區
_NSConcreteMallocBlock 堆區 引用計數加一

5、copy和dispose

C結構體裡不能含有被__strong修飾的變數,因為編譯器不知道應該何時初始化和廢棄C結構體。但是OC的執行時庫能夠準確把握Block從棧複製到堆,以及堆上的block被廢棄的時機,在實現上是通過__main_block_copy_0函式和__main_block_dispose_0函式進行的

函式 呼叫時機
copy 棧上的 Block 複製到堆時
dispose 堆上的 Block 被廢棄(釋放)時

那麼什麼時候棧上的Block會被複制到堆上呢?

  • 呼叫Block的copy例項方法時
  • Block作為函式返回值返回時
  • 將Block賦值給附有__strong修飾符id型別的類或Block型別成員變數時
  • 將方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時

五、Block迴圈引用

如果在Block內部使用__strong修飾符的物件型別的自動變數,那麼當Block從棧複製到堆的時候,該物件就會被Block所持有。

// self 持有 someBlock 物件
self.someBlock = ^(Type var){
    // 在Block內部,持有self
    [self dosomething];
};
複製程式碼

解決方式:

  • 使用 __weak
// weakSelf 對 self進行弱引用
__weak typeof(self) weakSelf = self;

// self 持有 someBlock 物件
self.someBlock = ^(Type var){

    // 在Block內部,持有weakSelf
   [weakSelf dosomething];
};
複製程式碼
  • 使用__block
- (instancetype)init {
    self = [super init];
    
    __block id blockSelf = self;  // blockSelf 持有 self
    
    //self持有someBlock
    someBlock = ^{
        NSLog(@"self = %@",blockSelf); //someBlock持有blockSelf
        blockSelf = nil;
    };
    return self;
}

- (void)doSomething() {
    someBlock();
}
複製程式碼

此時,blockSelf 持有 self, self 持有someBlock, 而someBlock持有blockSelf。此時,三者形成了一個迴圈。如果doSomething不執行,blockSelf不能置為nil,則無法打破這個迴圈。

一旦執行了doSomething,則迴圈被打破,物件也就可以被釋放。

相關文章