深入学习block

系统地深入学习block对象及其变量调用与内存管理

block

简介

block是我们经常使用到的一种对象。可以认为block就是一个函数指针,我们利用block在不同的函数或类中传递函数操作。
从函数的角度出发,我们非常好理解block的语法:
声明一个block需要指明三个部分:返回值,block名称和参数类型。定义一个block只需要声明参数并实现方法内容。参数如果是void可以省略。
block使用方式与C语言函数完全相同。

1
2
3
4
void (^print)(NSString *) = ^(NSString *info) {
NSLog(@"%@", info);
};
print(@"log");

如果仅从语法上来看,block的内容真是少的可怜。但要真正深入理解block还是需要一番功夫的。block在ARC下和MRC下的实现有很大区别,我们以下的代码均通过ARC实现

block“对象”

我们之所以为对象添加引号是由于block并不是严格意义上的对象,它是通过C的struct模拟出的具有Object C对象特征的结构体。

1
2
3
4
5
int val = 10;

void (^block)(void) = ^{
NSLog(@"%d", val);
};

我们来看一下上面这个block的结构

1
2
3
4
5
6
7
8
9
10
11
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcretMallocBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

可以看出block会被处理为C的struct,其中包含三个组成部分:block的实现、block的信息以及block的参数。
我们待会儿再看block的参数传递问题,先来看block的实现部分。impl有一个跟NSObject对象一样的isa指针指向了_NSConcretMallocBlock。从实现上我们可以看出block和标准的OC对象非常类似。
block共有三种类:

  • _NSConcretStackBlock 存储在栈上的block,引用了外部变量,且用__weak限定创建的block是这种类型
  • _NSConcretGlobalBlock 存储在全局的block,没有引用外部变量的都是这种类型(注意跟block变量本身是否是全局的没有任何关系)
  • _NSConcretMallocBlock 存储在堆上的block,引用了外部变量,且用__strong限定或不用限定创建的block都是这种类型

当我们使用block的copy方法时,_NSConcretStackBlock类型的block会被变为_NSConcretGlobalBlock类型的。

block的变量调用

block调用变量方法是block使用中非常重要的一环,直接影响了我们对于block内存管理的理解。

全局变量访问

全局变量以及静态本地变量都是可以直接访问并修改的。在我测试的block结构生成中,全局变量并没有作为变量加入到block结构中,而是直接访问该全局变量。故我们可以直接进行变量的赋值。

成员和属性变量访问

block在访问这两种变量时,应当是直接将self retain后作为block的参数传入的。故成员和属性变量可以通过self访问和赋值。

局部变量访问

变量会通过值传递的方式传入block结构中,OC对象传值之前会retain。由于传递方式是按值传递,所以无论是否可以赋值都不会对原变量产生影响。故OC里面索性直接让使用者当其是const变量来对待。

带block的局部变量访问

block不能修饰全局变量。其他的变量在有block修饰时会变为结构体。例如如下的代码:

1
2
3
4
5
6
__block int i = 1;
void (^block1)(void) = ^{
i++;
NSLog(@"%d", i);
};
i++;

其中的变量i会被翻译为:

1
2
3
4
5
6
7
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};

block中调用变量实际是通过__forwarding指针来进行调用。

1
2
3
4
5
6
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_i_0 *i = __cself->i; // bound by ref

(i->__forwarding->i)++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_4f7t75rj34d2w41kwhrn7ptr0000gp_T_test_bf9a0f_i_0, (i->__forwarding->i));
}

当有__block修饰后,变量的调用也发生变化,例如上面例子中block外面的对于i的调用会变为:

1
2
   (i->__forwarding->i)++;
}

我们可以发现与block中调用方式完全相同。这也是为什么__block修饰的变量可以赋值的原因。

最后一个问题就是为什么局部变量在栈中却能在出栈后也能访问。
这是由于出栈运行且调用变量的block一定是在堆中存储的。在block进堆中时,会将局部变量复制到堆中,并让__forwarding指针指向堆中的block。所以之后我们访问的所谓局部变量其实早已经是堆中的变量了,不会因为出栈消失。

block内存管理

block在使用过程中很容易出现内存泄漏的情况。通过上文对于block变量调用的分析,我们知道block会在访问属性和成员变量时强引用self。如果self本身拥有了block或者block在self该dealloc时没有回收,会出现内存泄漏问题。
解决内存泄漏通常有两种办法,weak和block。在实际使用中,我们需要根据需求选择两种方法。

__weak

__weak修饰符能够保证block不会retain其使用的对象。好处是不需要额外操作,其使用对象的释放与block本身是否释放毫无关系。坏处就是block执行的时候,对象有可能已经成nil了。

__block

__block修饰符的变量会retain其使用的对象,在block释放时会自动解除引用。好处是block之行的时候,引用的对象一定都存在。坏处是,若block不释放则必须手动置block变量为nil才能解除对外部变量的强引用,如:

1
2
3
4
5
__block id tmp = self;
void(^block)(void) = ^{
tmp = nil;
};
block();

当tmp设置为nil时,block对于self的强引用就解除了。

文章目录
  1. 1. block
    1. 1.1. 简介
    2. 1.2. block“对象”
    3. 1.3. block的变量调用
      1. 1.3.1. 全局变量访问
      2. 1.3.2. 成员和属性变量访问
      3. 1.3.3. 局部变量访问
      4. 1.3.4. 带block的局部变量访问
    4. 1.4. block内存管理
      1. 1.4.1. __weak
      2. 1.4.2. __block
,