漫谈Block

一、Objective-C发展史

Objective-C从1983年诞生,已经走过了30多年的历程。随着时间的推移,Objective-C支持很多特性,下面是几个重要的发展节点:

  • Object Oriented C —1983
  • Retain and Release
  • Properties —2006(Objective-C2.0发布)
  • Blocks —Mac OS X 10.6(June 8, 2009) & iOS4.0(June 21, 2010)
  • ARC —Mac OS X 10.6&iOS5(2011)

以上发展节点来自这里,时间来自维基百科和其他网站。至于为什么会先说这些,主要我发现很多同学在了解一项技术时,没有一个立体的概念,经常看后会很快忘记。当我们对一个知识的来龙去脉了解时,这个知识点就不再是个点,而会变成一个面,起到帮助我们记忆的作用。另外我建议大家深入学习某项技术时,不要仅看一些人的分享,现在知识分享已经没有门槛,经常会看到人云亦云的博客(当然也可能包括我:)),最好的方式就是先从博客上了解知识,然后从官方途径上去学习验证。推荐大家几个网站,第一个当然是苹果的开发文档;第二个就是WWDC视频;如果你还是不尽兴,想从源码方面上了解,可以去看下苹果开放的源码-runtimelibDispatchlibclosure,和clang文档

二、Block的由来

闭包

在讲Block之前,我们先了解下闭包的概念:

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。—维基百科

从上面维基百科的解释中,我们可以看到,一个闭包即有捕获自由变量的特性,又具有函数的特性。闭包在我们日常工作中,经常用于回调,尤其在延迟调用(也称为惰性求值)的情况下非常有用。比如我们经常需要在网络请求完成时更新UI,网络请求需要一定时间,在请求结束时再调用更新UI的方法。

Block

先从苹果官方文档上,看下关于Block的描述

Block objects are a C-level syntactic and runtime feature. They are similar to standard C functions, but in addition to executable code they may also contain variable bindings to automatic (stack) or managed (heap) memory. A block can therefore maintain a set of state (data) that it can use to impact behavior when executed.

简单来讲Block是Apple实现的闭包,它适用于C++,Objective-C,Objective-C++。另外Block需要runtime和clang的支持,下面会讲到

三、Block使用

先看一个使用Block的场景,代码如下:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

Block的使用和函数指针使用非常相似,最大的不同,可能就是函数指针的*变成了^。下面我们看下Block有哪些东西

blocks.jpg

三、Block存储域

在iOS开发过程中,有三种不同类型的Block,分别是:

  • _NSConcreteStackBlock, 栈上的Block,在出了作用域之后会被释放
  • _NSConcreteMallocBlock,堆上的Block,在MRR环境需要手动释放
  • _NSConcreteGlobalBlock,全局的Block,存在data区域,生命周期和应用一样

在我们初始化Block时只有_NSConcreteStackBlock/_NSConcreteGlobalBlock两种类型。注意我们是无法生成_NSConcreteMallocBlock的,只有在_NSConcreteStackBlock调用__Block_copy时才会被copy到堆上,生成_NSConcreteMallocBlock。这一点可以从clang文档中了解到,可能现在看懂这些还比较困难,没关系,我们下面在Block内部实现会讲到。

有两个问题需要我们注意下:

  • 在初始化Block时,我们怎么判断是_NSConcreteStackBlock还是_NSConcreteGlobalBlock

    这里有个快速判断的方法。如果Block的body里使用到了外部的非全局变量和非static静态变量,那么这个Block就会在栈上创建即_NSConcreteStackBlock。反之如果没有引用变量或者仅引用了全局变量或者static静态变量则是全局Block_NSConcreteGlobalBlock,下面有几个例子:

    // _NSConcreteGlobalBlock
    int multiplier = 7; // 全局变量
    {
        int (^myBlock)(int) = ^(int num) {
          return num * multiplier;
      };
    }
    // _NSConcreteGlobalBlock
    static int multiplier = 7; // 静态变量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
    // _NSConcreteStackBlock MRR环境
    int multiplier = 7; // 局部变量或者实例变量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
    // _NSConcreteMallocBlock ARC环境
    // 注意这里虽然在打印myBlock时显示为NSMallocBlock,但是这并不是说我们创建出了_NSConcreteMallocBlock,而是被隐式地调用了copy
    int multiplier = 7; // 局部变量或者实例变量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    
  • Block在MRR环境和ARC环境下使用时,需要注意哪些情况?

    1、在MRR环境下系统提供了两种方式把Block从栈上copy到堆上,第一种是显式调用copy,第二种是显式调用Block_copy宏。当然如果使用属性的话,则需要用copy标记,如

    @property (copy) int(^Blk)(int)

    而使用retain标记属性是不行的,大家可以试一下

    2、在ARC环境下,编译器帮助我们自动插入了copy操作,省去了我们很多的工作量,以下几种情况编译器会隐式执行copy操作:

    • 将Block作为函数返回值时
    • 将Block赋值给__strong修改的局部变量,或者标记为strongcopy的属性时
    • 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时

    虽然上面的几种情况几乎涵盖了所有Block使用场景,不过仍然一种情况需要我们考虑到,就是把Block当方法参数传递给我们自定义方法时进行传递时,系统是不帮我们copy的,在使用时需要我们手动进行copy,如:

    {
        int multiplier = 7; 
        print(^(int num) {
          return num * multiplier;
      };)
    }
    void print(Blk blk) {
        // NSStackBlock
      blk(1);
    }
    

四、Block的内部实现

在看源码之前,我们先大概捋下如果让我们自己实现一个闭包该怎么实现,或者需要考虑什么?比如如何捕获外部变量;如何管理外部变量的内存;调用方式是怎样的?如果解决了这些问题,我们也能写一个闭包的实现,无非是没有编译器的加持,调用起来不是那么优雅。好了,说了这么多,我们带着这些疑问去看Block的实现源码,思路会更加清晰。

我们先准备好clang里的说明文档(这个文档解释Block内部实现)和Block实现源码

Block的实现主要包括四个文件:Block.hBlock_private.hdata.cruntime.c

  • Block.h

    // Create a heap based copy of a Block or simply add a reference to an existing one.
    // This must be paired with Block_release to recover memory, even when running
    // under Objective-C Garbage Collection.
    BLOCK_EXPORT void *_Block_copy(const void *aBlock);
    
    // Lose the reference, and if heap based and last reference, recover the memory
    BLOCK_EXPORT void _Block_release(const void *aBlock);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);
    
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_dispose(const void *, const int);
    
    // Used by the compiler. Do not use these variables yourself.
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
    BLOCK_EXPORT void * _NSConcreteStackBlock[32];
    
    #if __cplusplus
    }
    #endif
    
    // Type correct macros
    
    #define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
    #define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
    

    这个文件包括Block向外提供的方法,我们可以看到Block_copy宏实现,其实就是调用了_Block_copy方法,Block_release宏调用了_Block_release方法,所以我们也可以将宏调用改为方法调用。这个文件是暴露出去的,我们可以在工程中看到:路径在/usr/include/Block.h,这四个文件只有这个文件是暴露的,用来给我们调用

  • data.c

    BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; 
    BLOCK_EXPORT void * _NSConcreteAutoBlock[32] = { 0 }; // only used in GC
    BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32] = { 0 }; // only used in GC
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32] = { 0 }; // only used in GC
    

    这个文件定义了6种Block类型。其中三种类型只有GC环境才会出现,因为GC在ARC推出后,已被苹果废弃了。所以我们只关心其中三种类型即可,这也印证了我们上面说的三种Block类型

    BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 };
    BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; 
    BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
    
  • Block_private.h

    这个文件就是Block的具体结构

  • runtime.c

    这个文件描述了Block的copy/release和Block持有变量的copy/release操作。

好了,这就是Block的所有文件,Block_private.hruntime.c两个文件是我们需要重点关注。哦,其实还有一个关联的文件没说,先放着不管,在讲到runtime.c会引出该文件,这个文件很重要,如果没有这个文件我们在阅读到某块代码时会有点莫名其妙。

Block的内部结构

首先我们从Block内部结构说起,上面我们已经讲到,它在Block_private.h文件中,先贴下Block的结构代码

#define BLOCK_DESCRIPTOR_1 1
/* Block描述1
 * 如果是_NSConcreteStackBlock时,在copy到堆上时,需要确定Block的大小,用来在堆上分配空间
*/
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size; // Block's size
};

#define BLOCK_DESCRIPTOR_2 1
/* Block描述2
 * 如果是_NSConcreteStackBlock并且捕获了__block修饰的变量或者(id, NSObject, __attribute__((NSObject)), block, ...)类型的变量,在copy到堆上时需要生成copy方法,这个方法用来解决捕获的变量在被copy时要执行的动作。
 * 如果是_NSConcreteGlobalBlock则不会生成该结构
 */
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

// Block的内部布局
struct Block_layout {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 
    volatile int32_t flags; // contains ref count 这个值主要用来告知系统Block在copy时应该执行什么操作
    int32_t reserved; 
    void (*invoke)(void *, ...); // function ptr
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

从上面我们可以了解到,Block由三个struct构成,每个struct我都做了详细的注释。Block_layout中的flags在文件中也有描述:

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime  only use in GC
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code  only use in GC
    BLOCK_IS_GC =             (1 << 27), // runtime  only use in GC
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE only use in GC
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler  only use in GC
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler  only use in GC
};

去掉GC环境需要的值

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
};

flags只有这四种值需要我们关注,其中是BLOCK_HAS_COPY_DISPOSEBLOCK_IS_GLOBAL是我们在使用block时由编译器根据上下文生成的。另外两个是在Block被copy时,runtime用到和修改的,这里的runtime特指runtime.c文件,在分析runtime.c文件时,再具体说明。

Block在捕获非全局和非静态变量时,都是copy的不可变变量,在Block的body里是不能修改这个值的,编译器会给我们提示如:

(注解:Block的body里使用全局变量或者静态变量,这些变量并不会进行copy,它只做使用,也不会去管理这些变量的内存,如果Block仅仅使用了全局变量或者静态变量,Block为_NSConcreteGlobalBlock类型)

block_const_copy.png

如果需要修改变量值,我们可以通过__block修饰变量:

__block int multiplier = 7; // 局部变量或者实例变量
int (^myBlock)(int) = ^(int num) {
    multiplier = 5;
    return num * multiplier;
};
myBlock(1);

__block修饰的变量会被Block_byref这样的结构包起来,具体如下

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding; // 初始化时会指向自己,当Block被copy时,Block_byref也会被copy到堆上,forwarding会指向堆上的Block_byref
    volatile int32_t flags; // contains ref count
    uint32_t size; // Block_byref大小,用来copy时分配内存
};

// Block_byref被copy到堆上和释放时需要的操作
struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
    void (*byref_destroy)(struct Block_byref *);
};

如果变量属于id, NSObject, __attribute__((NSObject)), block, …这种类型,则会生成byref_keepbyref_destroy方法,来管理变量的内存,如果是修饰的是自动变量如int,CGPoint,enum类型,则不会生成结构体Block_byref_2

Block如何管理内存

这里引出另外一个文件runtime.c,该文件包含了Block的copy/release、id, NSObject, __attribute__((NSObject)), block, …类型变量的copy/release和__block修饰的变量的copy/release的具体实现。里面有几个方法是我们需要关注的:

  • Block的copy方法:_Block_copy

    _Block_copy该方法用来确定Block怎样进行copy

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

这个方法通过aBlock->flags确定不同类型Block应该怎么copy,flags的值都在Block_private.h文件中,上 面分析这个文件时有讲到,如果忘记了,往前翻一下。

flagsBLOCK_IS_GLOBAL即Block的isa_NSConcreteGlobalBlock时直接返回不做任何操作

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    ...
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    ...
}

当为BLOCK_HAS_COPY_DISPOSE即Block为_NSConcreteStackBlock时,则在堆上生成同样大小的Block,把堆上的Block的flags改为BLOCK_HAS_COPY_DISPOSE|BLOCK_NEEDS_FREE|2,把isa指向_NSConcreteMallocBlock

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    ...
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

另外会调用_Block_call_copy_helper方法,这个方法实际上会调用Block_descriptor_2Block_private.h里有讲到)的copy方法

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

如果Block已经被copy到堆上,再调用copy方法,则只需要增加Block的引用计数即可,从实现上看,Block在连续调用Block_copy时仅增加了Block的引用计数,并没有增加对Block持有的变量的引用计数

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    ...
}
  • 上面已经说了,如果捕获的变量为id, NSObject, __attribute__((NSObject)), block, …类型变量和__block修饰的变量,则需要调用Block_descriptor_2->copy()方法,该方法最终会调用到_Block_object_assign方法(为什么能调用到它,我只能说是编译器干的,现在不需要关心,等到我们使用clang命令重写Block的使用时,就会明白了)。

    _Block_object_assign方法用来确定被捕获的变量怎样进行copy

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break;
      
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
      default:
        break;
    }
}

该方法根据不同的flags值,执行不同的操作。如果为id, NSObject, __attribute__((NSObject))类型,则flags为BLOCK_FIELD_IS_OBJECT,从上面可以看到调用了_Block_retain_object

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        _Block_retain_object(object);
        *dest = object;
        break;
      ...
    }
}

默认_Block_retain_object方法什么也没做,如果在MRR环境下,libDispatch会调用_Block_use_RR2方法,_Block_retain_object方法会被赋值为retain操作,具体调用看object.m这个文件的void_os_object_init(void)方法,但是在ARC环境下,该文件是不会编译的,文件中也做了说明

object.m
#if _OS_OBJECT_OBJC_ARC
#error "Cannot build with ARC"
#endif

_Block_retain_object实现如下

static void _Block_retain_object_default(const void *ptr __unused) { }
// 默认_Block_retain_object被赋值为_Block_retain_object_default,即什么都不做
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

// Called from CF to indicate MRR. Newer version uses a versioned structure, so we can add more functions
// without defining a new entry point.
// 上面的注释其实不是libclosure-65版本的,我从libclosure-63版本摘过来的,不知道苹果为啥把这个注释去掉了,让人读起源码来,会感觉莫名其妙
void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
    _Block_retain_object = callbacks->retain;
    _Block_release_object = callbacks->release;
    _Block_destructInstance = callbacks->destructInstance;
}

通过上面的分析,可以知道如果在MRR环境下,Block会通过_Block_retain_object方法持有id, NSObject, __attribute__((NSObject))类型变量。而在ARC环境下,_Block_retain_object方法是个空操作,并不会持有该类型变量,那ARC环境下Block怎么样达到持有对象类型的变量呢?ARC环境有了更完善的内存管理,如果外部变量由__strongcopystrong修饰时,Block会把捕获的变量用__strong来修饰进而达到持有的目的。

在使用Block时,最大的困扰就是RetainCycle(循环引用),原因很简单:对象A持有了Block,Block又持有了对象A。因为Block持有了对象A,所以对象A想释放则必须要先释放Block,而Block又由于被对象A持有也释放不了,这就造成了循环引用。解决循环引用有两种办法,一种是手动把Block置为nil来释放Block对对象A的引用;另外一种就是禁止Block强持有对象A,在MRR和ARC环境下禁止Block持有对象A的做法是不一样,ARC环境下只需要把变量加上__weak修饰就可以避免Block持有变量;而在MRR环境下只能通过避免调用_Block_retain_object方法,怎么避免呢,可以往下继续看

(这里吐槽下,苹果的文档真是乱,什么地方都有,找起来很麻烦不说,libDispatch里面竟然也有Block实现,看文档的日期,竟然比libclosure-65还新,一度让我不知道该看哪个,最后我发现,libDispatch里的版本其实不是最新的Block实现,比如libclosure-65版本废弃了_Block_use_RR方法,彻底使用_Block_use_RR2按照文档上的解释,说这种调用方式可以随意增加方法;()

当变量由__block修饰时,该变量会被打包成Block_byref类型,flags会被标记为BLOCK_FIELD_IS_BYREF

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_FIELD_IS_BYREF:
        *dest = _Block_byref_copy(object);
        break;
      ...
    }
}

我们可以看到实际上是调用了_Block_byref_copy方法

该方法先在堆上生成同样大小的Block_byref赋值给堆上的Block,并把flags设置为src->flags | BLOCK_BYREF_NEEDS_FREE | 4

static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;

        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            ...

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    ...
    return src->forwarding;
}

从上面可以看到最终会调用Block_byref的copy方法,该方法又会调用_Block_object_assign方法,如果__block修饰的变量是id, NSObject, __attribute__((NSObject))类型,则flags会被设置成BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT,我们看到仅仅做了赋值操作。所以通过__block修饰可以避免调用到_Block_retain_object方法,也就是在MRR环境下我们可以通过__block来避免Block强持有变量,进而避免循环引用

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        *dest = object;
        break;
      ...
    }
}

当然Block也支持嵌套Block使用,flags会被标记为BLOCK_FIELD_IS_BLOCK,被捕获的Block被copy就是调用上面的_Block_copy方法

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      ...
      case BLOCK_FIELD_IS_BLOCK:
        *dest = _Block_copy(object);
        break; 
      ...
    }
}

回头看下runtime.c这个文件的几个方法

_Block_copy方法用来确定_NSConcreteStackBlock_NSConcreteGlobalBlock_NSConcreteMallocBlock三种类型在Block调用copy时执行的动

_Block_object_assign方法用来确定Block捕获了id, NSObject, __attribute__((NSObject)), block, …类型变量和__block修饰的变量在_NSConcreteStackBlock类型Block copy到堆上时执行的动作

_Block_byref_copy方法用来确定被__block修饰的变量在_NSConcreteStackBlock类型Block copy到堆上时执行的动作

通过上面的分析,Block在使用不同类型的外部变量时,内存管理有一下几种情况:

  • 当外部变量是全局变量或者static静态变量时,只使用且不需要管理内存;
  • 当外部变量是值类型如int、CGPoint时进行值const copy,不需要管理内存;
  • 当外部变量是对象型变量id, NSObject, __attribute__((NSObject))时,进行指针const copy,在Block被copy到堆上时增加引用计数用来持有该变量,在MRR环境通过_Block_retain_object方法持有变量,在ARC环境下通过__strong持有变量。这里是解决循环引用的关键
  • 当外部变量被__block修饰时,会使用Block_byrefstruct包装该变量,在Block被copy到堆上时copyBlock_byref,如果__block修饰的是id, NSObject, __attribute__((NSObject))对象型类型,该变量只做指针copy不会增加变量的引用计数。当__block修饰的是值类型时,做值const copy
  • 当外部变量是block类型时,在Block被copy到堆上时,调用_Block_copy 进行持有,如果外部block类型变量也持有变量,则递归进行copy

五、Block使用-续

第三部分讲到Block怎么样使用,第四节讲到了Block的内部结构和内存管理。这里大家可能有个疑问,Block在使用时,并没有声明那一堆struct啊,而且像什么isa、flags值都是谁设置的呢。

先看下Block使用例子:

int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
    return num * multiplier;
};

再看下Block_private.h文件中Block的结构:

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size; // Block's size
};

struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    void (*copy)(void *dst, const void *src);
    void (*dispose)(const void *);
};

// Block的内部布局
struct Block_layout {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock 
    volatile int32_t flags; // contains ref count 这个值主要用来告知系统Block在copy时应该执行什么操作
    int32_t reserved; 
    void (*invoke)(void *, ...); // function ptr
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

正常来讲,我们应该先声明Block_layout对象,然后实现invoke方法,告知系统Block是_NSConcreteStackBlock还是_NSConcreteGlobalBlock,再实现BLOCK_DESCRIPTOR_2的copy方法,管理捕获的对象的内存。可以看到,这种调用方式太麻烦了,而且需要我们深刻理解Block_layout里每个变量的含义,稍不留神就会出错。为了让我们使用更简单,这一堆变量的设置都交给了编译器去实现。编译器可以聪明地把上面的结构转成下面的结构。这里我们借助编译器前端clang,使用clang -rewrite-objc xxx.m命令重写成c++代码,看下编译器是怎么转换的

好了,我们先建个工程,假设叫BlockImpl;然后创建文件,比如也叫BlockImpl,把上面那段代码copy进来

#import "BlockImpl.h"

@implementation BlockImpl
- (void)test {
    int multiplier = 7; // 局部变量或者实例变量
    int (^myBlock)(int) = ^(int num) {
        return num * multiplier;
    };
    myBlock(1);
}
@end

然后,我们在终端中输入命令clang -rewrite-objc BlockImpl.m(当然你首先要cd 该文件所在文件夹:)),回车,你会发现在BlockImpl.m同级文件夹中生成了一个名字叫BlockImpl.cpp文件。这个文件就是重写后的c++文件。打开后如下:

#ifndef __OBJC2__
#define __OBJC2__
#endif
struct objc_selector; struct objc_class;
struct __rw_objc_super { 
    struct objc_object *object; 
    struct objc_object *superClass; 
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};
#ifndef _REWRITER_typedef_Protocol
typedef struct objc_object Protocol;
#define _REWRITER_typedef_Protocol
#endif
#define __OBJC_RW_DLLIMPORT extern
...

几行代码被重写成将近10万行代码,没事儿,不要怕,我们只需要关注跟我们相关的代码,在这我把相关代码摘出来,如下:

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

// @implementation BlockImpl

struct __BlockImpl__test_block_impl_0 {
    struct __block_impl impl;
    struct __BlockImpl__test_block_desc_0* Desc;
    int multiplier;
    __BlockImpl__test_block_impl_0(void *fp, struct __BlockImpl__test_block_desc_0 *desc, int _multiplier, int flags=0) : multiplier(_multiplier) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static int __BlockImpl__test_block_func_0(struct __BlockImpl__test_block_impl_0 *__cself, int num) {
    int multiplier = __cself->multiplier; // bound by copy
    return num * multiplier;
}

static struct __BlockImpl__test_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __BlockImpl__test_block_desc_0_DATA = { 0, sizeof(struct __BlockImpl__test_block_impl_0)};

static void _I_BlockImpl_test(BlockImpl * self, SEL _cmd) {
    int multiplier = 7;
    int (*myBlock)(int) = ((int (*)(int))&__BlockImpl__test_block_impl_0((void *)__BlockImpl__test_block_func_0, &__BlockImpl__test_block_desc_0_DATA, multiplier));
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}
// @end

摘完后,代码是不是清爽了许多,好了,下一步我们就尝试对应下,看看重写后的代码是不是和Block_private.h文件中定义的结构一样:

1、__BlockImpl__test_block_impl_0__block_impl加起来对应上了Block_layout。这里有个知识点注意下,struct并没有改变变量在内存的位置,所以这两个是可以划等号的

2、__BlockImpl__test_block_desc_0Block_descriptor_1对应

3、Block的body被转换成了函数指针__BlockImpl__test_block_func_0

从上面的分析,可以看到它们俩是完全可以对应上的。另外我们也可以看到系统为做了一些自动化的工作,比如为isa赋值&_NSConcreteStackBlockmyBlock(1)调用转换为函数调用((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);;局部变量multiplier被copy到了__BlockImpl__test_block_impl_0上,函数调用时也是使用的__BlockImpl__test_block_impl_0multiplier值,而不是那个局部变量,这也说明了Block不能随意地修改外部局部变量(当然添加__block修饰符才可以,第三、四部分有讲到原因)。

clang -rewrite-objc BlockImpl.m以MRR方式重写,如果要想要以ARC方式,加上-fobjc-arc-fobjc-runtime=macosx-10.7,即clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m,通过-rewrite-object命令重写下你的应用吧,结合第四部分,你会深刻了解Block的工作方式和循环引用产生的原因和解决方案(第四部分已经加粗说明)

循环引用

很多人搞不明白,ARC的循环引用是怎么引起的,总以为和MRR是一样的,其实第四部分已经说明过一次了,在这里单拎出来强调下。MRR环境下是_Block_retain_object实现强引用外部变量的,这个可以自己写下代码用-rewrite-objc命令重写下,很容易理解。在ARC环境_Block_retain_object其实是个空操作,在第四部分已经说明。ARC是通过__strong实现变量的持有的,下面我们写一个循环引用的例子

@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
    self.myBlock = ^(int num) {
        NSLog(@"%@", self);
    };
    self.myBlock(1);
}
@end

使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m,得到

***
// 只贴出来最重要的部分
struct __BlockImpl__testRetainCycle_block_impl_0 {
  struct __block_impl impl;
  struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
  BlockImpl *const __strong self; // 看这里,Block通过__strong持有了self 
  __BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
***

如果禁止Block的持有,则只能通过__weak修饰self,如下

@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
    __weak __typeof(self) weakSelf = self;
    self.myBlock = ^(int num) {
        NSLog(@"%@", weakSelf);
    };
    self.myBlock(1);
}
@end

转化为

struct __BlockImpl__testRetainCycle_block_impl_0 {
  struct __block_impl impl;
  struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
  BlockImpl *const __weak weakSelf; // 看这里,__weak修饰,Block不再强持有self
  __BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

所以ARC环境下__weak可以解决循环引用

小结

从Objective-C的发展史引出了主题Block。在讲Block前,先熟悉了闭包的概念,然后了解到Block其实就是闭包的一种实现。闭包的实质就是捕获了外部变量的函数,Block要解决捕获变量和变量内存管理相关的问题。在使用时又用让编译器简化了我们使用的成本。第四部分在讲Block的内存管理时,又讲到了MRR环境和ARC环境循环引用的原因和解决方案

网上已经有很多篇关于Block的实现,为什么我还要再写一篇?有两个原因:其实在2014年底的时候,我写过一篇关于Block原理的文章,当时还有自己的博客,后来博客到期了,文章也丢了;(;另外我找遍了所有网上的博客,发现千篇一律,而且并没有说明白循环引用产生的原因和为什么加了__block(MRR)、__weak(ARC)就能避免循环引用。

最后感谢你花了这么长时间看我的这篇絮絮叨叨的文章:)

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

推荐阅读更多精彩内容