Block底层分析

1,396 阅读9分钟
  • block简介

    Block块是封装工作单元的对象,是可以在任何时间执行的代码段。其本质上是可移植的匿名函数,可以作为方法和函数的参数传入,可以从方法和函数中返回。—(翻译自官方文档)

    块是对C语言的一种扩展,它并未作为标准的ANSI C所定义的部分,而是有苹果公司添加到语言中的。块看起来更像是函数,可以给块传递参数,块也可以具有返回值。

  • block类型

    • __NSGlobalBlock__全局block,存储在全局区
      先看如下代码 image.png 发现就是简单的声明一个block,block没有入参,没有block内部代码块只是简单的打印没有引用外界变量,则此时的block类型是__NSGlobalBlock__
    • __NSStackBlock__栈区block
      image.png 在外界变量没有处理之前此时的类型是__NSStackBlock__,外界变量处理之后底层会对block进行拷贝从栈区拷贝到堆区。在ARC下,编译器做了很多的优化,往往看不到本质,上面的代码输出结果找不到,因为编译器对__NSStackBlock__自动进行了copy操作。改为MRC就可以看到不管是否对变量进行处理都会打印__NSStackBlock__

      改为MRC方法: Build Settings 里面的Automatic Reference Counting改为NO。 当然也可以用__weak修饰block,不做强引用一样不会copy所以此时打印的block还是栈区block image.png

    • __NSMallocBlock__堆区block
      image.png 栈区block底层拷贝之后变成堆区block
  • block本质

    • 编译后的代码探索
      首先写一个简单的block然后查看编译后的代码 image.png image.png 发现底层是将__main_block_impl_0函数地址赋值给了block此时我们再看__main_block_impl_0的结构 image.png 从这里可以发现block本质其实就是一个结构体,结构体中又存在着isa,也可以说block本质就是一个OC对象,一个封装了函数调用以及函数调用环境的OC对象
      从结构体中也看到了函数的赋值,这也说明了为什么block需要调用后才会执行代码块,应为在底层只是函数指针的赋值,并没有主动调用。 image.png编译后的代码我们也可以看出在调用的时候是调用的FuncPtr,并将block作为入参传递
  • block如何捕获变量

    上文分析本质的时候写了一个简单的block没有任何入参,也没有使用外界变量,再写一个使用外界变量的block,看看底层block如何使用这些外界变量的,在外界定义一个变量a,然后在代码块中打印a,查看编译后的代码: image.png image.png image.png

    • 总结
      发现底层block结构体中多了一个同名的参数,初始化的时候将外界的变量赋值给这个同名变量,其中局部变量会生成一个变量进行值拷贝,全局变量不捕获变量直接使用外界变量,静态变量是是有一个同名变量指针,是指针拷贝
  • __block原理

    block代码块中直接修改外界没有使用__block修饰的变量时回报如下错误 image.png此时我们在使用__block修饰发现没有报错并且修改成功 image.png通过clang查看编译后的代码 image.png 发现编译后a此时取得是地址不单单是简单的数值并且被转换成了__Block_byref_a_0类型 image.png通过编译后的代码发现__Block_byref_a_0是一个结构体,然后block的结构体同时也多了一个__Block_byref_a_0类型的指针所以底层a是指针复制也就是和外界的a变量指向了同一片内存地址,所以此时使用__block修饰的变量能够修改成功

  • block真正的类型Block_layout

    • 通过代码调试找到block的真正类型
      通过汇编跟踪发现首先声明block的时候首先会走到objc_retainBlock方法中去如图image.png此时我们下一个符号断点objc_retainBlock,发现objc_retainBlock方法里面会跳转到_Block_copy方法中去,此时断点继续往下跟发现第一次会走进libobjc.A.dylib库中的_Block_copy方法,方法内部呢又跳转了一个_Block_copy方法,同样的继续往下跟发现走进了libsystem_blocks.dylib库中的_Block_copy方法,此时我们再冲源码中找block的真正类型image.png发现block的真正类型是Block_layout
    • Block_layout源码探索
      通过源码发现底层就是一个结构体如图 image.png flags标识说明
      • 第1位 - BLOCK_DEALLOCATING,释放标记,-般常用BLOCK_NEEDS_FREE做位与操作,一同传入Flags,告知该block可释放。
      • 低16位 - BLOCK_REFCOUNT_MASK,存储引用计数的值;是一个可选用参数
      • 第24位 - BLOCK_NEEDS_FREE,低16是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的值;
      • 第25位 - BLOCK_HAS_COPY_DISPOSE,是否拥有拷贝辅助函数(a copy helper function);
      • 第26位 - BLOCK_IS_GC,是否拥有block析构函数;
      • 第27位,标志是否有垃圾回收;//OS X
      • 第28位 - BLOCK_IS_GLOBAL,标志是否是全局block;
      • 第30位 - BLOCK_HAS_SIGNATURE,与BLOCK_USE_STRET相对,判断当前block是否拥有一个签名。用于 runtime 时动态调用。 descriptor说明:
        block的附加信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。有三类
      • Block_descriptor_1是必选的
      • Block_descriptor_2Block_descriptor_3都是可选的 image.png 再看Block_descriptor_2Block_descriptor_3的构造函数 image.png 发现都是通过Block_descriptor_1的内存地址平移得来
  • block三层拷贝分析

    注意:能出发三层拷贝的情况是,在block中捕获用__block修饰的对象的时候出发

    • 第一层拷贝_Block_copy
      这一层拷贝主要是将block从栈区拷贝到堆区 image.png 源码也比较简单,主要分为以下几个步骤
      1. 首先判断入参是否为空是空则返回空
      2. 判断block是否需要释放,需要则不拷贝
      3. 判断是否为全局block是则不拷贝
      4. 最后则是栈区block,首先第一步就是开辟内存空间
      5. 然后就是内存拷贝,将aBlock拷贝到result
      6. 最后就是简单的赋值操作然后返回result
    • 第二层拷贝_Block_byref_copy
      主要针对外界变量使用__block修饰的时候拷贝成Block_byref结构体 从下文中的_Block_object_assign源码分析知道如果是__block修饰的对象会交给_Block_byref_copy去处理此时我们再看_Block_byref_copy的源码 image.png 从改源码中就可以看到为什么使用__block修饰的变量在block中能够直接修改对应的值,我看再看编译后的代码image.png发现用__block修饰的对象转换成__Block_byref_person_0类型再看__Block_byref_person_0结构体 image.png发现多了两个函数__Block_byref_id_object_copy__Block_byref_id_object_dispose,暂时不知道多的这两个函数是怎么用的,这时候我们再回到源码先看Block_byref结构体 image.png 发现一个普通的Block_byref结构中是没有这两个函数的,但是Block_byref_2刚好存在着两个函数分别是copydispose,再看_Block_byref_copy函数的源码发现如果存在copydispose方法的时候会有调用copy方法image.png,这里再回到编译后的代码上看copy的实现 image.png发现又是调用的_Block_object_assign方法,此时传入的就是普通的对象交给系统arc去处理,然后做一个指针拷贝,至此这一层拷贝就找到了
    • 第三层拷贝_Block_object_assign
      先看编译后的源码 image.png 发现有两个方法__main_block_copy_0__main_block_dispose_0方法里面的实现分别调用的是_Block_object_assign__main_block_dispose_0,冲源码中也可以知道这两个方法分别对应着Block_descriptor_2中的copydispose此时我们再看_Block_object_assign的源码实现 image.png 通过源码发现最主要的步骤有三种
      1. 判断如果是简单的普通对象类型则交给arc去处理
      2. 如果是block类型则交给_Block_copy去处理
      3. 如果是使用__block修饰的对象则交给_Block_byref_copy去处理该方法的源码分析可看面内容
  • block循环引用

    • 造成循环引用的原因
      造成循环引用的根本原因就是相互持有都不能释放,比如当前类自己有个block,然后还有个属性name如果在block中使用name,那么在block中就会只有当前这个对象(上文中也分析过了,此情况,block内部结构体会添加一个同样的对象做持有当前对象,底层传入普通对象会进行指针拷贝并且引用计数加一)这就造成了相互持有循环引用。
    • 循环引用解决办法
      • **__weak、__strong结合使用 **
        • 如果是简单的block中需要使用self中的属性直接使用__weak就可以了,这样两个指针都指向同一片内存地址但是使用__weak修饰不会造成引用计数加一。
        • 如果是block中又嵌套block的情况
          __weak typeof(self) weakSelf = self;
          self.tdBlock = ^(void){
              __strong typeof(weakSelf) strongSelf = weakSelf;
              dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                  NSLog(@"%@",strongSelf.name);
              });
          };
          self.tdBlock();
          
          这个情况外面一样的使用__weak去修饰,但是在第一个block里面在使用__strong去修饰。应为__strong修饰的变量在代码块中是个局部变量,所以block代码执行完成之后会自动释放,所以不会造成循环引用,为什么要在block中在使用__strong修饰呢?主要是应为里面的block是一个延时操作如果不是用__strong修饰那么执行完析构函数weakSelf就变成nil了,所以在嵌套的block,中拿到的weakSelf,就是nil,此时在使用__strong是为了延长weakSelf声明周期,让其在嵌套的block执行完成之后再销毁
      • __block修饰变量
        使用__block修饰是利用了在block内部可以修改变量的值的属性,同样的在定义block的时候self的引用计数会加一,但是在使用完成之后可以吧变量置为nil,那么self的引用计数又会减一,所以不会造成循环引用。(注意:这种方法block必须要调用如果不调用变量无法置空一样会造成循环引用
      • 对象self作为参数
        当做参数传入,此时的self就会被作为临时变量压栈进来,所以就不会造成持有,也就不会造成循环引用(函数参数是存在栈区的,是由编译器自动分配和释放的)
      • NSProxy 虚拟类
        • OC是只能单继承的语言,但是它是基于运行时的机制,所以可以通过NSProxy来实现 伪多继承,填补了多继承的空白
        • NSProxy NSObject是同级的一个类,也可以说是一个虚拟类,只是实现了NSObject的协议
        • NSProxy其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重写下面两个方法来实现消息转发到另一个实例
          - (void)forwardInvocation:(NSInvocation *)invocation;
          - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel