BLOCK 基础到深入

一 简介

Objective-C 中的block,是匿名函数,匿名函数在别的语言中也被称作闭包、λ表达式等。Objective-C 中的 block,有以下几个特性:

  1. 可以将block作为函数,也能作为对象,当做属性持有,定义内存修饰词。
  2. 可以长期存在,出了定义的作用域,内部实现也能执行。
  3. 可以捕获外部变量,甚至使用__block修饰后,block 可以修改这个捕获变量。

这背后的原理是什么呢,看了很多博客和代码,理清楚了其中奥秘,在这里总结一下。

二 结构

block的结构,用clang -rewrite-objcobjc代码转成cpp代码,就能很清楚的看到了。在cpp中,block实际被编译器转写成结构体了。结构体的介绍网上已经有很多完善的资料了,我这里直接参考了这篇博客

// objc 代码
int test()
{
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    blk();
    return 0;
}
// cpp 重写后
// 自己定义的 test 方法
int test()
{
    // 声明&赋值
    void (*blk)(void) = ((void (*)())&__test_block_impl_0/*构造函数*/(
                                      (void *)__test_block_func_0,
                                      &__test_block_desc_0_DATA)
                                      );
    // 执行
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

// 我们创建的 block 的结构体
struct __test_block_impl_0 {
  struct __block_impl impl; // 核心:函数指针
  struct __test_block_desc_0* Desc; // 描述
  // 构造函数
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;// 栈 block
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __block_impl {
  void *isa; // isa指针,所以是objc对象
  int Flags;
  int Reserved;
  void *FuncPtr; // 函数指针
};

// 静态函数指针
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
    printf("Block\n");
}

// 描述
static struct __test_block_desc_0 {
  size_t reserved;
  size_t Block_size; // 大小
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)}; // __test_block_desc_0_DATA 是这个结构体的实例

这个结构刚才引用的博客中已经分析的很透彻了,这里就不一步步细讲了。直接说一下我总结的结构。

__xx_block_impl_0 // block完整结构体
{
    __block_impl, // 核心结构,具体结构在下面
    desc, // block 描述,具体结构在下面
    int foo, // 捕获的变量,依次排列开
    ...
    __xx_block_impl_0(fp, desc, param0,param1...)//构造函数
}

struct __block_impl
{
    id isa, // isa 指针
    void *fp, // 函数指针
    flag // 标记位,用来描述 block 类型
}

block_desc // block 描述
{
    size,// 大小
    void *copy,// 捕获对象时,捕获变量们的copy 函数
    void *dispose// 捕获对象时,捕获变量们的dispose 函数
}
// 静态函数指针
static void __xx_block_func_0() {
    // 具体实现
}

在讲一下该结构引出的比较让人疑惑的点:

  1. 一个 block 定义会被编译器clang编译生成一套特殊结构体,依据block行为不同(入参、捕获变量),生成的结构体也不一样。从结构体名称类名__方法名__block__impl__序号也可以知道,它是动态创建的。
    相关的创建详情可以参考 clanggenCode 模块,GCBlocks
  2. 结构体是对象,分成三类:
  • 栈block_NSConcreteStackBlock
  • 全局block_NSConcreteGlobalBlock
  • 堆block_NSConcreteMallocBlock

详细的区别参考这里。这里只说一点,栈block在赋值时,会被拷贝到堆上,这样就通过引用计数管理了(引用计数的内容可以看我的博客坑位、待填)。
关于这个拷贝操作,开始我并没有在代码中看见,只看见了赋值void (*blk)(void) = ((void (*)())&__test_block_impl_0...,找了很久,才发现真实依据是NSObject.mm

// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}
  1. 一个奇怪的细节:block声明时,得到的是__test_block_impl_0类型变量 blk;执行时,却转成了__block_impl类型。
    这里是因为__block_impl__test_block_impl_0的第一个变量,二者的指针地址相同的,所以可以直接强转,详见这篇的 2.3 部分

三 参数捕获

上面说到block捕获的变量不同,动态生成的结构也不一样。block可以捕获:objc 对象基础数据类型另一个 block。我们常用的是捕获基础数据类型和 objc 对象基础数据类型
下面就常用情况再分成四种情况讨论:

这部分有大牛珠玉在前,解释的比较透彻了。所以我就简单总结一下:

  1. 捕获基础数据,在 block 结构中,会将捕获参数添加进去。前面讲了,在 block 赋值时,会被执行一个 _Block_copy 操作,这其中对整个 block 结构进行拷贝,捕获的基础数据值也会被拷贝到堆上。
  2. 捕获对象变量时,除了会被添加到block结构体中,还会额外生成一对被捕获变量的拷贝函数销毁函数,保存在__block_desc结构中。这里面描述了被捕获的变量,如何被拷贝到堆上去。
// 捕获变量的copy
// _Block_copy 的时候会调用
// _Block_copy 在 block 被赋值时候调用
static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
    _Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

// 捕获变量的dispose
// _Block_release 的时候会调用
// _Block_release 的调用时机是堆 block 引用计数为 0 时
static void __TestClass__testMethod_block_dispose_2(struct __TestClass__testMethod_block_impl_2*src) {
    _Block_object_dispose((void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
  1. 捕获__block修饰的基础数据,该基础数据会被转化成一个特殊的结构体:
// 定义处,在栈上
__block int tmpB = 1;
void(^blk003)(int a) = ^(int a) {
    // 使用处,在堆上
    NSLog(@"tmpB=%d", tmpB);
};

// __block 修饰的 int 变量 tempB
struct __Block_byref_tmpB_0 {
  void *__isa;
__Block_byref_tmpB_0 *__forwarding; 
 int __flags;
 int __size;
 int tmpB;
};

我们称它为byref 封装,可以看到它也是一个 objc 对象。所以byref 封装和被捕获变量一样,会被添加到block 结构里之外,还会生成copydispose函数指针。
byref 封装除了封装了自身的实际值之外,还持有一个自己类型的__forwarding指针。当byref 封装在栈上的时候,__forwarding指针会指向它堆上的拷贝,在堆上的时候回指向自己。这样就保证了定义处和使用处调用方式是一样的,至于源码是如何实现的,下面会讲到。

  1. 捕获__block修饰的对象,同样也会生成一个byref 封装,结构也和上一条类似:
 __block NSString * tempC = [NSString stringWithFormat:@"1"];
 void (^test)() = ^ {
        NSLog(@"%@",tempC);
 };

// __block 修饰的 int 变量 tempC
struct __Block_byref_tempC_0 {
  void *__isa;
__Block_byref_tmpC_0 *__forwarding; 
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);//实际赋值__Block_byref_id_object_copy_131
 void (*__Block_byref_id_object_dispose)(void*);//同上
 NSString *tempC;
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

但是多处量函数指针变量,这俩变量在创建时,被传值__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131。这俩在byref 封装拷贝时会调用到,用来拷贝NSString *tempC的。

总结:可以看到 __xx_block_impl_0结构随着捕获变量的复杂而变得复杂。而block从栈到堆的拷贝,是由自身结构->捕获变量->byref的value,层层进行的。

四 堆 block 拷贝

上面总结了拷贝的流程,这一节撸一下代码,一来加深印象,二来找下几个常见问题的答案:

  1. 在 block 中被修改了,栈上的原值也会被修改吗?
  2. 循环引用是如何产生的,weak-strong-dance 为何能解决?
  3. 使用weak-strong-dance,被引用对象何时会为 nil ?

block 结构体的拷贝方法 _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 =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        // !!!!将内容移到指定堆上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // 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 参数拷贝助手

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
}

static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
    _Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

_Block_object_assign 参数拷贝函数

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:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
// static void _Block_retain_object_default(const void *ptr __unused) { }
// static void _Block_destructInstance_default(const void *aBlock __unused) {}
// !!!!都是空方法
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
// !!!!block 参数,再调一次 `_Block_copy`
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/
// !!!!__block 修饰的基础数据类型
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/
// !!!!__block 修饰的对象
// !!!!直接赋值,对其引用计数+1
        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/
// !!!!__block __weak 双重修饰的对象
// !!!!直接赋值,对其引用计数+1
// !!!!相当于 __weak 没有起作用
        *dest = object;
        break;

      default:
        break;
    }
}

_Block_byref_copy__forwarding 指针的操作

// Runtime entry points for maintaining the sharing knowledge of byref data blocks.

// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
// Otherwise we need to copy it and update the stack forwarding pointer
static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;
 // !!!! 通过引用计数来判断原 byref 的 forwarding 是否为堆上变量
    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;
 // !!!! 堆上 byref 指向自己地址本身
        copy->forwarding = copy; // patch heap copy to point to itself
 // !!!! 原栈上 byref 指向堆上地址
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
 // !!!! 判断有 copy 或 dispose 函数,会执行相应函数
        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;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*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));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
//!!!! 引用计数+1 难道所有对象的引用计数,都是用 flag 来表示的??
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}


static int32_t latching_incr_int(volatile int32_t *where) {
    while (1) {
        int32_t old_value = *where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
            return old_value+2;
        }
    }
}

五 总结

关于 __block 修饰的变量,怎么同修改的
1、__block 修饰的变量,会生成一个结构体 blk_byref。里面有一个 forwarding 指针、一个实际值,开始时 forwarding 指向自己。
2、在 blk 赋值时,(void (^blk2)(void) = ^{ .....} )会把 blk copy 到堆上,同时 copy 捕获的参数(刚才的栈上 blk_byref)到堆上。 见 _Block_copy 方法。
3、copy 捕获的参数时,设置 forwarding 指针,栈->堆,堆->自己。见 _Block_object_assign 方法。
4、一个参数如果被多个 blk 捕获了(如果 forwarding 指针有值),在 copy 时候,就不在堆上创建新的 blk_byref,这样就能保证一个变量被多个blk捕获时,有且只有一份堆结构 blk_byref。见 _Block_byref_copy 方法。
5、实际上,所有访问操作(blk内,blk外),都是 blk_byref->forwardig->val,因为栈/堆forwarding都到堆上了,最后实际修改的都是堆上的blk_ref ->val,不会改栈上blk_ref ->val。

sunnyxx 面试题
看完了上面内容,可以用下面的几道题测试下自己的掌握程度


sunnyxx面试题.png

参考:
破弓的《iOS Block》系列
libclosure 源码
objc4 源码
https://www.jianshu.com/p/51d04b7639f1
https://www.jianshu.com/p/b554e813fce1
https://www.jianshu.com/p/e42f86a81045
http://clang.llvm.org/docs/Block-ABI-Apple.html#block-escapes
GCBlocks

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

推荐阅读更多精彩内容