【iOS】OC底层系列七 - block的内存管理

一、block的内存管理

  • 当block在栈上时,并不会对__block变量产生强引用。
  • 当block被copy到堆时,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(retain)

示例代码:

1
2
3
4
5
6
__block int age = 10;
DBBlock block = ^{
age = 20;
NSLog(@"age is %d", age);
};
block();

默认情况下,变量age和定义的block都是在栈上的,由于是ARC环境,定义的block被变量block强引用,block自动从栈copy到堆。当block被copy到堆时,会把block内部用到的变量也从栈copy到堆。

注意:如果多个block内部捕获的是同一个变量,该变量copy到堆中时只会copy一份,内部操作的是引用计数。

  • 当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)。

1.1. __block变量、对象类型的auto变量

  • 当block在栈上时,对它们都不会产生强引用。

  • 当block拷贝到堆上时,都会通过copy函数来处理它们

    • __block变量(假设变量名叫做a)
      • _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    • 对象类型的auto变量(假设变量名叫做p)
      • _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
  • 当block从堆上移除时,都会通过dispose函数来释放它们

    • __block变量(假设变量名叫做a)
      • _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    • 对象类型的auto变量(假设变量名叫做p)
      • _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

1.2. 被__block修饰的对象类型

  • __block变量在栈上时,不会对指向的对象产生强引用。
  • __block变量被copy到堆时,会调用__block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据所指向对象的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain)。
  • 如果__block变量从堆上移除,会调用__block变量内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放指向的对象(release)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@interface DBPerson : NSObject

@end

@implementation DBPerson

- (void)dealloc {
NSLog(@"%s", __func__);
}

@end

typedef void (^DBBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
DBBlock block;
{
__block DBPerson *person = [[DBPerson alloc] init];
block = ^{
NSLog(@"%@", person);
};
}
block();
NSLog(@"block stop");
}
return 0;
}

/*
输出:
<DBPerson: 0x10073a940>
block finished.
-[DBPerson dealloc]
*/
1
2
3
4
5
6
7
8
9
struct __Block_byref_person_0 {
void *__isa;
__Block_byref_person_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
DBPerson *__strong person;
};

上面代码中的person对象使用__block修饰,并且person对象是在block被释放后才开始释放的。因此,block内部对__Block_byref_weakPerson_0类型的person进行的是强引用,而且person转成的结构体对象__Block_byref_weakPerson_0中的person也是强引用。

只要使用__block修饰对象,转成的结构体对象被copy到堆时都会生成一个copy和dispose函数,用来进行内存管理。

如果把上面的person对象使用__weak修饰会怎么样?

1
2
3
4
5
6
7
8
9
DBPerson *person = [[DBPerson alloc] init];
__block __weak DBPerson *weakPerson = person;

/*
输出:
-[DBPerson dealloc]
(null)
block finished.
*/
1
2
3
4
5
6
7
8
9
struct __Block_byref_weakPerson_0 {
void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
DBPerson *__weak weakPerson;
};

person被提前释放了,说明使用__weak修饰后,person转成的结构体对象__Block_byref_weakPerson_0中的person是弱引用,但是impl中的__Block_byref_weakPerson_0类型的person是强引用。

从上面的案例可以看出,__weak仅针对变量转为结构体后,对结构体内部的变量进行弱引用。如果不使用__weak,在ARC环境下就是强引用。在MRC环境下,不管是否使用__weak,最终都是弱引用,如果想要强引用,就不能加上__block,相当于block直接捕获外部对象。

1.3. __block__forwarding指针

经过前面的了解我们知道,__block修饰的变量会被封装成一个结构体对象,而且操作这个变量时,访问的是__forwarding中的变量,那么这个结构体中的__forwarding指针为什么要指向自己呢?主要是防止block被copy到堆后,操作的还是栈上的block,__forwarding能够保证操作的一定是堆上的数据。

1
2
3
4
5
6
7
8
9
10
// age被包装成对象
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};

// 使用:age->__forwarding->age

二、block的循环引用

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef void (^DBBlock)(void);

@interface DBPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) DBBlock block;
- (void)test;

@end

@implementation DBPerson

- (void)test {
self.block = ^{
NSLog(@"%@", self.name);
};
}

- (void)dealloc {
NSLog(@"%s", __func__);
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
DBPerson *person = [[DBPerson alloc] init];
person.name = @"daben";
[person test];
}
NSLog(@"111111");
return 0;
}

// 输出:111111

疑问:为什么DBPerson类的dealloc方法没有执行?

因为person类中的成员变量block和self形成了循环引用。在main方法中创建了person对象(person对象引用计数是1),在test方法中,block内部捕获了局部变量self(person对象引用计数是2),而self又持有成员变量block。当person出了作用域后就会被释放一次(此时person对象引用计数是1),由于block内部强引用了self(self就是person对象),因此不会执行dealloc

2.1. 解决循环引用

__weak__unsafe_unretained可以解决block的循环引用问题。

如上面的示例代码可以修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)test {    
/*
__weak id weakSelf = self;
__weak DBPerson *weakSelf = self;
上面两句代码和下面的代码是等价的
typeof(self) 返回的是DBPerson * const,由于不容易出错,所以建议开发时这样写
*/
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%@", weakSelf.name);
};
}
/*
输出:
-[DBPerson dealloc]
111111
*/

使用__weak修饰self后,person对象被完全正常释放了,因为block中的self是弱引用。

__unsafe_unretained字面意思是不安全、不会产生强引用。和__weak的使用效果是一样的,唯一区别:使用__weak修饰的对象被block引用后,当对象释放后,指向对象的**指针会指向nil。使用__unsafe_unretained修饰的对象最终被释放后,指向对象的指针不会指向nil**,依旧指向原来对象的内存(已经被释放),此时的指针也叫作野指针。

__block也可以解决循环引用问题(必须要调用block)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)test {
__block DBPerson *weakSelf = self;
self.block = ^{
NSLog(@"%@", weakSelf.name);
weakSelf = nil;
};
self.block();
}
/*
输出:
daben
-[DBPerson dealloc]
111111
*/

上面代码如果不调用block,三个对象(__block生成的结构体对象、person对象、block结构体对象)就会形成相互引用。

调用block后就会打破这种强引用关系:让__block结构体对象弱引用person对象,在block执行体中加上weakSelf = nil就可以断掉__block变量对person对象的强引用关系。

如果是MRC环境,不支持__weak,使用__unsafe_unretained解决循环引用。但是使用__block也能够解决循环引用,因为在MRC环境,__block生成的结构体对象内部不会对原来的对象产生强引用。

思考:如果在block内部使用强指针引用block外部的弱指针,会出现什么情况?

1
2
3
4
5
6
7
8
9
10
11
12
- (void)test {
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog(@"%@", strongSelf->_name);
};
}
/*
输出:
-[DBPerson dealloc]
111111
*/

上面的代码打印后显示person对象被释放,也就是没有产生循环引用。而且在block内部不能使用weakSelf访问成员变量_name,因为weakSelf指向的对象可能会被提前释放。如果使用__strong修饰一个弱引用指针,weakSelf就不会立即被释放。


面试题1:block的原理是怎样的?本质是什么?

解答:封装了函数调用以及调用环境的OC对象。

面试题2:__block的作用是什么?有什么使用注意点?

解答: __block主要是为了在ARC环境下能够在block内部修改外部的变量。使用时需要注意循环引用。

面试题3:block的属性修饰词为什么是copy?使用block有哪些使用注意?
解答:block一旦没有进行copy操作,就不会在堆上(堆上的目的:自由控制block的生命周期)。使用注意:循环引用问题。

面试题4:block在修改NSMutableArray,需不需要添加__block

解答:不需要。因为操作的是array指针,不是对象。