深入了解Block的奥秘

作者: 人仙儿a | 来源:发表于2017-11-28 14:11 被阅读89次

前言

block可以叫回调代码块,是iOS开发中至关重要的形式之一。不同的编程语言都会用到block, 只是体现形式有所不同,例如c/c++函数指针javascript叫它闭包。它用简单的方式帮我们解决了很多复杂的问题。

block如何将变量传递及持有

测试代码:

传递原则:

  1. 捕获对象是基础类型变量,如int, double类型时,是值传递。
  2. 捕获对象是一个object,那么它会被强引用

我们来验证一下,我们在block赋值之后修改a的值
ViewController.m:

    int a = 10;
    _foo.testBlock= ^() {
        _testView.backgroundColor = nil;//持有了self
        NSLog(@"a:%d", a);
    };
    a+= 10;
    _foo.testBlock();

输出结果:仍然是10,它不会被外界改变。
但是如果我们用__block来修饰int a,也就是 __block int a = 10, 最终a的值就是20,它被外界改变了,__block帮我们解决问题。

但是Why? block内部是以什么形式存在,并捕获值的呢?接下来我们要一探究竟。

准备工作:clang命令

大家可以用clang(或者gcc) -rewrite-objc xxxxx.m命令来查看转化成的c++代码来了解内幕。如果你引用了UIKIt库,这个命令会报错,那个因为命令里没有指定sdk的版本,此时用下面的命令完美解决:

clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

__block的奥秘

不带__block的转化cpp代码:

    int a = 10;
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, a, 570425344)));
    a+= 10;
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();

带__block转化代码:

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};//对a的封装进行初始化
    ((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, (__Block_byref_a_0 *)&a, 570425344)));
    (a.__forwarding->a)+= 10;
    ((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();

__Block_byref_a_0的定义如下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;//---->它就是传递进来的a
};

我们发现编译器把int a封装成了一个叫__Block_byref_a_0的结构体,最终用&符号将它的地址传递给了block,所以后面a被修改时,block里面的a也被同时修改。__block的奥秘就是这个!

Block的存在形式

object-c的Block最终以转化成多个了结构体,每个结构体都不同的职责。
__ViewController__viewDidLoad_block_impl_0 包含了block相关的所有信息的
基本构造是:

  1. impl
  2. desc
  3. 引用变量列表:ivar1, ivar2, ivar...
struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;//block的一个简化类,它里面存储了函数指针
  struct __ViewController__viewDidLoad_block_desc_0* Desc;//block的描述类
  ViewController *self;//被引用的viewcontroller
  __Block_byref_a_0 *a; // 被引用的a的封装
  __ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *_self, __Block_byref_a_0 *_a, int flags=0) : self(_self), a(_a->__forwarding) {//构造函数
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__block_impl定义如下,很像一个类,里面有isa指针和block的函数指针,所以我们可以把它当作一个类

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

我们可以发现__ViewController__viewDidLoad_block_impl_0结构体的第一个参数是__block_impl,所以这两个结构体实例的地址是相同的。那到底oc的block是谁呢?我们一般会认为oc的block就是__block_impl,因为它正好有isa,可以作为一个类,这样大家都容易理解。

Block的三个子类

1.我们可以打印一个block的类及父类的名字:(这段代码摘自facebook的FBRetainCycleDetector)

static Class _BlockClass() {
  static dispatch_once_t onceToken;
  static Class blockClass;
  dispatch_once(&onceToken, ^{
    void (^testBlock)() = [^{} copy];
    blockClass = [testBlock class];
    while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) {
      blockClass = class_getSuperclass(blockClass);
    }
    [testBlock release];
  });
  return blockClass;
}

结果是__NSGlobalBlock -> NSBlock -> NSObject
事实上block有三种形式:

  • __NSGlobalBlock 全局 (未捕获变量)
  • __NSStackBlock 栈 捕获变量
  • __NSMallocBlock 堆 捕获变量

在 ARC 中,捕获外部了变量的 block 的类会是 NSMallocBlock 或者 NSStackBlock,如果 block 被赋值给了某个变量在这个过程中会执行 _Block_copy 将原有的 NSStackBlock 变成 NSMallocBlock;但是如果 block 没有被赋值给某个变量,那它的类型就是 NSStackBlock;没有捕获外部变量的 block 的类会是 NSGlobalBlock 即不在堆上,也不在栈上,它类似 C 语言函数一样会在代码段中。

2.那什么时候在堆上,什么时候在栈上呢?
在ARC有效时,大多数情况下编译器会进行判断,自动生成将Block从栈上复制到堆上的代码,以下几种情况栈上的Block会自动复制到堆上:

  1. 调用Block的copy方法
  2. 将Block作为函数返回值时
  3. 将Block赋值给__strong修改的变量时
  4. 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时

3.block用strong修饰还是copy修饰呢?
实际上调用retain方法时, block会调用copy方法,所以这两种修饰是相同的。但是为了语义的明确,推荐用copy修饰。

获取block引用的对象

现在不讲源码,只讲实际应用。如何知道一个block对象,如何知道它持有的所有对象呢?facebook的FBRetainCycleDetector检测循环引用的库就实现这样的功能,非常的巧妙!我们就FBRetainCycleDetector的源码展出分析。
1.调用allRetainedObjects来获取:

- (NSSet *)allRetainedObjects
{
  NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy];

  // Grab a strong reference to the object, otherwise it can crash while doing
  // nasty stuff on deallocation
  __attribute__((objc_precise_lifetime)) id anObject = self.object;//objc_precise_lifetime 翻译一下就是:精确生命周期,其实是强引用了object对象,防止在运行期间被释放

  void *blockObjectReference = (__bridge void *)anObject;
  NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference);//获得所有引用对象,就是下面要讲的方法

  for (id object in allRetainedReferences) {
    FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration);//对每个对象进行包装成box对象
    if (element) {
      [results addObject:element];
    }
  }

  return [NSSet setWithArray:results];
}

2.FBGetBlockStrongReferences方法通过调用_GetBlockStrongLayout(block)方法返回的持有对象的位置Index, 然后通过偏移量来取得对应的对象:

NSArray *FBGetBlockStrongReferences(void *block) {
  if (!FBObjectIsBlock(block)) {//是否是block类型
    return nil;
  }
  
  NSMutableArray *results = [NSMutableArray new];

  void **blockReference = block;
  NSIndexSet *strongLayout = _GetBlockStrongLayout(block);//得到block里强引用的对象
  [strongLayout enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
    void **reference = &blockReference[idx];//把它当作了一个链表来取值,它本是block结构体,正好它的align对齐字节大小是void*类型大小。怪不得blockReference要用void **来修饰,要以把它当作(void *)*blockReference,所以blockReference是一个指向(void *)的指针。

    if (reference && (*reference)) {
      id object = (id)(*reference);

      if (object) {
        [results addObject:object];
      }
    }
  }];

  return [results autorelease];
}

我写了一个demo, block里引用了viewcontroller, 但是viewController的idx是4,为什么呢?我们打印了blockLiteral结构体及成员变量的地址:


B8A28F62-BFEB-4BEF-B26A-41D0DBC475CA.png
struct BlockLiteral {
  void *isa;  //0x00000001702493f0  占8个字节
  int flags;//0x00000001702493f8  占4个字节
  int reserved;//0x00000001702493fc 占4个字节
  void (*invoke)(void *, ...);//0x0000000170249400 占8个字节
  struct BlockDescriptor *descriptor;//0x0000000170249408 占8个字节
  // imported variables
};

所有指针类型全是占8个字节,int类型4个字节,所以flags+reserved加起来是相当于一个指针类型,不管怎么说,BlockLiteral的大小是固定的,它的对齐字节是8,它的大小正好是sizeof(void)的整数倍。这也解释了为什么定义了void **blockReference = block;,它把blockReference定义成了指向(void)的指针!(这里写的有点啰唆,实际上void*是最长的字节8,因为Int类型字节长只有4,所以是以void*作为进行字节对齐的)
3._GetBlockStrongLayout是整个过程最关键,也是最巧妙的地方:

static NSIndexSet *_GetBlockStrongLayout(void *block) {
  struct BlockLiteral *blockLiteral = block;

  /**
   BLOCK_HAS_CTOR - Block has a C++ constructor/destructor, which gives us a good chance it retains
   objects that are not pointer aligned, so omit them.

   !BLOCK_HAS_COPY_DISPOSE - Block doesn't have a dispose function, so it does not retain objects and
   we are not able to blackbox it.
   */
  if ((blockLiteral->flags & BLOCK_HAS_CTOR)
      || !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) {
    return nil;
  }

  void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;
  const size_t ptrSize = sizeof(void *);

  // Figure out the number of pointers it takes to fill out the object, rounding up.
  const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;

  // Create a fake object of the appropriate length.
  void *obj[elements];
  void *detectors[elements];

  for (size_t i = 0; i < elements; ++i) {
    FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
    obj[i] = detectors[i] = detector;
  }

  @autoreleasepool {
    dispose_helper(obj);
  }

  // Run through the release detectors and add each one that got released to the object's
  // strong ivar layout.
  NSMutableIndexSet *layout = [NSMutableIndexSet indexSet];

  for (size_t i = 0; i < elements; ++i) {
    FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]);
    if (detector.isStrong) {
      [layout addIndex:i];
    }

    // Destroy detectors
    [detector trueRelease];
  }

  return layout;
}

重写了FBBlockStrongRelationDetector类的release方法:

- (oneway void)release
{
  _strong = YES;
}

这个过程是:

  1. 获取销毁函数dispose_helper的函数指针
  2. 通过descriptor->size计算出block里成员变量个数(上面我们说过它把两个int算作了一个void*, 所以成员变量的个数实际上少了1个)
  3. 创建两个相同个数的fake数组obj,detectors,然后通过dispose_helper来只释放obj数组,dispose_helper会向调用每个对象的release方法, 而它又重写了release方法,在release时作了标记,通过这个标记就可以判断是为是引用的对象。它其实是欺骗了dispose_helper函数,因为它只认位置Index, 并不关心数组存储的是什么...

不得不赞叹fb的源码的深度和创新~

结语

还是那句话:源码下面无秘密
苹果底层对于block实现真是煞费苦心。我们了解了原理,用起来会更加得深应手。

相关文章

  • 深入了解Block的奥秘

    前言 block可以叫回调代码块,是iOS开发中至关重要的形式之一。不同的编程语言都会用到block, 只是体现形...

  • 深入了解Block的奥秘

    前言 block可以叫回调代码块,是iOS开发中至关重要的形式之一。不同的编程语言都会用到block, 只是体现形...

  • Objective-C block 深入了解

    Objective-C block 深入了解

  • Block的深入了解

    block的结构体如下 Block是带有自动变量的匿名函数; 有三种类型的Block: _NSConcreteGl...

  • 简单深入了解block

    用了block很长时间,也能避免相关的使用问题,想研究下大体底层实现,看了很多的优秀博客,这里写一下自己的理解。 ...

  • 【瑜乐圈】—身体是可见的灵魂,灵魂是不可见的身体

    整个宇宙里最大的奥秘就是身体。我们需要很深的了解它的奥秘,它的存在必须被深入的探究。 在今天世界上,需要一种全新的...

  • 身体是可见的灵魂,灵魂是不可见的身体!

    1 整个宇宙里最大的奥秘就是身体。我们需要很深的了解它的奥秘,它的存在必须被深入的探究。 在今天世界上,需要一种全...

  • 关于Block的一些日常用法

    对于iOS开发者来说,相信block大家应该都耳熟能详。网上关于block的介绍和深入了解也有很多文章,这里就不对...

  • 探究 Block 的奥秘

    闲来无事,总结了一下 block 的几点知识,以作巩固,欢迎指正。 一、block 的本质block 本质上是一个...

  • Block的使用

    本文简介 本文不会太深入的去了解block只是简单的介绍一下block的实际使用,总体来说是比较适合没有使用过bl...

网友评论

    本文标题:深入了解Block的奥秘

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