深入了解Block的奥秘

前言

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实现真是煞费苦心。我们了解了原理,用起来会更加得深应手。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 前言 block可以叫回调代码块,是iOS开发中至关重要的形式之一。不同的编程语言都会用到block, 只是体现形...
    人仙儿a阅读 270评论 0 0
  • 摘要 Blocks是C语言的扩充功能, iOS 4中引入了这个新功能“Blocks”,那么block到底是什么东西...
    CholMay阅读 1,099评论 2 10
  • Blocks Blocks Blocks 是带有局部变量的匿名函数 截取自动变量值 int main(){ ...
    南京小伙阅读 882评论 1 3
  • 摘要block是2010年WWDC苹果为Objective-C提供的一个新特性,它为我们开发提供了便利,比如GCD...
    西门吹雪123阅读 882评论 0 4
  • 1. Block的底层结构 以下是一个没有参数和返回值的最简单的Block: 为了探索Block的底层结构,需要将...
    再好一点点阅读 417评论 0 4