如下代碼就是個block,block不會主動調用
^{
NSLog(@"this is a block!");
};
運行之后,發現并沒有打印。
如果在block后面加個(),發現block就立馬調用了:
^{
NSLog(@"this is a block!");
}();
運行后:
this is a block!
一般我們都把block保存起來,在需要的時候才調用。
一. block的底層結構(block的本質)
block是封裝了函數調用以及函數調用環境的OC對象
下面我們驗證上面這句話。
寫個簡單的block,其中block內部使用了block外部的age變量:
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 20;
//block定義
void (^block)(int, int) = ^(int a , int b){
NSLog(@"this is a block! -- %d", age);
};
age = 30;
//block調用
block(10, 10);
}
return 0;
}
block調用之后打印:this is a block! -- 20 。為什么不是30?
源碼分析
通過“xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 輸出的CPP文件”指令,將上面代碼轉成C++代碼之變成這樣:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int age = 20;
//block底層定義
void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
//block底層調用
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
}
return 0;
}
但是由于底層的代碼添加了許多強轉,我們簡化代碼,如下:
//block底層定義
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
//block底層調用
block->FuncPtr(block, 10, 10);
一共就兩行代碼,我們就一個一個研究
① block底層定義
//block底層定義
void (*block)(void) = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA
);
先看__main_block_impl_0這個函數,我們發現它被定義在一個同名結構體里面,這個__main_block_impl_0結構體就是block的底層實現
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
// 構造函數(類似于OC的init方法),返回結構體對象
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock; //isa指向類對象,就比如str的isa指向NSString
impl.Flags = flags;
impl.FuncPtr = fp; //外面的__main_block_func_0函數地址傳進來,保存在這里
Desc = desc; //外面的__main_block_desc_0結構體地址傳進來,保存在這里
}
};
先看結構體里面的__main_block_impl_0函數,其實這個函數在C++中是構造方法,類似于OC的init方法,返回結構體對象。函數的第一個參數是fp指針,第二個參數是desc指針。
接下來再看看__main_block_impl_0結構體,它第一個成員是個結構體:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
發現這個結構體里面第一個成員就是isa,驗證了block本質上也是一個OC對象。
第二個成員是指向__main_block_desc_0結構體的指針:
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_block_desc_0_DATA,默認傳入了兩個值0和sizeof(struct __main_block_impl_0),block的底層就是__main_block_impl_0結構體,所以這個結構體第二個值保存的是block的大小。
接下來我們看一下__main_block_impl_0函數的參數,第一個參數是指向__main_block_func_0函數的指針,如下:
//封裝了block執行邏輯的函數
//第一個參數是block,后面是block調用的時候傳入的參數
void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age);
}
可以發現,我們寫的代碼塊里面的NSLog被封裝成了__main_block_func_0函數,它引用了block外面的成員變量age。
第二個參數就是上面我們說的__main_block_desc_0結構體的地址。
現在我們知道了,首先__main_block_impl_0函數有兩個參數,第一個參數是__main_block_func_0函數的地址(這個函數里面封裝了我們block里面執行的代碼),第二個參數是__main_block_desc_0結構體的地址(這個結構體里面有保存block的大小),整個函數的返回值是個__main_block_impl_0結構體,block底層就是__main_block_impl_0結構體,最后再獲取__main_block_impl_0結構體的地址,賦值給左邊的“block”變量,然后我們拿到“block”變量就可以做其他事情了,至此,block定義完成。
② block底層調用
//block底層調用
block->FuncPtr(block, 10, 10);
這句代碼就很簡單了,直接取出block里面的FuncPtr函數,傳入參數進行調用。
這里你可能會有個小疑問,不應該是通過“block-> impl->FuncPtr(block, 10, 10)”來拿到FuncPtr嗎?
其實我們在簡化之前,代碼是這樣的:
((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
可以發現系統把block強轉成__block_impl類型的了,由于impl又是__main_block_impl_0結構體的第一個成員,所以impl的地址和__main_block_impl_0結構體的地址是一樣的,強轉之后可以直接獲取到FuncPtr。
根據如上分析,驗證了,block是封裝了函數調用以及函數調用環境的OC對象。
總結:
如上圖所示,block底層就是一個__main_block_impl_0結構體,它由三個部分組成:
- 第一部分是impl,它是個結構體,里面有isa指針和FuncPtr指針,FuncPtr指針指向__main_block_func_0函數,這個函數里面封裝了block需要執行的代碼。
- 第二部分是desc,它是個指針,指向__main_block_desc_0結構體,它里面有一個Block_size用來保存block的大小。
- 第三部分是age,它把外面訪問的成員變量age封裝到自己里面了。
關于block的本質,網上還有一張圖,可自己參考:
二. block的變量捕獲(capture)
在上面我們留了一個問題,block調用之后打印:this is a block! -- 20 。為什么不是30?
局部變量和全局變量的捕獲
- 如果是被auto修飾的局部變量,會被捕獲,是值傳遞
- 如果是被static修飾的局部變量,會被捕獲,是指針傳遞
- 如果是全局變量,不會被捕獲,因為可以直接訪問
auto自動變量,離開作用域就銷毀,默認省略auto。比如我們常見的 int age = 10,其實就是默認省略了auto,本來應該是auto int age = 10
我們執行如下代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
auto int age = 10;
static int height = 10;
void (^block)(void) = ^{
// age的值捕獲進來
// height的指針捕獲進來
NSLog(@"age is %d, height is %d", age, height);
};
age = 20;
height = 20;
block();
}
return 0;
}
打印:
age is 10, height is 20
這就解釋了上面的疑問,因為age是值捕獲,所以修改外面的age值不會影響block里面的age值。
① 源碼分析
為了探究block內部是怎么做到的,我們將上面的代碼轉成C++代碼,抽取關鍵的代碼,如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
int *height;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
int *height = __cself->height; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2w_t9gvrhjs7gv_m4kb_8q3r_980000gn_T_main_12eb7d_mi_1, age, (*height));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
auto int age = 10;
static int height = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 20;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看出age和height都被捕獲了,age是值捕獲,height是指針捕獲。
- 我們定義block,其實就是初始化__main_block_impl_0結構體,定義block的時候會把age和&height傳進去,這個結構體里面有一個age一個height指針用于接收傳進去的值,這行代碼“age(_age), height(_height)”就是保證外面的變量改變的時候實時改變結構體里面的age和height指針的值。
- 我們調用block的時候,其實就是執行__main_block_func_0函數,這個函數會獲取__main_block_impl_0結構體中age和height指針的值,所以打印的時候就會把age和*height的值打印出來。
- 所以執行完block之后age的值沒改變,因為是值傳遞,height的值改變了,因為是指針傳遞。
② 為什么auto變量是值傳遞,static變量是指針傳遞呢?
因為auto變量在{}結束之后就會被銷毀,被銷毀之后變量的內存就消失了,將來在其他地方執行block的時候就不可能再去訪問auto變量的內存了。
但是static變量不一樣,被static修飾的變量一直在內存中,只要捕獲它的指針就可以隨時訪問它的內存了。
③ 為什么全局變量不需要捕獲呢?
下面我們驗證下,如下代碼:
int age_ = 10;
static int height_ = 10;
void (^block)(void);
void test()
{
auto int a = 10;
static int b = 10;
block = ^{
NSLog(@"age is %d, height is %d", a, b);
};
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
test();
block();
}
return 0;
}
轉成C++文件之后,代碼如下:
int age_ = 10;
static int height_ = 10;
void (*block)(void);
struct __test_block_impl_0 {
struct __block_impl impl;
struct __test_block_desc_0* Desc;
int a;
int *b;
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
int *b = __cself->b; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_fd2a14_mi_0, a, (*b));
}
void test()
{
auto int a = 10;
static int b = 10;
block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, a, &b));
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
test();
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看出age_和height_沒被捕獲,a和b被捕獲了,相信不用解釋上面的代碼也能看懂。
總結
為什么局部變量需要捕獲,全局變量不需要捕獲呢?
因為作用域的問題。局部變量的作用域在{}中,當在其他函數中想要訪問局部變量的值,肯定要把它捕獲啊(auto變量是值捕獲,static變量是指針捕獲)。對于全局變量,任何地方都可以訪問,所以沒必要捕獲。
④ self的捕獲
下面代碼,創建MJPerson對象:
MJPerson.h
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)test;
- (instancetype)initWithName:(NSString *)name;
@end
MJPerson.m
#import "MJPerson.h"
@implementation MJPerson
- (void)test
{
void (^block)(void) = ^{
NSLog(@"-------%d", [self name]);
};
block();
}
- (instancetype)initWithName:(NSString *)name
{
if (self = [super init]) {
self.name = name;
}
return self;
}
@end
如上代碼,在test方法里面訪問name屬性,那么self會被捕獲嗎?name會被捕獲嗎?
將MJPerson.m轉成C++代碼:
struct __MJPerson__test_block_impl_0 {
struct __block_impl impl;
struct __MJPerson__test_block_desc_0* Desc;
MJPerson *self;
__MJPerson__test_block_impl_0(void *fp, struct __MJPerson__test_block_desc_0 *desc, MJPerson *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MJPerson__test_block_func_0(struct __MJPerson__test_block_impl_0 *__cself) {
MJPerson *self = __cself->self; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_MJPerson_1027e6_mi_0, ((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("name")));
}
static void _I_MJPerson_test(MJPerson * self, SEL _cmd) {
void (*block)(void) = ((void (*)())&__MJPerson__test_block_impl_0((void *)__MJPerson__test_block_func_0, &__MJPerson__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
static instancetype _I_MJPerson_initWithName_(MJPerson * self, SEL _cmd, NSString *name) {
if (self = ((MJPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJPerson"))}, sel_registerName("init"))) {
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)name);
}
return self;
}
可以看出self被捕獲了,并且是指針捕獲,既然被捕獲,就說明self是局部變量。
為什么self是局部變量呢?
其實每個方法都兩個隱式參數,一個是self一個是_cmd,self是方法調用者,_cmd是方法名,既然self被當做參數了,那self肯定是局部變量了,也可以在上面的代碼中進行驗證,如下:
void _I_MJPerson_test(MJPerson * self, SEL _cmd)
對于[self name],在上面的代碼可以看出是給self發送消息,如下:
objc_msgSend((id)self, sel_registerName("name"))
所以,block會捕獲self,如果想要訪問self中的成員變量就給self發送消息就好了(self都被捕獲了,肯定可以獲取到self中的其他信息了)。
總結:局部變量會捕獲,全局變量不會捕獲。
Demo地址:block的本質和變量捕獲
三. block的類型
接下來我們講的都是在MRC環境下。
block有3種類型,可以通過調用class方法或者isa指針查看具體類型,最終都是繼承于NSBlock類型
__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
1. 驗證block是OC對象
前面我們說了,block是OC對象,既然是OC對象就可以調用class方法查看類型
下面驗證block是OC對象,運行代碼:
void (^block)(void) = ^{
NSLog(@"Hello");
};
NSLog(@"%@", [block class]);
NSLog(@"%@", [[block class] superclass]);
NSLog(@"%@", [[[block class] superclass] superclass]);
NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
打印結果:
__NSGlobalBlock__
__NSGlobalBlock
NSBlock
NSObject
可以看出上面的block繼承關系是:__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject
上面的block最終繼承于NSObject,說明block是OC對象。現在我們也明白了,block內部的isa就是從NSObject中獲取的,而且isa指向block的類對象。
2. 三種block在內存中的分布
剛才說了block有三種類型,這三種block在內存中的分布如下圖:
- 上圖的text區就是代碼段,我們編寫的代碼都在代碼段,代碼段內存地址比較小,上圖從上往下,內存地址越來越大。
- data區就是數據段,數據段一般都放一些全局變量。
- 堆:動態分配內存,需要程序員自己申請內存,也需要程序員自己管理內存。比如[NSObject alloc]或者malloc()創建的對象就是存放在堆,需要我們自己管理內存(只不過ARC不需要你管了)。
- 棧:系統自動分配內存,自動銷毀內存。存放局部變量,系統會在{}結束之后銷毀局部變量。棧的內存地址最大。
驗證內存地址由低到高:
int age = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
NSLog(@"數據段:age %p", &age);
NSLog(@"堆:obj %p", [[NSObject alloc] init]);
NSLog(@"棧:a %p", &a);
NSLog(@"數據段:class %p", [MJPerson class]);
}
return 0;
}
打印:
數據段:age 0x100002488
堆:obj 0x10061b5e0
棧:a 0x7ffeefbff46c
數據段:class 0x100002438
如上,如果想知道MJPerson類對象放在哪里,可以看它的地址和哪個比較接近,比較地址可知,放在數據段。
現在我們知道了三種block在內存中的分布:
__NSGlobalBlock__是放在數據段的,也就是和全局變量放在一起。
__NSMallocBlock__是放在堆區的,和一般的OC對象一樣的,需要我們自己管理內存。
__NSStackBlock__是放在棧區的,和局部變量是一樣的,系統自動管理內存。
但是什么樣的block才是這三種block的某一種類型呢?
先看結論:
block類型 | 環境 |
---|---|
__NSGlobalBlock__ | 沒有訪問auto變量 |
__NSMallocBlock__ | __NSStackBlock__調用了copy |
__NSStackBlock__ | 訪問了auto變量 |
GlobalBlock沒有訪問auto變量,這種類型的block都可用方法代替,不常用。
StackBlock訪問了auto變量,放在棧區,這時候block捕獲了auto變量的值,然后存儲在block結構體內部,棧區是系統自動管理的,所以在代碼塊結束之后,block內存會被銷毀,這時候block結構體內部的值就是亂七八糟的了,block就會有問題,如下:
void (^block)(void);
void test2()
{
// NSStackBlock
int age = 10;
block = ^{
NSLog(@"block---------%d", age);
};
NSLog(@"%@", [block class]);
}
執行方法:
test2();
block();
打印:
__NSStackBlock__
block---------2634434;
可以看出打印age的值,就是亂的。
那么如何解決這個問題呢?
可以把block從棧放到堆里面,每一種類型的block調用copy后的結果如下所示:
block類型 | 副本源的配置存儲域 | 復制效果 |
---|---|---|
__NSGlobalBlock__ | 程序的數據區段 | 什么也不做 |
__NSMallocBlock__ | 堆 | 引用計數器增加 |
__NSStackBlock__ | 棧 | 從棧復制到堆 |
比如,將上面NSStackBlock加個copy變成NSMallocBlock,打印就是正確的,如下:
void test2()
{
// NSStackBlock ->NSMallocBlock
int age = 10;
block = [^{
NSLog(@"block---------%d", age);
} copy];
NSLog(@"%@", [block class]);
[block release]; //如果是MAC,由于放在堆區了,要自己release
}
打印:
__NSMallocBlock__
block---------10
關于NSGlobalBlock的copy操作和NSMallocBlock的copy操作的結果可自行驗證。
Demo地址:block的類型