美文网首页
编码篇-Block里面的小天地

编码篇-Block里面的小天地

作者: 進无尽 | 来源:发表于2018-01-22 17:14 被阅读0次

    前言

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

    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 的数据结构定义如下(图片来自 这里):

    对应的结构体定义如下:

    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 部分构成:

    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 语言实现的。

     //我们使用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 修饰才行。

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

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

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

    Block的使用

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

      typedef void(^Blo)(NSString *s1,UIColor *c);
    

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

      页面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不方便的地方,不像代理可以实现也可以不实现。

    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到这个对象中时是会 崩溃的。

      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

    相关文章

      网友评论

          本文标题:编码篇-Block里面的小天地

          本文链接:https://www.haomeiwen.com/subject/nxqlyttx.html