Block底层分析

  • block简介

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

    块是对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修饰的变量可以修改为功ui

  • 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修饰的对象的时候出发spa

    • 第一层拷贝_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
相关文章
相关标签/搜索