iOS中使用Clang查看Block(Closure)的实现

什么是Block

Apple文档说:“A block is an anonymous inline collection of code, and sometimes also called a "closure".

一个block是一个匿名内联代码的集合,有时候也叫”Closure“。

block提供了一种新的方式进行回调,并且用block进行回调还可以直接访问局部变量,这是一般的函数做不到的。

Bblock的几种适用场合:任务完成时回调处理,消息监听回调处理,错误回调处理,枚举回调,视图动画、变换,排序。

写一个简单的Block

在开发中使用Block的地方很多,最常见的是UIView的animation(**)动画,以及GCD。使用Block的时候最需要注意的是内存泄漏,在block中使用的局部变量需要用__block修饰;防止循环引用的时候,要使用__weak修饰的对象。

使用C写一个Block:打开终端,输入:

vi test.c

然后键入i开始插入代码:

#include <stdio.h>
int main(){
void(^blk)(void) = ^{
  printf("Block\n");
};
blk();
return 0;
}

键入esc,键入:,键入wq

得到文件test.c,可在🏠家目录看到此文件。

查看实现代码

然后在终端中键入gcc test.c,得到编译后的输出文件 a.out
终端中键入clang -rewrite-objc test.c,得到C++文件test.cpp
双击使用Xcode打开此文件,就是实现代码了。
看到了两个熟悉的面孔:

weak

此文件有近600行代码,我们只看自己和本次研究相关的代码。

分析代码

首先在CPP文件中找到main函数,对应我们写的代码:


CPP代码对比

两行代码相对应。

  1. 第一行:
void(*blk)(void) =
    ((void (*)()) &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) );

通过__main_block_impl_0创建了一个变量blk。
__main_block_impl_0是一个struct,包含了 __block_impl 的 impl。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到,在构造函数里有三个参数,一个是函数指针,一个是desc,一个flags。
注意到:isa指针指向了_NSConcreteStackBlock,把传进来的函数指针fp记录到了impl里的FuncPtr。
_NSConcreteStackBlock是Block的类型,共有三种:

  1. _NSConcreteGlobalBlock(全局)
  2. _NSConcreteStackBlock(栈)
  3. _NSConcreteMallocBlock(堆)

虚拟内存段的分布图如下:


分布图

这里不对类型进行深入研究,不过最后一种是私有,基本不会遇到。第一种类型出现的情况是定义了一个全局的Block,比如:

void (^globalBlock)() = ^{
};

int main(int argc, const char * argv[]) {
    return 0;
}

书归正传,接着讨论。传进来的函数指针fp记录到了impl里的FuncPtr,这个impl变量是__block_impl结构体,查看这个struct:

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

回到CPP里main函数里的第一行代码,在调用这一函数的时候,传入的函数指针参数是

__main_block_func_0

找到这个函数:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

  printf("Block\n");
}

这正是我们写进block里的代码,也就是说__main_block_impl_0要执行的就是这一段代码。

这一行,只是创建了blk,并未执行。

  1. 第二行
    这一行代码就是执行这个Block里面的内容。
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

调用了blk的FuncPtr(__block_impl *)blk)->FuncPtr,即执行了__main_block_func_0函数,输出了"Block\n"这一字符串.

写一个闭包捕获

我们大多数情况需要在闭包里访问闭包外的局部变量、全局变量、静态变量。想一下,闭包如果捕获了局部变量,会出现什么问题?
先写一个捕获变量的block代码:
如先前所述方法再创建一个文件testBlock.c:

#include <stdio.h>
int main(){
    int a = 2;
    void(^blk)(void) = ^{
        a = 3;
        printf("Block\n");
    };
    blk();
    return 0;
}

代码有问题吗?
终端键入:gcc testBlock.c编译一下,出错了:

出错了

看到错误提示:

error: variable is not assignable (missing __block type
specifier)

是的,我们需要添加__block来说明这个变量是需要在block中使用的。
为什么要添加这个说明符?是如何作用的?我们大概猜一下,应该是更改了这个变量的作用域,把它从栈挪到了堆中。
写正确的代码继续实验,更改代码为:

#include <stdio.h>
int main(){
    __block int a = 2;
    void(^blk)(void) = ^{
        a = 3;
        printf("Block\n");
    };
    blk();
    return 0;
}

继续编译,成功。
使用Clang来获得实现代码,得到的C++的代码testBlock.cpp
仍然先找到main入口函数:

int main(){
    __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 2};
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

代码太长,挤得太密,断个句,放个图:


main()

emm...多了些。
先找到熟悉的代码,第一行是定义了一个__block修饰的变量a
第二行就是刚才所见的第一行的内容,创建一个Block的blk,第三行就是执行。

我们先研究第一行,首先注意到的是__Block_byref_a_0这个结构体:

__Block_byref_a_0

__isa指针,也是个对象。发现有一个同样是__Block_byref_a_0类型的变量__forwarding,发现了古怪:

__Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 2}

__forwarding竟然指向了自己,所以以后这个东西改变会指向谁呢?
研究第二行,创建block的时候,多了两个参数,一个是(__Block_byref_a_0 *)&a,即需要捕获的变量a,还有一个参数570425344,什么鬼?
它是一个标记值,570425344的值是1<<29,即BLOCK_HAS_DESCRIPTOR这个枚举值。Soga,原来是标记了“闭包有描述符”。

原来如此

这时候注意__main_block_desc_0_DATA,变了嘿,多了两个参数__main_block_copy_0, __main_block_dispose_0

__main_block_desc_0_DATA

看看什么东东:

copy

分别调用了_Block_object_assign_Block_object_dispose函数,追下去,看看什么:
dllexport

看到了__declspec(dllexport),__declspec(dllexport)用于动态库中,声明导出函数、类、对象等供外面调用。
我找了下资料,这就到了Objc的Block源代码中,有这么一个实现,截取有用的代码:

// _Block_object_assign源码
void _Block_object_assign(void *destAddr, const void *object, const int flags) {
...
    else if ((flags & BLOCK_FIELD_IS_BYREF) == BLOCK_FIELD_IS_BYREF)  {
        // copying a __block reference from the stack Block to the heap
        // flags will indicate if it holds a __weak reference and needs a special isa
        _Block_byref_assign_copy(destAddr, object, flags);
    }
...
}

注意到了_Block_byref_assign_copy(destAddr, object, flags);,注释为copying a __block reference from the stack Block to the heap.果不其然,把__block标注的引用从栈拷贝到了堆中。
查看_Block_byref_assign_copy(destAddr, object, flags);方法如下:

// _Block_byref_assign_copy源码
static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
    struct Block_byref **destp = (struct Block_byref **)dest;
    struct Block_byref *src = (struct Block_byref *)arg;
...
    else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // 从main函数对__Block_byref_a_0的初始化,可以看到初始化时将flags赋值为0
        // 这里表示第一次拷贝,会进行复制操作,并修改原来flags的值
        // static int _Byref_flag_initial_value = BLOCK_NEEDS_FREE | 2;
        // 可以看出,复制后,会并入BLOCK_NEEDS_FREE,后面的2是block的初始引用计数
        ...
        copy->flags = src->flags | _Byref_flag_initial_value;
        ...
    }
    // 已经拷贝到堆了,只增加引用计数
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    // 普通的赋值,里面最底层就*destptr = value;这句表达式
    _Block_assign(src->forwarding, (void **)destp);
}

由于block的拷贝最终都会调用_Block_copy_internal函数,所以观察这个函数就可以知道堆中block是如何被创建的了:

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    ...
    aBlock = (struct Block_layout *)arg;
    ...
    // Its a stack block.  Make a copy.
    if (!isGC) {
        // 申请block的堆内存
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        // 拷贝栈中block到刚申请的堆内存中
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 1;
        // 改变isa指向_NSConcreteMallocBlock,即堆block类型
        result->isa = _NSConcreteMallocBlock;
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock);
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    else {
        ...
    }
}

从以上代码以及注释可以很清楚的看出,函数通过memmove将栈中的block的内容拷贝到了堆中,并使isa指向了_NSConcreteMallocBlock。
block主要的一些学问就出在栈中block向堆中block的转移过程中了。

书归正传,回到咱们通过clang得到的C++实现代码中来,调用的函数指针指向的方法__main_block_func_0为:

__main_block_func_0

注意到(a->__forwarding->a) = 3;,这时候修改a的值的时候是修改的__forwarding的值,即已经是堆中的值。

这时候,就不会出现block回调的时候,局部变量已经释放的问题了,因为已经“捕获”到了。

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

推荐阅读更多精彩内容