编码篇-Block里面的小天地

2018-09-12 18:01:50 浏览数 (1)

前言

本文不用于商业用途,只是对个人知识的一个梳理和总结,其中借鉴引用了其他博客里面的内容,文末会给出本文的参考文章,如果侵犯到原著者的权益请在评论区留言,我会马上删除对应文段。

Block是什么

Block是iOS4.0 和Mac OS X 10.6 引进的对C语言的扩展,用来实现匿名函数的特性。 通常来说,block都是一些简短代码片段的封装,适用作工作单元,通常用来做并发任务、遍历、以及回调。

block是什么?在回答这个问题之前,先介绍一下什么是闭包。在 wikipedia 上,闭包的定义) 是: In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function. 翻译过来,闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作自由变量)。简而言之,所谓闭包就是能够读取其它函数内部变量的函数。

block 实际上就是 Objective-C 语言对于闭包的实现。这个解释用到block来也很恰当:一个函数里定义了个block,这个block可以访问该函数的内部变量。

Block是对象吗?

block是不是对象?答案显而易见:是的。 下图是block的数据结构定义,显而易见,在Block_layout里,我们看到了isa指针,为什么说block是对象呢,原因就在于isa指针,在objective-c语言的内部,每一个对象都有一个isa指针,指向该指针的类。 因为所有对象的都有isa 指针,用于实现对象相关的功能。

block 的数据结构定义如下(图片来自 这里):

对应的结构体定义如下:

代码语言:javascript复制
struct Block_descriptor {
  unsigned long int reserved;
  unsigned long int size;
  void (*copy)(void *dst, void *src);
  void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

通过该图,我们可以知道,一个 block 实例实际上由 6 部分构成:

代码语言:javascript复制
isa 指针,所有对象都有该指针,用于实现对象相关的功能。
flags,用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用。
reserved,保留变量。
#invoke,函数指针,指向具体的 block 实现的函数调用地址。
descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针。
variables,capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

通过对 block内部结构的分析,我们知道了一个 block 实际是一个对象,它主要由一个 isa 和 一个 invoke(函数指针,指向具体的 block 实现的函数调用地址) 和 一个 descriptor 组成 。

Block的分类

在 Objective-C 语言中,一共有 3 种类型的 block:

  • _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。 简单地讲,如果一个block中没有引用外部变量并且没有被其他对象持有,就是NSConcreteGlobalBlock。NSConcreteGlobalBlock是全局的block,在编译期间就已经决定了,如同宏一样。
  • _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。 NSConcreteStackBlock就是引用了外部变量的block,但是只是简单的引用,不会持有外部对象。
  • _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。 NSConcreteMallocBlock其实就是一个block被copy时,将生成NSConcreteMallocBlock,不过值得注意的是NSConcreteMallocBlock会持有外部对象。只要这个NSConcreteMallocBlock存在,内部对象的引用计数就会 1

内存和复制

Block的声明属性时的关键字

block方法常用声明:@property (copy) void(^MyBlock)(void); 如果超出当前作用域之后仍然继续使用block,那么最好使用copy关键字,拷贝到堆区,防止栈区变量销毁。

由于block也是NSObject,我们可以对其进行retain操作。不过在将block作为回调函数传递给底层框架时,底层框架需要对其copy一份。比方说,如果将回调block作为属性,不能用retain,而要用copy。我们通常会将block写在栈中,而需要回调时,往往回调block已经不在栈中了,使用copy属性可以将block放到堆中。

并且在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。并且因为block是一段代码,即不可变。所以对于block 使用copy 还是strong 效果是一样的。亲测是这样的,网上有些解释说不能使用 strong 是错误的。

Block对于局部变量的修改问题

为了研究编译器是如何实现 block 的,我们需要使用 clang。clang 提供一个命令,可以将 Objetive-C 的源码改写成 c 语言的,借此可以研究 block 具体的源码实现方式。 该命令是 : clang -rewrite-objc block.c block.c 是一个文件名称。 命令行中输入clang -rewrite-objc block1.c即可在目录中看到 clang 输出了一个名为 block1.cpp 的文件。该文件就是 block 在 c 语言实现的。

代码语言:javascript复制
 //我们使用clang来分析 使用__block和不使用 __block时,block内部的实现机制。
 #下面是未使用__block的情况

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int a;
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    }
};
    # 我们可以看到 main_block_impl_0 中增加了一个变量 a,在 block 中引用的变量 a 实际是在申明 block 时,
    # 被复制到 main_block_impl_0 结构体中的那个变量 a。因为这样,我们就能理解,在 block 内部修改变量 a 的内容,
    # 不会影响外部的实际变量 a。

 # 下面这个是 使用 __block的情况
   struct __main_block_impl_0 {
       struct __block_impl impl;
      struct __main_block_desc_0* Desc;
      __Block_byref_i_0 *i; // by ref
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    }
 };
   # main_block_impl_0 中引用的是 Block_byref_i_0 的结构体指针,这样就可以达到修改外部变量的作用。
    __Block_byref_i_0 结构体中带有 isa,说明它也是一个对象

对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的,如下图所示(图片来自 这里):

对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的,如下图所示(图片来自 这里):

担心循环引用?

只要看持有block的对象是不是也被block持有,如果没有持有,就不用担心循环引用问题了。 __weak typeof(self)weakSelf = self; 也可以打断循环引用的引用链

typeof()的作用:gcc的一个扩展。可以看成一个一元运算符。 typeof和sizeof用法非常类似! sizeof(exp.)返回的是exp.的数据类型大小; typeof(exp.)返回的就是exp.的数据类型。exp.可以是任意类型,所以返回的也是和exp.对应的任意类型。 通俗的说就是:可以根据typeof()括号里面的变量,自动识别变量类型并返回该类型。 __weak UIViewController* weakSelf = self; __weak typeof(self)weakSelf = self; 作用是等同的。

block对于以参数形式传进来的对象,会不会强引用?

其实block与函数和方法一样,对于传进来的参数,并不会持有

我们对截获的变量可以进行操作,而不能直接进行赋值,如果在Block内部修改局部变量的值需要用到 _block 修饰才行。

代码语言:javascript复制
# 对截获的变量可以进行操作进
 NSMutableArray *array = [[NSMutableArray alloc]init];
    void (^blo)() = ^{
    [array addObject:@"Obj"];
  };
#  _block 修饰才可以修改局部变量
__block int b = 0;
 void (^blo)() = ^{
    b = 3;
  };

为什么需要Block变量名称?我们可以这样理解,我们通过这个Block变量名称来获取Block的指针,然后通过这个指针就可以来使用Block函数。我们先来看一下如何声明一个Block变量

代码语言:javascript复制
# 反编译 block
clang -rewrite-objc main.m  
# 可以理解为block的基类
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

Block的使用

Block不但可以作为独立的函数使用(有参数和返回值),也能够当作函数参数,首先我们声明一个Block类型变量 ,并加上typedef修饰符即可。

代码语言:javascript复制
  typedef void(^Blo)(NSString *s1,UIColor *c);

逆向传值 前面我们已经知道Blcok是一个匿名函数,同时也是一个指针,那么使用Block就可以弥补在iOS中函数传递的功能。通常是这么用的:

代码语言:javascript复制
  页面B的.h文件中定义了这样一个Block执政,然后声明了一个变量,像这样:

  typedef void(^Blo)(NSString *s1,UIColor *c);
  @property (nonatomic, copy) Blo block;
  然后我们在页面A当中有这么一段代码:

  ViewController *b = [[ViewController alloc]init];
  __weak  ViewController *wself = self;
  b.block = ^(NSString *s1,UIColor *c){
      NSLog(@"%@",s1);
      wself.view.backgroundColor = c;
  };
  [self.navigationController pushViewController:b animated:true];
  然后在页面B的任意地方我们调用block变量,像这样:

   self.block(@"str",[UIColor redColor]);
 # 就会在A页面中调用B页面传过来的参数,在A页面进行操作,对控制器A进行改变,这样的做法通常用做 控制器 反向传值。

Block的使用中很容易出现的问题

(1)一个类中有一个Block性质的属性,并且在代码里面有用到,如果在对象初始化的时候,不做处理是会崩溃的,这也是block不方便的地方,不像代理可以实现也可以不实现。

代码语言:javascript复制
iOS block中 EXC_BAD_ACCESS(code=1,address= 0x10)
[self.navigationController pushViewController:[[XSDCSearchViewController alloc]init] animated:NO];
  XSDCSearchViewController
           self.seachParameter(dic);
报错  EXC_BAD_ACCESS(code=1,address= 0x10)

有两处的跳转VC都需要实现block性质的属性,只设置了一处,忘记了这处设置,造成了崩溃。

(2)在block中 alloc init一个变量 并且 push到这个对象中时是会 崩溃的。

代码语言:javascript复制
  block 中引用一个对象。
  XMGPerson *p = [[XMGPerson alloc] init]; 
  __weak XMGPerson *weakP = p;
  如果直接在block中  alloc init一个变量  并且 push到这个对象中时是会 崩溃的,
  崩溃发生在这个VC的视图刚刚出现没有多久后。

对于Block我们需要认识到

  • 是C 中的Struct(本文未提到)。
  • 用来弥补iOS中函数传递的功能。
  • 他是一段代码块的内存的指针。
  • 和delegate一样的功能,但是显的更加简洁。
  • block的代码是内联的,效率高于函数调用
  • block对于外部变量默认是只读属性
  • block被Objective-C看成是对象处理

小结

后续会持续更新

本文参考文章 深入浅出-iOS Block原理和内存中位置 唐巧-谈Objective-C block的实现 深究Block的实现 Objective-C中的Block

0 人点赞