关于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 addition = 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,其他结构如图。



在内存布局中,最重要的就是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];

}

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值