iOS容易造成循環(huán)引用的三種場景

  • 整理自 編程小翁@博客園

  • http://www.cnblogs.com/wengzilin/p/4347974.html

  • ARC已經出來很久了,自動釋放內存的確很方便,但是并非絕對安全絕對不會產生內存泄露。導致iOS對象無法按預期釋放的一個無形殺手是——循環(huán)引用。循環(huán)引用可以簡單理解為A引用了B,而B又引用了A,雙方都同時保持對方的一個引用,導致任何時候引用計數都不為0,始終無法釋放。若當前對象是一個ViewController,則在dismiss或者pop之后其dealloc無法被調用,在頻繁的push或者present之后內存暴增,然后APP就duang地掛了。下面列舉我們變成中比較容易碰到的三種循環(huán)引用的情形。

(1)計時器NSTimer

  • 一方面,NSTimer經常會被作為某個類的成員變量,而NSTimer初始化時要指定self為target,容易造成循環(huán)引用。 另一方面,若timer一直處于validate的狀態(tài),則其引用計數將始終大于0。先看一段NSTimer使用的例子(ARC模式):
1 #import <Foundation/Foundation.h>
2 @interface Friend : NSObject
3 - (void)cleanTimer;
4 @end
 1 #import "Friend.h"
 2 @interface Friend ()
 3 {
 4     NSTimer *_timer;
 5 }
 6 @end
 7 
 8 @implementation Friend
 9 - (id)init
10 {
11     if (self = [super init]) {
12         _timer = [NSTimer scheduledTimerWithTimeInterval:1 
                       target:self selector:@selector(handleTimer:) 
                       userInfo:nil repeats:YES];
14     }
15     return  self;
16 }
17 
18 - (void)handleTimer:(id)sender
19 {
20     NSLog(@"%@ say: Hi!", [self class]);
21 }
22 - (void)cleanTimer
23 {
24     [_timer invalidate];
25     _timer = nil;
26 }
27 - (void)dealloc
28 {
29     [self cleanTimer];
30     NSLog(@"[Friend class] is dealloced");
31 }
  • 在類外部初始化一個Friend對象,并延遲5秒后將friend釋放(外部運行在非arc環(huán)境下)
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失效,對象被析構。但結果卻是如此:
2015-03-18 18:00:35.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:36.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:37.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:38.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:39.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!//運行了5次后沒按照預想的停下來
2015-03-18 18:00:40.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:41.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:42.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:43.299 WZLCodeLibrary[41422:3390529] Friend say: Hi!
2015-03-18 18:00:44.300 WZLCodeLibrary[41422:3390529] Friend say: Hi!//.......根本停不下來.....
  • 這是為什么呢?主要是因為從timer的角度,timer認為調用方(Friend對象)被析構時會進入dealloc,在dealloc可以順便將timer的計時停掉并且釋放內存;但是從Friend的角度,他認為timer不停止計時不析構,那我永遠沒機會進入dealloc。循環(huán)引用,互相等待,子子孫孫無窮盡也。問題的癥結在于-(void)cleanTimer函數的調用時機不對,顯然不能想當然地放在調用者的dealloc中。一個比較好的解決方法是開放這個函數,讓Friend的調用者顯式地調用來清理現場。如下:
Friend *f = [[Friend alloc] init];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 
5*NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    [f cleanTimer];
    [f release];
});

(2)block

  • block在copy時都會對block內部用到的對象進行強引用(ARC)或者retainCount增1(非ARC)。在ARC與非ARC環(huán)境下對block使用不當都會引起循環(huán)引用問題,一般表現為,某個類將block作為自己的屬性變量,然后該類在block的方法體里面又使用了該類本身,簡單說就是self.someBlock = ^(Type var){[self dosomething];或者self.otherVar = XXX;或者_otherVar = ...};block的這種循環(huán)引用會被編譯器捕捉到并及時提醒。舉例如下,依舊以Friend類為例子:
#import "Friend.h"

@interface Friend ()
@property (nonatomic) NSArray *arr;
@end

@implementation Friend
- (id)init
{
    if (self = [super init]) {
         self.arr = @[@111, @222, @333];
        self.block = ^(NSString *name){
            NSLog(@"arr:%@", self.arr);
        };
    }
    return  self;
}
  • 我們看到,在block的實現內部又使用了Friend類的arr屬性,xcode給出了warning, 運行程序之后也證明了Friend對象無法被析構:
01.png
  • 網上大部分帖子都表述為block里面引用了self導致循環(huán)引用,但事實真的是如此嗎?我表示懷疑,其實這種說法是不嚴謹的,不一定要顯式地出現"self"字眼才會引起循環(huán)引用。我們改一下代碼,不通過屬性self.arr去訪問arr變量,而是通過實例變量_arr去訪問,如下:
02.png
  • 由此我們知道了,即使在你的block代碼中沒有顯式地出現"self",也會出現循環(huán)引用!只要你在block里用到了self所擁有的東西!但對于這種情況,我們無法通過加\__weak聲明或者__block聲明去禁止block對self進行強引用或者強制增加引用計數。但我們可以通過其他指針來避免循環(huán)引用(多謝xq_120的提醒),具體是這么做的:
__weak typeof(self) weakSelf = self;
self.blkA = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;//加一下強引用,避免weakSelf被釋放掉
NSLog(@"%@", strongSelf->_xxView); //不會導致循環(huán)引用.
};
  • 對于self.arr的情況,我們要分兩種環(huán)境去解決:

    • 1)ARC環(huán)境下:ARC環(huán)境下可以通過使用_weak聲明一個代替self的新變量代替原先的self,我們可以命名為weakSelf。通過這種方式告訴block,不要在block內部對self進行強制strong引用:(如果要兼容ios4.3,則用\__unsafe_unretained代替__weak,不過目前基本不需考慮這么low的版本)
1          self.arr = @[@111, @222, @333];
2         __weak typeof(self) weakSelf=self;
3         self.block = ^(NSString *name){
4             NSLog(@"arr:%@", weakSelf.arr);
5         };
  • 2)MRC環(huán)境下:解決方式與上述基本一致,只不過將\__weak關鍵字換成__block即可,這樣的意思是告訴block:小子,不要在內部對self進行retain了!

(3)委托delegate

  • 在委托問題上出現循環(huán)引用問題已經是老生常談了,本文也不再細講,規(guī)避該問題的殺手锏也是簡單到哭,一字訣:聲明delegate時請用assign(MRC)或者weak(ARC),千萬別手賤玩一下retain或者strong,畢竟這基本逃不掉循環(huán)引用了!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容