我所理解的 Block

做者:bool周 原文連接:我所理解的 Blockhtml

關於 block 的文章,網上已經有不少了。我這裏只是將這個知識點再梳理一下,從 block 使用到底層原理。畢竟年紀大了,容易忘事。編程

拋磚引玉

圍繞 block 所產生的問題,太多太多。這裏我將這些問題羅列出來,若是你對某些問題感到懵逼,能夠在下文中找到答案。找不到,私信我。數組

  • 爲何要用 block?畢竟它的語法難記,還容易產生內存泄漏。
  • block 的各類書寫格式,你是否瞭解?
  • 按內存區這一維度劃分,block 能夠分爲哪幾種類型,如何定義的?
  • block 是 Objective-C 對象嗎?
  • block 內部實現原理是怎樣的?
  • 怎樣寫會形成循環引用,又是如何避免循環引用?
  • 若是以上問題你都瞭解,能夠不用往下看了。

爲何使用 Block

block 的惟一好處就是:使代碼變得更簡潔bash

咱們能夠向一個方法以參數的形式傳遞一個 block,做爲方法的 callback 函數。相似於向方法傳遞一個函數指針。這樣就沒必要再聲明一個新的方法,並調用,在必定程度上簡化了代碼。下面有一個例子:app

使用 notification 時,常規方式是註冊一個 selector 並實現對應的方法,像這樣:框架

- (void)viewDidLoad {
   [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(keyboardWillShow:)
        name:UIKeyboardWillShowNotification object:nil];
}
 
- (void)keyboardWillShow:(NSNotification *)notification {
    // Notification-handling code goes here.
}
複製代碼

若是使用 block,能夠寫成這樣:async

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification
         object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
             // Notification-handling code goes here. 
    }];
}
複製代碼

另一個簡化代碼的特性就是,block 能夠捕獲外部變量。這樣就沒必要再以參數的形式傳遞,簡化的方法的定義和調用。ide

Block 長什麼樣

在最初接觸 block 時,我常常寫不對,它的語法太另類。fucking block syntax 提供了各類 block 的寫法,我這裏就直接照搬過來了。函數

  • 做爲局部變量
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
複製代碼
  • 做爲屬性(property)
@property (nonatomic, copy, nullability) returnType (^blockName)(parameterTypes);
複製代碼
  • 定義方法時,做爲方法參數
- (void)someMethodThatTakesABlock:(returnType (^nullability)(parameterTypes))blockName;
複製代碼
  • 調用方法時,做爲參數傳遞
[someObject someMethodThatTakesABlock:^returnType (parameters) {...}];
複製代碼
  • 做爲類型別名 (typedef),增長代碼可讀性
typedef returnType (^TypeName)(parameterTypes);
TypeName blockName = ^returnType(parameters) {...};
複製代碼

Block 內部原理是怎樣的

在編譯時,編譯器會將 block 語法轉化成 C 的源代碼,再將這部分 C 的源代碼編譯爲編譯器處理的代碼。咱們可使用 clange (LLVM 編譯器) 來完成 "將 block 語法轉化爲 C++ 源代碼 (本質仍是 C)" 這一階段。具體命令以下:ui

clang -rewrite-objc 源代碼文件名
複製代碼
1.一個簡單 Block 的結構

下面咱們轉化一段 OC 代碼來分析 block。

使用 clang -rewrite-objc main.m 轉化以下代碼:

int main(int argc, char * argv[]) {
    void (^myBlock) (void) = ^{printf("test block");};
    myBlock();
    
    return 0;
}
複製代碼

轉化接入後是下面這個樣子(主要代碼)。由於語法和命名的關係,代碼看着很亂,可是邏輯很清晰。爲了方便理解,我加了部分註釋。

// block 結構體。能夠理解爲 'block' 這種類型的基本結構
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// 整個 block 的結構,命名有點歧義,理解便可
struct __main_block_impl_0 {
  struct __block_impl impl;	 // __block_impl 類型的成員變量
  struct __main_block_desc_0* Desc; // Desc 指針
  
  // 構造函數主要是爲兩個成員變量賦值
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block 的代碼塊,實際執行部分
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
	printf("test block");
}

// 版本升級所需的區域和 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)};

// main 方法
int main(int argc, char * argv[]) {

	 // 定義 block
    void (*myBlock) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    
    // 執行 block
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}
複製代碼

上述代碼中,定義了三個結構體:block 基本結構 __block_impl、Desc 指針 __main_block_desc_0、整個 block 的結構 __main_block_impl_0。其中 __main_block_impl_0 包含兩個成員變量,分別爲 __block_impl 結構體實例和 __main_block_desc_0 指針。

上述還定義了兩個方法:block 實際執行方法 __main_block_func_0main() 方法。

__main_block_func_0 方法爲輸出對應的字符串("test block")。

main 方法主要分爲兩步:

定義 block。將 block 實際執行方法,也就是 __main_block_func_0 的函數指針和 __main_block_desc_0_DATA 的地址傳入 __main_block_desc_0 的構造方法,構形成一個完整的 block。根據定義能夠看出 __main_block_desc_0 初始化時全部的大小爲 __main_block_impl_0 結構體大小。

執行 block。實際能夠簡化爲 *myBlock->impl.FuncPtr,就是調用對應的方法。

瞭解了這個基本結構,後面的都是在這基礎上追加部分代碼,很容易理解。

2.Block 結構與 isa 指針

在上述代碼中,咱們能夠看出 block 結構體,也就是 __block_impl 中有一個 isa 指針。咱們先來看看這個 isa 指針。

「id" 這一變量類型用於存儲 OC 對象。在 runtime.h 中,它的定義以下:

typedef struct objc_objct {
	Class isa;
} *id;
複製代碼

Class 類型屬於一個結構體指針類型,定義爲:

typedef struct objc_class *Class
複製代碼

objc_class 結構體定義以下:

struct objc_class {
	Class isa;
}
複製代碼

綜上可知,OC 中每一個類的結構體就是基於 objc_class 結構體。

在上面能夠看到這樣一段代碼:

impl.isa = &_NSConcreteStackBlock;
複製代碼

isa 被賦值爲 _NSConcreteStackBlock 類型的指針。那麼 _NSConcreteStackBlock 又是什麼?經過 debug 界面咱們能夠看到以下狀況 :

block 一供有三種類型,分別爲 __NSGlobalBlock____NSStackBlock____NSMallocBlock__,這三種類型後面會詳細解釋。這裏轉化的代碼和 debug 界面顯示的類型不同,可是基本類型以信仰,都是 Class 類型,沒必要糾結。

能夠看出 _NSConcreteStackBlock 實際是 Class 類型。那麼,block 本質就是 Objective-c 對象

3.捕獲自動變量

咱們將源代碼改成以下狀況:

int main(int argc, char * argv[]) {
	 int val = 10;
    void (^myBlock) (void) = ^{printf("value is %i", val);};
    myBlock();
    
    return 0;
}
複製代碼

使用 clang 進行轉化。咱們只看轉化後的關鍵部分。即整個 block 結構:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : 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 val = __cself->val; 
	printf("value is %i", val);
}
複製代碼

能夠看到局部變量 val 被自動追加到了 __main_block_impl_0 結構體中,並在構造函數中添加了參數。經過構造函數初始化 block 時,會將外部變量捕獲進來。這裏捕獲的是引用,因此在 block 內部改變局部變量的值以後,並不會傳出去

4.關於 __block

正常狀況下,block 捕獲的變量是不能夠修改的。可是有兩種方式可讓其修改:

  • 使用靜態變量、靜態全局變量、全局變量。由於前兩個生成在靜態數據區,最後一個生成在堆區。它們都不會隨着 block 棧的消失而被釋放。出了 block 做用域依然有效。可是平時使用這種變量諸多不變。
  • 使用 __block 關鍵字修飾。它相似於 staticautoregister 這些關鍵字,主要來指定變量存儲在哪一個區域。

爲何使用 __block 關鍵字修飾以後就能夠修改。咱們使用 clang 轉化以下一段代碼:

int main(int argc, char * argv[]) {
	 __block int val = 10;
    void (^myBlock) (void) = ^{val = 20;};
    myBlock();
    
    return 0;
}
複製代碼

轉換後以下,能夠看出加了一句 __block 多了不少代碼,依然是代碼很亂,可是邏輯很清晰,咱們只看主要部分 :

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  	__Block_byref_val_0 *val = __cself->val; // bound by ref
	(val->__forwarding->val) = 20;
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
	_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
	_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

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(int argc, char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
    
    void (*myBlock) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
    
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

    return 0;
}
複製代碼

咱們能夠看出局部變量轉化爲一個結構體:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
複製代碼

__main_block_impl_0 中追加了一個 __Block_byref_val_0 結構體指針,後續的初始化和修改 val 的值也是經過指針來操做。因此修改後的值就能夠傳出去了。

5.block 的存儲類型

前面有提到過,block 按照存儲類型劃分,能夠分爲三種:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock

他們在內存中的存儲結構以下圖所示,對號入座:

咱們分別來解釋一下。

**_NSConcreteGlobalBlock,也叫全局 block。**有兩種生成方式: 一種是在全局的地方生成,不存在捕獲局部變量的狀況。例如:

void(^globalBlock)(void) = ^{printf("this is global block");};

int main(int argc, char * argv[]) {
    globalBlock();
    return 0;
}
複製代碼

另外一種是,block 中不截獲局部變量。例如:

typedef int (^TestBlock) (int);
int main(int argc, char * argv[]) {
    TestBlock block = ^(int num) {printf("num is %d",num);}
    return 0;
}
複製代碼

**_NSConcreteStackBlock,也叫棧 block。**除了上述的初始化方式,經過其餘方式初始化爲的 block 都是棧 block。

_NSConcreteMallocBlock,也叫堆 block。 堆 block 不是由代碼初始化來的,而是由棧 block 調用 copy 方法時從棧內存拷貝到堆內存而得來的。

至於何時會發生 copy 操做,能夠總結爲一下幾點 (ARC 環境):

  • Cocoa 框架的方法且方法名中含有 usingBlock。
  • GCD 中的方法。
  • block 賦值給強引用對象時。
  • 做爲返回值時。
  • 顯示調用 copy 方法。

下面是一些例子:

typedef BOOL (^TestBlock)(NSString *);
typedef void (^paramBlock)(NSString *);

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    int val = 10;
    
    // block1 is global block
    void (^block1)(NSString *) = ^(NSString *name) {
        NSLog(@"this is global block");
    };
    
    // block2 is malloc block
    void (^block2)(NSString *) = ^(NSString *name) {
        int value = 10 * val;
        NSLog(@"this is malloc block");
    };
    
    // block3 is stack block
    __weak void (^block3)(NSString *) = ^(NSString *name) {
        int value = 10 * val;
        NSLog(@"this is stack block");
    };
    
    // block4 is malloc block
    TestBlock block4 = [self testWithBlock:^(NSString *name) {
        NSLog(@"noting");
    }];
    
    // block5 is global block
    TestBlock block5 = [self getGlobalBlock];

}

- (TestBlock)testWithBlock:(paramBlock)block {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"capture block is %@",block); // malloc block
    });
    
    int val = 10;
    return ^BOOL(NSString *name) {
    		int value = val * 10;
        NSLog(@"noting");
        return YES;
    };
}

- (TestBlock)getGlobalBlock {
    return ^BOOL(NSString *name) {
        NSLog(@"nothing");
        return YES;
    };
}
複製代碼
6. __block 變量結構中的 __forwarding

在前面的代碼中,咱們發現 __block 代碼中有一個 __forwarding,以下面的代碼:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
複製代碼

長話短說。當一個棧 block 捕獲了一個在棧上生成的 __block 變量,那麼隨着 block 從棧上 copy 到堆上,這個 __block 變量也從棧上 copy 到堆上。由於有一個 __forwarding 指針,使得不管從從棧上仍是堆上,訪問的都是一個變量。若是沒有明白看下面的圖和代碼。

__block int val = 10;

void (^block)(int) = [^(int count) { val++;} copy];

val++;
block();

NSLog(@"val is %d",val); // val is 12;
複製代碼

不管是操做棧上的 val 變量仍是堆上的 val 變量,最終修改的是同一個值。

7.block 與循環引用

發生循環引用說明出現了互相持有的現象,例以下面這樣:

上圖中 self 持有 blk 屬性,blk 持有 block,block 持有 self,這就造成了一個環。現現在的 Xcode 已經很智能,這種簡單的循環引用,會出現警告。

爲避免循環引用,可使用 __weak 關鍵字。例以下面這樣:

__weak typeof(self) weakSelf = self;

self.blk = ^BOOL(NSString *name) {
   [weakSelf log];
   return YES;
};
複製代碼

爲了不在 block 內使用 self 期間,self 被釋放。能夠在 block 內部對 self 進行強引用。由於這個強引用生成在 block 棧內,會隨着 block 的做用域消失而消失。不會產生循環引用。

__weak typeof(self) weakSelf = self;

self.blk = ^BOOL(NSString *name) {
	__strong typeof(self) self = weakSelf;
   [self log];
   return YES;
};
複製代碼

如何使用 Block

前面講了不少原理,過程當中也講了不少使用。這裏只總結幾點,使用 block 必定要注意:

  • block 的命名方式,牢記。
  • 對於要再 block 內修改的變量,加 __block 修飾符。對於 OC 中的一些對象,例如 NSMutableArray,若是隻修改數組內的元素,不須要加 __block;若是要修改數組的指針,須要加 __block
  • 使用自定義 block 時,注意循環引用的問題。尤爲是各類間接關係產生的循環引用。
  • 對於捕獲到 block 中的弱引用,若是怕使用期間被釋放,須要再 block 內部再次強引用一下。

綜上,block 總結完畢,祝好運。

參考文獻

1.A Short Practical Guide to Blocks

2.How Do I Declare A Block in Objective-C?

3.Objective-C高級編程

相關文章
相關標籤/搜索