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 的拷贝底层实现

你可能感兴趣的:(iOS 题目详解 部分三)