主要讲解Block 内部使用strongSelf的理由和用法
iOS 题目详解 部分一
iOS 题目详解 部分二
iOS 题目详解 部分三
Block 内部 self 的正确用法
问题背景:
当前Controller有一个 Block属性
@interface ViewController2 ()
@property (nonatomic, strong) NSMutableArray *nameArr;
@property (nonatomic, copy) Block block;
@end
而在 self.block中要访问当前 Controller也就是self;
对Block没有强硬用的问题背景, 分析思路类似, 不再探讨;
我们日常开发中最常用的就是Block内部使用weakSelf可以防止循环引用, 但是这样会有个问题, 如果Block内部有延时任务执行时这样就不满足需求了, 因为执行延时任务时self已经被释放;
外部使用weak后内部使用strongSelf可以解决这个问题;
__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);
});
};
self.block();
但是为什么这样写可以保证不产生循环引用呢? 就这个问题通过clang看下以下几种情况其底层代码研究下;
- 1.1 首先直接在
Block内部使用self会造成循环引用, 这点毋庸置疑;
self.block = ^{
NSLog(@"%@", self);
};
通过clang后的C++代码如下:
struct __ViewController2__viewDidLoad_block_impl_1 {
struct __block_impl impl;
struct __ViewController2__viewDidLoad_block_desc_1* Desc;
#Block 内部对 self 强硬用
ViewController2 *const __strong self;
__ViewController2__viewDidLoad_block_impl_1(void *fp, struct __ViewController2__viewDidLoad_block_desc_1 *desc, ViewController2 *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
由于Block是当前 Controller的属性, 所以self对Block强引用, 而Block内部又对self强引用;
- 1.2 在
Block内部中使用weakSelf有效的解决循环引用问题;
- (void)viewDidLoad {
[super viewDidLoad];
#weak 修饰 self 不会造成循环引用
__weak typeof(self) weakSelf = self;
self.block = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[weakSelf delayTask];
});
};
self.block();
}
#延时任务
- (void)delayTask {
NSLog(@"%s", __func__);
}
#副本 Block
struct __ViewController2__viewDidLoad_block_impl_1 {
struct __block_impl impl;
struct __ViewController2__viewDidLoad_block_desc_1* Desc;
#Block 对 Controller 弱引用
ViewController2 *const __weak weakSelf;
__ViewController2__viewDidLoad_block_impl_1(void *fp, struct __ViewController2__viewDidLoad_block_desc_1 *desc, ViewController2 *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
#原始 Block
struct __ViewController2__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController2__viewDidLoad_block_desc_0* Desc;
ViewController2 *const __weak weakSelf;
__ViewController2__viewDidLoad_block_impl_0(void *fp, struct __ViewController2__viewDidLoad_block_desc_0 *desc, ViewController2 *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这种方式我们都知道不会造成循环引用,但是造成的问题随之而来, 如果我们在Block内部执行了延时的任务(目的是为了执行任务时Controller已经被销毁); 则会发现, 这个延时任务并不会被执行, 因为执行[weakSelf delayTask]这句代码时controller已经被销毁, 给一个 nil发送消息, 是不会响应的;

- 1.3 正确用法: 外部使用
weak修饰, 内部使用strongSelf;
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[strongSelf delayTask];
});
};
self.block();
}
#延时任务
- (void)delayTask {
NSLog(@"%s", __func__);
}
退出当前Controller后延时任务可以正常执行, 而且Controller可以正常释放;
#延时任务被执行
2020-09-08 10:27:32.253938+0800 Test[12274:2082073] -[ViewController2 delayTask]
#延时任务被执行后, Controller正常销毁, 说明没有循环引用
2020-09-08 10:27:32.254415+0800 Test[12274:2082073] -[ViewController2 dealloc]
但是, 为什么这样写就可以保证没有循环引用呢?首先请了解下Block底层各个函数的含义
首先看下ViewDidLoad通过clang后转化为如下, 因为ARC下我们都知道系统会帮我们把Block从栈区拷贝到堆区, 我们实际操作的是堆区的那一份Block副本;
static void _I_ViewController2_viewDidLoad(ViewController2 * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController2"))}, sel_registerName("viewDidLoad"));
__attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
#Block 的实现
((void (*)(id, SEL, Block))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__ViewController2__viewDidLoad_block_impl_1((void *)__ViewController2__viewDidLoad_block_func_1, &__ViewController2__viewDidLoad_block_desc_1_DATA, weakSelf, 570425344)));
#调用 Block
((Block (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}
可以确认这个self.block 的底层实现是__ViewController2__viewDidLoad_block_impl_1, 在Block内部调用的函数是__ViewController2__viewDidLoad_block_func_1;
首先看下self.block的底层结构实现
struct __ViewController2__viewDidLoad_block_impl_1 {
struct __block_impl impl;
struct __ViewController2__viewDidLoad_block_desc_1* Desc;
#对 self 弱引用
ViewController2 *const __weak weakSelf;
__ViewController2__viewDidLoad_block_impl_1(void *fp, struct __ViewController2__viewDidLoad_block_desc_1 *desc, ViewController2 *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
其实到这一步我们已经可以断定知道为什么不会造成循环引用了, 因为Controller对self.block是强引用, 而self.block的底层实现如上, 对Controller是weak修饰的弱引用;
不过我们还是继续往下看探究下具体的调用流程, 内部调用函数__ViewController2__viewDidLoad_block_func_1的实现如下;
static void __ViewController2__viewDidLoad_block_func_1(struct __ViewController2__viewDidLoad_block_impl_1 *__cself) {
ViewController2 *const __weak weakSelf = __cself->weakSelf; // bound by copy
#调用原始 Block, 注意副本 Block 只是调用原始 Block的实现, 并不对其强引用或者持有
__attribute__((objc_ownership(strong))) typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time((0ull), (int64_t)(5 * 1000000000ull)), dispatch_get_main_queue(), ((void (*)())&__ViewController2__viewDidLoad_block_impl_0((void *)__ViewController2__viewDidLoad_block_func_0, &__ViewController2__viewDidLoad_block_desc_0_DATA, strongSelf, 570425344)));
}
原始Block的实现和调用函数封装逻辑如下;
#原始 Block的底层结构
struct __ViewController2__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController2__viewDidLoad_block_desc_0* Desc;
#对 self 强硬用, 但是没有关系, 因为 self 不对原始 block 强引用;
ViewController2 *const __strong strongSelf;
__ViewController2__viewDidLoad_block_impl_0(void *fp, struct __ViewController2__viewDidLoad_block_desc_0 *desc, ViewController2 *const __strong _strongSelf, int flags=0) : strongSelf(_strongSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
#实际执行延时任务
static void __ViewController2__viewDidLoad_block_func_0(struct __ViewController2__viewDidLoad_block_impl_0 *__cself) {
ViewController2 *const __strong strongSelf = __cself->strongSelf; // bound by copy
#调用延时任务
((void (*)(id, SEL))(void *)objc_msgSend)((id)strongSelf, sel_registerName("delayTask"));
}
下面我们来总结下这个流程; 首先是在 ARC环境下我们知道系统会对栈区的原始Block执行拷贝操作到堆区, 我们实际操作的是堆区那份副本;
-
self强硬用拷贝后的副本Block, 副本Block中对self是弱引用; - 副本
Block中调用原始Block; - 原始
Block对self强引用;
大致的持有/调用关系如图
至此我们可以确定, 因为原始Block对self有一个强引用, 肯定会导致self的引用计数+1 , 关于这点引用计数+1的验证, 由于验证篇幅较长不再贴出, 大家可以自己验证, 或者移步看下这篇文章
下面探讨下释放的流程: ARC 环境下为什么可以正常释放(, MRC下类似):
- 我们假设当
controller即将销毁时, 也就是退出当前界面后, 因为原始Block对其有个强引用, 所以它不能销毁, 需要等待延时任务执行; - 执行到延时任务时,也就是执行到副本
Block时, 副本Blcok调用原始Block的funptr执行具体Block内部的实现逻辑; - 我们知道原始
Block处于栈区, 栈区的内存管理是系统进行的, 所以延时任务执行完毕, 原始Block自动销毁, 同时对强硬用的self释放(引用计数-1); - 当
self的引用计数为0时, 就可以被销毁了, 因为self对副本Block有强引用, 所以self释放的同时会对堆区的副本Block销毁; - 至此, 不论是
Controller(self), 原始Block, 副本Block都可以正常的释放; 而且可以正常的在Block内部执行延时任务;
补充部分
1. 便捷宏定义使用waekfSelf 和 stongSelf;
每次使用 Block都写一遍weak和strong的定义太麻烦, 将其定义成宏, 方便快捷;
/**
弱引用/强引用 宏定义
示例:
@weakify(self)
[self block^{
@strongify(self)
if (!self) return;
...
}];
*/
#ifndef weakify
#if DEBUG
#if __has_feature(objc_arc)
#define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object;
#endif
#else
#if __has_feature(objc_arc)
#define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object;
#else
#define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object;
#endif
#endif
#endif
#ifndef strongify
#if DEBUG
#if __has_feature(objc_arc)
#define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object;
#endif
#else
#if __has_feature(objc_arc)
#define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object;
#else
#define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object;
#endif
#endif
#endif
使用示例:
- (void)test {
@weakify(self)
self.block = ^{
@strongify(self);
if (!self) {
return;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self delayTask];
});
};
self.block();
NSLog(@"Class: %@", [self.block class]);
}
2. 验证下: 关于ARC下系统自动帮我们把Block从栈区拷贝到了堆区;
- 在
MRC环境下, 如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
Block block = ^{
NSLog(@"%@", self);
};
block();
NSLog(@"Class: %@", [block class]);
}
打印结果如下
#这是一个栈区 Block
2020-09-08 12:21:12.140994+0800 Test[12396:2111280] Class: __NSStackBlock__
- 在
ARC环境下, 同样的代码:
- (void)viewDidLoad {
[super viewDidLoad];
Block block = ^{
NSLog(@"%@", self);
};
block();
NSLog(@"Class: %@", [block class]);
}
打印结果如下
#这是一个堆区 Block
2020-09-08 12:22:07.533464+0800 Test[12399:2111818] Class: __NSMallocBlock__
至此可以验证, 在ARC环境下系统帮我们默认执行了拷贝操作把Block从栈区拷贝到了堆区;
备注: 补上Block的拷贝流程
参考文章
iOS Block 部分一
iOS Block内部使用 strongSelf引用计数
基本的Clang语句
iOS Block 的拷贝底层实现
