欢迎阅读iOS探索系列(按序阅读食用效果更加)
- iOS探索 alloc流程
- iOS探索 内存对齐&malloc源码
- iOS探索 isa初始化&指向分析
- iOS探索 类的结构分析
- iOS探索 cache_t分析
- iOS探索 方法的本质和方法查找流程
- iOS探索 动态方法解析和消息转发机制
- iOS探索 浅尝辄止dyld加载流程
- iOS探索 类的加载过程
- iOS探索 分类、类拓展的加载过程
- iOS探索 isa面试题分析
- iOS探索 runtime面试题分析
- iOS探索 KVC原理及自定义
- iOS探索 KVO原理及自定义
- iOS探索 多线程原理
- iOS探索 多线程之GCD应用
- iOS探索 多线程之GCD底层分析
- iOS探索 多线程之NSOperation
- iOS探索 多线程面试题分析
- iOS探索 细数iOS中的那些锁
- iOS探索 全方位解读Block
今天遇到一道方法交换面试题跟大家分享下...我还是太年轻了
相信大家对本文的主角block
都有一定的了解,日常开发中也经常能看到它的身影。本文会从block概念
、blcok循环引用
、block底层
三方面进行讲解
带有自动变量(局部变量)的匿名函数叫做block
,又叫做匿名函数
、代码块
在不同语言中的叫法不同
程序语言 | Block的名称 |
---|---|
C | Block |
Smalltalk | Block |
Ruby | Block |
Python | Lambda |
C++ | Lambda |
JS | Anonymous function |
__NSGlobalBlock__
void (^block)(void) = ^{ NSLog(@"111"); }; NSLog(@"%@", block); --------------------输出结果:------------------- <__NSGlobalBlock__: 0x10a870050> --------------------输出结果:------------------- 复制代码
__NSMallocBlock__
int a = 0; void (^block)(void) = ^{ NSLog(@"%d", a); }; NSLog(@"%@", block); --------------------输出结果:------------------- <__NSMallocBlock__: 0x600002dca2b0> --------------------输出结果:------------------- 复制代码
__NSStackBlock__
int a = 0; NSLog(@"%@", ^{ NSLog(@"%d", a); }); --------------------输出结果:------------------- <__NSStackBlock__: 0x7ffeec41e1b8> --------------------输出结果:------------------- 复制代码
总结:
__NSGlobalBlock__
类型__NSMallocBlock__
类型__NSStackBlock__
类型除此之外,还有三种系统级别的block类型(能在libclosure源码中看到)
_NSConcreteAutoBlock
_NSConcreteFinalizingBlock
_NSConcreteWeakBlockVariable
循环引用
,与此同时编译器也发出了警告
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)
正常释放时:A发送dealloc信号让Bdealloc
循环引用时:A、B互相引用,引用计数不能为0,dealloc不会被调用
接下来就介绍一下解决循环引用的几种办法
__weak typeof(self) weakSelf = self; self.name = @"Felix"; 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 = @"Felix"; 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 = @"Felix"; 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秒)
既然有“自动置空”,那么也可以“手动置空”
__block ViewController *vc = self; self.name = @"Felix"; self.block = ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@", vc.name); vc = nil; }); }; 复制代码
上述代码也是使用 中介者模式 打破循环应用的——使用vc
作为中介者代替self
从而打破循环引用
此时的引用情况为vc -> self -> block -> vc
(vc在用完之后手动置空)
但是只要不调用block,仍然存在着循环应用
解决循环引用还有一种方式——不引用
self.name = @"Felix"; self.block = ^(ViewController *vc) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"%@", vc.name); vc = nil; }); } 复制代码
上述代码使用当前vc
作为参数传入block时拷贝一份,就不会出现持有的情况,同时还能使用self
的内存空间,能够完美避免循环引用
Masonry
中是否存在循环引用?Monsary
使用的block是当做参数传递的,即便block内部持有self,设置布局的view持有block,但是block不持有view,当block执行完后就释放了,self的引用计数-1,所以block也不会持有self,所以不会导致循环引用
UIView动画
是类方法,不被self持有(即self持有了view,但view没有实例化)所以不会循环引用
- (void)checkLeaks { FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; [detector addCandidate:self]; NSSet *retainCycles = [detector findRetainCycles]; NSLog(@"%@", retainCycles); } 复制代码
int main(){ __block int a = 10; void(^block)(void) = ^{ a++; printf("Felix - %d",a); }; block(); return 0; } 复制代码
用clang
将上述代码输出成cpp文件来查看底层实现
clang -rewrite-objc main.c -o main.cpp 复制代码
int main(){ 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++中的构造函数全局搜索__main_block_impl_0
能看到它的定义
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 的结构体对象,这就是为什么能用 %@ 打印出block的原因了
fp
传递了具体的block实现__main_block_func_0
,然后保存在block结构体的impl
中这就说明了block声明只是将block实现保存起来,具体的函数实现需要自行调用
int main(){ int a = 10; void(^block)(void) = ^{ printf("Felix %d ", a); }; block(); return 0; } 复制代码
此时的block构造函数
中就会多出一个参数a
,并且在block结构体中
也会多出一个属性a
接着把目光转向__main_block_func_0
实现
__cself
是__main_block_impl_0
的指针,即block本身int a = __cself->a
即int a = block->a
a++
会报错int main(){ int a = 10; void(^block)(void) = ^{ printf("Felix %d ", a); }; block(); return 0; } 复制代码
__block
修饰的属性在底层会生成响应的结构体,保存原始变量的指针,并传递一个指针地址给block——因此是指针拷贝
接下来就来到libclosure源码
中仔细看一看瞧一瞧
首先来看block结构体对象Block_layout
(等同于clang编译出来的__Block_byref_a_0
)
#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; }; #define BLOCK_DESCRIPTOR_3 1 struct Block_descriptor_3 { // requires BLOCK_HAS_SIGNATURE const char *signature; const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT }; struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; BlockInvokeFunction invoke; struct Block_descriptor_1 *descriptor; // // imported variables }; 复制代码
其中Block_layout
是基础的block结构空间,而部分block则拥有Block_descriptor_2
和Block_descriptor_3
结构,其中的flags
标识记录了一些信息
但部分block则拥有Block_descriptor_2
和Block_descriptor_3
结构这句话又该怎么去理解呢?请看下面的解释
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock) { if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL; uint8_t *desc = (uint8_t *)aBlock->descriptor; desc += sizeof(struct Block_descriptor_1); return (struct Block_descriptor_2 *)desc; } static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock) { if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL; uint8_t *desc = (uint8_t *)aBlock->descriptor; desc += sizeof(struct Block_descriptor_1); if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) { desc += sizeof(struct Block_descriptor_2); } return (struct Block_descriptor_3 *)desc; } 复制代码
aBlock->flags & BLOCK_HAS_COPY_DISPOSE
满足,则_Block_descriptor_2
存在,反之则block没有_Block_descriptor_2
这个结构
_Block_descriptor_2
可以通过Block_descriptor_1
内存偏移得到aBlock->flags & BLOCK_HAS_SIGNATURE
满足,则_Block_descriptor_3
存在
_Block_descriptor_3
可以通过Block_descriptor_2
内存偏移得到决定这两个结构是否存在的绝对因素其实就是Block_layout
的flags
// Values for Block_layout->flags to describe block objects enum { BLOCK_DEALLOCATING = (0x0001), // runtime BLOCK_REFCOUNT_MASK = (0xfffe), // runtime BLOCK_NEEDS_FREE = (1 << 24), // runtime BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code BLOCK_IS_GC = (1 << 27), // runtime BLOCK_IS_GLOBAL = (1 << 28), // compiler BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE = (1 << 30), // compiler BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler }; 复制代码
接下来就用汇编来看看block中的签名
__NSGlobalBlock__签名(_Block_copy进入时)
__NSStackBlock__签名(_Block_copy进入时)
__NSMallocBlock__签名(_Block_copy返回时)
@?
,代表着不明对象接下来就来研究下栈block
转换成到堆block
的过程——_Block_copy
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; } } 复制代码
整段代码主要分成三个逻辑分支
flags
标识位——存储引用计数的值是否有效block的引用计数不受runtime处理的,是由自己管理的
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的第一号位置已经存储着释放标记
是否是全局block——是的话直接返回block
栈block
-> 堆block
的过程
malloc
在堆区开辟一片空间memmove
将数据从栈区拷贝到堆区invoke
、flags
同时进行修改_NSConcreteMallocBlock
为了更好地进行探究,我们在OC
的main文件
中进行clang编译
xcrun -sdk iphonesimulator clang -rewrite-objc main.m 复制代码
#import <UIKit/UIKit.h> #import "AppDelegate.h" int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); __block NSString *name = [NSString stringWithFormat:@"Felix"]; void (^fxBlock)(void) = ^{ // block_copy name = @"Feng Felix"; }; fxBlock(); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } 复制代码
block中的第一层拷贝其实已经讲过了——_Block_copy
将block从栈拷贝到堆
__main_block_desc_0_DATA结构体
,在里面又会去调用__main_block_copy_0
函数,__main_block_copy_0
里面会调用_Block_object_assign
——这就是第二层拷贝的调用入口
接下来就来看看_Block_object_assign
在底层都做了什么(注意传参)
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_byrefcopy->forwarding = copy; & src->forwarding = copy;
——原来的区域和新的区域都指向同一个对象,使得block具备了修改能力(*src2->byref_keep)(copy, src)
开始第三层拷贝(*src2->byref_keep)(copy, src)
跟进去会来到Block_byref
结构来,而byref_keep
是Block_byref
的第5个属性
struct Block_byref { void *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; }; 复制代码
__block
修饰的变量在底层其实调用了如下的构造方法——此时的第5位就等于byref_keep
,所以在第二层拷贝时会调用__Block_byref_id_object_copy_131
static void __Block_byref_id_object_copy_131(void *dst, void *src) { _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131); } static void __Block_byref_id_object_dispose_131(void *src) { _Block_object_dispose(*(void * *) ((char*)src + 40), 131); } 复制代码
第五个函数会去调用_Block_object_assign
函数
这个(char*)dst + 40
看着觉得好莫名其妙啊...其实看到__Block_byref_name_0
就顿悟了,刚好取得变量name
对象
struct __Block_byref_name_0 { void *__isa; __Block_byref_name_0 *__forwarding; int __flags; int __size; void (*__Block_byref_id_object_copy)(void*, void*); void (*__Block_byref_id_object_dispose)(void*); NSString *name; }; 复制代码
而_Block_object_assign
在对BLOCK_FIELD_IS_OBJECT
情况时会做出如下操作:
case BLOCK_FIELD_IS_OBJECT: /******* id object = ...; [^{ object; } copy]; ********/ _Block_retain_object(object); *dest = object; break; 复制代码
_Block_retain_object
是个空函数,因为block捕获的外接变量由ARC自动管理name
进行拷贝看完了三层拷贝,再来看一下释放函数_Block_object_dispose
void _Block_object_dispose(const void *object, const int flags) { switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) { case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK: case BLOCK_FIELD_IS_BYREF: // get rid of the __block data structure held in a Block _Block_byref_release(object); break; case BLOCK_FIELD_IS_BLOCK: _Block_release(object); break; case BLOCK_FIELD_IS_OBJECT: _Block_release_object(object); break; case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK: case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK: break; default: break; } } static void _Block_byref_release(const void *arg) { struct Block_byref *byref = (struct Block_byref *)arg; // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?) byref = byref->forwarding; if (byref->flags & BLOCK_BYREF_NEEDS_FREE) { int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK; os_assert(refcount); if (latching_decr_int_should_deallocate(&byref->flags)) { if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) { struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1); (*byref2->byref_destroy)(byref); } free(byref); } } } 复制代码
__block
修饰,就将指向指回原来的区域并使用free
释放__main_block_impl_0
的结构体对象,所以能用%@
打印自行调用
自动生成一个属性
来保存变量__block
修饰的属性在底层会生成响应的结构体,保存原始变量的指针,并传递一个指针地址
给blockblock还有hook
一块也是需要去学习了解的
小小的block也是有很多底层知识需要研究的,越学会发现自己越渺小,其实不然,只是你的视角开阔了,正是如此才会进步