Objective-C 与 block 详解

309 阅读8分钟

前言

学如逆水行舟,不进则退。我是平平无奇游荡于各平台的搬运工。作为iOS开发,在平时的工作中也会高频使用到block,所以block是非常重要的,在学习Swift闭包时,我突然觉得可以将Objective-C block和闭包一起对比学习,可以让我们更快的掌握。

一、block的数据结构

(一)block 语法解析

作为硬核派,了解block数据结构我们肯定不能Google别人的结论,我们有自己的clang工具,使用clang工具,可以将 OC 代码转成 C++ 代码。

首先,我们准备 main.m 这个类,类内容为:

// main.m 

int main() {
    return 1;
}

我们切到main.m类所在文件夹,使用指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m,发现语法解析生成的cpp代码如下main.cpp

// main.cpp

#import <UIKit/UIKit.h>

int main() {
    void (^block)(void) = ^ {
        NSLog(@"Hello World!");
    };
    block();
    return 1;
}

接着我们在main.m中添加block代码:

// main.m 

int main() {
    void (^block)(void) = ^ {
    };
    block();
    return 1;
}

继续使用clang解析main.m,发现生成的cpp代码如下:

// main.cpp

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_9b_w0ymsg0n3yqdlb90w49xqmz40000gn_T_main_2428cf_mi_0);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
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 1;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };

去除强制转换后我们可以看出声明block的时候,block底层调用了__main_block_iml_0结构体,传入的参数分别是__main_block_func_0(方法函数)&__main_block_desc_0_DATA(结构体地址)

(二)block Cpp 数据结构解析

1.__main_block_func_0 的方法代码段

//需要传入的参数是结构体: __main_block_impl_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_9b_w0ymsg0n3yqdlb90w49xqmz40000gn_T_main_2428cf_mi_0);
    }
}

可以看到,这个函数体中传入了 __cself 和 block 中调用的方法 NSLog

2. __main_block_desc_0_DATA 的结构体代码段

static struct __main_block_desc_0 {
  size_t reserved;       //作用不大,不需要理会
  size_t Block_size;     //整个block的在内存中占的字节大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};//计算blcok主结构体__main_block_impl_0的大小

总的来言,此结构体就是为了保存block结构体的大小

3. __main_block_impl_0

__main_block_impl_0 是承载 block 最重要的结构,研究__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;
  }
};

可以发现,__main_block_impl_0结构中主要包含了两个结构体:

  • struct __block_impl impl :函数指针
  • struct __main_block_desc_0* Desc : block 大小等内存信息

__block_impl__main_block_desc_0均由构建 block的方法传入,比如上面我们构建block传入的两个参数:__main_block_func_0__main_block_desc_0,就是用来构建 impl 和 Desc的。

二、block约束问题

通过分析普通 block 函数的Cpp结构,我们明确了block函数的基本数据结构,下面我们进一步分析:为什么使用block时,需要有以下的关键词修饰:

  • 捕获的变量需要使用 __block 才能修改
  • 使用self需要用关键词 weakify、strongify 修饰

(一)捕获的变量需要使用 __block 才能修改

#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block NSInteger num = 0;
    void (^blockTest)(void) = ^ {
        num = 1;
    };
    blockTest();
}

@end

代码示例如上,我们可以发现,如果我们定义的 NSInteger num 如果不用 __block 修饰,编译器会报错:Variable is not assignable (missing __block type specifier),那么我们会产生一个疑问:block究竟是如何捕获变量的呢?为什么我要修改的变量必须要用__block关键词进行修饰才能在block中对其进行修改?

使用指令将上述代码生成对应的 cpp 代码,内容如下:

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

    __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 0};
    void (*blockTest)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blockTest)->FuncPtr)((__block_impl *)blockTest);
}

我们发现和第一部分我们在 main.m 定义简单的 block 不同,这里的生成的block并不是struct __main_block_impl_0,而是struct __ViewController__viewDidLoad_block_impl_0

所以我们首先可以明确的是,不同的block有其自己的命名规范,但后缀基本都是_block_impl_0

接着我们查看 __ViewController__viewDidLoad_block_impl_0 的定义:

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

与第一部分__main_block_impl_0相比,__ViewController__viewDidLoad_block_impl_0增加了 __Block_byref_num_0 *num 这个字段,也就是说我们在block中引用的字段,都会出现在block结构体中。

我们看到__Block_byref_num_0 *num后标注了// by ref,也就意味着block中对num实际是引用(不是copy),所以我们需要对num使用关键词__block将其转成__Block_byref_num_0引用类型。

(二)使用self需要用关键词 weakify/strongify 修饰

1. 不使用 weakify/strongify 修饰,会发生什么?

初学block时,我们大概率都会遇到一个问题:block中使用的self没有进行 weakify/strongify处理,我们也知道这样做的问题:

block中使用了self(没有进行weakify/strongify声明),当执行block时,self如果已经被释放,那么在block中执行self方法应用就会 crash,因为self已经被释放。

如果不对block中使用的self声明weakify/strongify,生成的 cpp 代码会是什么情况:

#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    void (^blockTest)(void) = ^ {
        self.view.backgroundColor = [UIColor orangeColor];
    };
    blockTest();
}

@end

生成的cpp代码

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  ViewController *self;
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
  ViewController *self = __cself->self; // bound by copy

        ((void (*)(id, SEL, UIColor * _Nullable))(void *)objc_msgSend)((id)((UIView *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("view")), sel_registerName("setBackgroundColor:"), ((UIColor * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("UIColor"), sel_registerName("orangeColor")));
    }
static void __ViewController__viewDidLoad_block_copy_0(struct __ViewController__viewDidLoad_block_impl_0*dst, struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __ViewController__viewDidLoad_block_dispose_0(struct __ViewController__viewDidLoad_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __ViewController__viewDidLoad_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __ViewController__viewDidLoad_block_impl_0*, struct __ViewController__viewDidLoad_block_impl_0*);
  void (*dispose)(struct __ViewController__viewDidLoad_block_impl_0*);
} __ViewController__viewDidLoad_block_desc_0_DATA = { 0, sizeof(struct __ViewController__viewDidLoad_block_impl_0), __ViewController__viewDidLoad_block_copy_0, __ViewController__viewDidLoad_block_dispose_0};

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

    void (*blockTest)(void) = ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blockTest)->FuncPtr)((__block_impl *)blockTest);
}

关键语句是:

static void __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself) {
  ViewController *self = __cself->self; // bound by copy

        ((void (*)(id, SEL, UIColor * _Nullable))(void *)objc_msgSend)((id)((UIView *(*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("view")), sel_registerName("setBackgroundColor:"), ((UIColor * _Nonnull (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("UIColor"), sel_registerName("orangeColor")));
    }

bound by copy ,使用 copy 的方式进行绑定,我们知道copy意味着浅拷贝,被引用的对象引用计数会+1,那么这样就会出问题: 0. self 中定义了 block,相当于self持有了block

  1. 同时 block 中又持有了 self
  2. 导致循环引用,该释放的对象无法被释放,内存泄露

为了验证上面所说的循环引用导致无法回收的情况,我们来模拟一个场景:

@implementation BNDestroyDemoView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"BNDestroyDemoView initWithFrame:%@",self);
        });
    }
    return self;
}

@end

我们新建了一个类BNDestroyDemoView.h,这个类被创建后会被立刻置nil:

@interface ViewController ()

@property (nonatomic, strong) BNDestroyDemoView *demoView;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.demoView = [[BNDestroyDemoView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
    self.demoView = nil;
}


@end

这个类会延迟3秒执行dispatch_after中的block内容,即使我们已经将 self.demoView置nil,3秒后我们依旧可以看到如下的日志打印,表明self并没有被系统回收:

image.png

2. weakify/strongify 修饰,是进行深拷贝吗?

通过上面的分析,我们明白一个道理:使用block时要避免产生循环引用,既然浅拷贝会导致引用计数+1。

既然如上的浅拷贝逻辑会导致循环引用,我们有什么办法解决循环引用呢?

  • 深拷贝
  • 弱引用,强使用

深拷贝的方法是将self的内存直接拷贝一份,不对原self的引用计数新增,这种方法首先从开销上会比较大,而且有时self如果被重置为nil,我们的目标就是不执行self的方法,而不是执行深拷贝后的self方法。

所以那就只有使用弱引用,强使用的方法了,这种方法在iOS开发中是一种通用的解决方案,在 Runloop循环引用TimerYYAsyncLabel等技术方案中都有使用,下面进行具体的阐述。

弱引用的意思是:我传入 block 中的 self 通过 weak进行修饰,不增加 self 的引用计数

强使用的意思是:我在执行block方法体期间,需要将弱引用self改为强引用,避免在执行block期间self被回收。

对应的代码实现如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak __typeof(self) weakSelf  = self;
    void (^block)(void) = ^ {
        __strong __typeof(self) strongSelf = weakSelf;
        NSLog(@"Hello World!");
    };
    block();
}

于此我们便解决了使用block会导致循环引用的问题,但继而又产生了一个问题:

强引用self有可能为nil吗?

答案是:可能。如果在执行block之前,self就已经被回收,因为block在执行前对self是弱引用,所以self是有可能变为nil的。

@implementation BNDestroyDemoView

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        __weak __typeof(self) weakSelf  = self;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            __strong __typeof(self) strongSelf = weakSelf;
            NSLog(@"BNDestroyDemoView initWithFrame:%@",@[strongSelf]);
        });
    }
    return self;
}

@end

上面这段代码是非常不健壮的,如果BNDestroyDemoView在执行 block 之前被系统回收,就会导致crash:

image.png

三、dispatch_block_t 的应用场景

通常我们写一个不带参数的块回调函数是这样写的:

在 . h 头文件中
typedef void (^leftBlockAction)(); // 定义类型
-(void)leftButtonAction:(leftBlockAction)leftBlock; // 在定义一个回调函数:.m 文件中:
-(void)leftButtonAction:(leftBlockAction)leftBlock{
	leftBlock();
}

使用dispatch_block_t 只要在.h 头文件定义属性方法

@property (nonatomic,copy) dispatch_block_t leftBlockAction;

在.m文件 调用的方法里调用

if (self.leftBlockAction) {
    self.leftBlockAction();
}

在另个模块里直接:

MyAlertView *alert = [[MyAlertView alloc]init];
alert.leftBlockAction = ^() {

    NSLog(@"left button clicked");
};

结尾

然后今天的分享就到这里为止了,希望读完这篇文章的你能够对你能够有所帮助。能够帮助你对block和Swift的学习能够有所启发。在文章的最后希望各位读者能够点点赞,点点关注。毕竟他们都不花钱,就支持一下勤劳的坐着吧,不然南风我就要去喝西南风了,为了感谢大家帮我点赞我给各位也送一波福利。

iOS大厂面试必刷面试题

FLutter视频教程,手把手带你入门到精通

收录:地址