iOS——Block与内存管理
需要内存管理的情况
1、对象类型的auto变量。
2、引用了 __block 修饰符的变量。
三种block类型
全局类型 (NSGlobalBlock)
如果一个block里面没有访问普通局部变量(也就是说block里面没有访问任何外部变量或者访问的是静态局部变量或者访问的是全局变量),那这个block就是__NSGlobalBlock__。__NSGlobalBlock__类型的block在内存中是存在数据区的(也叫全局区或静态区,全局变量和静态变量是存在这个区域的)。__NSGlobalBlock__类型的block调用copy方法的话什么都不会做。
栈类型 (NSStackBlock)
如果一个block里面访问了普通的局部变量,那它就是一个__NSStackBlock__,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用__NSStackBlock__类型block时要注意,一定要确保它还没被释放。如果对一个__NSStackBlock__类型block做copy操作,那会将这个block从栈复制到堆上。
堆类型 (NSMallocBlock)
一个__NSStackBlock__类型block做调用copy,那会将这个block从栈复制到堆上,堆上的这个block类型就是__NSMallocBlock__,所以__NSMallocBlock__类型的block是存储在堆区。如果对一个__NSMallocBlock__类型block做copy操作,那这个block的引用计数+1。
特殊情况
在 ARC 环境下,编译器会自动将栈上的 block 复制到堆上。以下是会触发这种情况的四种情况:
作为函数返回值时
typedef void (^MyBlock)(void);
MyBlock createBlock() {
int localVar = 50;
return ^{
NSLog(@"Local variable: %d", localVar);
};
}
这里返回的 block 是 NSMallocBlock 类型,因为它是作为函数返回值返回的。
赋值给强指针时
void testStrongPointerBlock() {
int localVar = 60;
void (^stackBlock)(void) = ^{
NSLog(@"Local variable: %d", localVar);
};
void (^strongBlock)(void) = stackBlock;
}
strongBlock 是 NSMallocBlock 类型,因为它被赋值给一个强指针。
作为函数参数时
void executeBlock(void (^block)(void)) {
block();
}
void testFunctionParameterBlock() {
int localVar = 70;
void (^stackBlock)(void) = ^{
NSLog(@"Local variable: %d", localVar);
};
executeBlock(stackBlock);
}
传递给 executeBlock 的 stackBlock 被复制到堆上,因此是 NSMallocBlock 类型。
作为 GCD 的参数时
void testGCDParameterBlock() {
int localVar = 80;
void (^stackBlock)(void) = ^{
NSLog(@"Local variable: %d", localVar);
};
dispatch_async(dispatch_get_main_queue(), stackBlock);
}
stackBlock 作为 GCD 的参数时被复制到堆上,因此是 NSMallocBlock 类型。
__block关键字
__block 修饰的变量会被封装成一个结构体,而不是简单地复制值。这个结构体包含该变量的指针。
当 Block 从栈复制到堆时,__block 变量的引用也会被复制到堆上,并且 Block 会对其产生强引用,确保变量的生命周期和 Block 一致。
当 Block 从堆中移除时,会通过调用 dispose 函数释放 __block 变量,管理其内存。
比如有如下例子:
__block int val = 0;//修改后的代码
转化为c++代码:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
__Block_byref_val_0 *val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,__Block_byref_val_0 *_val, int flags=0) : val(_val->__forwrding){
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
__Block_byref_val_0 *val = __cself->val;
printf("val = %d",val->__forwarding->val);
}
可以看出,在原来的block对象struct __main_block_impl_0
中,多了一个 __Block_byref_val_0 *val;
这个就是指向了封装了val信息的结构体的指针。因此任何对这个指针的操作,是可以影响到原来的变量的。
在__Block_byref_val_0
的int val
才是我们真正捕获到的val变量的值。实际上外部的val的地址也确实是指向这里的。所以不管是外面还是block里面修改age时其实都是通过地址找到这里来修改的。而且我们可以看见,在__Block_byref_val_0
这个结构体中是有isa指针的,这就说明,我们实际上可以把它看作一个对象。
__block的内存管理方面的问题
既然是一个对象,那block内部如何对它进行内存管理呢?
当block在栈上时,block内部并不会对__Block_byref_val_0
产生强引用。
当block调用copy函数从栈拷贝到堆中时,它同时会将__Block_byref_val_0
也拷贝到堆上,并对__Block_byref_val_0
产生强引用。
当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部又会调用_Block_object_dispose
函数来释放__Block_byref_val_0
。
进一步,我们考虑截获的自动变量是Objective-C的对象的情况。在开启ARC的情况下,将会强引用这个对象一次。这也保证了原对象不被销毁,但与此同时,也会导致循环引用问题。
需要注意的是,在未开启ARC的情况下,如果变量附有_ _block修饰符
,将不会被retain,因此反而可以避免循环引用的问题。
__forwarding指针
当__block
变量在栈上时, __forwarding
指向是自己本身的指针,可以取到值。
2、当__block
变量在堆上时,__forwarding
指向也是自己本身的指针,可以取到值。
3、当__block
变量从栈上复制到堆上时,_Block_object_assign
函数会对__block
变量形成强引用(retain),此时栈上的 __forwarding
指向复制到堆上的 __block
变量的结构体指针。

__block的循环引用
__block
的循环引用主要出现在block从栈复制到堆的时候,如果在block中使用用__strong
修饰的对象时,在从栈复制到堆的时候就容易引起循环引用。
比如假如有一个block,它的成员变量在A类中被定义且为强引用,因此在这个类中,self是强引用这个block的,但是在这个block中它又使用了self,因此在这个block从栈复制到堆的时候,block会强引用self,self同时也在强引用block,此时就产生了循环引用。

因此这时,可以使用将self赋值给一个弱引用的id类型的变量,再在block中使用这个id类型的变量,使得block对self的引用为弱引用,因此来解决循环引用的问题。

还有一种方法是使用block来避免循环引用:就是在__block
中将其强引用的对象置为nil。

解决方法总结:
- ARC
- 使用
__weak
- 使用
__unsafe_unretained
- 使用
__block
解决(必须要调用block)
- MRC
- 使用
__unsafe_unretained
- 使用
__block
解决
block对象与OC对象相互持有(强引用) 才会造成相互循环引用
block对象持有__block
变量对象 __block
变量对象持有oc对象 oc对象持有block对象 构成3角循环引用
循环引用会导致实例对象不能释放 也就是实例对象所占用的内存不能及时被系统回收,会造成内存泄漏。
block如何截获变量的
假如有如下代码:
typedef void (^Block)(void);
Block block;
{
int val = 0;
block = ^(){
NSLog(@"val = %d",val);
};
}
block();
在这段代码中,val是用block捕获的变量。
将其转化为cpp文件:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int val;//这里多了一个名为val的变量
//这里的构造函数会增加一个方法列表为val赋值,这里的val(_val)的意思就是val=_val
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc,int _val, int flags=0) : val(_val){
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct void __main_block_func_0(struct __main_block_impl_0 *__cself){
int val = __cself->val;
printf("val = %d",val);
}
可以看出来,当使用block捕获了一个变量,首先会在__main_block_impl_0
结构体中增加一个成员变量并且在结构体的构造函数中对变量赋值。以上这些对应着block对象的定义。
在block被执行的时候,把__main_block_impl_0
结构体,也就是block对象作为参数传入__main_block_func_0
结构体中,取出其中的val的值,进行接下来的操作。
delegate 和 block的区别
从源头上理解和区别block和delegate
- delegate运行成本低,block的运行成本高。
block出栈需要将使用的数据从栈内存拷贝到堆内存,当然对象的话就是加计数,使用完或者block置nil后才消除。delegate只是保存了一个对象指针,直接回调,没有额外消耗。就像C的函数指针,只多做了一个查表动作。
从使用场景区别block和delegate
- 有多个相关方法。假如每个方法都设置一个 block, 这样会更麻烦。而 delegate 让多个方法分成一组,只需要设置一次,就可以多次回调。当多于 3 个方法时就应该优先采用 delegate。当1,2个回调时,则使用block。
- delegate更安全些,比如: 避免循环引用。使用 block 时稍微不注意就形成循环引用,导致对象释放不了。这种循环引用,一旦出现就比较难检查出来。而 delegate 的方法是分离开的,并不会引用上下文,因此会更安全些。
Block使用规范
在调用 Block 之前检查其是否为 nil:
在执行 Block 前,应先检查该 Block 是否存在(即不为 nil),以防止因调用空指针而导致程序崩溃。
OC对象函数与block调用在汇编层面上有区别,这种区别导致了对于block的调用需要进行判空后才能确保安全。如果调用的block是nil,程序会崩溃。
判空代码例如:
!block ?: block();
调用多层对象的block时,也需要进行判空,即使d对象与其block必然存在,也可能因为a、b、c对象中任意一个为nil,导致出现测试用例3的场景,调用一个nil对象的block产生崩溃,比如:
//不安全调用
a.b.c.d.block();
//安全调用
!a.b.c.d.block ?: a.b.c.d.block();
对于这种情况,可以对将该block进行一层函数封装,可以避免过长的判断逻辑:
//d类
- (void)callBlock {
!self.block ?: self.block();
}
//调用
[a.b.c.d callBlock];
使用 Block 参数时判空:
在方法或函数内部使用 Block 参数时,也应先判空,确保安全调用。
两个问题:
-
为什么block中不能修改普通变量的值?
由于无法直接获得原变量,技术上无法实现修改,所以编译器直接禁止了。 -
__block
的作用就是让变量的值在block中可以修改么?
都可以用来让变量在block中可以修改,但是在非ARC模式下,__block
修饰符会避免循环引用。注意:block的循环引用并非__block
修饰符引起,而是由其本身的特性引起的。