Block底層本質(zhì)
- block就是Objective-C對閉包的實現(xiàn),閉包就是一個沒有名字的函數(shù)或者指向函數(shù)的指針。block本質(zhì)上也是一個OC對象,它內(nèi)部有isa指針;
- block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境(參數(shù))的OC對象;
我們來看一段代碼
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void(^block)(int, int) = ^(int b,int c){
NSLog(@"%d",a);
NSLog(@"Hello World!");
};
block(10,10);
}
return 0;
}
把上面這段代碼轉(zhuǎn)化為C++底層語言,轉(zhuǎn)化后:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
//定義block變量
void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
//執(zhí)行block內(nèi)部的代碼
((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
}
return 0;
}
在C++代碼中,block代碼塊底層調(diào)用__main_block_impl_0。這句代碼調(diào)用以下代碼
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;
}
};
我們發(fā)現(xiàn),block的底層也是一個結(jié)構(gòu)體。搜索struct __block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
block的第一個結(jié)構(gòu)體成員是一個isa指針。這說明,block也是一個OC對象。
__main_block_desc_0結(jié)構(gòu)體成員包括兩個參數(shù),如下:
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_func_0函數(shù)內(nèi)部封裝了block執(zhí)行邏輯的函數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int b, int c) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_0,a);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_7cbb05_mi_1);
}
在struct __block_impl結(jié)構(gòu)體內(nèi),有一個成員void *FuncPtr,
Block變量捕獲
為了保證block內(nèi)部能夠正常訪問外部的變量,block有個變量捕獲機制。
先來看一段代碼
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
a = 20;
block();
}
return 0;
}
執(zhí)行上面這段代碼,打印值是10,而不是20。之所以執(zhí)行block(),結(jié)果是10,而不是20,這個就使用了變量捕獲。
我們來看下C++底層代碼
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
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);
}
return 0;
}
在上面的代碼中,編譯時,在block內(nèi)部已經(jīng)捕獲到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;
}
};
在上面代碼中,根據(jù)傳遞過來的值,賦值給NSLog(@"%d",a);。當a = 20;時,僅僅是改變int a = 10;的值。而block內(nèi)部獲取不到a的值。

-
int a = 10;默認是auto修飾,是值傳遞;auto,自動變量,auto修飾的變量,內(nèi)存會自動消失。所以,block內(nèi)部不會捕獲會自動消失的內(nèi)存。 - 如果
int a = 10;使用static修飾,則傳遞的是地址,block內(nèi)部捕獲到變量的地址,如果在外部修改變量的值,則根據(jù)地址找到變量存儲的值。 - 全局變量并不會被捕獲到
block內(nèi)部。在block內(nèi)部會直接訪問全部變量。 -
self是一個局部變量,在block內(nèi)部,也會捕獲self。
Block類型
因為block是一個對象,所以block也是有類型的。block有三種類型,可以通過class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型。
-
NSGlobalBlock
存儲在數(shù)據(jù)區(qū)域,沒有訪問auto。 -
NSStackBlock
存儲在棧區(qū),會自動銷毀,訪問了auto。 -
NSMallocBlock
存儲在堆區(qū),需要程序員銷毀,NSStackBlock調(diào)用了copy(ARC環(huán)境下,block會自動調(diào)用copy,從棧上賦值到堆上,所以一般block類型是NSMallocBlock)。
ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復制到堆上,比如以下情況:
block作為函數(shù)返回值時;
將block賦值給__strong指針時;
block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時;
block作為GCD API的方法參數(shù)時。
MRC下block屬性的建議寫法
@property (copy,nonatomic) void (^block)(void);ARC下block屬性的建議寫法
@property (copy,nonatomic) void (^block)(void);
@property (strong,nonatomic) void (^block)(void);
對象類型的auto變量以及Block的內(nèi)存管理
類似于局部變量,有auto修飾的對象在block內(nèi)部,也會存在block類型。來看一段代碼
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
#import "Person.h"
@implementation Person
- (void)dealloc{
NSLog(@"delloc--Person");
}
@end
main.m
#import <Foundation/Foundation.h>
#import "Person.h"
typedef void(^HYBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
HYBlock myBlock;
{
Person *p = [[Person alloc] init];
p.age = 10;
myBlock = ^{
NSLog(@"%d",p.age);
};
myBlock();
}
}
return 0;
}
當block內(nèi)部訪問了對象類型的auto變量時
如果block是在棧上,將不會對auto變量產(chǎn)生強引用。當block被拷貝到堆上
??會調(diào)用block內(nèi)部的copy函數(shù);
??copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign;
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
??_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong、__weak、__unsafe unretained)做出相應的操作,形成強引用(retain)或者弱引用。
- 如果block從堆上移除
??會調(diào)用blokc內(nèi)部的dispose函數(shù)
??dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
??_Block_object_dispose函數(shù)會自動釋放引用的auto變量。
Block修飾符
我們知道不能再block內(nèi)部修改外部變量的值,我們來看下原因:
#import <Foundation/Foundation.h>
typedef void(^HBlock) (void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
HBlock block= ^{
NSLog(@"%d",a);
};
block();
}
return 0;
}
轉(zhuǎn)化為C++代碼
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int a = 10;
HBlock block= ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
在上面的代碼中,定義了int a = 10;。而輸出這個變量值是在下面這個函數(shù)中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_5b923f_mi_0,a);
}
因為變量a不是全局變量,只是局部變量,所以不能在另外一個函數(shù),修改變量值。
- 如果
int a = 10;使用static修飾,可以在block內(nèi)部修改變量的值。
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;傳遞的是變量的地址值,在block內(nèi)部,先找到變量的地址值,直接修改變量a的值,而不是直接修改變量值。
- 如果
int a = 10;是全局變量,則在當前文件的函數(shù)中,都可以修改變量值。 - 使用
static修飾變量,或者使用全局變量,則這個變量一直在內(nèi)存中。如果使用__block修改,也可以在block內(nèi)部修改變量值,并且,變量會自動釋放,不會一直存在內(nèi)存中。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
...
};
從上面的代碼可以看出,使用__block修飾變量,在__main_block_impl_0內(nèi)部,變量a為__Block_byref_a_0 *a; // by ref。而__Block_byref_a_0是一個對象(內(nèi)部有isa指針)。在這個結(jié)構(gòu)體內(nèi)部,有成員變量,存儲變量值。
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
當修改變量值時,利用__Block_byref_a_0指針先找到結(jié)構(gòu)體,通過變量名找到__forwarding,在通過__forwarding找到變量,來修改變量值。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
(a->__forwarding->a) = 20;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_2n_ksb7n0n131n2y7v9xcf411fm0000gn_T_main_b8c75c_mi_0,(a->__forwarding->a));
}
- 注意
??使用__block修飾int a,則在block內(nèi)部a成為對象。
??創(chuàng)建NSMutableArray *array = [NSMutableArray array];,在block內(nèi)部使用[array addObject:@123]是使用這個指針,而不是改變array的值。
block循環(huán)引用
循環(huán)引用是指兩個或以上對象互相強引用,導致所有對象無法釋放的現(xiàn)象。這是內(nèi)存泄露的一種情況。
#import <Foundation/Foundation.h>
typedef void(^HYBlock)(void);
@interface Person : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, copy) HYBlock block;
@end
#import "Person.h"
@implementation Person
- (void)dealloc{
NSLog(@"%s",__func__);
}
@end
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 20;
person.block = ^{
NSLog(@"%d", person.age);
};
person.block();
}
return 0;
}
在上面的代碼中,當執(zhí)行person.block();時,Person對象并沒有釋放,產(chǎn)生循環(huán)引用。
我們來看下,產(chǎn)生循環(huán)引用的原因,首先轉(zhuǎn)化為C++代碼
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__strong person;
...
};
在ARC環(huán)境下,HYBlock被拷貝到堆上,當內(nèi)部調(diào)用person時,則在函數(shù)__main_block_impl_0內(nèi)部,Person對象生成Person *__strong person;也即是強引用這個對象。Person對象強引用HYBlock,HYBlock又強引用Person對象,則HYBlock不釋放,Person對象也不會釋放。
解決循環(huán)引用
- 使用__weak,__unsafe_unretained解決;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
...
};
使用weak修飾對象,則在函數(shù)__main_block_impl_0內(nèi)部,不在強引用Person對象。__unsafe_unretained同理,也不在強引用Person對象。
- 使用__block解決(必須調(diào)用block);
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
__block Person *person = [[Person alloc] init];
person.age = 20;
person.block = ^{
NSLog(@"%d", person.age);
person = nil;
};
person.block();
}
return 0;
}