iOS 關(guān)于循環(huán)引用的那些事

循環(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)系圖如下所示:


MainStoryBoard.png

接著,我們新建一個(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)系如下圖所示:


LoopReference.png

當(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

以上,感謝閱讀!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容