iOS 题目详解 部分三

主要讲解Block 内部使用strongSelf的理由和用法

iOS 题目详解 部分一
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的属性, 所以selfBlock强引用, 而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;
  }
};

其实到这一步我们已经可以断定知道为什么不会造成循环引用了, 因为Controllerself.block是强引用, 而self.block的底层实现如上, 对Controllerweak修饰的弱引用;
不过我们还是继续往下看探究下具体的调用流程, 内部调用函数__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;
  • 原始Blockself强引用;
    大致的持有/调用关系如图

至此我们可以确定, 因为原始Blockself有一个强引用, 肯定会导致self的引用计数+1 , 关于这点引用计数+1的验证, 由于验证篇幅较长不再贴出, 大家可以自己验证, 或者移步看下这篇文章

下面探讨下释放的流程: ARC 环境下为什么可以正常释放(, MRC下类似):

  • 我们假设当 controller即将销毁时, 也就是退出当前界面后, 因为原始Block对其有个强引用, 所以它不能销毁, 需要等待延时任务执行;
  • 执行到延时任务时,也就是执行到副本Block时, 副本Blcok调用原始Blockfunptr执行具体Block内部的实现逻辑;
  • 我们知道原始Block处于栈区, 栈区的内存管理是系统进行的, 所以延时任务执行完毕, 原始Block自动销毁, 同时对强硬用的self释放(引用计数-1);
  • self的引用计数为0时, 就可以被销毁了, 因为self对副本Block有强引用, 所以self释放的同时会对堆区的副本Block销毁;
  • 至此, 不论是Controller(self), 原始Block, 副本Block都可以正常的释放; 而且可以正常的在Block内部执行延时任务;

补充部分

1. 便捷宏定义使用waekfSelf 和 stongSelf;

每次使用 Block都写一遍weakstrong的定义太麻烦, 将其定义成宏, 方便快捷;

/**
弱引用/强引用 宏定义
示例:
@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 的拷贝底层实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352