首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >编码篇-Block里面的小天地

编码篇-Block里面的小天地

作者头像
進无尽
发布2018-09-12 18:01:50
5910
发布2018-09-12 18:01:50
举报
文章被收录于专栏:進无尽的文章進无尽的文章

前言

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

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.01.22 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • Block是什么
      • Block是对象吗?
        • Block的分类
      • Block的声明属性时的关键字
        • Block对于局部变量的修改问题
      • 担心循环引用?
        • Block的使用
          • Block的使用中很容易出现的问题
            • 对于Block我们需要认识到
              • 小结
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档