1. Block的底層結構
以下是一個沒有參數和返回值的最簡單的Block:
#include <stdio.h>
int main () {
void(^blk)() = ^{
printf("Block");
};
blk();
return 0;
}
為了探索Block的底層結構,需要將main.m文件轉化為C++的源碼,打開終端命令行。cd到包含main.m文件的文件夾,然后輸入:clang -rewrite-objc main.m(以上內容放在main.m內),這個時候在該文件夾的目錄下會生成main.cpp文件。
(如果執行:clang -rewrite-objc main.m報錯,首先cd到該文件的目錄,然后
在終端將clang -rewrite-objc main.m
的命令替換成為:
$ clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m
然后, 你會發現, 你的文件夾中多了一個.cpp的文件, 證明解決了這個問題。
如果你覺得這個命令很繁瑣不易記, 你可以采用 alias來起一個別名來代替這個命令。
1.打開終端, 鍵入命令 vim ~/.bash_profile
2.在vim界面輸入i進入編輯編輯狀態并且鍵入:
alias rewriteoc=’clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk’
3.鍵入完畢后, esc退出編輯狀態, 再鍵入:wq退出vim并保存
4.鍵入命令source ~/.bash_profile)
好了接下來繼續探索block
這個文件非常長,我們直接拉到文件的最下面會看到這些函數:
1.__main_block_impl_0
2.__main_block_func_0
3.__main_block_desc_0
4.main
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* 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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("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 () {
void(*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
這幾個函數名不是一直這樣的,在不同的文件中會有不同的函數前綴,如在ViewController.m的viewDidLoad方法中顯示是以下這樣的,但是函數內容是一樣的
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController__viewDidLoad_block_desc_0* Desc;
int a;
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_v8_yfvtg6s17zq2xn62sgjsf6b00000gn_T_ViewController_d37495_mi_0, a);
}
static struct __ViewController__viewDidLoad_block_desc_0 {
size_t reserved;
size_t Block_size;
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0)};
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
int a = 1100;
void (*block)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
文件靠上部分還有這樣一個結構體:__block_impl
#ifndef BLOCK_IMPL
#define BLOCK_IMPL
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
首先分析main函數:
int main () {
void(*blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
第一行代碼定義一個block變量,第二行代碼是調用block。這兩行代碼看起來非常復雜。但是我們可以去簡化一下,怎么簡化呢?
變量前面的()一般是做強制類型轉換的,比如在調用block這一行,block前面有一個()是(__block_impl *),這就是進行了一個強制類型轉換,將其轉換為一個_block_impl類型的結構體指針,那像這樣的強制類型轉換非常妨礙我們理解代碼,我們可以暫時將這些強制類型轉換去掉,這樣可以幫助我們理解代碼。
化簡后的代碼如下:
void (* blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
blk->FuncPtr(blk);
以上內容看著應該就比較清楚了。
接下來逐行分析,首先第一行:
void (* blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
這句代碼的意思就是調用_main_block_impl_0這個函數,給這個函數傳進兩個參數_main_block_func_0和&_main_block_desc_0_DATA,然后得到這個函數的返回值,取函數返回值的地址,賦值給blk這個指針。
接下來看函數:__main_block_impl_0
這是一個C++的結構體。而且在這個結構體內還包含一個函數,這個函數的函數名和結構體名稱一致,這在C語言中是沒有的,這是C++特有的。
在C++的結構體包含的函數稱為結構體的構造函數,它就相當于是OC中的init方法,用來初始化結構體。OC中的init方法返回的是對象本身,C++的結構體中的構造方法返回的也是結構體對象。
那么我們就知道了,__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);返回的就是_main_block_impl_0這個結構體對象,然后取結構體對象的地址賦值給blk指針。換句話說,blk指向的就是初始化后的_main_block_impl_0結構體對象。
我們再看一下初始化_main_block_impl_0結構體傳進去的參數:
第一個參數是_main_block_func_0,定義如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block");
}
這個函數其實就是把我們Block中要執行的代碼封裝到這個函數內部了。我們可以看到這個函數內部就一行代碼,就是一個printf函數;這句代碼。
把這個函數指針傳給_main_block_impl_0的構造函數的第一個參數,然后用這個函數指針去初始化_main_block_impl_0這個結構體的第一個成員變量impl的成員變量FuncPtr。也就是說FuncPtr這個指針指向_main_block_func_0這個函數。
第二個參數是&_main_block_desc_0_DATA。
我們看一下這個結構:
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)};
在結構體的構造函數中,0賦值給了reserved,sizeof(struct __main_block_impl_0)是賦值給了Block_size,可以看出這個結構體存放的是_main_block_impl_0這個結構體的信息。在_main_block_impl_0的構造函數中我們可以看到,_main_block_desc_0這個結構體的地址被賦值給了_main_block_impl_0的第二個成員變量Desc這個結構體指針。也就是說Desc這個結構體指針指向_main_block_desc_0_DATA這個結構體。
那么我們總結一下:
void (* blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
blockblock->FuncPtr(blk);
1.創建一個函數_main_block_func_0,這個函數的作用就是將我們block中要執行的代碼封裝到函數內部,方便調用。
2.創建一個結構體_main_block_desc_0,這個結構體中主要包括 _main_block_impl_ 這個結構體占用的存儲空間大小等信息。
3.將1中創建的_main_block_func_0這個函數的地址,和2中創建的_main_block_desc_0這個結構體的地址傳給_main_block_impl_0的構造函數。
4.利用_main_block_func_0初始化_main_block_impl_0結構體的第一個成員變量impl的成員變量FuncPtr。這樣_main_bck_impl_0這個結構體也就得到了block中那個代碼塊的地址。
5.利用_mian_block_desc_0_DATA去初始化_mian_block_impl_0的第二個成員變量Desc。
下面我們再看第二步調用block:
blk->FuncPtr(blk);
我們知道,block實質上就是指向_main_block_impl_0這個結構體的指針,而FuncPtr是_main_block_impl_0的第一個成員變量impl的成員變量,正常來講,block想要調用自己的成員變量的成員變量的成員變量,應該像下面這樣調用:
blk->impl->FuncPtr
然而事實卻不是這樣,這是為什么呢?
原因就在于之前我們把所有的強制類型轉換給刪掉了,之前block前面的()是(__block_impl *),為什么可以這樣強制轉換呢?因為block指向的是_main_block_impl_0這個結構體的首地址,而_main_block_impl_0 的第一個成員變量是struct __block_impl impl;,所以impl和_main_block_impl_0的首地址是一樣的,因此指向_main_block_impl_0的首地址的指針也就可以被強制轉換為指向impl的首地址的指針。
之前說過,FuncPtr這個指針在構造函數中是被初始化為指向_mian_block_func_0這個函數的地址。因此通過blk->FuncPtr調用也就獲取了_main_block_func_0這個函數的地址,然后對_main_block_func_0進行調用,也就是執行block中的代碼了。這中間block又被當做參數傳進了_main_block_func_0這個函數。
現在還沒有用到static void __main_block_func_0(struct __main_block_impl_0 *__cself)
函數里邊的參數,接下來就要用到了
2.變量捕獲-auto變量
auto變量是聲明在函數內部的變量,比如int a = 0;這句代碼聲明在函數內部,那a就是auto變量,等價于auto int a = 0;auto變量時分配在棧區,當超出作用域時,其占用的內存會被系統自動銷毀并生成。下面看一段代碼:
int main () {
int a = 10;
void (^block)(void) = ^{
printf("%d", a);
};
a = 20;
block();
}
這是一個很簡單的Block捕獲自動變量的例子,我們看一下打印結果:
2019-02-19 09:18:14.088934+0800 Test[761:36119] 10
自動變量a的值明明已經變為了20,為什么輸出結果還是10呢?我們把這段代碼轉化為C++的源碼看看。
int main () {
int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
對比一下上面分析的沒有捕獲自動變量的源代碼,我們發現這里_main_block_impl_0中傳入的參數多了一個a。然后我們往上翻看看_main_block_impl_0的結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在_main_block_impl_0這個結構體中我們發現多了一個int類型的成員變量a,在結構體的構造函數中多了一個參數int _a,并且用這個int _a去初始化成員變量a。
所以在void (*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);中傳入了自動變量a用來初始化_main_block_impl_0的成員變量a。那這個時候_main_block_impl_0的成員變量a就被賦值為10了。
由于上面這一步是值傳遞,所以當執行a = 20時,_main_block_impl_0結構體的成員變量a的值是不會隨之改變的,仍然是10。
然后我們再來看一下_main_block_func_0的結構有何變化:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%d", a);
}
可以看到__main_block_func_0函數中傳進來的block使用上了,他就是_cself,里邊包含block_impl_0的成員變量,所以這里可以打印a
3.變量捕獲-static變量
上面講的捕獲的是自動變量,在函數內部聲明的變量默認為自動變量,即默認用auto修飾。那么如果在函數內部聲明的變量用static修飾,又會帶來哪些不同呢?static變量和auto變量的不同之處在于變量的內存的回收時機。auto變量在其作用域結束時就會被系統自動回收,而static變量在變量的作用域結束時并不會被系統自動回收。
先看一段代碼:
int main () {
static int a = 10;
void (^block)(void) = ^{
printf("%d", a);
};
a = 20;
block();
}
我們看一下打印結果:
2019-02-19 09:18:14.088934+0800 Test[761:36119] 20
結果是20,這個和2中的打印結果不一樣,為什么局部變量從auto變成了static結果會不一樣呢?我們還是從源碼來分析:
int main () {
static int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
和2不一樣的是,這里傳入_main_block_impl_0的是&a,也即是a這個變量的地址值。那么這個&a是賦值給誰了呢?我們上翻找到_main_block_impl_0的結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *a;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
這里我們可以看到結構體多了一個指針類型的成員變量int *a,然后在構造函數中,將傳遞過來的&a,賦值給這個指針變量。也就是說,在_main_block_impl_0這個結構體中多了一個成員變量,這個成員變量是指針,指向a這個變量。所以當a變量的值發生變化時,能夠得到最新的值。
4.變量捕獲-全局變量
2和3分析了兩種類型的局部變量,auto局部變量和static局部變量。這一部分則分析全局變量。全局變量會不會像局部變量一樣被block所捕獲呢?我們還是看一下實例:
#include <stdio.h>
static int a = 10;
int main () {
void (^block)(void) = ^{
printf("%d", a);
};
a = 20;
block();
}
打印結果:
2019-02-19 09:18:14.088934+0800 Test[761:36119] 20
我們還是查看一下源碼:
static int a = 10;
int main () {
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
a = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
這里我們可以看到,a這個全局變量沒有作為參數傳入_main_block_impl_0中去。然后我們再查看一下_main_block_impl_0的結構:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* 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;
}
};
可以看到,_main_block_impl_0中并沒有增加成員變量。然后我們再看_main_block_func_0的結構:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("%d", a);
}
可以看到,這個地方在調用的時候是直接調用的全局變量a。
所以我們可以得出結論,block并不會捕獲全局變量。
總結:
變量類型 | 是否捕獲到block內部 | 訪問方式 |
---|---|---|
局部變量auto | 是 | 值傳遞 |
局部變量static | 是 | 指針傳遞 |
全局變量 | 否 | 直接訪問 |
思考 為什么對于不同類型的變量,block的處理方式不同呢?
這是由變量的生命周期決定的。對于自動變量,當作用域結束時,會被系統自動回收,而block很可能是在超出自動變量作用域的時候去執行,如果之前沒有捕獲自動變量,那么后面執行的時候,自動變量已經被回收了,得不到正確的值。對于static局部變量,它的生命周期不會因為作用域結束而結束,所以block只需要捕獲這個變量的地址,在執行的時候通過這個地址去獲取變量的值,這樣可以獲得變量的最新的值。而對于全局變量,在任何位置都可以直接讀取變量的值。
5.變量捕獲-self變量
直接上代碼:
@implementation Person
- (void)test{
void(^block)(void) = ^{
NSLog(@"%@", self);
};
block();
}
@end
這個Person類中只有一個東西,就是test這個函數,那么這個block有沒有捕獲self變量呢?
要搞清這個問題,我們只需要知道搞清楚這里self變量是局部變量還是全局變量,如果是局部變量,那么是一定會捕獲的,而如果是全局變量,則一定不會被捕獲。
我們把這個Person.m文件轉化為c++的源碼,然后找到test函數在c++中的表示:
static void _I_Person_test(Person * self, SEL _cmd) {
void(*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
我們可以看到,本來Person.m中,這個test函數我是沒有傳任何參數的,但是轉化為c++的代碼后,這里傳入了兩個參數,一個是self參數,一個是_cmd。self很常見,_cmd表示test函數本身。所以我們就很清楚了,self是作為參數傳進來,也就是局部變量,那么block應該是捕獲了self變量,事實是不是這樣呢?我們只需要查看一下_Person_test_block_impl_0的結構就可以知道了。
_Person_test_block_impl_0的結構:
struct __Person__test_block_impl_0 {
struct __block_impl impl;
struct __Person__test_block_desc_0* Desc;
Person *self;
__Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看到,self確實是作為成員變量被捕獲了。
6.Block的類型
前面已經說過了,Block的本質就是一個OC對象,既然它是OC對象,那么它就有類型。
在搞清楚Block的類型之前,先把ARC關掉,因為ARC幫我們做了太多的事,不方便我們觀察結果。關掉ARC的方法在Build Settings里面搜索Objective-C Automatic Reference Counting,把這一項置為NO。
5-1MRC捕獲變量
int b = 10;
- (void)test{
int a = 10;
void (^block)(void) = ^{
NSLog(@"%d %d", b, a);
};
NSLog(@"\n %@\n %@\n %@", [[block class] superclass], [[[block class] superclass] superclass], [[[[block class] superclass] superclass] superclass]);
}
上面的代碼的打印結果是:
__NSStackBlock
NSBlock
NSObject
這說明上面定義的這個block的類型是NSStackBlock,并且它最終繼承自NSObject,也說明Block的本質是OC對象。
5-2MRC不捕獲變量
再看一下沒有捕獲局部變量的block
- (void)test{
void (^block)(void) = ^{
NSLog(@"test");
};
NSLog(@"\n %@\n %@\n %@", [[block class] superclass], [[[block class] superclass] superclass], [[[[block class] superclass] superclass] superclass]);
}
上面的代碼的打印結果是:
__NSGlobalBlock
NSBlock
NSObject
接下來切換回ARC模式,再次打印以上兩次結果:
5-1MRC捕獲變量修改為ARC以后打印結果為:
__NSMallocBlock
NSBlock
NSObject
5-2MRC不捕獲變量修改為ARC以后打印結果為:
__NSGlobalBlock
NSBlock
NSObject
5-3不論是ARC還是MRC下手動copy
void (^block)(void) = [^{
NSLog(@"test");
} copy];
結果如下:
__NSGlobalBlock
NSBlock
NSObject
由此可見Block有三種類型,分別是NSGlobalBlock,NSMallocBlock,NSStackBlock。
這三種類型的Block對象的存儲區域如下:
類 | 對象的存儲域 |
---|---|
NSStackBlock | 棧 |
NSGlobalBlock | 程序的數據區域(.data區) |
NSMallocBlock | 堆 |
截獲了自動變量的Block是NSStackBlock類型,沒有截獲自動變量的Block則是NSGlobalBlock類型,NSStackBlock類型的Block進行copy操作之后其類型變成了NSMallocBlock類型。
Block的類型 | 副本的配置存儲域 | 復制效果 |
---|---|---|
NSStackBlock | 棧 | 從棧復制到堆 |
NSGlobalBlock | 程序的數據區域(.data區) | 什么也不做 |
NSMallocBlock | 堆 | 引用計數增加 |
下面我們一起分析一下NSStackBlock類型的Block進行copy操作后Block對象從棧復制到了堆有什么道理,我們首先來看一段代碼:
此時環境為MRC
void (^block)(void);
void test() {
int age = 10;
block = ^{
NSLog(@"age=%d", age);
};
}
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
block();
return 0;
}
}
不出意外的話,打印結果應該是10,那么結果是不是這樣呢?我們打印看一下:
2019-02-19 13:43:29.705085+0800 Test[4256:623794] age=-436931384
很奇怪,打印了一個這么奇怪的數字。這是為什么呢?
block使用了自動變量age,所以它是NSStackBlock類型的,因此block是存放在棧區,age是被捕獲作為結構體的成員變量,其值也是被保存在棧區。所以當test這個函數調用完畢后,它棧上的東西就有可能被銷毀了,一旦銷毀了,age值就不確定是多少了。通過打印結果也可以看到,確實是影響到了block的執行。
如果我們對block執行copy操作,結果會不會不一樣呢?
void (^block)(void);
void test() {
int age = 10;
block = [^{
NSLog(@"age=%d", age);
} copy];
}
int main(int argc, char * argv[]) {
@autoreleasepool {
test();
block();
return 0;
}
}
打印結果:
2019-02-19 13:45:19.065669+0800 Test[4312:632135] age=10
這個時候得出了正確的輸出。
因為對block進行copy操作后,block從棧區被復制到了堆區,它的成員變量age也隨之被復制到了堆區,這樣test函數執行完之后,它的棧區被銷毀并不影響block,因此能得出正確的輸出。
7.ARC環境下自動為Block進行copy操作
因為在ARC環境下編譯器為我們做了很多copy操作。其中有一個規則就是如果Block被強指針指著,那么編譯器就會對其進行copy操作。我們看到這里:
^{
NSLog(@"age=%d", age);
};
這個Block塊是被強指針指著,所以它會進行copy操作,由于其使用了自動變量,所以是棧區的Block。經過復制以后就到了堆區,這樣由于Block在堆區,所以就不受Block執行完成的影響,隨時可以獲取age的正確值。
總結一下ARC環境下自動進行copy操作的情況一共有以下幾種:
1.block作為函數返回值時。
2.將block賦值給__strong指針時。
3.block作為Cocoa API中方法名含有usingBlock的方法參數時。
4.GCD中的API。
block作為函數返回值時
typedef void(^Block)(void);
Block test() {
int age = 10;
return ^{
NSLog(@"age=%d", age);
};
}
int main(int argc, char * argv[]) {
@autoreleasepool {
Block block = test();
block();
return 0;
}
}
test函數的返回值是一個block,那這種情況的時候,在棧區的
^{
NSLog(@"age=%d", age);
};
這個block會被復制到堆區
將block賦值給強指針時
7中第一個例子就是將block賦值給強指針時,進行了copy操作的情況。
block作為Cocoa API中方法名含有usingBlock的方法參數時
比如說遍歷數組的函數:
NSArray *array = [[NSArray alloc] init];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"%d", idx);
}];
enumerateObjectsUsingBlock:這個函數中的block會進行copy操作
GCD中的API
GCD中的很多API的參數都有block,這個時候都會對block進行一次copy操作,比如下面這個dispatch_after函數:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"wait");
});