Block详解

一、Block本质

Block是“带有自动变量值的匿名函数”。

所谓的匿名函数就是不带有名称的函数

typedef int (^blk_t)(int)
blk_t = ^(int count){
  return count+1;
}

但它究竟是什么呢?

转码

通过-rewrite-objc选项将含有Block语法的源代码变换为C++代码

变换前:

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

终端:clang -rewrite-objc 源代码文件名

变换后:(变换后有568行,精简后如下)

struct __block_impl {
  void *isa; //isa指针
  int Flags; //标志位
  int Reserved; 
  void *FuncPtr; //函数指针
};

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; //block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; //block描述信息
  //构造函数(类似于OC的init方法),返回结构体对象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; //isa指针
    impl.Flags = flags; //标志位
    impl.FuncPtr = fp;//函数指针
    Desc = desc; //block描述信息
  }
};

/*
  ^{printf("Block");};变换后的样子
  Block匿名函数实际上被作为简单的C函数来处理
  函数名的命名规则:根据Block语法所在的函数名(此处为mian)和该Block语法在该函数出现的顺序值(此处为0)来命名的
 __cself相当于OC中的self 
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("Block");
}

int main() {
    /*
    void (^blk)(void) = ^{printf("Block");};转换后的代码
    
    构造函数构造后,__main_block_impl_0结构体结果如下
    isa = &_NSConcreteStackBlock;
    Flags = 0;
    FuncPtr = __main_block_func_0; //函数指针,指向__main_block_func_0函数
    Desc = &__main_block_desc_0_DATA;
    */
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
  
    /*
      blk();转换后的代码如下
    */
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

分析

1、分析void (^blk)(void) = ^{printf("Block");}

上面C++代码

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

去掉转换后的部分如下

//创建一个结构体实例
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA));
//将结构体实例的指针赋给blk
struct __main_block_impl_0 * blk = &tmp;

通过简化后的代码可知,源代码将__main_block_impl_0结构体类型的自动变量,即栈上生成的__main_block_impl_0结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量blk。

而最初的Block源代码是void (^blk)(void) = ^{printf("Block");};

因此,将Block语法生成的Block赋给Block类型变量blk。它等同于将__main_block_impl_0结构体实例的指针赋给变量blk。

  • 堆:动态分配内存,需要程序员自己申请,程序员自己管理

  • 栈:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况

2、分析blk()

blk();转换后的代码如下:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉转换后的部分如下:

(*blk->impl.FuncPtr)(blk);

可见,这就是简单地使用函数指针调用函数。__main_block_func_0的函数指针被赋值到成员变量FuncPtr中。另外也说明了,__main_block_func_0函数的参数__cself指向Block值。在调用该函数的源代码中可以看出Block正是作为参数进行传递。

__main_block_impl_0结构体相当于基于objc_object结构体的Objective-C类对象的结构体。

_NSConcreteStackBlock相当于class_t结构体实例。在将Block作为Objective-C对象处理时,关于该类的信息放置于_NSConcreteStackBlock中。

因此,Block本质上也是一个OC对象(最终继承NSObject),它内部也有个isa指针。

二、Block捕获变量值

Block是带有自变量的匿名函数,其中的"带有自变量值"是什么意思呢?"带有自变量值"在block中表现为"捕获自变量值"。

为了保证Block内部能够正常访问外部的变量,Block有个变量捕获机制:

局部变量(auto):捕获到Block内,值传递;

局部变量(static):捕获到Block内,指针传递;

全局变量:不捕获到Block内,直接访问;

Q:下列代码输出值分别为多少?

  int val = 10;
  const char * fmt = "val = %d\n";
  void (^blk)(void) = ^{
    printf(fmt,val);
  };
  
  val = 2;
  fmt = "Change the value of val,val = %d\n";
  
  blk();

输出结果为:val = 10

原因:在执行Block语法时,会捕获自动变量值,即Block语法表达式所使用到的自动变量值被保存到Block的结构体实例中。

源码证明:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
  printf(fmt,val);
}

int main() {
    int val = 10;
    const char * fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

    val = 2;
    fmt = "Change the value of val,val = %d\n";

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

Q:下列代码输出值分别为多少?

auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
    printf("age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();

输出结果为:age:10,num:11
原因:auto变量Block访问方式是值传递,static变量Block访问方式是指针传递

源码证明:

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *num = __cself->num; // bound by copy
  printf("age:%d,num:%d",age,(*num));
}

int main() {
    auto int age = 10;
    static int num = 25;
    void (*Block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &num));
    age = 20;
    num = 11;
    ((void (*)(__block_impl *))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block);
    return 0;
}

上述代码可知static修饰的变量,是根据指针访问的

Q:为什么block对auto和static变量捕获有差异?

auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可

三、__block说明符

__block说明符类似于static、auto和register说明符,它们用于指定将变量值设置到哪个存储域中(参见下文)。如auto表示作为自动变量存储在栈中,static表示作为静态变量存储在数据区中。

自变量被Block截获后,Block保存的是当前的瞬间值,保存后就不能修改该值。若想在Block语法中修改捕获到的自变量的值,则需要在该值变量前加上__block说明符,如果不加该说明符,则运行会报错

__block int val = 0;
void (^blk)(void) = ^{
    val = 1; //修改捕获到的自变量的值
};

blk();
printf("val = %d\n",val); 

系统对__block int val = 0;做了什么?

编译器会将__block变量包装成一个对象,具体的C++代码如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding; //val的地址
 int __flags;
 int __size;
 int val; //val的值
};

从C++代码可知,结构体持有相当于原自动变量的成员变量。通过成员变量__forwarding访问成员变量val。(成员变量val是该实例自身持有的变量,它相当于原自动变量本身),如下图:


访问__block变量.png

因此,加了__block修饰的变量,Block截获后是通过指针去操作该变量,因此可以修改变量的值。

栈上__block__forwarding指向本身

栈上__block复制到堆上后,栈上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身

复制__block变量后的变化.png

捕获OC对象(不用__block修饰),调用变更对象的方法是可以的,而向捕获的变量赋值则会产生编译错误

id arr = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
  id obj = [[NSObject alloc] init];
  [arr addObject:obj]; //这样是可以的
  
  arr = [[NSMutableArray alloc] init]; //这样是不行的(编译报错)
}

现在的Block中,捕获自变量的方法并没有实现对C语言数组的截获,因此在访问C语言数组时会产生编译错误,可以通过使用指针解决该问题

const char text[] = "hello";
void (^blk)(void) = ^{
  printf("%c\n",text[2]); //这样使用时不行的
};

const char *text = "hello";
void (^blk)(void) = ^{
  printf("%c\n",text[2]); //改成指针可以
};

__block总结

  • __block可以用于解决block内部无法修改auto变量值的问题
  • __block不能修饰全局变量、静态变量(static)
  • 当__block变量在栈上时,不会对指向的对象产生强引用
  • 编译器会将__block变量包装成一个对象
  • __block修改变量:age->forwarding->age
  • __Block_byref_val_0结构体内部地址和外部变量val是同一地址

四、Block类型

Block的类型取决于isa指针,可以通过调用class方法查看具体类型,最终都继承自NSBlock。

  • __NSGlobalBlock __ ( _NSConcreteGlobalBlock )
  • __NSStackBlock __ ( _NSConcreteStackBlock )
  • __NSMallocBlock __ ( _NSConcreteMallocBlock )

代码示例

void (^block1)(void) = ^{
    NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);

输出结果:
NSGlobalBlock
__NSGlobalBlock
NSBlock
NSObject
null

上述代码输出了block1的类型,也证实了block是对象,最终继承NSObject

代码展示block的三种类型:

    /*
    全局block
    没有访问auto变量的block是__NSGlobalBlock__,放在数据段
    因为在使用全局变量的地方不能使用自动变量,所有不存在对自动变量进行截获
    由于此Block结构体实例的内容不依赖于执行时的状态,所以整个程序中只需一个实例
    因此,将Block结构体实例设置在与全局变量相同的数据区域中即可
    */
    void (^blk1)(void) = ^{ NSLog(@"blk1"); };
    NSLog(@"%@",[blk1 class]);
    
    /*
     堆block
     将Block赋值给__strong指针时,ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上
     如果void (^blk2)(void) = ^{  写成  void __weak (^blk2)(void) = ^{
     则是栈block(编译器没有将其复制到堆上)
     */
    int age = 1;
    void (^blk2)(void) = ^{
        NSLog(@"blk2:%d",age);
    };
    NSLog(@"%@",[blk2 class]);
    
    /*
     栈block
     访问了变量,并且没有做copy操作
     */
    NSLog(@"%@",[^{
        NSLog(@"blk3:%d",age);
    } class]);

输出结果:
NSGlobalBlock

NSMallocBlock

NSStackBlock

  • __NSGlobalBlock __ 在数据区

  • __NSMallocBlock __ 在堆区

  • __NSStackBlock __ 在栈区

  • 堆:动态分配内存,需要程序员自己申请,程序员自己管理

  • 栈:自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况

    Block存储域.png

    如何判断Block是哪种类型

  • 没有访问auto变量的block是__NSGlobalBlock __ ,放在数据段

  • 访问了auto变量的block是__NSStackBlock __

  • [__NSStackBlock __ copy]操作就变成了__NSMallocBlock __

在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上的情况有如下几种

  • Block作为函数返回值返回时,编译器会自动生成复制到堆上的代码;
  • 将Block赋值给__strong指针时;
  • Cocoa框架的方法,并且方法名中包含有usingBlock等时;
  • Block作为GCD API的方法参数时;

如在使用NSArray类的enumerateObjectsUsingBlock实例方法以及dispatch_async函数时,不用手动复制。相反

在NSArray类的initWithObjects实例方法上传递Block时需要手动复制。

备注:将Block从栈上复制到堆上是相当消耗CPU的。

对每种类型Block调用copy操作后是什么结果?

Block调用copy的情况.png

不管Block配置在何处,用copy方法复制都不会引起任何问题,在不确定时调用copy方法即可。

__block变量存储域

__block变量的存储域.png

若一个Block中使用__block变量,则当该Block从栈复制到堆时,由于其使用的所有__block变量也必定配置在栈上。所以这些__block变量也会一并从栈复制到堆,并且被该Block所持有。
复制__block变量.png

当多个Block使用同一个__block变量时,因为Block最先是配置在栈上的,所以__block变量也都配置在栈上。当其中一个Block被复制到堆上时,__block变量也会一并从栈复制到堆,并被Block所持有。当其他的Block从栈复制到堆时,被复制的Block持有__block变量,并增加__block变量的引用计数。如下图:
多个block持有同一个__block变量..png

如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。

MRC下Block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

ARC下Block属性的建议写法
@property (copy, nonatomic) void (^block)(void);

Block的属性修饰词为什么是copy

Block需要通过copy才会被复制到堆上,只有在堆上,程序员才能对它做内存管理、控制Block生命周期等操作。

五、Block循环引用

__strong强引用

__strong修饰符是id类型和对象类型,默认的所有权修饰符,可以不写

__weak弱引用

__weak弱引用,不持有对象,在超出其变量作用域时(如函数花括号之外),对象立即被释放

__weak可以避免循环强引用

__weak在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效,且被置为nil

__weak修饰符只能在iOS5以上使用

__unsafe_unretained

在iOS5以下用__unsafe_unretained修饰符

尽管ARC式的内存管理是编译器的工作,但符有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。

赋值给__unsafe_unretained修饰的变量的对象,需确保对象不为空,否则会产生悬垂指针,导致运行奔溃。

如下:

id __unsafe_unretained obj1 = nil
{
    id __strong obj0 = [[NSObject alloc] init];  //obj0持有对象
  obj1 = obj0; //虽然obj0变量赋给obj1,但obj1变量既不持有对象的强引用,也不持有对象的弱引用
  NSLog(@"A:%@",obj1);
}
//obj0超出其作用域,强引用失效,所以自动释放自己持有的对象,因为[[NSObject alloc] init]对象没有持有者,所以废弃该对象

NSLog(@"B:%@",obj1);
//obj1变量表示的对象已被废弃(悬垂指针),因此访问出错

@autoreleasepool自动释放池

@autoreleasepool {
    id __strong obj = [NSMutableArray array];
} //到这个花括号释放池代码块结束,随着@autoreleasepool块的结束,注册到autoreleasepool中的所有对象被自动释放

解决循环引用:

__weak:不会产生强引用,指向的对象销毁时,会自动让指针置为nil

    MyObject * object = [[MyObject alloc] init];
    object.age = 10;
    __weak typeof(object) weakObject = object;
    object.blk = ^{
        NSLog(@"age is %d", weakObject.age);
    };
    object.blk();

__block:必须把引用对象置为nil,并且要调用该block

    __block MyObject * object = [[MyObject alloc] init];
    object.age = 10;
    object.blk = ^{
        NSLog(@"age is %d", object.age);
        object = nil;
    };
    object.blk();

参考资料

Objective-C高级编程

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

推荐阅读更多精彩内容

  • 第一部分:Block本质 Q:什么是Block,Block的本质是什么? block本质上也是一个OC对象,它内部...
    sheldon_龙阅读 549评论 0 0
  • __block说明符 Block只能保存局部变量瞬间的值,所以当我们尝试修改截获的自动变量值,就会报错。例如: 该...
    CharmecarWang阅读 85评论 0 1
  • Block概要 Block:带有自动变量的匿名函数。 匿名函数:没有函数名的函数,一对{}包裹的内容是匿名函数的作...
    zweic阅读 494评论 0 2
  • 转自李峰峰博客 一、概述 闭包 = 一个函数「或指向函数的指针」+ 该函数执行的外部的上下文变量「也就是自由变量」...
    Joshua520阅读 916评论 0 0
  • 开始之前,我想先提几个问题,看看大家是否对此有疑惑。唐巧已经写过一篇对block很有研究的文章,大家可以去看看(本...
    高思阳阅读 1,505评论 0 1