关于Block的那些事

理解“块”这一个概念

    块可以实现闭包。这项语言特性是作为“扩展”而加入GCC编译器中的,从技术上讲,这是个C语言层面的特性,因此,只要有支持此特性的编译器,以及能执行块的运行期组件,就可以在C、C++,OC,OC++代码中使用

块的基本知识

    块与函数类似,只不过是直接定义在另一个函数里的,和定义它的那个函数共享一个范围内的东西。块用“^”符号来表示,后面跟着一对花括号,括号里面是块的实现代码,例如,下面就是一个简单的块:

^{

    //Block implementation here

}

    块其实就是一个值,而且有其相关类型。与int、float或者OC对象一样,也可以吧块赋值给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:

void (^someBlock)() = ^{

    //Block implementation here

}

    下面就是有两个参数并且有返回值的:

int (^addBlock)(int a,int b) = ^(int a,int b){

    //Block implementation here

    return a + b;

};

    我们可以像使用C语言的函数一样使用:

int add = addBlock(4,5);

    块的强大之处是:在声明它的范围内,所有的变量都可以为其所捕获。也就是说,那个范围的全部变量,在块里依然可用,比如,下面这段代码所定义的快,就使用了块外的变量:

int admition = 6;

int (^addBlock)(int a,int b) = ^(int a,int b){

    //Block implementation here

    return a + b + addition;

};

int add = addBlock(4,5);

    默认情况下,为块捕获的变量,是不可以在块里面修改的,在本例中,假如块内的代码改动了addition变量的值,那么编译器就会报错,不过声明变量的时候可以加上__block修饰符,这样就可以在块内修改了。Block不允许修改外部变量的值Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。

    我们可以打印下内存地址来进行验证:

__block int a = 0;

NSLog(@"定义前:%p", &a);        //栈区

void (^foo)(void) = ^{

    a = 1;

    NSLog(@"block内部:%p", &a);    //堆区

};

NSLog(@"定义后:%p", &a);        //堆区

foo();

打印结果

2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义前:0x16fda86f8

2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义后:0x155b22fc8

2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block内部: 0x155b22fc8

    “定义后”和“block内部”两者的内存地址是一样的,我们都知道 block 内部的变量会被 copy 到堆区,“block内部”打印的是堆地址,因而也就可以知道,“定义后”打印的也是堆的地址。

那么如何证明“block内部”打印的是堆地址?

把三个16进制的内存地址转成10进制就是:

定义后前:6171559672

block内部:5732708296

定义后后:5732708296

中间相差438851376个字节,也就是 418.5M 的空间,因为堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,显然a已经是在堆区了。

    这也证实了:a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。

    理解到这是因为堆栈地址的变更,而非所谓的“写操作生效”,这一点至关重要,要不然你如何解释下面这个现象:

以下代码编译可以通过,并且在block中成功将a的从Tom修改为Jerry。

NSMutableString *a = [NSMutableString stringWithString:@"Tom"];

NSLog(@"\n 定以前:------------------------------------\n\

a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);              //a在栈区

void (^foo)(void) = ^{

    a.string = @"Jerry";

    NSLog(@"\n block内部:------------------------------------\n\

    a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);              //a在栈区

    a = [NSMutableString stringWithString:@"William"];

};

foo();

NSLog(@"\n 定以后:------------------------------------\n\

a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);

打印结果


定以前:------------------------------------

a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fff5c5e4a58

block内部:------------------------------------

a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fdd88c180e0

定以后:------------------------------------

a指向的堆中地址:0x7fdd8aa01260;a在栈中的指针地址:0x7fff5c5e4a58

我们还能经常看到“内联块” 的用法:

NSArray *array = [NSArray array];

[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];

    这种常见的编码习惯也是可以看出来块为何如此有用。在OC中引入块这个特性之前,想要编出同样功能的代码,就必须传入函数指针或者是选择子的名称,这样就会再写几行代码了,而且还会令方法变得有些松散,与之相反,若声明内联形式的块,就能把业务逻辑都放在一起了。

    如果块所捕获的变量就是对象类型,那么就睡自动保存它,系统在释放这个块的时候,也会将其一并释放。这就引出了一个与块有关的问题。块本身可视为对象。实际上,在其他OC对象所能响应的选择子中,有很多是块也可以响应的。而最重要之处则在于,块本身也和其他对象一样有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获是所执行的保留操作。

    如果将块定义在OC类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用self变量。块总能修改实例变量,所以在声明时无需加__block。不过,如果通过读取或者写入操作捕获了实例变量,那么也会自动把self变量一并捕获了,因为实例变量与self所指代的实例关联在一起。

@property (nonatomic,copy) NSString *value;

@end

@implementation ViewController

- (void)viewDidLoad

{

    [super viewDidLoad];

    void (^someBlock)() = ^{

    _value = @"someBlock";

    };

}

@end

    在这种情况下,self变量就指向此block。由于在块内没有明确的使用self变量,所以很容易忘记self变量其实以为块所捕获了。直接访问实例变量和通过self来访问是等效的。然而一定要记住:self也是一个对象,因而块在捕获它时也会将其保留。如果self所指代的那个对象同时保留了块,那么就会出现保留环,在这样的情况下,我们一般会采用weak-strong dance方法(在ARC的情况下)来解决这个问题。

块的内部结构

每个OC对象都占据着某个内存区域。因为实例变量的个数及对象所包含的关联数据互不相同,所以每个对象所占的内存区域也有大小之分。块本身也就是对象,在存放块对象内存区域中,首个变量是指向Class对象的指针,该指针叫做isa,其他结构如图。

block结构

    在内存布局中,最重要的就是invoke变量,这是一个函数指针,指向块的实现代码。函数原型至少要接受一个void*型 的参数,此参数代表块。descriptor变量是指向结构体指针,每个块里都包含这个结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃对象时运行,其中会执行一些操作,比方说,前者要保留捕获的对象,后者将之释放块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间,请注意,拷贝并不是对象的本身,而是指向这些对象的指针变量。

栈块和堆块

    定义块的时候,其所站的内存区域是分配在栈中的。这就是说,块只在定义它的那个范围内有效。例如,下面这段代码就是有危险的:

void (^block)();

if (<#condition#>) {

    block = ^{

        NSLog(@"BlockA");

       };

}else{

    block = ^{

    NSLog(@"BlockB");

    };

}

    定义在if和else语句中的两个块都分配在栈内存中。编译器会给每个块分配好栈内存,然而等离开了相应的范围之后,编译器有可能把分配给块的内存覆写掉。于是,这两个块只能保证在对应的if和else语句范围内有效。这样写出来的代码可以编辑,但是运行起来有时正确有时不正确,若编译器没有覆写待执行的块,程序正常运行,若覆写,程序崩溃。为解决这个问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆 了。拷贝后的块,可以在定义它的那个范围之外使用。而且,一旦复制到了堆上,块就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,只是递增块对象的引用计数。如果不再使用这个块,那就应将其释放,在ARC下会自动释放,而在手动管理应用计数则需要自己来调用release方法。当应用计数降为0后,“分配到堆上的块”就会被系统回收。在“栈上的块”无须释放,占内存本身就会自动回收。

void (^block)();

if (<#condition#>) {

    block = [^{

    NSLog(@"BlockA");

    } copy];

}else{

    block = [^{

    NSLog(@"BlockB");

    } copy];

}

    这样就能够变得安全了,如果手动管理引用计数,那么在用完块之后还需要将其释放。

站在巨人的肩膀上,同时有个人的想法……

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

推荐阅读更多精彩内容