[iOS开发]block再学习
Posted Billy Miracle
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[iOS开发]block再学习相关的知识,希望对你有一定的参考价值。
一、认识block
(一)block定义
带有自动变量(局部变量)的匿名函数叫做block
。
(二)block分类
- 全局block——
__NSGlobalBlock__
- 堆block——
__NSMallocBlock__
- 栈block——
__NSStackBlock__
总结:
不使用外界变量的block是
__NSGlobalBlock__
类型
使用外界变量的block是__NSMallocBlock__
类型
在堆block拷贝前的block是__NSStackBlock__
类型
除此之外,还有三种系统级别的block类型
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
_NSConcreteWeakBlockVariable
二、block循环引用
(一)循环引用的产生
self.name = @"Billy";
self.block = ^
NSLog(@"%@", self.name);
;
同时,编译器给出警告:
⚠️Capturing 'self' strongly in this block is likely to lead to a retain cycle
循环引用的问题在于:
self
持有了block
block
持有了self
(self.name
)
这样就形成了self -> block -> self
的循环引用。循环引用时:A、B互相引用,引用计数不能为0,dealloc
不会被调用。
(二)解决循环引用
1. 强弱共舞
__weak typeof(self) weakSelf = self;
self.name = @"Billy";
self.block = ^
NSLog(@"%@", weakSelf.name);
;
使用 中介者模式 __weak typeof(self) weakSelf = self
将循环引用改为weakself -> self -> block -> weakself
。表面看上去还是一个“引用圈”,但是weakself -> self
这一层是弱引用——引用计数不处理,使用weak表
管理。所以此时在页面析构时self
就能正常的调用dealloc
了。
但并不是最终的解决方案,此时仍有可能存在着问题,比如如下代码:
__weak typeof(self) weakSelf = self;
self.name = @"Billy";
self.block = ^
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
NSLog(@"%@", weakSelf.name);
);
;
这种延时情况,如若调用block
之后立马返回上一页进行页面释放,3秒后weakself
指向的self
已经为nil
了,此时的打印就只能打印出null
。
于是就有了强弱共舞
:
__weak typeof(self) weakSelf = self;
self.name = @"Billy";
self.block = ^
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
NSLog(@"%@", strongSelf.name);
);
;
再加一层临时的强持有,此时的引用就变成了strongself -> weakself -> self -> block -> strongself
看上去又是一个循环引用,但实际上strongSelf
是个临时变量,当block
作用域结束后就会释放,从而打破循环引用进行释放(让释放延后了3秒)。
2. 其他中间者模式
既然有自动置空,那么也可以手动置空。
__block UIViewController *viewController = self;
self.name = @"Billy";
self.block = ^
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
NSLog(@"%@", viewController.name);
viewController = nil;
);
;
上述代码也是使用 中介者模式 打破循环应用的——使用viewController
作为中介者代替self
从而打破循环引用
此时的引用情况为viewController -> self -> block -> viewController
(viewController在用完之后手动置空),这里依然会存在问题:但是只要不调用block,仍然存在着循环应用。
解决循环引用还有一种方式——不引用
self.name = @"Felix";
self.block = ^(UIViewController *viewController)
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^
NSLog(@"%@", viewController.name);
viewController = nil;
);
上述代码使用当前viewController
作为参数传入block
时拷贝一份,就不会出现持有的情况,同时还能使用self
的内存空间,能够完美避免循环引用。
3. Q&A
Q:Masonry
中是否存在循环引用?
A:
Monsary
使用的block是当做参数传递的,即便block内部持有self
,设置布局的view
持有block
,但是block
不持有view
,当block
执行完后就释放了,self
的引用计数-1,所以block也不会持有self
,所以不会导致循环引用
Q:[UIView animateWithDuration: animations:]
中是否存在循环引用?
A:
UIView
动画是类方法,不被self
持有(即self
持有了view
,但view
没有实例化)所以不会循环引用
三、block底层
(一)block本质
1.
int main(int argc, const char * argv[])
// int a = 10;
void(^block)(void) = ^
printf("Billy");
// printf("Billy - %d",a);
;
block();
return 0;
转化成C++代码:
int main(int argc, const char * argv[])
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
从main
函数中可以看到block的赋值是__main_block_impl_0
类型,它是C++中的构造函数:
struct __main_block_impl_0
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
;
block的本质是个
__main_block_impl_0
的结构体对象
fp
传递了具体的block的实现__main_block_func_0
,然后保存在block结构体的impl
中,这就说明了block声明只是将block实现保存起来,具体的函数实现需要自行调用。
2. 当block为堆block时(外接传入变量)重新clang编译
int main(int argc, const char * argv[])
int a = 10;
void(^block)(void) = ^
// printf("Billy");
printf("Billy - %d",a);
;
block();
return 0;
此时的block构造函数中就会多出一个参数a
,并且在block结构体中也会多出一个属性a
。
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)
...
;
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
int a = __cself->a; // bound by copy
printf("Billy - %d",a);
...
int main(int argc, const char * argv[])
int a = 10;
void(*block)(void) = ((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;
接着看__main_block_func_0
的实现
__cself
是__main_block_impl_0
的指针,即block本身
int a = __cself->a
即int a = block->a
由于a
只是个属性,所以是堆block只是值拷贝(值相同,内存地址不同)
这也是为什么捕获的外界变量不能直接进行操作的原因,如a++
会报错
__block修饰外界变量
int main(int argc, const char * argv[])
__block int a = 10;
void(^block)(void) = ^
// printf("Billy");
printf("Billy - %d",a);
;
block();
return 0;
__block
修饰的属性在底层会生成响应的结构体,保存原始变量的指针,并传递一个指针地址给block,因此是指针拷贝。
(二)block的copy
我们打断点调试:
可以看到objc_retainBlock
,继续step into:
可以看到调用block的copy函数:_Block_copy
。
1.
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
// 判断flags标识位
if (aBlock->flags & BLOCK_NEEDS_FREE)
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
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 而不是 +1 ?因为flags的第一号位置已经存储着释放标记。
2.
else if (aBlock->flags & BLOCK_IS_GLOBAL)
return aBlock;
是否是全局block
——是的话直接返回block
3.
else
// Its a stack block. Make a copy.
size_t size = Block_size(aBlock);
struct Block_layout *result = (struct Block_layout *)malloc(size);
// 开辟堆空间
if (!result) return NULL;
memmove(result, aBlock, size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
result->invoke = aBlock->invoke;
#if __has_feature(ptrauth_signed_block_descriptors)
if (aBlock->flags & BLOCK_SMALL_DESCRIPTOR)
uintptr_t oldDesc = ptrauth_blend_discriminator(
&aBlock->descriptor,
_Block_descriptor_ptrauth_discriminator);
uintptr_t newDesc = ptrauth_blend_discriminator(
&result->descriptor,
_Block_descriptor_ptrauth_discriminator);
result->descriptor =
ptrauth_auth_and_resign(aBlock->descriptor,
ptrauth_key_asda, oldDesc,
ptrauth_key_asda, newDesc);
#endif
#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;
- 先通过
malloc
在堆区开辟一片空间 - 再通过
memmove
将数据从栈区拷贝到堆区 invoke
、flags
同时进行修改- block的
isa
标记成_NSConcreteMallocBlock
(三)__block的深入探究
1. 第一层拷贝(block)
block中的第一层拷贝其实就是上面的_Block_copy
,将block从栈拷贝到堆。
2. 第二层拷贝(捕获变量的内存空间)
在函数声明时会传__main_block_desc_0_DATA
结构体,在里面又会去调用__main_block_copy_0
函数,__main_block_copy_0
里面会调用_Block_object_assign
——这就是第二层拷贝的调用入口。
//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
//
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_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^ object; copy];
********/
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^ x; copy];
********/
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^ object; copy];
********/
*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:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^ object; copy];
********/
*dest = object;
break;
default:
break;
根据flags & BLOCK_ALL_COPY_DISPOSE_FLAGS
进到不同分支来处理捕获到的变量
枚举值 | 数值 | 含义 |
---|---|---|
BLOCK_FIELD_IS_OBJECT | 3 | 对象 |
BLOCK_FIELD_IS_BLOCK | 7 | block变量 |
BLOCK_FIELD_IS_BYREF | 8 | __block修饰的结构体 |
BLOCK_FIELD_IS_WEAK | 16 | __weak修饰的变量 |
BLOCK_BYREF_CALLER | 128 | 处理block_byref内部对象内存的时候会加的一个额外的标记,配合上面的枚举一起使用 |
此时捕获到的变量是被__block
修饰的BLOCK_FIELD_IS_BYREF
类型,就会调用*dest = _Block_byref_copy(object);
。
static struct Block_byref *_Block_byref_copy(const void *arg)
// 临时变量的保存
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0)
// src points to stack
// 用原目标的大小在堆区生成一个Block_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
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// 原来的区域和新的区域都指向同一个对象,使得block具备了修改能力
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
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
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
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);
copy3->layout = src3->layout;
// 第三层拷贝
(*src2->byref_keep)(copy, src);
else
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE)
latching_incr_int(&src->forwarding->flags);
return src->forwarding;
- 用原目标
name
的大小在堆区生成一个Block_byref
copy->forwarding = copy; & src->forwarding = copy;
——原来的区域和新的区域都指向同一个对象,使得block具备了修改能力(*src2->byref_keep)(copy, src)
开始第三层拷贝
3. 第三层拷贝(拷贝对象)
(*src2->byref_keep)(copy, src)
点进去会来到Block_byref
结构来,而byref_keep
是Block_byref
的第5个属性
struct Block_byref
void * __ptrauth_objc_isa_pointer isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
;
struct Block_byref_2
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
BlockByrefKeepFunction byref_keep;
BlockByrefDestroyFunction byref_destroy;
;
struct Block_byref_3
// requires BLOCK_BYREF_LAYOUT_EXTENDED
const char *layout;
;
第5位就等于byref_keep
,所以在第二层拷贝时会调用__Block_byref_id_object_copy_131
。
static void __Block_byref_id_object_copy_131(void *dst,以上是关于[iOS开发]block再学习的主要内容,如果未能解决你的问题,请参考以下文章