文章目录
- 前言
- 准备工作
- 1. Block的分类
- 2. Block的内存分析
- 捕获外部变量引用计数的变化
- 堆栈释放的差异
- 总结
- 3. Block的循环引用
- 3.1 什么是循环引用?
- 案例引入
- 循环引用解决方法
- 1. 强弱共舞
- 2. 手动中断循环引用
- 3. 参数形式解决循环引用(block传参)
- Block循环引用案例
- 总结
前言
本来打算这周看看源码的,因为某些原因进度耽误了,另外强调了 GCD多线程,Runtime RunLoop,BLock都是面试必问必考的点,想起之前看的比较简略,就借着Block的循环引用顺带复习一下Block的整个流程。
在学习之前 对于ARC以及涉及到的属性关键字都要有了解还是要有了解的基础,尤其是weak。
准备工作
weak的实现原理
iOS ARC实现
iOS Block基础再探究
1. Block的分类
简单复习
block的分类主要分为以下三种
__NSGlobalBlock__全局block__NSStackBlock__栈区block__NSMallocBlock__堆区block
他们的分类原因和block的定义有关系 block是一个带有自动变量值的匿名函数。
- 什么是带有自动变量?
- 带有自动变量值”在Block中表现为“截取自动变量值
例如如下blk即截取了外部变量。
- (void)mallocBlock {
NSString* str = @"mallocBlk";
void (^glbBlk)(void) = ^{
NSLog(@"%@", str);
};
NSLog(@"%@", glbBlk);
}
Block分类简单总结如下
- A、没有引用外部变量 — block存放在全局区
- B、引用了外部变量----显式声明为
weak类型的block则存放在栈区,反之则是存在在堆区的,也就是说block是strong类型的。
2. Block的内存分析
捕获外部变量引用计数的变化
ARC环境,案例中定义了两个block,跟踪objc的引用计数变化。
#pragma mark- Blk内存分析
- (void)blkMemory {
NSObject *objc = [NSObject new];
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)(objc))); // 1
void(^strongBlock)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
strongBlock();
void(^__weak weakBlock)(void) = ^{
NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(objc)));
};
weakBlock();
void(^mallocBlock)(void) = [weakBlock copy];
mallocBlock();
}
运行结果如下:

- 第一个打印为1就是简单创建引用计数+1
- 第二个为3:
strongBlock是一个堆区的block,这里捕获了objc这个外部变量会进行加一,这里会把栈区的objc拷贝到堆区又进行了加一,所以打印结果为 3。 - 打印 4是因为这里
weakBlock是栈block,没有进行拷贝只是捕获+1,所以为4。 - 最后打印5是因为
[weakBlock copy]进行了拷贝操作,再赋值给mallocBlock也是+1操作,所以打印结果为 5。
如何区分堆栈Blk 这个需要了解blk的实现过程,在之前已作学习。
堆栈释放的差异
差异1
#pragma mark -堆栈释放差异
- (void)blockDemo1 {
int a = 777;
void(^__weak weakBlock)(void) = nil;
{
void(^__weak strongBlock)(void) = ^{
NSLog(@"%d", a);
};
weakBlock = strongBlock;
NSLog(@"1 - %@ - %@", weakBlock,strongBlock);
}
weakBlock();
}

该段代码的运行并无任何问题:
- 声明了一个
weakBlock,该block为NSStackBlock; - 在代码块中,定义了
strongBlock,其也为NSStackBlock; - 对weakBlock进行了赋值,此时两个
block均指向同一个NSStackBlock; - 因为这两个栈
block的生命周期到blockDemo方法运行结束,并不会被提前释放;(特地查了它的生命周期当创建 NSStackBlock 的函数返回时,栈上分配的内存将被释放,NSStackBlock 对象也将被销毁。) - 所以调用
weakBlock()可以正常运行,并能够输出a的值。 差异2
对上面的案例strongBlock修改成了NSMallocBlock,取消了__weak修饰符。
- (void)blockDemo2 {
int a = 0;
void(^__weak weakBlock)(void) = nil;
{
void(^strongBlock)(void) = ^{
NSLog(@"---%d", a);
};
weakBlock = strongBlock;
NSLog(@"1 - %@ - %@", weakBlock,strongBlock);
}
weakBlock();
}

案例崩溃 ,lldb调试发现在代码块{}结束之前 调用p weakBlock都是有地址的,但是出了代码块{}发现就没有了。

结合lldb调试结果分析:
strongBlock为NSMallocBlock,其生命周期范围在代码块{}内,也就是出了代码块其就会被释放;- 在代码块中对
weakBlock进行了赋值,指针拷贝,指向了对应的NSMallocBlock,但是并没有强引用指向这个block(__weak修饰符不会带有强引用,创建暨被释放); -
- 这里的指针拷贝可以从下图看到,二者指向相同的一片内存地址,

- 这里的指针拷贝可以从下图看到,二者指向相同的一片内存地址,
- 代码块执行完毕后,该
NSMallocBlock就会被释放,此时weakBlock指向的对象已经被释放,形成野指针,所以无法正常执行。
最后把两个block的__weak都去掉,则能够正常运行并打印结果
- (void)blockDemo3 {
int a = 0;
void(^weakBlock)(void) = nil;
{
void(^strongBlock)(void) = ^{
NSLog(@"---%d", a);
};
weakBlock = strongBlock;
NSLog(@"1 - %@ - %@", weakBlock, strongBlock);
}
weakBlock();
}

上面的例子崩溃了,是因为堆区的 block出了作用域,被释放了。
此时的赋值,weakBlock对堆中的block进行了强引用,代码块运行结束后不会释放掉,也就不存在野指针的问题了。
总结
block分为全局block、堆block、栈blockblock可以捕获外部变量- 堆区的
block捕获外部变量会拷贝到堆区引计数+1 block在使用的时候,堆block注意作用域的问题,弱引用指针赋值,出了作用域就释放了,可以通过强引用解决
3. Block的循环引用
- 参考自:iOS_Block循环引用
3.1 什么是循环引用?
- 正常情况对于对象的持有和释放是如下逻辑

- 循环引用就是对面持有导致对象不能及时的正常释放,容易造成内存泄漏。

案例引入
如下代码可能造成循环引用
#import "ViewController.h"
typedef void (^TBlock)(void);
@interface ViewController ()
@property (nonatomic, strong) TBlock block;
@property (nonatomic, strong) NSString *name;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 循环引用
self.name = @"ViewController";
self.block = ^() {
NSLog(@"%@", self.name);
};
self.block();
}
self持有了block,block持有了self,导致循环引用。 编译器也会提示:Capturing 'self' strongly in this block is likely to lead to a retain cycle

- 如果我们取消某一方的持有即可取消循环引用,恢复到正常的逻辑里面去。
// 不会引起循环引用
void(^blk1)(void);
blk1 = ^() {
NSLog(@"%@", self.name);
};
blk1();
这个案例就没有出现循环引用是因为当前self,也就是ViewController并没有对block进行强持有,block的生命周期只在viewDidLoad方法内,viewDidLoad方法执行完,block就会释放。
循环引用解决方法
在开始也提到过weak是基础,weak也是最为熟知的解决循环引用的方法,__weak不会进行引用计数的操作
weak的使用
__weak typeof(self) weakSelf = self;
typeof(self):typeof 是一个运算符,用于获取表达式的类型。在这种情况下,表达式是 self,它代表当前对象的引用
避免循环引用方法:
__weak typeof(self) weakSelf = self;
self.block = ^(){
NSLog(@"%@", weakSelf.name);
} ;
self.block();
此时self持有block,block弱引用self,弱引用会自动变为nil,强持有中断,所以不会引起循环引用。
- 反面案例
之前在学习GCD的时候学到过将其他线程的麻烦的方法执行完之后再次回到主线程,但是万一在其他线程执行的过程里面ViewController被销毁的话是什么情况,由于ARC不能显式调用dealloc 我们模拟一下延时。
。。。。。。。
__weak typeof(self) weakSelf = self;
self.block = ^(){
// 延迟2秒钟
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)( 2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf.name);
});
};
self.block();
[self fakeDealloc];
}
- (void)fakeDealloc {
NSLog(@"调用了dealloc 模拟ViewController模拟销毁");
// 模拟viewController被销毁
self.name = nil;
}
dealloc方法调用了,VC 已经销毁了,block 也就释放了,内部的延时函数里面的打印还来不及打印,所以 name为 null.

虽然没有引起循环引用,但是block中延迟2秒钟执行任务,如果此时ViewController被销毁,此时block已经无法获取ViewController的属性name,很不合理。
1. 强弱共舞
为了解决上面的问题 由此引出了强弱共舞
则一切就会正常打印。
原理:因为__weak会自动置为nil,所以这里使用__strong(strong-weak-dance)暂时延长 self的生命周期,使得可以正常打印。
为什么强弱共舞能够避免循环引用,不是也调用了self? 因为这里strongSelf是一个临时的变量,出了作用域也跟着释放了,所以不会出现循环引用🐮
简单分析:
- 在完成block中的操作之后,才调用了
dealloc方法。添加strongWeak之后,持有关系为:self->block->strongWeak->weakSelf->self。 weakSelf被强引用了就不会自动释放,因为strongWeak只是一个临时变量,它的声明周期只在block内部,block执行完毕后,strongWeak就会释放,而弱引用weakSelf也会自动释放
2. 手动中断循环引用
self.name = @"ViewController";
__block ViewController * VC = self;
self.block = ^(){
__strong __typeof(weakSelf)strongWeak = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", ctrl.name);
VC = nil;
});
};
self.block();
这里使用一个临时的VC变量之后,持有关系为: self --> block --> VC–> self,VC在block使用完成后就被置为nil,block不构成对self的持有关系了,因此这里就不构成循环引用问题。
用__block是因为需要对外部变量,进行赋值操作
3. 参数形式解决循环引用(block传参)
- (void)parameterMethod {
self.name = @"ViewController";
// blk传参解决循环引用
// 循环引用
self.block = ^(ViewController * ctrl){
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", ctrl.name);
});
};
self.block(self);
将self作为参数参入block中,进行指针拷贝,并没有对self进行持有。
Block循环引用案例
- 静态变量持有
// staticSelf_定义:
static ViewController *staticSelf_;
- (void)blockWeak_static {
__weak typeof(self) weakSelf = self;
staticSelf_ = weakSelf;
}
weakSelf虽然是弱引用,但是staticSelf_静态变量,并对weakSelf进行了持有,staticSelf_释放不掉,所以weakSelf也释放不掉!导致循环引用!
__strong持有问题
- (void)block_weak_strong {
__weak typeof(self) weakSelf = self;
self.doWork = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));
weakSelf.doStudent = ^{
NSLog(@"%@", strongSelf);
NSLog(@"B objc -----retainCount : %lu", CFGetRetainCount(((__bridge CFTypeRef)strongSelf)));
};
weakSelf.doStudent();
};
self.doWork();
}
- 在
doWork内部,__strong typeof(self) strongSelf = weakSelf; - 用强引用持有了
weakSelf,和前的情况类似,strongSelf的生命周期也就在doWork方法内; - 这里需要注意的是,doStudent这个内部block调用了外部变量,所以他会从栈
block copy到堆中,从而导致strongSelf的引用计数增加,无法释放掉,进而导致循环引用!
总结
通过今天对Block的再学习,也算是复习了block的小知识点,同时学习了几种循环引用出现的场景和如何解决循环引用的方法,记录并复习。



















