這個知識點,是個iOS開發人員從初期都會遇到。之前也看了很多別人的博客,自己想要重新歸納總結一下。
首先,我強烈推薦唐巧
的博客,作為一個優秀的iOS開發者,他之前也談過循環引用的問題。
一、循環引用的原理
1、基本知識
首先,得說下內存中和變量有關的分區:堆、棧、靜態區。其中,棧和靜態區是操作系統自己管理的,對程序員來說相對透明,所以,一般我們只需要關注堆的內存分配,而循環引用的產生,也和其息息相關,即循環引用會導致堆里的內存無法正常回收。說起對內存的回收,肯定得說下以下老生常談的回收機制:
對堆里面的一個對象發送release消息來使其引用計數減一;
查詢引用計數表,將引用計數為0的對象dealloc;
那么循環引用怎么影響這個過程呢?
2、樣例分析
- In some situations you retrieve an object from another object, and then directly or indirectly release the parent object. If releasing the parent causes it to be deallocated, and the parent was the only owner of the child, then the child (heisenObject in the example) will be deallocated at the same time (assuming that it is sent a release rather than an autorelease message in the parent’s dealloc method).
大致意思是,B對象是A對象的屬性,若對A發送release消息,致使A引用計數為0,則會dealloc A對象,而在A的dealloc的同時,會向B對象發送release消息,這就是問題的所在。
看一個正常的內存回收,如圖1:
接下來,看一個循環引用如何影響內存回收的,如圖2:
那么推廣開來,我們可以看圖2,是不是很像一個有向圖,而造成循環引用的根源就是有向圖中出現環。但是,千萬不要搞錯,下面這種,并不是環,如圖3:
3、結論
由以上的內容,我們可以得到一個結論,當堆中的引用關系圖中,只要出現環,就會造成循環引用。
細心的童鞋肯定還會發現一個問題,即是不是只有A對象和B對象這種關系(B是A的屬性)才會出現環呢,且看第二部分的探究:環的產生。
二、環的產生
1、堆內存的持有方式
仔細思考下可以發現,堆內存的持有方式,一共只有兩種:
方式a:將一個外部聲明的空指針指向一段內存(例如:棧對堆的引用),如圖4:
方式b:將一段內存(即已存在的對象)中的某個指針指向一段內存(堆對堆的引用),如圖5:
一中所講的B是A的屬性無疑是方式b,除去這種關系,還有幾種常見的關系也屬于方式b,比如:block對block所截獲變量的持有,再比如:容器類NSDictionary,NSArray等對其包含對象的持有。
2、方式a對產生環的影響
如圖6:
3、方式b對產生環的影響
如圖7:
4、結論
方式b是造成環的根本原因,即堆對堆的引用是產生循環引用的根本原因。
可能有的童鞋可能說,那方式a的指針還有什么用呢?當然是有用的,a的引用和b的引用共同決定了一個對象的引用計數,即,共同決定這個對象何時需要dealloc,如圖8:
三.誤區
1.就是不是所有循環引用,系統都會提示你的,實際上,大部分的循環引用系統都不會默認提醒你,所以不要以為系統沒有提示的時候就是安全的。
2.不是只要block里面含有self,就會導致循環引用
舉例:
2.當 block 本身不被 self 持有,而被別的對象持有,同時不產生循環引用的時候,就不需要使用 weak self 了。最常見的代碼就是 UIView 的動畫代碼,我們在使用 UIView 的 animateWithDuration:animations 方法 做動畫的時候,并不需要使用 weak self,因為引用持有關系是:
UIView 的某個負責動畫的對象持有了 block
block 持有了 self
因為 self 并不持有 block,所以就沒有循環引用產生,因為就不需要使用 weak self 了。
[UIView animateWithDuration:0.2 animations:^{
self.alpha = 1;
}];
當動畫結束時,UIView 會結束持有這個 block,如果沒有別的對象持有 block 的話,block 對象就會釋放掉,從而 block 會釋放掉對于 self 的持有。整個內存引用關系被解除。
另一個例子
[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.otherView.mas_centerY);
}];
block中持有了self,但是self.view并沒有持有這個block,因為看到Masonry的源碼是這樣的:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
它僅僅是block(constrainMaker)。如果改成了self.block = block(constrainMaker),那么view也持有了block,這就是為什么開發者使用Masonry進行開發,但是不加weakSelf,strongSelf也不會導致循環引用的原因。
四.日常開發中常遇到的循環引用的例子
例子1
//Student.m
#import <Foundation/Foundation.h>
typedef void(^Study)();
@interface Student : NSObject
@property (copy , nonatomic) NSString *name;
@property (copy , nonatomic) Study study;
@end
//ViewController.m
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
student.name = @"Hello World";
student.study = ^{
NSLog(@"my name is = %@",student.name);
};
}
這里形成環的原因block里面持有student本身,student本身又持有block。
例子2
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@property (copy,nonatomic) NSString *name;
@property (strong, nonatomic) Student *stu;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
self.name = @"halfrost";
self.stu = student;
student.study = ^{
NSLog(@"my name is = %@",self.name);
};
student.study();
}
這里也會產生循環引用,因為student持有block,block持有self,self持有student,正好三個形成圓環了,所以這也是循環引用,不過你用Instrument的leak會監測不出來的,但是實際上確實循環引用了
例子3
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
__block Student *stu = student;
student.name = @"Hello World";
student.study = ^{
NSLog(@"my name is = %@",stu.name);
stu = nil;
};
}
這里是因為student持有block,block持有_block,_block持有student,依舊是形成環
例子4(解決方案)
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
student.name = @"Hello World";
__block Student *stu = student;
student.study = ^{
NSLog(@"my name is = %@",stu.name);
stu = nil;
};
student.study();
}
這里不會循環引用,因為student.study()執行后,stu=nil(也就是說_block=nil),也就是說student持有block,block不持有_block了(因為_block = nil后,就正常釋放了).這是一種解決循環引用的方法。
點評(使用__block解決循環引用雖然可以控制對象持有時間,在block中還能動態的控制是__block變量的值,可以賦值nil,也可以賦值其他的值,但是有一個唯一的缺點就是需要執行一次block才行。否則還是會造成循環引用。)
例子5(解決方案)
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
student.name = @"Hello World";
__weak typeof(student) weakSelf = student;
student.study = ^{
NSLog(@"my name is = %@",weakSelf.name);
};
student.study();
}
乖乖的使用weakSelf即可,就是讓block不持有self,這樣就可以避免循環引用
例子6(引用Effective Objective-c 書中的一段代碼)
EOCNetworkFetcher.h
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion;
@end
EOCNetworkFetcher.m
@interface EOCNetworkFetcher ()
@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy) EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData *downloadData;
@end
@implementation EOCNetworkFetcher
- (id)initWithURL:(NSURL *)url {
if(self = [super init]) {
_url = url;
}
return self;
}
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion {
self.completionHandler = completion;
//開始網絡請求
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_downloadData = [[NSData alloc] initWithContentsOfURL:_url];
dispatch_async(dispatch_get_main_queue(), ^{
//網絡請求完成
[self p_requestCompleted];
});
});
}
- (void)p_requestCompleted {
if(_completionHandler) {
_completionHandler(_downloadData);
}
}
@end
EOCClass.m
@implementation EOCClass {
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchedData;
}
- (void)downloadData {
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
_networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchedData = data;
}];
}
@end
上面的循環引用是
1、completion handler的block因為要設置_fetchedData實例變量的值,所以它必須捕獲self變量,也就是說handler塊保留了EOCClass實例。(block持有EOCClass)
2、EOCClass實例通過strong實例變量保留了EOCNetworkFetcher,最后EOCNetworkFetcher實例對象也會保留了handler的block。(EOCClass持有block)
所以循環引用了,書本上教了有三種釋放的方法
方法一:手動釋放EOCNetworkFetcher使用之后持有的_networkFetcher,這樣可以打破循環引用
- (void)downloadData {
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
_networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchedData = data;
_networkFetcher = nil;//加上此行,打破循環引用
}];
}
//釋放對象屬性
方法二:直接釋放block。因為在使用完對象之后需要人為手動釋放,如果忘記釋放就會造成循環引用了。如果使用完completion handler之后直接釋放block即可。打破循環引用
- (void)p_requestCompleted {
if(_completionHandler) {
_completionHandler(_downloadData);
}
self.completionHandler = nil;//加上此行,打破循環引用
}
//釋放block
方法三:使用weakSelf、strongSelf
- (void)downloadData {
__weak __typeof(self) weakSelf = self;
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
_networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
__typeof(&*weakSelf) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.fetchedData = data;
}
}];
}
//使用weakSelf,這樣block就不持有對象了.
六.為何要用StrongSelf,用weakSelf不就可以解決循環引用了嗎
這個問題,公司的同事也問過我,無奈我原理理解不透徹,和他說strongSelf可以保證block里面的self可以完全執行完block中的所有操作,然后block再完全釋放。可是當時同事直接用weakSelf,跑了一次項目,沒什么問題,所以他覺得沒必要寫strongSelf,我也找不到漏洞,只好接受了他們的觀點。后面看到下面的例子,才明白為何一定要用StrongSelf
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
student.name = @"Hello World";
__weak typeof(student) weakSelf = student;
student.study = ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"my name is = %@",weakSelf.name);
});
};
student.study();
}
輸出:
my name is = (null)
重點就在dispatch_after這個函數里面。在study()的block結束之后,student被自動釋放了。又由于dispatch_after里面捕獲的__weak的student,根據第二章講過的__weak的實現原理,在原對象釋放之后,__weak對象就會變成null,防止野指針。所以就輸出了null了。
究其根本原因就是weakSelf之后,無法控制什么時候會被釋放,為了保證在block內不會被釋放,需要添加__strong。
在block里面使用的__strong修飾的weakSelf是為了在函數生命周期中防止self提前釋放。strongSelf是一個自動變量當block執行完畢就會釋放自動變量strongSelf不會對self進行一直進行強引用。
所以StrongSelf是在這個時候才發揮作用
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc]init];
student.name = @"Hello World";
__weak typeof(student) weakSelf = student;
student.study = ^{
__strong typeof(student) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"my name is = %@",strongSelf.name);
});
};
student.study();
}
輸出
my name is = Hello World