iOS开发之block详解

来源:互联网 发布:centos 7开机密码忘记 编辑:程序博客网 时间:2024/05/16 13:43

前言

今天研究了一下iOS开发中的block,有些心得,故写下来。默认读者会是有 iOS开发经验的,故相关内容不再科普。


1.block入门

老子说:学习编程,从翻译开始。block的翻译是什么?是代码块。代码块是个什么鬼?

block就是一块可以使用的代码,在定义和声明上和变量类似,在使用上和函数类似。

下面上demo,首先是全局的代码块:

void (^foo)(void) = ^(void){    NSLog(@"Hello World!");};

以及全局变量:

static int _index = 0;

和函数

void func(void) {    NSLog(@"Hello World!");}

由上面可以看到,block即像函数一样有参数列表,又像基本数据类型一样是有定义声明,最后需要接分号“;”的。
而block在使用中,更加像函数:

foo();

而拆开的block的定义和声明,又和变量的定义和声明比较像:

void (^block)(void);block = ^(void){    NSLog(@"Hello World!");};

对block有了基本的认识,下面来看下block有哪些特性,以及需要注意的问题。

2.block访问局部变量

block对上下文局部变量的访问,又有一个高大上的名词,就是“闭包性”。事实上,block的上下文访问,并不是直接访问,而是做了一个备份,对备份的访问。

2.1.block访问局部变量——读取

block对内部局部变量的控制,类似于一个维护着的参数表。block内部使用到的所有变量,都会另外申请新的内存空间。对于block的参数,自然是和函数的参数类似,遵循一般的取值取址操作。对于block访问的外部的局部变量,是获取的一份局部变量的数据拷贝。

所以,局部变量的声明周期结束,所占的内存释放后,block仍旧可以获取其中的数据:

void (^block)(void);- (void)blockTest01 {    int i = 10;    block = ^(void) {        NSLog(@"%d",i);    };}- (void)blockTest11 {    [self blockTest1];    block();}

通过打印地址,我们可以看到,block对局部变量的访问确实是值访问。

- (void)blockTest02 {    int i = 10;    NSLog(@"局部变量i --- %p", &i);    block = ^(void) {        NSLog(@"block访问i --- %p",&i);    };}

运行结果为:

局部变量i   --- 0x7fff58ef2a7cblock访问i --- 0x7fd8fa507280

以上,基本可以确定,block访问局部变量是单独拷贝了一份数据。说[访问][6]其实不是很准确,因为局部变量的声明周期早已结束,这里实际上是block私下备了个份。
而block备份的方式是值拷贝,所以block对局部变量的访问会出现和函数参数类似的值传递、址传递的问题。具体下面说。

2.2.block访问局部变量——址传递的修改

有C语言功底的朋友应该知道,指针中存放的值,是实际值在内存中的地址。而block中对局部变量的访问,就是一种值访问。
当该局部变量是一般的数据时:

int i = 0;

拷贝的是i的值,即:“0”。因此,block对于该局部变量的修改,仅限于其备份,而对其本身是起不到任何作用的。
然而,当局部变量是指针时:

char *str = "Hello";

str中存储的是“Hello”这个字符串在内存中所在的位置。即便局部变量的声明周期结束后,由于block中的str的备份仍然持有该内存,故该内存是不会被释放,此处遵守C语言内存管理原则[谁拥有谁释放][6]。由于block持有了str的地址,因此对其备份的修改,就是对内存地址所指向内容的修改。
当然,上例中,str是在常量区的,是无法直接修改的,这又是另一个话题了。

至此,block对一般的局部变量的访问就讲完了。

2.3.block访问局部变量——值传递的修改

接上节,当遇到block值传递数据,比如:

int i = 0;

我们怎么实现对其的修改呢。根据我们对C语言值传递的经验,我们很容易知道,只要重新获得i的地址,进行传递就可以了,比如:

- (void)blockTest03 {    static int i = 10;    int *pi = &i;    block = ^(void) {        (*pi)++;        NSLog(@"值:%d,址:%p",*pi, pi);    };    NSLog(@"局部变量i:%d",i);}

当我们多次运行block,再重新打印局部变量时:

    [self blockTest03];    block();    block();    block();    [self blockTest03];

会发现

局部变量i:10值:11,址:0x10205fe18:12,址:0x10205fe18:13,址:0x10205fe18局部变量i:13

也就是说,我们成功的通过址传递,完成了在block内部对局部变量的修改。
当然,这种方法很C风格,对OC程序员很不友好,因此苹果给了我们一个代码糖__block,来实现相同的功能。

__block typeof(i) bi = i;

这里的bi在使用中和*pi相同。

总结一下:

__block是用来将局部变量的访问改为取址操作的。
简单的讲,就是解决无法修改局部变量值的一个方法。

2.4.block访问局部变量——循环引用

当出现以下场景时:某个类FooClass有一个成员变量barBlock,以及一个成员变量name。
当barBlock内部通过self使用了name时,就会出现以下情况:barBlock持有self的地址,self持有barBlock的地址,二者内存地址相互持有,导致二者均无法正确释放,造成内存泄露。
用OC的说法就是,二者相互强引用,导致内存无法释放。解决办法就是,将其中一个设置为若引用。
这个就是我们常说的block循环引用,以及其解决办法:

__weak typeof(self) wSelf = self;

总结一下:

__weak是用来解决在block调用“拥有该block的对象的成员变量”时,造成的循环引用问题的。
简单的将,就是当block里面用到self时,要用上面的代码。

2.5.__block和__weak的区别

如果读过上面的部分,应该知道,二者的区别是很大的。只是由于二者用的时候看起来很像,故而有可能会被混淆。
总结一下:

__block是对于无法修改的局部变量,使得block能够修改的方法
__weak是对于可修改的变量self,解决其使用中会造成循环引用问题的一个办法。

3.block的本质

其实从底层实现上,block和NSObject一样,是用struct实现的。这也就解释了,为什么block的定义和声明会和变量的声明非常相似。
block的实现以及其闭包性,就是通过该struct实现的。该struct内有两个比较重要的成员:指向实际执行的函数指针、block内部调用的成员变量表。就是在该变量表中,存放着上下文临时变量的拷贝。
具体的细节,有一篇转载的博文Block的引用循环问题 (ARC & non-ARC),以及Objective-C中的Block),讲的比较详细,有兴趣的同学可以去研究一下。

1 0
原创粉丝点击