循環(huán)引用的概念:即對象之間互相引用,造成彼此都不能被釋放。
示例代碼:
我們先把代碼的大體框架準(zhǔn)備好。
我們先新建一個(gè)繼承自 UIViewController 的控制器類 BlockTestViewController。
工程創(chuàng)建好之后 Main StoryBoard 已經(jīng)存在一個(gè) View Controller,它是應(yīng)用程序啟動(dòng)時(shí)的初始視圖控制器。我們將它對應(yīng)的類改為 BlockTestViewController。
首先,我們在 Main StoryBoard 拖入一個(gè) Navigation Controller,它會自帶一個(gè) Table View Controller,并且該 Table View Controller 會作為 Navigation Controller 的 Root View Controller。
我們選中 Navigation Controller 并勾選 Is Initial View Controller。這樣,在應(yīng)用程序啟動(dòng)時(shí),Navigation Controller 會作為初始視圖控制器顯示在屏幕上。
然后,我們在 Table View Controller 的 Navigation Bar 的右邊添加一個(gè) Right Bar Button Item,選中它按住 Control 鍵拖向 BlockTestViewController,在彈出的菜單中選擇 Show。這樣,當(dāng)點(diǎn)擊這個(gè)Right Bar Button Item,應(yīng)用程序就會跳轉(zhuǎn)顯示 BlockTestViewController。
控制器的關(guān)系圖如下所示:

接著,我們新建一個(gè)網(wǎng)絡(luò)層 Network Tool 類
NetworkTool.h 文件代碼如下:
#import <Foundation/Foundation.h>
@interface NetworkTool : NSObject
- (void)loadData:(void(^)(NSString *))finished;
@end
NetworkTool.m 文件代碼如下:
#import "NetworkTool.h"
@interface NetworkTool ()
@property (nonatomic,copy) void(^finishedBlock)(NSString *);
@end
@implementation NetworkTool
- (void)loadData:(void(^)(NSString *))finished {
self.finishedBlock = finished;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"耗時(shí)操作");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"準(zhǔn)備回調(diào)");
[self handleBlock];
});
});
}
- (void)handleBlock {
if (self.finishedBlock != nil) {
self.finishedBlock(@"hello...");
}
}
- (void)dealloc {
NSLog(@"網(wǎng)絡(luò)工具類 88");
}
@end
上面是網(wǎng)絡(luò)層的代碼。我們通過 loadData: 方法模擬數(shù)據(jù)加載的過程,當(dāng)數(shù)據(jù)加載完成之后,通過 block 參數(shù)將加載后得到的結(jié)果回調(diào)用調(diào)用方。
我們添加了一個(gè) Block 屬性,Block 屬性一般用 copy 修飾。用來記錄傳進(jìn)來的 Block。這里 Network Tool 強(qiáng)引用著 Block。
我們還添加了 dealloc 方法,如果 Network Tool 對象被釋放,這個(gè)方法就會被執(zhí)行,控制臺會打印 “網(wǎng)絡(luò)工具類 88”。以此來檢驗(yàn)是否有循環(huán)引用的問題。
回到我們的 BlockTestViewController 類。我們在 BlockTestViewController 類通過調(diào)用 Network Tool 對象來完成數(shù)據(jù)加載。代碼如下:
#import "BlockTestViewController.h"
#import "NetworkTool.h"
@interface BlockTestViewController ()
@property (nonatomic,strong) NetworkTool *networkTool;
@end
@implementation BlockTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
[self demo1];
}
- (void)demo1 {
self.networkTool = [[NetworkTool alloc] init];
[self.networkTool loadData:^(NSString * str) {
NSLog(@"%@ -- %@",str,self.view);
}];
}
- (void)dealloc {
NSLog(@"控制器 88");
}
@end
通過代碼可以看到,控制器強(qiáng)引用著 NetworkTool 對象。
而 Block 中執(zhí)行回調(diào)的代碼是打印 self.view。可以看到 Block 強(qiáng)引用著 self ,即 Block 強(qiáng)引用著控制器。
到此,類與類之間的關(guān)系如下圖所示:

當(dāng)我們點(diǎn)擊Right Bar Button Item,BlockTestViewController控制器顯示,我們再點(diǎn)返回按鈕,控制器并沒有銷毀。Network Tool 的 dealloc 方法也沒有走。至此,循環(huán)引用現(xiàn)象產(chǎn)生。
解決方法:
打破循環(huán)引用鏈條中的其中一環(huán)。
示例1:我們可以將 NetworkTool 從強(qiáng)引用的屬性形式改成 局部變量。當(dāng) demo2 方法執(zhí)行完之后, Network Tool 被釋放。
代碼如下:
#import "BlockTestViewController.h"
#import "NetworkTool.h"
@interface BlockTestViewController ()
@property (nonatomic,strong) NetworkTool *networkTool;
@end
@implementation BlockTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
[self demo2];
}
//這段代碼會有循環(huán)引用的問題
- (void)demo1 {
self.networkTool = [[NetworkTool alloc] init];
[self.networkTool loadData:^(NSString * str) {
NSLog(@"%@ -- %@",str,self.view);
}];
}
//將 NetworkTool 從強(qiáng)引用的屬性形式改成 局部變量
- (void)demo2 {
NetworkTool *networkTool = [[NetworkTool alloc] init];
[networkTool loadData:^(NSString * str) {
NSLog(@"%@ -- %@",str,self.view);
}];
}
- (void)dealloc {
NSLog(@"控制器 88");
}
@end
當(dāng)返回按鈕被點(diǎn)擊,控制臺打印如下:
2023-06-24 11:00:19.740285+0800 Block演練[50889:10916802] 耗時(shí)操作
2023-06-24 11:00:21.744836+0800 Block演練[50889:10916542] 準(zhǔn)備回調(diào)
2023-06-24 11:00:21.747016+0800 Block演練[50889:10916542] hello... -- <UIView: 0x12a10b9c0; frame = (0 0; 393 852); autoresize = W+H; backgroundColor = UIExtendedSRGBColorSpace 1 1 0 1; layer = <CALayer: 0x600002adf460>>
2023-06-24 11:00:21.747359+0800 Block演練[50889:10916802] 網(wǎng)絡(luò)工具類 88
2023-06-24 11:00:21.747609+0800 Block演練[50889:10916542] 控制器 88
可以看到,網(wǎng)絡(luò)工具類和控制器都能正常被銷毀了。
示例2 :使用 __weak 或者 __unsafe_unretained 來打破循環(huán)鏈條。
代碼如下:
#import "BlockTestViewController.h"
#import "NetworkTool.h"
@interface BlockTestViewController ()
@property (nonatomic,strong) NetworkTool *networkTool;
@end
@implementation BlockTestViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor yellowColor];
[self demo3];
}
//這段代碼會有循環(huán)引用的問題
- (void)demo1 {
self.networkTool = [[NetworkTool alloc] init];
[self.networkTool loadData:^(NSString * str) {
NSLog(@"%@ -- %@",str,self.view);
}];
}
//將 NetworkTool 從強(qiáng)引用的屬性形式改成 局部變量
- (void)demo2 {
NetworkTool *networkTool = [[NetworkTool alloc] init];
[networkTool loadData:^(NSString * str) {
NSLog(@"%@ -- %@",str,self.view);
}];
}
//使用 __weak 或者 __unsafe_unretained 來打破循環(huán)鏈接
- (void)demo3 {
self.networkTool = [[NetworkTool alloc] init];
//方式一
// __weak typeof(self) weakSelf = self;
//方式二:
__unsafe_unretained typeof(self) weakSelf = self;
[self.networkTool loadData:^(NSString * str) {
NSLog(@"%@ -- %@",str,weakSelf.view);
}];
}
- (void)dealloc {
NSLog(@"控制器 88");
}
@end
通過 __weak 或者 __unsafe_unretained 都能解決循環(huán)引用的問題,那它們到底有什么區(qū)別?
實(shí)際上, __unsafe_unretained 是 iOS 4.0 版本推出的;而 __weak 是iOS 5.0 版本推出的。我們通過修改耗時(shí)操作的代碼,就能看出他們之間的區(qū)別。
在 Network Tool 的 loadData: 方法中添加一行代碼,
[NSThread sleepForTimeInterval:2.0];
如下所示:
- (void)loadData:(void(^)(NSString *))finished {
self.finishedBlock = finished;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"耗時(shí)操作");
[NSThread sleepForTimeInterval:2.0];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"準(zhǔn)備回調(diào)");
[self handleBlock];
});
});
}
因?yàn)槲覀儾⒉荒鼙WC耗時(shí)操作會順利執(zhí)行完,所以我們模擬它加載2秒。
當(dāng)我們點(diǎn)擊 Right Bar Button Item 之后,控制器顯示。在任務(wù)還沒執(zhí)行完的情況下,我們點(diǎn)返回按鈕,可以看到,此時(shí)控制器因?yàn)槭? __weak 或者 __unsafe_unretained 修飾,所以控制器被釋放了。這時(shí)當(dāng) Block 執(zhí)行回調(diào)時(shí),因?yàn)锽lock 引用了控制器,而控制器已經(jīng)被銷毀了。所以用 __weak 修飾的控制器,控制臺會打印 nil;而用 __unsafe_unretained 修飾的控制器,則會有閃退的問題,報(bào)錯(cuò)信息是:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x20)
這是 MRC 中最經(jīng)典的錯(cuò)誤,沒有之一。意思是壞內(nèi)存訪問。即對象已經(jīng)被釋放掉了,仍然訪問該對象的地址導(dǎo)致的錯(cuò)誤。
至此,可以看出 __weak 或者 __unsafe_unretained 之間的區(qū)別:
//方式一:下面這行代碼,返回按鈕在任務(wù)沒執(zhí)行完的情況下被點(diǎn)擊,控制器被回收,控制器的內(nèi)存地址會被置為 nil。
// __weak typeof(self) weakSelf = self;
//方式二:下面這行代碼,返回按鈕在任務(wù)沒執(zhí)行完的情況下被點(diǎn)擊,控制器被回收,控制器內(nèi)存地址不變。再次訪問控制器會出現(xiàn)野指針的問題。
__unsafe_unretained typeof(self) weakSelf = self;
所以,一般建議使用 __weak 來代替 __unsafe_unretained ,因?yàn)?__weak 更安全。
在開發(fā)中,你或許還能看到這樣的代碼:
//使用 __weak 或者 __unsafe_unretained 來打破循環(huán)鏈接
- (void)demo3 {
self.networkTool = [[NetworkTool alloc] init];
//方式一
// __weak typeof(self) weakSelf = self;
//方式二:
__unsafe_unretained typeof(self) weakSelf = self;
[self.networkTool loadData:^(NSString * str) {
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@ -- %@",str,strongSelf.view);
}];
}
上面的代碼使用了 __strong 來修飾。實(shí)際上,__strong 是因?yàn)楹芏嗳诉@么寫,大家形成了套路也跟著這么寫,但基本沒有做深究,僅此而已。實(shí)際上__strong 沒啥作用,用 __unsafe_unretained 修飾后再用 __strong 指向它,還是會出現(xiàn)野指針問題。用 __weak 修飾的控制器在返回按鈕點(diǎn)擊后,還是會在任務(wù)沒執(zhí)行完的情況下被釋放。
所以,目前來看,下面這句代碼實(shí)際上是個(gè)棒錘。
__strong typeof(self) strongSelf = weakSelf;
附上 github Demo 地址:https://github.com/linguoqun2017/iOSLoopReference.git
以上,感謝閱讀!