iOS之Block
一、Block的三种类型
1.全局Block(NSGlobalBlock) 2.栈Block(NSMallocBlock) 3.堆Block(NSStackBlock)
区别在于:如果没有引用局部变量,或者只引用了静态变量和全局变量,则为全局Block,如果内部有使用局部变量,如果有被强指针引用过,就是堆Block,如果没有则为栈Block。
二、Block的本质
我们通过对一段Block代码的clang运行得到关于Block的结构体,我们可以得知Block的一些本质:
1.Block是OC对象,因为他有isa指针 2.Block本身是在栈上的 3.Block具备捕获对象的能力,因为他可以捕获到obj对象
4.Block的本质是一个 Block_layout 类型的结构体
5.copy和dispose函数是用来对block内部的对象进行内存管理的,block拷⻉到堆上会调用copy函 数,在block从堆上释放的时候会调用dispose函数
三、Block的循环引用
Block循环引用最容易出现问题的地方就是self和block之间的循环引用导致双方都无法释放的情况。下面这个就是最简单的一种情况:
- (void)test { self.block = ^{ self.name = @"LG"; }; }
解决循环引用的办法:
3.1 使用_weak + _strong 协作
3.1.1 仅通过_weak解决循环引用(是可行的,但是在遇到某些情况会有问题)
- (void)test { __weak typeof(self) weakSelf = self; self.block = ^{ weakSelf.name = @"LG"; }; }
3.1.2 通过_weak + _strong 协作解决
如果仅用_weak可能出现下面这种情况:如果controller还没有等到执行dispatch_after就被提前pop,等到3s dispatch 运行的时候name已经为nil了,这显然是不合理的。这种情况下我们就需要使用_weak + _strong 协作解决
-(void)test { self.name = @"lg"; __weak typeof(self) weakSelf = self; self.block = ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@",weakSelf.name); }); }; self.block(); }
//解决方式: -(void)method1 { self.name = @"lg"; __weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; NSLog(@"%p",&strongSelf); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@",strongSelf.name); }); }; self.block(); }
此时block对象里使用的是strongself这个临时变量,他的生命周期仅在block内部,在block内部,self的引用计数+1,出了作用域,引用计数又-1,这样既没有引起循环引用,又延长了self的生命周期,也可以取到正常的值。
3.2 __block
-(void)method2 { self.name = @"lg"; __block LGViewController *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(); }
__block会在下面讲,使用这种方式也可以解决循环引用,但是block执行完一次,下一次执行之前要重新复制self,不然会出问题,block内部将局部变量设为nil,否则会循环引用,不推荐使用。
3.1.3 参数传递
-(void)method3 { self.name = @"lg"; self.block = ^(LGViewController *vc){ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@",vc.name); }); }; self.block(self); }
通过使用block的参数进行传递,同样可以解决循环引用的问题。
3.2 staic修饰self的情况,因为static不会自动释放,因此需要手动的把_staticSelf = nil
static MyViewController *_staticSelf; -(void)test2 { __weak typeof(self) weakSelf = self; // self是MyViewController的实例对象 _staticSelf = weakSelf; } // 产生循环引用
3.3 weakself->block2->strongself->weakself
- (void)test3 { __weak typeof(self) weakSelf = self; self.block1 = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; NSLog(@"%p", &strongSelf); weakSelf.block2 = ^{ NSLog(@"%@", strongSelf); }; weakSelf.block2(); }; self.block1(); } // 产生循环引用
这里可以将block捕获的强引用改为弱引用就正常了。
四、Block对象中的引用计数(通过下面的代码深入了解)
- (void)blockDemo1{ //1 3 4 NSObject *objc = [NSObject new]; NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); //1 //栈block的时候会自动引用计数+1 栈变成堆的时候会有一次copy 使得引用计数又+1 void(^block1)(void) = ^{ NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); }; block1(); //3 //栈block自动+1 void(^__weak block2)(void) = ^{ NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); }; block2(); //4 //5 3从2来(2是4) 引用计数+1 void(^block3)(void) = [block2 copy]; block3(); //5 //4 4从1来(1是3) 引用计数+1 void(^block4)(void) = [block1 copy]; block4(); //4 }
五、捕获变量
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);//11 21 31 }; NSLog(@"%@",block); block(); return NSApplicationMain(argc, argv); }
1.在block内部可以修改静态变量和全局变量的值,不可以改变局部变量的值(因为不能修改的是栈中局部的值,因为没有forwarding指针链接) 2.1 全局变量不会捕获可以直接使用 2.2 静态变量捕获的是地址 2.3 局部变量捕获的是值 3.要想修改局部变量的值,需要在局部变量之前加上__block
六、__block
6.1 __block的底层结构(重点关注forwarding指针)
struct __Block_byref_obj_0 { void *__isa; __Block_byref_obj_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); NSObject *obj; };
用__block修饰的变量在编译过后会变成 __Block_byref__XXX 类型的结构体,在结构体内部有一 个 __forwarding 的结构体指针,指向结构体本身,这个 __forwarding 就是用来操作Block内部的代码的。
block创建的时候是在栈上的,在将栈block拷⻉到堆上的时候,同时也会将block中捕获的对象拷⻉到堆上,然后就会将栈上的__block修饰对象的__forwarding指针指向堆上的拷⻉之后的对象(原来forward ing指针指向的是自己)。 这样的话我们在block内部修改的时候虽然是修改堆上的对象的值,但是因为栈上的对象的 __forwarding指针将堆和栈的对象链接起来。因此就可以达到修改的目的。
__block与Block相似,如果在栈区就会深拷贝一份,forward指针都指向堆区,如果是在堆区,则引用计数+1
6.2 __block不可以修饰静态变量和全局变量
6.3 __block的copy是通过_Block_byref_copy来进行拷贝的
// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3, // 被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable, // 原来 byref 的 forwarding 也会指向堆上的 byref; // 2. 如果 byref 已经在堆上,就只增加一个引用计数。 // 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref static struct Block_byref *_Block_byref_copy(const void *arg) { // arg 强转为 Block_byref * 类型 struct Block_byref *src = (struct Block_byref *)arg; // 引用计数等于 0 if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) { // src points to stack // 为新的 byref 在堆中分配内存 struct Block_byref *copy = (struct Block_byref *)malloc(src->size); copy->isa = NULL; // byref value 4 is logical refcount of 2: one for caller, one for stack // 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。 // 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack // one for caller 很好理解,那 one for stack 是为什么呢? // 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4; // 堆上 byref 的 forwarding 指向自己 copy->forwarding = copy; // patch heap copy to point to itself // 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref src->forwarding = copy; // patch stack to point to heap copy // 拷贝 size copy->size = src->size; // 如果 src 有 copy/dispose helper if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { // Trust copy helper to copy everything of interest // If more than one field shows up in a byref block this is wrong XXX // 取得 src 和 copy 的 Block_byref_2 struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1); struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1); // copy 的 copy/dispose helper 也与 src 保持一致 // 因为是函数指针,估计也不是在栈上,所以不用担心被销毁 copy2->byref_keep = src2->byref_keep; copy2->byref_destroy = src2->byref_destroy; // 如果 src 有扩展布局,也拷贝扩展布局 if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) { struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1); struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1); // 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上 copy3->layout = src3->layout; } // 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数 (*src2->byref_keep)(copy, src); } else { // Bitwise copy. // This copy includes Block_byref_3, if any. // 如果 src 没有 copy/dispose helper // 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3 memmove(copy+1, src+1, src->size - sizeof(*src)); } } // already copied to heap // src 已经在堆上,就只将引用计数加 1 else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) { latching_incr_int(&src->forwarding->flags); } return src->forwarding; }
七、block的copy
7.1调用时机
1.有强指针第一次指向的时候,会调用一次copy 2.[block copy]
1.堆[block copy],只进行引用计数+1,相当于浅拷贝 2.如果是全局block,[block copy]直接返回,相当于不拷贝 3.如果是栈block,[block copy]是重新开辟新的内存并创建,并且isa指向_NSConcreteMallocBlock,设置类型为堆block,然后返回
在ARC下, 对于block使用copy与strong其实都一样,
因为block的retain就是用copy来实现 的,
所以在ARC下 block使用 copy 和 strong 都可以
八、通过两道题来熟悉一下不同类型的block
//会崩溃; - (void)blockDemo1{ int a = 1; //栈block void(^ __weak weakBlock)(void) = ^{ NSLog(@"-----%d", a); }; struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock; void(^strongBlock)(void) = weakBlock;//【weakBlock copy】就不会崩溃了 //将栈block拷贝到堆上 是深拷贝 //weakBlock、blc、strongBlock指向的是同一块内存空间 blc->invoke = nil; //这里将函数指针置为nil,导致其无法实现 strongBlock();//这里的调用就崩溃了 } //正常运行 - (void)blockDemo2{ int a = 1; //栈block void(^__weak block1)(void) = nil; { //栈block void(^__weak block2)(void) = ^{ NSLog(@"%d",a);//打印5 }; block1 = block2;//【block2 copy】就会崩溃,因为此时的block2在堆上,他的释放和作用域有关,就会崩溃,相当于此时的block指向了nil导致崩溃 }//block2在作用域之外也没有被释放 //栈上的变量出了作用域之后不会被释放,而是在函数的作用域释放的,也就是说栈上的释放仅和函数的生命周期有关(blockdemo2) block1(); }