前言
很多人在面试的时候都会被问到Block,那么Block分为哪几种类型呢? 其实Block共有6种类型,其中三种常用级别,分别是:_NSConcreteGlobalBlock
_NSConcreteStackBlock
_NSConcreteMallocBlock
,三种系统级别 ,分别是_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
_NSConcreteWeakBlockVariable
,本次只介绍3种常用类型。
Block常见的三种类型
- 全局Block(
__NSGlobalBlock__
)
- 堆Block(
__NSMallocBlock__
)
- 栈Block(
__NSStackBlock__
)
Block的本质是什么?
__main_block_impl_0
的构造函数,我们在往上看可以发现,它其实就是一个结构体对象。而这个构造函数传了两个值__main_block_func_0
和__main_block_desc_0_DATA
。__main_block_func_0
:就是Block所执行的内容,在C++底层则变成了一个函数。__main_block_desc_0_DATA
:有两个成员:reserved
和Block_size
。
Block是如何捕获外界变量的?
我们创建一个局部变量a=10,然后看C++代码,可以发现在
__main_block_impl_0
结构体中创建了一个属性int a,并且在函数实现中创建了一个临时变量a,将结构体中的属性赋值给他。由于这次拷贝是值拷贝,所以在函数里面不能对当前的属性进行修改。为了能够改变a的值,要加上__block
,如下图:
__block
修饰的临时变量在C++中变成了一个结构体__Block_byref_a_0
。并且在调用函数的时候,传的是a的指针,并且在函数的实现中,__Block_byref_a_0 *a = __cself->a
则是进行一次指针拷贝,所以用__block
修饰的变量可以在Block内部进行修改。
解决Block循环引用的几种方式
//循环引用 self.name = @"1234"; self.block = ^{ NSLog(@"%@",self.name); }; self.block();
复制代码
很多人都知道Block会引起循环引用,如同上面这段代码,当self持有block,block持有self,就产生了循环引用。接下来就介绍3种解决循环引用的方式
1、__block
:我们只需要在self.name的下面创建一个中介者vc(__block ViewController *vc = self
),然后将block里面的self.name替换成vc.name,用完之后将vc置为nil即可。(俗称中介者模式)
// 循环引用
self.name = @"1234";
__block ViewController *vc = self;
self.block = ^{
NSLog(@"%@",vc.name);
vc = nil;
};
self.block();
复制代码
2、传参:我们只需要将当前的self当做参数传入到block里面,block内部会创建一个临时变量vc,此时我们就打破了互相持有,就会解决循环引用的问题了
// 循环引用
self.name = @"1234";
self.block = ^(ViewController *vc){
NSLog(@"%@",vc.name);
};
self.block(self);
复制代码
3、weak and strong:这种方式也是最多人用的,既创建weak,但是有一个问题,如果添加一个延迟执行,weakSelf就会提前释放,导致访问不到外界变量,所以我们又需要在blcok里面strong一下
// 循环引用
self.name = @"1234"; __weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(weakSelf) strongSelf = weakSelf; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@",strongSelf.name); }); }; self.block(); 复制代码
Block打破循环引的原理
在汇编里面,Block会掉用一个叫_Block_object_assign
的api。而解决循环引用的关键之处也在下面的代码中。
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: _Block_retain_object(object); *dest = object; break; case BLOCK_FIELD_IS_BLOCK: *dest = _Block_copy(object); break; case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK: case BLOCK_FIELD_IS_BYREF: *dest = _Block_byref_copy(object); break; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK: *dest = object; break; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK: *dest = object; break; default: break; } } 复制代码 以__weak为例。我们为了解决循环引用,通常都会写上这样一句代码:__weak typeof(self) weakSelf = self;,将self加入到弱引用技术表,但是这并不足以解释打破循环引用的原因。而是weakSelf和self并不是同一个点,他们指向的空间都是同一个。而最关键的是这一行代码const void **dest = (const void **)destArg;,他copy的是weakSelf的指针地址,而不是self,所以weakSelf被释放的时候,不会影响到self。 Block是如何从栈到堆? void *_Block_copy(const void *arg) { struct Block_layout *aBlock; if (!arg) return NULL; // The following would be better done as a switch statement aBlock = (struct Block_layout *)arg; if (aBlock->flags & BLOCK_NEEDS_FREE) { // latches on high latching_incr_int(&aBlock->flags); return aBlock; } else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } else { // Its a stack block. Make a copy. struct Block_layout *result = (struct Block_layout *)malloc(aBlock->descriptor->size); if (!result) return NULL; 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 result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1 _Block_call_copy_helper(result, aBlock); // Set isa last so memory analysis tools see a fully-initialized object. result->isa = _NSConcreteMallocBlock; return result; } }
复制代码
这里就是当前Block从栈->堆的过程。 1、aBlock->flags & BLOCK_NEEDS_FREE
会判断当前Block的引用计数器,因为block的引用计数器是不受runtime下层处理的,所以它由自己来进行管理。而且这里的引用计数器是+2,而不是+1,因为+1会对别的属性有影响,所以这里是+2(如下)。
static int32_t latching_incr_int(volatile int32_t *where) { while (1) { int32_t old_value = *where; if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) { return BLOCK_REFCOUNT_MASK; } if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) { return old_value+2; } } }
复制代码
2、aBlock->flags & BLOCK_IS_GLOBAL
判断当前的Block是否为全局变量,是的话就直接返回
3、else就是将block复制到栈中,首先会创建一个新的结构体result,然后将旧的Block全部拷贝到新的Block中,然后isa就被标记为_NSConcreteMallocBlock
。
总结
1、解决block循环引用的思路就是中介者模式。
2、Block的本质就是结构体
3、当Block捕获到外界变量时,ARC下就会从全局block变成堆block,MRC下依然还是栈Block
作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:651612063 进群密码111,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!
进群文件可领取2020年iOS大厂面试题
[点击进群 密码:111](https://jq.qq.com/?_wv=1027&k=sUBNztG4)
[原文](https://juejin.im/post/5eba9de86fb9a04351050ea7)