iOS 进阶知识点整理--分析block

本文将围绕以下几个问题分析block:

  1. block是不是函数指针?如果不是它们有什么区别?

  2. block修改局部变量为什么必须要加__block修饰?

  3. block为什么会产生循环引用?应该怎么避免?

要完全搞清楚这几个问题,首先我们需要搞清block的本质是什么?

结论:在iOS中block本质上就是一个oc对象,内部同其他OC对象一样有一个isa指针。block是一个将函数定义、实现、调用封装在一起的OC对象;

第一步:先写一个简单的测试block

- (void)test {
    void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b){
        NSLog(@"block a = %ld, b = %ld",a, b);
    };
    block(10, 20);
}

用命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m 将OC文件转换成c++代码查看OC内部实现结构

static void _I_ViewController_test(ViewController * self, SEL _cmd) {
    void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger,                 NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA));
    ((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);
}

从上述代码可以看到,block的定义被转换成了

void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA));

实际上是把函数__ViewController__test_block_impl_0的函数地址赋值给了block,\color{red}{注意:block与函数指针有一点相似,但block不是函数指针}

应该在这段代码之上我们可以看到__ViewController__test_block_impl_0其实是一个结构体,结构如下:

  struct __ViewController__test_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__test_block_desc_0* Desc;
  __ViewController__test_block_impl_0(void *fp, struct __ViewController__test_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

该结构体内部包含了两个变量implDesc和一个同名构造函数__ViewController__test_block_impl_0,全局搜索__block_impl找到其结构如下:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

  • isa:类型指针,这里代表block的类型

  • Flags:这里从构造函数的调用看到它有一个默认值0,并未对其赋值,也许在更深层有其它的用处

  • Reserved:应该同Flags差不多在底层有使用吧

  • FuncPtr:函数指针,这里指向__ViewController__test_block_func_0函数

    到这里足以证明block就是一个OC对象,同样有这一个指向它类型的isa指针;结构体__ViewController__test_block_impl_0的下方应该就能看到另一个变量Desc其结构如下:

    static struct __ViewController__test_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __ViewController__test_block_desc_0_DATA = { 0, sizeof(struct __ViewController__test_block_impl_0)};
    
    

    __ViewController__test_block_desc_0包含了两个变量,并在定义时创建了一个__ViewController__test_block_desc_0_DATA结构体

  • reserved:字面意思保留,应也是一个内部使用字段,这里能看到为其赋值0

  • Block_size:sizeof(),这里代表结构体__ViewController__test_block_impl_0的内存占用大小

void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA));

回看我们的block定义,__ViewController__test_block_impl_0调用时还传入了一个__ViewController__test_block_func_0函数

static void __ViewController__test_block_func_0(struct __ViewController__test_block_impl_0 *__cself, NSInteger a, NSInteger b) {
  // NSLog(@"block a = %ld, b = %ld",a, b); OC block 内部代码
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_dm_tq28fyls0cq42nl6fdhyqzd40000gn_T_ViewController_939d4d_mi_0,a, b);
        }

从函数结构就能看出该函数其实就是block{}里面的实现,因此我们得出block就是OC对象,内部封装了c函数定义、实现、和函数调用;

1.那么block是不是函数指针?如果不是它们有什么区别?

综上所述,block不是函数指针。函数指针是指向一段预先定义好的可执行代码,且该函数地址是在编译时就已经确定好的,函数内只能访问全局变量。block是OC对象,可以在运行时自由创建,不同作用域内创建的block对象的生命周期也不一样,通常动态创建的block都存储在栈上,可以调用[block copy]将其拷贝到堆上延长生命周期,另外block对局部变量拥有读的权限,对使用__block修饰的局部变量拥有读写权限

2.block修改局部变量为什么必须要加__block修饰?
- (void)test {
 int j = 10;
 int g = 10;
 static int s = 10;
 __block int i = 10;
 void(^block)(NSInteger, NSInteger) = ^(NSInteger a, NSInteger b){
 i++;
 s++;
 self;
// j++; 报错:Variable is not assignable (missing __block type specifier)
 NSLog(@"block a = %ld, b = %ld",a, b);
 NSLog(@"block i = %d, j = %d",i, j);
 };
 block(10, 20);
}

在原有test方法内新增成员变量j、g,static变量s,__block变量i,再次执行命令转换为c++,结果如下:

static void _I_ViewController_test(ViewController * self, SEL _cmd) {
    int j = 10;
    int g = 10;
    static int s = 10;
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 10};
    void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA, &s, self, j, g, (__Block_byref_i_0 *)&i, 570425344));
    ((void (*)(__block_impl *, NSInteger, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 20);
}

可以看到前三个变量并没有什么变化,只有__block 变量i被转换成了__Block_byref_i_0结构体:

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};

__isa=(void*)0,__forwarding=(__Block_byref_i_0 *)&i指向i自身地址的指针,__flags=0,__size=sizeof(__Block_byref_i_0),i=10为原来i的值,修改后block的实现变成如下:

struct __ViewController__test_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__test_block_desc_0* Desc;
  int *s;
  ViewController *self;
  int j;
  int g;
  __Block_byref_i_0 *i; // by ref
  __ViewController__test_block_impl_0(void *fp, struct __ViewController__test_block_desc_0 *desc, int *_s, ViewController *_self, int _j, int _g, __Block_byref_i_0 *_i, int flags=0) : s(_s), self(_self), j(_j), g(_g), i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

仔细看会发现在函数__ViewController__test_block_impl_0()后面还有这么一截: s(_s), self(_self), j(_j), g(_g), i(_i->__forwarding)这一截都是对结构体__ViewController__test_block_impl_0内的自定义变量进行复制s(_s)等价于s = _s其他同理,结合改变后的block定义:

    void(*block)(NSInteger, NSInteger) = ((void (*)(NSInteger, NSInteger))&__ViewController__test_block_impl_0((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA, &s, self, j, g, (__Block_byref_i_0 *)&i, 570425344));

看调用__ViewController__test_block_impl_0函数时传入的参数((void *)__ViewController__test_block_func_0, &__ViewController__test_block_desc_0_DATA, &s, self, j, g, (__Block_byref_i_0 *)&i, 570425344)前两个同修改前一样前面有介绍,从第三个开始分别为:静态变量s的地址、当前类对象self、j和g传入的都是int类型数值、(__block变量转换后)结构体i的地址,最后那个数字对应的是flags

那么,block修改局部变量为什么必须要加__block修饰?

因为,使用__block修饰后的变量在block内部会创建一个相应的结构体,该结构体内保留了变量的值和地址,block内部还为结构体定义了一个指针变量,内部对原来变量的读写都是通过这个结构体指针来实现的,所以就完成了对__block变量的修改;更多细节

3.block为什么会产生循环引用?应该怎么避免?

通过以上内容分析,我们知道block内会对外部变量、对象产生一个引用关系,OC对象在没有指定强弱引用的情况下,默认是强引用,那么一个对象如果间接或直接拥有block,block内部又拥有该对象那么就会产生循环引用

如图1中object----(strong)---->object1----(strong)---->block----(strong)---->object就是一个间接性引用产生的循环引用

图1

解决图1中问题其实只需将三条箭头中任意一条改用弱引用就能打破循环保留问题,但在实际开发过程中我们调用block一般都不是及时性的,所以如果object----(weak)---->object1或者object1----(weak)---->block当我们在调用block时可能对象已经被释放,代码运行会违背我们预期效果,即解决block循环引用问题,我们推崇将block-->object外部改成弱引用,block内部在将弱引用对象转强引用避免block内部访问还没结束object就被释放问题,如图2:

图2

参考文章:
iOS Block原理探究以及循环引用的问题

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1. Block的底层结构 以下是一个没有参数和返回值的最简单的Block: 为了探索Block的底层结构,需要将...
    再好一点点阅读 484评论 0 4
  • block.png iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实...
    全栈农民工阅读 601评论 0 1
  • iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实现匿名函数的特性,B...
    smile刺客阅读 2,375评论 2 26
  • 人与人初识,都只会觉得对方眉眼可爱志趣相投,大抵是因为大家深知第一印象的重要性,于是总能轻易化解心中不悦,给旁...
    最近爱放空阅读 191评论 0 0
  • dasfsad
    adamasp阅读 75评论 0 1