天天看點

iOS 循環引用解決方案

一、BLOCK 循環引用

一般表現為,某個類将block作為自己的屬性變量,然後該類在block的方法體裡面又使用了該類本身。構成循環引用。

// 定義 block 的時候,會對外部變量做一次 copy,強引用, self自身為強引用。

解決方案如下:

1 #import "ViewController.h"
 2 #import "NetworkTools.h"
 3 
 4 @interface ViewController ()
 5     @property (nonatomic, strong) NetworkTools *tools;
 6 @end
 7 
 8 @implementation ViewController
 9 // 1. 解除循環引用,需要注意打斷引用鍊條即可!
10 - (void)viewDidLoad {
11     [super viewDidLoad];
12     
13     // 局部變量不會産生循環應用,全局屬性會産生循環引用
14     self.tools = [[NetworkTools alloc] init];
15     
16     // 1. 定義 block 的時候,會對外部變量做一次 copy,會對 self 進行強引用
17     
18     // 解除循環引用方法1
19     // __weak 是 iOS 5.0 推出的
20     // 如果異步操作沒有完成,釋放控制器,__weak 本身是弱引用
21     // 當異步執行完畢,進行回調,self 已經被釋放,無法通路屬性,也無法調用方法
22     // __weak 相當于 weak,不會做強引用,但是如果對象被釋放,執行的位址,會指向 nil
23     // __weak 更安全
24     __weak typeof(self) weakSelf = self;
25     
26     // 解除循環引用方法2
27     // __unsafe_unretained 是 iOS 4.0 推出的
28     // MRC 經典錯誤,EXC_BAD_ACCESS 壞記憶體通路,野指針
29     // 相當于 assign,不會做強引用,但是如果對象被釋放,記憶體位址保持不變,如果此時再調用,就會出現野指針通路
30     // __unsafe_unretained typeof(self) weakSelf = self;
31     
32     [self.tools loadData:^(NSString *html) {
33         // strongSelf 強引用,對 weakSelf 進行強引用,本意,希望在異步完成後,繼續執行回調代碼
34         //然而并沒有什麼作用!!!!!!!!
35         __strong typeof(self) strongSelf = weakSelf;
36         
37         NSLog(@"%@ %@", html, strongSelf.view);
38     }];
39 }
40 
41 - (void)dealloc {
42     NSLog(@"控制器 88");
43 }
44 
45 @end      

二、計時器NSTimer循環引用

主要是因為從timer的角度,timer認為調用方self被析構時會進入dealloc,在dealloc可以順便将timer的計時停掉并且釋放記憶體;但是從self的角度,他認為timer不停止計時不析構,那我永遠沒機會進入dealloc。循環引用,互相等待,子子孫孫無窮盡也。

例子說明:

一方面,NSTimer經常會被作為某個類的成員變量,而NSTimer初始化時要指定self為target,容易造成循環引用。 另一方面,若timer一直處于validate的狀态,則其引用計數将始終大于0。先看一段NSTimer使用的例子(ARC模式):

1 import <Foundation/Foundation.h>
 2 
 3 interface Friend : NSObject
 4     -(void)cleanTimer;
 5 end
 6 
 7 import "Friend.h"
 8 
 9 interface Friend ()
10     STimer *_timer;
11 end
12 
13 implementation Friend
14 
15 -(id)init
16 {
17     if (self = [super init]) {
18         _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleTimer:)
19         userInfo:nil repeats:YES];
20     }
21     return self;
22 }
23 
24 - (void)handleTimer:(id)sender
25 {
26     NSLog(@"%@ say: Hi!", [self class]);
27 }
28 
29 - (void)cleanTimer
30 {
31     [_timer invalidate];
32     _timer = nil;
33 }
34 
35 - (void)dealloc
36 {
37     [self cleanTimer];
38     NSLog(@"[Friend class] is dealloced");
39 }
40 
41 @end      

在類外部初始化一個Friend對象,并延遲5秒後将friend釋放(外部運作在非arc環境下)

1 Friend *f = [[Friend alloc] init];
2 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
3     [f release];
4 });      

我們所期待的結果是,初始化5秒後,f對象被release,f的dealloc方法被調用,在dealloc裡面timer失效,對象被析構。但結果卻是如此:

1 2015-03-18 18:00:35.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
 2 2015-03-18 18:00:36.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
 3 2015-03-18 18:00:37.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
 4 2015-03-18 18:00:38.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
 5 2015-03-18 18:00:39.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!//運作了5次後沒按照預想的停下來
 6 2015-03-18 18:00:40.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
 7 2015-03-18 18:00:41.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
 8 2015-03-18 18:00:42.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
 9 2015-03-18 18:00:43.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
10 2015-03-18 18:00:44.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!<br>.......根本停不下來.....      

這是為什麼呢?主要是因為從timer的角度,timer認為調用方(Friend對象)被析構時會進入dealloc,在dealloc可以順便将timer的計時停掉并且釋放記憶體;但是從Friend的角度,他認為timer不停止計時不析構,那我永遠沒機會進入dealloc。循環引用,互相等待,子子孫孫無窮盡也。問題的症結在于-(void)cleanTimer函數的調用時機不對,顯然不能想當然地放在調用者的dealloc中。一個比較好的解決方法是開放這個函數,讓Friend的調用者顯式地調用來清理現場。如下:

1 Friend *f = [[Friend alloc] init];
2 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
3     [f cleanTimer];
4     [f release];
5 });      

三、委托delegate

在委托問題上出現循環引用問題已經是老生常談了,本文也不再細講,規避該問題的殺手锏也是簡單到哭,一字訣:聲明delegate時請用assign(MRC)或者weak(ARC),千萬别手賤玩一下retain或者strong,畢竟這基本逃不掉循環引用了!

上面說的是我們常見的,其實循環引用就是說我們的強引用形成了閉環,還會有很多自己寫的代碼中會出現,平時還是要注意寫法。

不好意思,下面再啰嗦一遍,進一步說明:

循環引用,指的是多個對象互相引用時,使得引用形成一個環形,導緻外部無法真正是否掉這塊環形記憶體。其實有點類似死鎖。

舉個例子:A->B->C->....->X->B ->表示強引用,這樣的B的引用計數就是2,假如A被系統釋放了,理論上A會自動減小A所引用的資源,就是B,那麼這時候B的引用計數就變成了1,所有B無法被釋放,然而A已經被釋放了,所有B的記憶體部分就肯定無法再釋放再重新利用這部分記憶體空間了,導緻記憶體洩漏。

情況一:delegate

Delegate是ios中開發中最常遇到的循環引用,一般在聲明delegate的時候都要使用弱引用weak或者assign

@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;
當然怎麼選擇使用assign還是weak,MRC的話隻能用assign,在ARC的情況下最好使用weak,因為weak修飾的變量在是否後自動為指向nil,防止不安全的野指針存在

情況二:Block

Block也是比較常見的循環引用問題,在Block中使用了self容易出現循環引用,是以很多人在使用block的時候,加入裡面有用到self的操作都會聲明一個__weak來修飾self。其實便不是這樣的,不是所有使用了Block都會出現Self循環引用問題,隻有self擁有Block的強引用才會出現這種情況。

是以一般在函數中臨時使用Block是不會出現循環應用的,因為這時候Block引用是屬于棧的。當棧上的block釋放後,block中對self的引用計數也會減掉

當然不一定要Self對Block有直接的引用才會出現,假如self的變量B,B中有個Block變量,就容易出現這種情況,好的是在block出現循環引用的,xcode7會出現警告提示(之前版本不确定)。

情況三:NSTimer

這是一個神奇的NSTimer,當你建立使用NSTimer的時候,NSTimer會預設對目前self有個強引用,所有在self使用完成打算是否的時候,一定要先使用NSTimer的invalidate來停止是否時間控制對self的引用

[_timer invalidate];      

轉載于:https://www.cnblogs.com/xujinzhong/p/8427572.html