常见的几种block:堆block,栈block,全局block
区分原则:
1.block没有使用外部的变量,或者只使用静态变量 或者全局变量----全局block
2.block使用外部的变量,不是静态变量或者全局变量。再看赋值给强引用--堆blcok,赋值给弱引用--栈block
3.block在创建的时候,其内存是分配到栈上的,分配到栈上内存是系统控制的,可能随时被释放掉,所以需要用copy拷贝到堆上面,用strong也是一样的
下面是打印后是一个全局的block。
void (^block)(void) = ^{
};
block();
NSLog(@"%@",block);
2022-06-07 21:13:17.617622+0800 Block的类型[45485:579489] <__NSGlobalBlock__: 0x104a240d8>
下面是打印后是一个堆block。
int a = 1;
void (^block)(void) = ^{
NSLog(@"%d",a);
};
block();
NSLog(@"%@",block);
2022-06-07 21:16:47.024473+0800 Block的类型[45620:584134] <__NSMallocBlock__: 0x600000e8c480>
下面是打印后是一个栈block。
int a = 1;
void (^__weak block)(void) = ^{
NSLog(@"%d",a);
};
block();
NSLog(@"%@",block);
2022-06-07 21:18:16.848433+0800 Block的类型[45687:586771] <__NSStackBlock__: 0x16bd77e48>
下面打印是其计数为1,3,4,为什么是这个顺序呢,我们带着这个问题去探究下?
- (void)blockDemo1{
NSObject *objc = [NSObject new];
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
//下面是堆block,其实在栈block上copy来的,也就是栈block +1,copy堆上+1
void(^block1)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block1();
//下面是一个栈block,进行捕获+1
void(^__weak block2)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
block2();
void(^block3)(void) = [block1 copy];//因为已经在堆上了copy就不会变
block3();
void(^block4)(void) = [block2 copy];//在栈上copy会加1
block4();
}
2022-06-07 22:06:28.254857+0800 Block的类型[46306:621069] ---1
2022-06-07 22:06:28.254907+0800 Block的类型[46306:621069] ---3
2022-06-07 22:06:28.254942+0800 Block的类型[46306:621069] ---4
2022-06-07 23:56:08.430500+0800 Block的类型[48642:710535] ---4
2022-06-07 23:56:08.430536+0800 Block的类型[48642:710535] ---5
首先我们在代码中添加如下block,然后用clang命令生成.cpp文件,查看里面的源码。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
NSObject *objc = [NSObject new];
void (^block)(void) = ^ {
NSLog(@"%@",objc);
};
return NSApplicationMain(argc, argv);
}
int main(int argc, const char * argv[]) {
static int a = 10;
__attribute__((__blocks__(byref))) __Block_byref_objc_0 objc = {(void*)0,(__Block_byref_objc_0 *)&objc, 33554432, sizeof(__Block_byref_objc_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_0,(objc.__forwarding->objc));
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a, (__Block_byref_objc_0 *)&objc, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_2,block);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return NSApplicationMain(argc, argv);
}
其实block就是一个结构体函数,从impl.isa = &_NSConcreteStackBlock这里看出block在创建的时候是放在栈里面的。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *a;
__Block_byref_objc_0 *objc; // 我们再程序里定义的变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, __Block_byref_objc_0 *_objc, int flags=0) : a(_a), objc(_objc->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我们在代码里定义的是一个堆block,那上面cpp里看出其为栈block,那是在什么时候进行转换的呢,
int main(int argc, const char * argv[]) {
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
}
NSObject *objc = [NSObject new];
void (^block)(void) = ^ {//堆block
NSLog(@"%@",objc);
};
return NSApplicationMain(argc, argv);
}
打断点调试其会进入汇编指令,其有个_Block_copy
下面是对block_copy源码的分析,这里分析也就是block的原理实现了。
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 如果 arg 为 NULL,直接返回 NULL
if (!arg) return NULL;
// The following would be better done as a switch statement
// 强转为 Block_layout 类型
aBlock = (struct Block_layout *)arg;
const char *signature = _Block_descriptor_3(aBlock)->signature;
// 如果现在已经在堆上
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
// 就只将引用计数加 1
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
// block 现在在栈上,现在需要将其拷贝到堆上
// 在堆上重新开辟一块和 aBlock 相同大小的内存
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
// 开辟失败,返回 NULL
if (!result) return NULL;
// 将 aBlock 内存上的数据全部复制新开辟的 result 上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
// 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// copy 方法中会调用做拷贝成员变量的工作
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
// isa 指向 _NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
当我们被__weak去修饰一个block的时候,也就是栈block的时候,系统不会修饰进行一个copy的操作。不会调用retain_copy
copy拷贝从栈拷贝到堆是浅拷贝
我们看其调用的是copy方法,然后调用的是_Block_object_assign ,然后是通过指针指向, *dest = object也就是一个浅拷贝。
// 调用 block 的 copy helper 方法,即 Block_descriptor_2 中的 copy 方法
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
// 取得 block 中的 Block_descriptor_2
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
// 如果没有 Block_descriptor_2,就直接返回
if (!desc) return;
// 调用 desc 中的 copy 方法,copy 方法中会调用 _Block_object_assign 函数
(*desc->copy)(result, aBlock); // do fixup
}
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
// 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数,
// 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
_Block_retain_object(object);
// 使 dest 指向的目标指针指向 object
*dest = object;
break;
循环引用
下面这个会发生循环引用,这个是因为block被self所持有,当block释放的时候,其self也要释放,self释放的话其里的block也要被释放掉。这里可以用__weak来解决循环引用。
self.block = ^{
self.name = @"hpw";
};
self.block = ^{//这里还会循环引用是因为这里的block还是捕获的self
_name= @"hpw";
};
通过下面的__weak可以解决循环引用问题,
__weak typeof(self) weakSelf = self;
self.block = ^{
weakSelf.name = @"hpw";
};
下面通过这个weakSelf进行修饰后,发现打印weakSelf.name竟然为null,这是因为被释放了,我们需要加个 __strong typeof(weakSelf) strongSelf = weakSelf,起到一个强弱共舞的效果,来解决这个问题。
-(void)method1 {
self.name = @"hpw";
__weak typeof(self) weakSelf = self;
self.block = ^{
// __strong typeof(weakSelf) strongSelf = weakSelf;这个strongSelf是在block释放完后再释放
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf.name);
//NSLog(@"%@",strongSelf.name);
});
};
self.block();
}
2022-06-08 21:33:45.338769+0800 Block的循环引用[4362:1496699] -[HPWViewController dealloc]
2022-06-08 21:33:47.097669+0800 Block的循环引用[4362:1496699] (null)
通过另外一种方式也可以解决循环引用,通过__block定义 __block HPWViewController *vc = self,然后name是vc里的一个属性,通过 NSLog(@"%@",vc.name)这种方式输出。最后需要设置vc=nil,这个是因为vc本身被捕获了,然后需要置nil。
self.name = @"hpw";
__block HPWViewController *vc = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.block();
2022-06-08 21:46:42.824182+0800 Block的循环引用[5321:1512113] hpw
2022-06-08 21:46:42.824486+0800 Block的循环引用[5321:1512113] -[HPWViewController dealloc]
通过变量传值也可以避免循环引用,
typedef void(^LGBlock1)(HPWViewController *);
@property (nonatomic, copy) LGBlock1 block1;
self.name = @"hpw";
self.block1 = ^(HPWViewController *vc){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.block1(self);
2022-06-08 22:05:54.408852+0800 Block的循环引用[7192:1542507] lg
2022-06-08 22:05:54.409131+0800 Block的循环引用[7192:1542507] -[HPWViewController dealloc]
题型一
下面代码我们运行后,发现dealloc方法没有被调用,说明这个会发生循环引用。 这个原因是因为,static是全局的在程序的生命周期里都会用到,程序停止运行了才会释放。_staticSelf会一直指向self,使得其引用计数加1,所以内存不会释放。
static HPWViewController *_staticSelf;
-(void)test1 {
__weak typeof(self) weakSelf = self;
_staticSelf = weakSelf;
}
题型二
下面会产生内存泄漏
strongSelf在跳出block标注1会进行销毁,引用计数会减一。block2里捕获的strongSelf会持有对象,增加内存的引用计数。
- (void)test2 {
__weak typeof(self) weakSelf = self;
self.block1 = ^{//block标注1
__strong typeof(weakSelf) strongSelf = weakSelf;//strongSelf出了这个作用域会被销毁
weakSelf.block2 = ^{
NSLog(@"%@", strongSelf);
};
weakSelf.block2();
};
}
Block为什么要用 block()进行调用
下面我们思考为什么在写block时候,要写一句 block(),这个是因为void (^__weak block)(void) 这个调用的源码只是对block的一个保存,通过block()调用下才会被执行。
int main(int argc, const char * argv[]) {
int a = 10;
void (^__weak block)(void) = ^{
NSLog(@"%d",a);
};
block();
return NSApplicationMain(argc, argv);
}
下面这个例子我们知道,全局变量和静态变量可以直接在block里进行修改。下面打印后为11,21,31,这个之所以可以修改是因为其内部压根都没有捕获b,c变量。在block内部可以修改全局变量和静态变量的值,但是不允许修改局部变量的
值。block内部修改局部变量的值需要⽤__block修饰。
int b = 20;
static int c = 30;
int main(int argc, const char * argv[]) {
static int a = 10;
__block NSObject *objc = [NSObject new];
NSLog(@"%@",objc);
void(^__weak block)(void) = ^{
a ++;
b ++;
c ++;
NSLog(@"%d %d %d %@",a,b,c,objc);
};
NSLog(@"%@",block);
block();
return NSApplicationMain(argc, argv);
}
下面是使用__block来修饰局部变量,使得其能被修改。为什么添加__block就可以呢,我们探究下。
__block int a = 10;
void (^block)(void) = ^{
a = 3;
NSLog(@"%d",a);
};
block();
2022-06-09 15:29:03.752914+0800 Block[31123:328829] 3
下面是clang命令后函数输出:
int main(int argc, const char * argv[]) {
static int a = 10;
__attribute__((__blocks__(byref))) __Block_byref_objc_0 objc = {(void*)0,(__Block_byref_objc_0 *)&objc, 33554432, sizeof(__Block_byref_objc_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"))};
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_0,(objc.__forwarding->objc));
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a, (__Block_byref_objc_0 *)&objc, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_cee0a9_mi_2,block);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return NSApplicationMain(argc, argv);
}
int b = 20;
static int c = 30;
struct __Block_byref_objc_0 {
void *__isa;
__Block_byref_objc_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSObject *objc;
};
__block的底层原理
⽤__block修饰的变量在编译过后会变成 __Block_byref__XXX 类型的结构体,在结构体内部有⼀
个 __forwarding 的结构体指针,指向结构体本身。
block创建的时候是在栈上的,在将栈block拷⻉到堆上的时候,同时也会将block中捕获的对象拷
⻉到堆上,然后就会将栈上的__block修饰对象的__forwarding指针指向堆上的拷⻉之后的对象。
这样我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的
__forwarding指针将堆和栈的对象链接起来。因此就可以达到修改的⽬的。
__block不能修饰全局变量和静态变量
Block的本质
block的本质就是生成了Block_layout的结构体,同时里面有个isa指针,还有一个Block_descriptor_1。
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
// libffi ->
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 如果 arg 为 NULL,直接返回 NULL
if (!arg) return NULL;
// The following would be better done as a switch statement
// 强转为 Block_layout 类型
aBlock = (struct Block_layout *)arg;
const char *signature = _Block_descriptor_3(aBlock)->signature;
// 如果现在已经在堆上
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
// 就只将引用计数加 1
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
// block 现在在栈上,现在需要将其拷贝到堆上
// 在堆上重新开辟一块和 aBlock 相同大小的内存
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
// 开辟失败,返回 NULL
if (!result) return NULL;
// 将 aBlock 内存上的数据全部复制新开辟的 result 上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#endif
// reset refcount
// 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
// 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// copy 方法中会调用做拷贝成员变量的工作
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
// isa 指向 _NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
在Block_descriptor_1里存的是copy、dispose、签名等信息。
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
下面的是signature的标识"V8@?0"
下面是copy和dispose的调用,也只有block捕获到外部变量才会使用到。
Block_descriptor_2的获取其实就是Block_descriptor_1的地址平移。
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
// Block_descriptor_2 中存的是 copy/dispose 方法,如果没有指定有 copy / dispose 方法,则返回 NULL
if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
// 先取得 Block_descriptor_1 的地址
uint8_t *desc = (uint8_t *)aBlock->descriptor;
// 偏移 Block_descriptor_1 的大小,就是 Block_descriptor_2 的起始地址
desc += sizeof(struct Block_descriptor_1);
return (struct Block_descriptor_2 *)desc;
}
下面是block,其中flag类似与之前我们mask掩码,与上啥就会得到对应的数据。
题型1
下面在调用的时候会奔溃,因为strongBlock和weakBlock指向的是同一块内存空间。如果在 void(^strongBlock)(void) = weakBlock改成 void(^strongBlock)(void) = [weakBlock copy]就不会奔溃,这个是因为copy把栈上拷贝到堆上,是一个深拷贝(当我们用copy修饰block的时候是把栈上拷贝到堆上,浅拷贝),那么这样就不会影响栈上block了。
- (void)blockDemo1{
int a = 1;
void(^ __weak weakBlock)(void) = ^{//这里是一个栈block
NSLog(@"-----%d", a);
};
struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;
void(^strongBlock)(void) = weakBlock;
blc->invoke = nil;//block实现的指针
strongBlock();
}
题型2
下面这个不会奔溃
- (void)blockDemo2{
int a = 1;
void(^__weak block1)(void) = nil;
{//blcok1
void(^__weak block2)(void) = ^{//这是个栈block ,当跳出这个外层的block1时候不会被释放。
NSLog(@"%d",a);
};
block1 = block2;
}
block1();
}
题型3
下面[NSObject new]是在堆上面的,因为是手动new创建的。下面打印objc会为null,这个是因为NSObject是在堆上,然后__weak block2这个是在栈上,所以出了作用域被释放了。
捕获对象就是生产一个指针指向obj。捕获对象是个浅拷贝。