主要講解Block 內(nèi)部使用strongSelf的理由和用法
iOS 題目詳解 部分一
iOS 題目詳解 部分二
iOS 題目詳解 部分三
Block 內(nèi)部 self 的正確用法
問題背景:
當(dāng)前Controller有一個 Block屬性
@interface ViewController2 ()
@property (nonatomic, strong) NSMutableArray *nameArr;
@property (nonatomic, copy) Block block;
@end
而在 self.block中要訪問當(dāng)前 Controller也就是self;
對Block沒有強(qiáng)硬用的問題背景, 分析思路類似, 不再探討;
我們?nèi)粘i_發(fā)中最常用的就是Block內(nèi)部使用weakSelf可以防止循環(huán)引用, 但是這樣會有個問題, 如果Block內(nèi)部有延時(shí)任務(wù)執(zhí)行時(shí)這樣就不滿足需求了, 因?yàn)閳?zhí)行延時(shí)任務(wù)時(shí)self已經(jīng)被釋放;
外部使用weak后內(nèi)部使用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();
但是為什么這樣寫可以保證不產(chǎn)生循環(huán)引用呢? 就這個問題通過clang看下以下幾種情況其底層代碼研究下;
- 1.1 首先直接在
Block內(nèi)部使用self會造成循環(huán)引用, 這點(diǎn)毋庸置疑;
self.block = ^{
NSLog(@"%@", self);
};
通過clang后的C++代碼如下:
struct __ViewController2__viewDidLoad_block_impl_1 {
struct __block_impl impl;
struct __ViewController2__viewDidLoad_block_desc_1* Desc;
#Block 內(nèi)部對 self 強(qiáng)硬用
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是當(dāng)前 Controller的屬性, 所以self對Block強(qiáng)引用, 而Block內(nèi)部又對self強(qiáng)引用;
- 1.2 在
Block內(nèi)部中使用weakSelf有效的解決循環(huán)引用問題;
- (void)viewDidLoad {
[super viewDidLoad];
#weak 修飾 self 不會造成循環(huán)引用
__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();
}
#延時(shí)任務(wù)
- (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;
}
};
這種方式我們都知道不會造成循環(huán)引用,但是造成的問題隨之而來, 如果我們在Block內(nèi)部執(zhí)行了延時(shí)的任務(wù)(目的是為了執(zhí)行任務(wù)時(shí)Controller已經(jīng)被銷毀); 則會發(fā)現(xiàn), 這個延時(shí)任務(wù)并不會被執(zhí)行, 因?yàn)閳?zhí)行[weakSelf delayTask]這句代碼時(shí)controller已經(jīng)被銷毀, 給一個 nil發(fā)送消息, 是不會響應(yīng)的;

- 1.3 正確用法: 外部使用
weak修飾, 內(nèi)部使用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();
}
#延時(shí)任務(wù)
- (void)delayTask {
NSLog(@"%s", __func__);
}
退出當(dāng)前Controller后延時(shí)任務(wù)可以正常執(zhí)行, 而且Controller可以正常釋放;
#延時(shí)任務(wù)被執(zhí)行
2020-09-08 10:27:32.253938+0800 Test[12274:2082073] -[ViewController2 delayTask]
#延時(shí)任務(wù)被執(zhí)行后, Controller正常銷毀, 說明沒有循環(huán)引用
2020-09-08 10:27:32.254415+0800 Test[12274:2082073] -[ViewController2 dealloc]
但是, 為什么這樣寫就可以保證沒有循環(huán)引用呢?首先請了解下Block底層各個函數(shù)的含義
首先看下ViewDidLoad通過clang后轉(zhuǎn)化為如下, 因?yàn)?code>ARC下我們都知道系統(tǒng)會幫我們把Block從棧區(qū)拷貝到堆區(qū), 我們實(shí)際操作的是堆區(qū)的那一份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 的實(shí)現(xiàn)
((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)));
#調(diào)用 Block
((Block (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("block"))();
}
可以確認(rèn)這個self.block 的底層實(shí)現(xiàn)是__ViewController2__viewDidLoad_block_impl_1, 在Block內(nèi)部調(diào)用的函數(shù)是__ViewController2__viewDidLoad_block_func_1;
首先看下self.block的底層結(jié)構(gòu)實(shí)現(xiàn)
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;
}
};
其實(shí)到這一步我們已經(jīng)可以斷定知道為什么不會造成循環(huán)引用了, 因?yàn)?code>Controller對self.block是強(qiáng)引用, 而self.block的底層實(shí)現(xiàn)如上, 對Controller是weak修飾的弱引用;
不過我們還是繼續(xù)往下看探究下具體的調(diào)用流程, 內(nèi)部調(diào)用函數(shù)__ViewController2__viewDidLoad_block_func_1的實(shí)現(xiàn)如下;
static void __ViewController2__viewDidLoad_block_func_1(struct __ViewController2__viewDidLoad_block_impl_1 *__cself) {
ViewController2 *const __weak weakSelf = __cself->weakSelf; // bound by copy
#調(diào)用原始 Block, 注意副本 Block 只是調(diào)用原始 Block的實(shí)現(xiàn), 并不對其強(qiáng)引用或者持有
__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的實(shí)現(xiàn)和調(diào)用函數(shù)封裝邏輯如下;
#原始 Block的底層結(jié)構(gòu)
struct __ViewController2__viewDidLoad_block_impl_0 {
struct __block_impl impl;
struct __ViewController2__viewDidLoad_block_desc_0* Desc;
#對 self 強(qiáng)硬用, 但是沒有關(guān)系, 因?yàn)?self 不對原始 block 強(qiáng)引用;
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;
}
};
#實(shí)際執(zhí)行延時(shí)任務(wù)
static void __ViewController2__viewDidLoad_block_func_0(struct __ViewController2__viewDidLoad_block_impl_0 *__cself) {
ViewController2 *const __strong strongSelf = __cself->strongSelf; // bound by copy
#調(diào)用延時(shí)任務(wù)
((void (*)(id, SEL))(void *)objc_msgSend)((id)strongSelf, sel_registerName("delayTask"));
}
下面我們來總結(jié)下這個流程; 首先是在 ARC環(huán)境下我們知道系統(tǒng)會對棧區(qū)的原始Block執(zhí)行拷貝操作到堆區(qū), 我們實(shí)際操作的是堆區(qū)那份副本;
-
self強(qiáng)硬用拷貝后的副本Block, 副本Block中對self是弱引用; - 副本
Block中調(diào)用原始Block; - 原始
Block對self強(qiáng)引用;
大致的持有/調(diào)用關(guān)系如圖
至此我們可以確定, 因?yàn)樵?code>Block對self有一個強(qiáng)引用, 肯定會導(dǎo)致self的引用計(jì)數(shù)+1 , 關(guān)于這點(diǎn)引用計(jì)數(shù)+1的驗(yàn)證, 由于驗(yàn)證篇幅較長不再貼出, 大家可以自己驗(yàn)證, 或者移步看下這篇文章
下面探討下釋放的流程: ARC 環(huán)境下為什么可以正常釋放(, MRC下類似):
- 我們假設(shè)當(dāng)
controller即將銷毀時(shí), 也就是退出當(dāng)前界面后, 因?yàn)樵?code>Block對其有個強(qiáng)引用, 所以它不能銷毀, 需要等待延時(shí)任務(wù)執(zhí)行; - 執(zhí)行到延時(shí)任務(wù)時(shí),也就是執(zhí)行到副本
Block時(shí), 副本Blcok調(diào)用原始Block的funptr執(zhí)行具體Block內(nèi)部的實(shí)現(xiàn)邏輯; - 我們知道原始
Block處于棧區(qū), 棧區(qū)的內(nèi)存管理是系統(tǒng)進(jìn)行的, 所以延時(shí)任務(wù)執(zhí)行完畢, 原始Block自動銷毀, 同時(shí)對強(qiáng)硬用的self釋放(引用計(jì)數(shù)-1); - 當(dāng)
self的引用計(jì)數(shù)為0時(shí), 就可以被銷毀了, 因?yàn)?code>self對副本Block有強(qiáng)引用, 所以self釋放的同時(shí)會對堆區(qū)的副本Block銷毀; - 至此, 不論是
Controller(self), 原始Block, 副本Block都可以正常的釋放; 而且可以正常的在Block內(nèi)部執(zhí)行延時(shí)任務(wù);
補(bǔ)充部分
1. 便捷宏定義使用waekfSelf 和 stongSelf;
每次使用 Block都寫一遍weak和strong的定義太麻煩, 將其定義成宏, 方便快捷;
/**
弱引用/強(qiáng)引用 宏定義
示例:
@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. 驗(yàn)證下: 關(guān)于ARC下系統(tǒng)自動幫我們把Block從棧區(qū)拷貝到了堆區(qū);
- 在
MRC環(huán)境下, 如下代碼:
- (void)viewDidLoad {
[super viewDidLoad];
Block block = ^{
NSLog(@"%@", self);
};
block();
NSLog(@"Class: %@", [block class]);
}
打印結(jié)果如下
#這是一個棧區(qū) Block
2020-09-08 12:21:12.140994+0800 Test[12396:2111280] Class: __NSStackBlock__
- 在
ARC環(huán)境下, 同樣的代碼:
- (void)viewDidLoad {
[super viewDidLoad];
Block block = ^{
NSLog(@"%@", self);
};
block();
NSLog(@"Class: %@", [block class]);
}
打印結(jié)果如下
#這是一個堆區(qū) Block
2020-09-08 12:22:07.533464+0800 Test[12399:2111818] Class: __NSMallocBlock__
至此可以驗(yàn)證, 在ARC環(huán)境下系統(tǒng)幫我們默認(rèn)執(zhí)行了拷貝操作把Block從棧區(qū)拷貝到了堆區(qū);
備注: 補(bǔ)上Block的拷貝流程
參考文章
iOS Block 部分一
iOS Block內(nèi)部使用 strongSelf引用計(jì)數(shù)
基本的Clang語句
iOS Block 的拷貝底層實(shí)現(xiàn)
