iOS 面试全方位剖析 -- Block篇


  • block 介绍
  • 截获变量
  • __block修饰符
  • Block的内存管理
  • Block的循环引用

截获变量

先看一个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 全局变量
int global_var = 4;
// 静态全局变量
static int static_global_var = 5;
- (void)method
{
int multiplier = 6;
int(^Block)(int) = ^int(int num)
{
return num * multiplier;
};
multiplier = 4;
NSLog(@"result is %d", Block(2));
}
输出是什么?
如果 将 int multiplier 改为静态变量 static int multiplier = 6, 结果又是什么?

带着问题往下看,有这几种类型的变量

  • 局部变量 – 基本数据类型 , 对象类型 (对于基本数据类型的局部变量截获其值,对于对象类型的局部变量连同 所有权修饰符 一起截获)

  • 静态局部变量 ( 以指针形式截获局部静态变量 )

  • 全局变量 (不截获全局变量)

  • 静态全局变量 (不截获静态全局变量)

从这段代码来深入了解一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int global_var = 4;
static int static_global_var = 5;
-(void)method1
{
int var = 1;
__unsafe_unretained id unsafe_obj = nil;
__strong id strong_obj = nil;
static int static_var = 3 ;
void(^block)(void) = ^{
NSLog(@"局部变量<基本数据类型> var %@",var);
NSLog(@"局部变量<__unsafe_unretained 对象类型> var %@",unsafe_obj);
NSLog(@"局部变量< __strong 对象类型> var %@",strong_obj);
NSLog(@"静态变量 %d",static_var);
NSLog(@"全局变量 %d",global_var);
NSLog(@"静态全局变量 %d",global_var);
}
}

使用 clang命令看一下编译后的源码 MCBlock.cpp
看这一段

  • 可以看到,局部变量截获的就是它的值
  • 静态局部变量以指针形式截取的
  • 对象类型的类型连同其修饰符一起截获,理解这个就能更好的理解 Block 循环引用的问题,后续会说
  • 全局和静态全局变量不截获

然后回到问题

1
2
3
int multiplier = 6 ,block(2)输出的是 12,因为block执行的时候截获的是 6
static int multiplier = 6 ,block(2) 输出的是8,因为截获以指针形式截获,所以获取到的 multiplier 是最新的值 4

__block修饰符

什么情况下需要用到 __block修饰符 呢?
对被截获变量进行赋值操作的时候 (区分 赋值 和使用)

看一些笔试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSMutableArray *array = [NSMutableArray array];
void(^block)(void) = ^{
[array addObject:@123];
};
Block();
这里 对 array 只是一个使用,而不是赋值,所以不需要 _ _block 进行修饰
NSMutableArray *array = nil;
void(^block)(void) = ^{
array = [NSMutableArray array];
};
Block();
这里就需要在array的声明处添加__block修饰符,不然编译器会报错

总结下,对变量进行赋值的时候,下面这些不需要__block修饰符

  • 静态局部变量
  • 全局变量
  • 静态全局变量
1
2
3
4
5
6
7
8
9
10
11
{
__Block int multiplier = 6;
int(^Block)(int) = ^int(int num)
{
return num * multiplier;
};
multiplier = 4;
NSLog(@"result is %d", Block(2));
}
//这里的结果就是 8 了

加了 __block 修饰之后,这个变量就变成了一个对象

在 multiplier 进行赋值的时候
forwarding指向原来的对象, 通过 forwarding 指针进行赋值,修改掉 multiplier 的值


Block的内存管理

Block类型

  • _NSConcreteGlobalBlock 全局
  • _NSConcreteStackBlock 栈类型
  • _NSConcreteMallocBlock 堆类型

看一下各个类型的Block在内存上面的分配

###Block的copy操作

比如现在声明一个成员变量Block,而在栈上创建这个Block去赋值,如果没有对Block进行Copy操作的话,当我们通过成员变量去访问这个Block的时候,可能会因为栈对应的函数退出之后在内存当中就销毁掉了,继续访问就会引起内存崩溃


Block的循环引用

下面这段代码就会造成循环引用

1
2
3
4
5
_array = [NSMutableArray arrayWithObject:@"block"];
_strBlk = ^NSString*(NSString*num){
return [NSString stringWithFormat:@"hello_%@",_array[0]];
};
_strBlk(@"hello");

self 持有 Block,而 Block 里有成员变量 array, 持有 self,所以就造成了循环引用,怎么解决呢?

1
2
3
4
5
6
_array = [NSMutableArray arrayWithObject:@"block"];
__weak NSArray *weakArray = _array;
_strBlk = ^NSString*(NSString*num){
return [NSString stringWithFormat:@"hello_%@",_array[0]];
};
_strBlk(@"hello");

为什么用_ weak 修饰符解决循环引用? 这个其实在截获变量里有讲过,截获对象的时候会连同修饰符一起截获,在外部定义的如果是 weak 修饰符,在 Block 里所产生的结构体里面所持有的成员变量也是 _weak 类型

再看一段代码,这样写有什么问题?

1
2
3
4
5
6
7
__block MCBlock*blockSelf = self;
_blk = ^int(int num){
//var = 2
return num * blockSelf.var ;
};
_blk(3);

这样在 ARC 模式下是会产生循环引用,引起内存泄漏的

__block修饰后的指向是原来的对象,会造成循环引用
怎么解决呢,首先想到的当然是断开其中一个环

1
2
3
4
5
6
7
8
9
__block MCBlock*blockSelf = self;
_blk = ^int(int num){
//var = 2
int result = num * blockSelf.var;
blockSelf = nil;
return result;
};
_blk(3);

在调用完 blockSelf 后将它置为nil,断开其中的一个环,就可以让内存得到释放和销毁
但是这样会有一个弊端,如果长期不调用这个block,这个循环引用的环就会一直存在


Block 面试总结

  • 什么是 Block?
  • 为什么 Block会产生循环引用?
  • 如何理解 Block 截获变量的特性?
  • 你都遇到过哪些循环引用?怎么解决的?

转载请标明出处