Objective-C block 实现机制

前言

在Objective-C中,block是一个很常见的东西,说白了就是个匿名函数,网上有很多关于block如何使用的文章,讲的都非常精彩,这里主要探讨下block的实现原理。关于如何使用block,请参考网上的教程。

实例

先来新建一个控制台工程,main.m里的代码如下,并思考下最后的输出结果是什么:

void blockFunc1() {
    int num = 100;
    void (^block)(void) = ^() {
        NSLog(@"num = %d\n", num);
    };
    num = 200;
    block();
}

void blockFunc2() {
    __block int num = 100;
    void (^block)(void) = ^() {
        NSLog(@"num = %d\n", num);
    };
    num = 200;
    block();
}

int num = 100;
void blockFunc3() {
    void (^block)(void) = ^() {
        NSLog(@"num = %d\n", num);
    };
    num = 200;
    block();
}

void blockFunc4()
{
    static int num = 100;
    void (^block)(void) = ^{
        NSLog(@"num = %d", num);
    };
    num = 200;
    block();
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        ^{ printf("Hello, World!\n"); } ();
        
        blockFunc1();
        blockFunc2();
        
        blockFunc3();
        blockFunc4();
    }
    return 0;
}

打印结果是:

Hello, World!
2018-03-21 15:50:01.996591+0800 BlockDemo[34825:4848536] num = 100
2018-03-21 15:50:01.997009+0800 BlockDemo[34825:4848536] num = 200
2018-03-21 15:50:01.997025+0800 BlockDemo[34825:4848536] num = 200
2018-03-21 15:50:01.997037+0800 BlockDemo[34825:4848536] num = 200

聪明的读者应该早就知道这个结果,😁,先简单解释一下:
1、^{ printf("Hello, World!\n"); } ();
这个没啥好说的,肯定就打印“Hello, World!”了。

2、blockFunc1里面,num是以值传递的方式被block获取,所以尽管后面更改了num的值,但是在block里面还是保持保持原来的值。

2、blockFunc2里面,num由__block修饰,num在block变成了外部的一个引用(后面会通过源码解释),所以在block外部改变num的值时,block里面的num也随着改变。

3、blockFunc3里面,block引用的是一个全局的num,所以,num改变的时候也会改变block内部num的值。

4、blockFunc3里面,block引用的是一个static的num,所以,num改变也会改变block内部的num的值。

源码分析

也许大家看到上面的解释还是不知道为啥会这样,所以接下,我通过源码来分析下其中的缘由,我们先把这段先转换成c++文件,cd到main.m所在的目录,并执行这条命令clang -rewrite-objc main.m,通过这条命令可以把main.m文件转换成cpp文件,里面可以看到block的结构。我们打开这份文件,这个文件比较长,直接拉到最后。可以看到在文件的最后是main函数的入口,代码如下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)) ();

        blockFunc1();
        blockFunc2();

        blockFunc3();
        blockFunc4();
    }
    return 0;
}

先看第一行代码,构造了一个__main_block_impl_0对象,__main_block_impl_0是一个结构体。相关代码如下:

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

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;
  }
};

从代码可知,最后block会转化成一个__block_impl对象,而block执行的代码会转化成一个静态函数,__block_impl里面的FuncPtr会指向这个静态函数。在这里printf("Hello, World!\n");这个block转换后的静态函数如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 printf("Hello, World!\n"); 
}

所以整个过程是这样的:
1、先构造一个__main_block_impl_0对象,构造的时候把__main_block_func_0传进去,当然还有别的参数,这里先不考虑。

2、在__main_block_impl_0的构造方法中,再把__main_block_func_0赋给__block_impl的FuncPtr。

3、调用FuncPtr。

所以,从上面可以看出,block实际上是转化为了一个__block_impl对象,这个对象有isa指针,用来表示block的类型,上面的block的isa指向&_NSConcreteStackBlock。同时block对象还有一个FuncPtr指针,用来指向block执行的方法(转换后的静态函数)。

再来看看blockFunc1相关的内容

struct __blockFunc1_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc1_block_desc_0* Desc;
  int num;
  __blockFunc1_block_impl_0(void *fp, struct __blockFunc1_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockFunc1_block_func_0(struct __blockFunc1_block_impl_0 *__cself) {
  int num = __cself->num; // bound by copy

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_0, num);
}
    
void blockFunc1() {
    int num = 100;
    void (*block)(void) = ((void (*)())&__blockFunc1_block_impl_0((void *)__blockFunc1_block_func_0, &__blockFunc1_block_desc_0_DATA, num));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

在这个函数里面,构造block的时候把num传了进去,而且是普通值传递,这样的话其实是拷贝了一份num。然后在执行block方法的时候,使用的是拷贝的那份num,从int num = __cself->num; // bound by copy可以看出。这个block也是_NSConcreteStackBlock类型的。

再来看看__block修饰过的num在block里面是怎么传递的,我们看看blockFunc2相关的代码:

// 封装num的结构
struct __Block_byref_num_0 {
  void *__isa;
__Block_byref_num_0 *__forwarding;
 int __flags;
 int __size;
 int num;
};

struct __blockFunc2_block_impl_0 {
  struct __block_impl impl;
  struct __blockFunc2_block_desc_0* Desc;
  __Block_byref_num_0 *num; // by ref
  __blockFunc2_block_impl_0(void *fp, struct __blockFunc2_block_desc_0 *desc, __Block_byref_num_0 *_num, int flags=0) : num(_num->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __blockFunc2_block_func_0(struct __blockFunc2_block_impl_0 *__cself) {
    __Block_byref_num_0 *num = __cself->num; // bound by ref
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_1, (num->__forwarding->num));
}
    
void blockFunc2() {
    __attribute__((__blocks__(byref))) __Block_byref_num_0 num = {(void*)0,(__Block_byref_num_0 *)&num, 0, sizeof(__Block_byref_num_0), 100};
    void (*block)(void) = ((void (*)())&__blockFunc2_block_impl_0((void *)__blockFunc2_block_func_0, &__blockFunc2_block_desc_0_DATA, (__Block_byref_num_0 *)&num, 570425344));
    (num.__forwarding->num) = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

从代码中可以看到,__block修饰的num在内部被包装成一个__Block_byref_num_0的对象,假设叫a,原来num的值100存储在对象a的num字段中,同时这个对象a有一个__forwarding字段,指向a本身。当改变num的值的时候(源代码是num = 200;),这段代码变为(num.__forwarding->num) = 200;,也就是说把对象a里面的num字段的值变为了200。同时,在block的执行函数__blockFunc2_block_func_0中,打印出来的取值是从__Block_byref_num_0 *num = __cself->num;取出,也就是取得是改变后的值,所以打印结果是200。这就是为什么用__block修饰的变量可以在block内部被修改。

那当num为全局变量的时候,block又是怎样的呢?请看代码:

static void __blockFunc3_block_func_0(struct __blockFunc3_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rv_338km0ws0gb2gk132_zrs0wc0000gp_T_main_b5d67f_mi_2, num);
    }
    
void blockFunc3() {
    void (*block)(void) = ((void (*)())&__blockFunc3_block_impl_0((void *)__blockFunc3_block_func_0, &__blockFunc3_block_desc_0_DATA));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

从代码里可以看出,这种情况很简单,block对象根本没有num字段,也就是打印的时候直接取得全局的num。

最后一种情况也很简单,当num时static的时候,构造block对象的时候直接用引用传值的方式把num放到block对象中。所以,当外部改变num的值的时候,也能反映到block内部。代码如下:

void blockFunc4()
{
    static int num = 100;
    void (*block)(void) = ((void (*)())&__blockFunc4_block_impl_0((void *)__blockFunc4_block_func_0, &__blockFunc4_block_desc_0_DATA, &num));
    num = 200;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

总结

这篇文章主要从源码的角度讲述了block的实现机制,并针对四种情况分析了block是如何引用外部变量的,分别是:
1、当引用局部变量的时候,如果没有__block修饰,那么在block内部获取的是外部变量的一份拷贝,改变外部变量不影响block内部的那份拷贝。

2、当引用局部变量的时候,同时局部变量用__block修饰,那么在block内部使用的实际上是外部变量的一个引用,所以改变外部变量会影响block内部变量的值。

3、当引用全局变量的时候,block并不持有这个变量。

4、当引用static变量的时候,block会以引用的方式持有这个变量。当在外部修改这个变量的时候,会影响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

推荐阅读更多精彩内容