Block源码分析

1.什么是Block

Block是带有自动变量(局部变量)的匿名函数。Block的内部数据结构如下:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};
  • isa指针:指向表明该block类型的类。
  • flags:按bit位表示一些block的附加信息,比如判断block类型、判断block引用计数、判断block是否需要执行辅助函数等。
  • reserved:保留变量,我的理解是表示block内部的变量数。
  • invoke:函数指针,指向具体的block实现的函数调用地址。
  • descriptor:block的附加描述信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针。
  • variables:因为block有闭包性,所以可以访问block外部的局部变量。这些variables就是复制到结构体中的外部局部变量或变量的地址。
2.block类型

C/C++/OC编译的程序占用内存分布的结构如下:

image.png
  • 栈区(stack):由系统自动分配,一般存放函数参数值、局部变量的值等。由编译器自动创建与释放。其操作方式后进先出、先进后出。
  • 堆区(heap):一般由程序员申请并指明大小,最终也由程序员释放。如果程序员不释放,程序结束时可能会由OS回收。对于堆区的管理是采用链表式管理的,操作系统有一个记录空闲内存地址的链表,当接收到程序分配内存的申请时,操作系统就会遍历该链表,遍历到一个记录的内存地址大于申请内存的链表节点,并将该节点从该链表中删除,然后将该节点记录的内存地址分配给程序。
  • 全局区/静态区:全局变量和静态变量存储在这个区域。程序结束后由系统释放。
  • 程序代码区:主要存放函数体的二进制代码。

block按照不同的内存区域可以分为:

  • _NSConcreteStackBlock,存放在栈区。只用到外部局部变量、成员属性变量,且没有强指针引用的block。
    StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。
  • _NSConcreteMallocBlock,存放在堆区。有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制。
  • _NSConcreteGlobalBlock, 存放在全局/静态区。没有用到外界变量或只用到全局变量、静态变量的block,生命周期从创建到应用程序结束。
int globalNum = 3;
int main(int argc, const char * argv[]) {
    int a = 3;
    NSLog(@"this block is: %@",^{NSLog(@"I use nothing");});
    NSLog(@"this block is: %@",[^{NSLog(@"I use ivar %d",a);} copy]);
    NSLog(@"this block is: %@",^{NSLog(@"I use ivar %d",a);});
    NSLog(@"this block is: %@",^{NSLog(@"%d",globalNum);});
    return 0;
}

其输出结果如下:

2017-12-26 09:01:27.668046+0800 BlockDemo[98660:1165488] this block is: <__NSGlobalBlock__: 0x100001050>
2017-12-26 09:01:27.668246+0800 BlockDemo[98660:1165488] this block is: <__NSMallocBlock__: 0x100507f80>
2017-12-26 09:01:27.668293+0800 BlockDemo[98660:1165488] this block is: <__NSStackBlock__: 0x7fff5fbfefe0>
2017-12-26 09:01:27.668367+0800 BlockDemo[98660:1165488] this block is: <__NSGlobalBlock__: 0x1000010d0>

注意:在ARC的作用下,block类型通过=进行传递时,会导致objc_retainBlock->_Block_copy->_Block_copy_internal方法链,导致__NSStackBlock__类型的block转换为__NSMallocBlock__类型。

3. block的实现原理

用clang工具重写为c++代码来探究一下block的内部实现。
测试代码如下:

typedef int(^blk)(int);

int globalNum = 3;
int main(int argc, const char * argv[]) {
    int a = 3;
    __block int blockVar = 3;
    static int staticNum = 3;
    blk blk = ^(int count) {
        NSLog(@"a = %d", a);
        staticNum --;
        globalNum --;
        blockVar --;
        return count * a * globalNum * staticNum;
    };
    a = 10;
    NSLog(@"%d", blk(1));
    return 0;
}

clang重写c++后的代码如下:

typedef int(*blk)(int);

int globalNum = 3;
struct __Block_byref_blockVar_0 {
  void *__isa;
  __Block_byref_blockVar_0 *__forwarding;
 int __flags;
 int __size;
 int blockVar;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *staticNum;
  __Block_byref_blockVar_0 *blockVar; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_staticNum, __Block_byref_blockVar_0 *_blockVar, int flags=0) : a(_a), staticNum(_staticNum), blockVar(_blockVar->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __main_block_func_0(struct __main_block_impl_0 *__cself, int count) {
  __Block_byref_blockVar_0 *blockVar = __cself->blockVar; // bound by ref
  int a = __cself->a; // bound by copy
  int *staticNum = __cself->staticNum; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_03_v9qmb8t568sgwcjv8bj5ng380000gn_T_main_6b603d_mi_0, a);
    (*staticNum) --;
    globalNum --;
    (blockVar->__forwarding->blockVar) --;
    return count * a * globalNum * (*staticNum);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->blockVar, (void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->blockVar, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {
    int a = 3;
    __attribute__((__blocks__(byref))) __Block_byref_blockVar_0 blockVar = {(void*)0,(__Block_byref_blockVar_0 *)&blockVar, 0, sizeof(__Block_byref_blockVar_0), 3};
    static int staticNum = 3;
    blk blk = ((int (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &staticNum, (__Block_byref_blockVar_0 *)&blockVar, 570425344));
    a = 10;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_03_v9qmb8t568sgwcjv8bj5ng380000gn_T_main_6b603d_mi_1, ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 1));
    return 0;
}

我们看到block其实就是struct __main_block_impl_0 结构体,它由4个成员变量和一个构造函数组成。
其中第一个成员变量是__block_impl结构体,指向的是__main_block_func_0函数。
第二个成员变量是__main_block_desc_0结构体,负责管理block的内存管理。__main_block_copy_0__main_block_dispose_0就是利用OC的runtime进行内存管理。在Block中使用 __strong或者__block修饰符的id类型或对象类型的自动变量时,当block从栈复制到堆时,当对象需要retain的时候调用__main_block_copy方法增加引用计数,当需要释放的时候则调用__main_block_dispose方法释放对象。
第三个是int变量。因为block内部引用了外部的自动变量,所以在block结构体中多了一个同类型的成员变量。
第四个是捕获到的静态变量。
第五个是__block修饰符的变量。具体介绍看下一节。

__main_block_func_0包含了block的内部代码。其中包含了一个参数__cself,指向__main_block_impl_0,及匿名block自身。这种写法类似与OC中的方法消息传递。
另外可以看到系统加的注释bound by copy,自动变量通过__cself->val方式捕获。局部变量传入的是值,静态变量传入的是地址。因此局部变量无法更改,而静态变量可以更改。同时全局变量由于作用域大,因此不需要传入就可以自由的在block内部更改。而__block修饰符的变量是通过bound by ref方式被捕获进来的,具体介绍看下一节。

image.png
4. __block 修饰符

我们直到,当想要修改block外面的局部变量时,需要用__block修饰符。那么__block的原理是如何的呢。
从上面的转换代码中可以看到,带有__block修饰符的局部变量被转换为一个结构体__Block_byref_blockVar_0。这个结构体有5个成员变量。

  • __isa,isa指针
  • __forwarding,指向自身类型的指针
  • __flags,标记
  • __size,结构体大小
  • __blockVar,局部变量
struct __Block_byref_blockVar_0 {
  void *__isa;
  __Block_byref_blockVar_0 *__forwarding;
  int __flags;
  int __size;
  int blockVar;
};

从代码中我们可以看到blockVar->__forwarding->blockVar这样的方式来对__block修饰的变量进行操作,但是为什么要搞这么复杂呢?
__forwarding 指针就是针对堆里的block。把原来指向自己的__forwarding指针指向堆上的__block变量。然后堆上的变量的__forwarding指向自己。

image.png
image.png
5. Block中的循环引用

循环引用,就是相互引用。比如A强引用B,B又强引用A。那么A和B的引用计数永远无法为0,造成内存泄漏。当然这种循环也可以是多个对象间的。


image.png

而对于block的循环引用,一般是一个对象引用block,而block内部又引用了这个对象。
打破这种循环引用一般有两种方法:1.弱引用;2.主动释放


image.png
5.1 弱引用
__weak typeof(self) weakSelf = self;
self.blk = ^{
    NSLog(@"%@",weakSelf);
};
5.2 主动释放
__block id blockSelf = self;
self.blk = ^{
    NSLog(@"%@",blockSelf);
    blockSelf = nil;
};
self.blk();

当然,弱引用和主动释放各有缺点:
主动释放的缺点:

  • 必须执行block才能避免循环引用

弱引用的缺点:

  • 不能保证self对象是否已经被销毁

当然对于弱引用,可以利用weak-strong-dance来保证不会出现因为self释放引起问题。

__weak typeof(self) weakSelf = self;
self.blk = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    NSLog(@"%@",strongSelf);
};
6. 参考链接

1.深入研究Block捕获外部变量和__block实现原理
2.老司机出品——源码解析之从Block说开去
3.A look inside blocks: Episode 1
4.A look inside blocks: Episode 2

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