這個知識點(diǎn),是個iOS開發(fā)人員從初期都會遇到漩绵。之前也看了很多別人的博客,自己想要重新歸納總結(jié)一下肛炮。
首先止吐,我強(qiáng)烈推薦唐巧
的博客,作為一個優(yōu)秀的iOS開發(fā)者,他之前也談過循環(huán)引用的問題侨糟。
一碍扔、循環(huán)引用的原理
1、基本知識
首先秕重,得說下內(nèi)存中和變量有關(guān)的分區(qū):堆不同、棧、靜態(tài)區(qū)溶耘。其中二拐,棧和靜態(tài)區(qū)是操作系統(tǒng)自己管理的,對程序員來說相對透明凳兵,所以百新,一般我們只需要關(guān)注堆的內(nèi)存分配,而循環(huán)引用的產(chǎn)生庐扫,也和其息息相關(guān)饭望,即循環(huán)引用會導(dǎo)致堆里的內(nèi)存無法正常回收形庭。說起對內(nèi)存的回收铅辞,肯定得說下以下老生常談的回收機(jī)制:
對堆里面的一個對象發(fā)送release消息來使其引用計(jì)數(shù)減一;
查詢引用計(jì)數(shù)表萨醒,將引用計(jì)數(shù)為0的對象dealloc斟珊;
那么循環(huán)引用怎么影響這個過程呢?
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發(fā)送release消息,致使A引用計(jì)數(shù)為0高职,則會dealloc A對象钩乍,而在A的dealloc的同時,會向B對象發(fā)送release消息怔锌,這就是問題的所在寥粹。
看一個正常的內(nèi)存回收,如圖1:
接下來埃元,看一個循環(huán)引用如何影響內(nèi)存回收的抠刺,如圖2:
那么推廣開來抓谴,我們可以看圖2,是不是很像一個有向圖,而造成循環(huán)引用的根源就是有向圖中出現(xiàn)環(huán)露泊。但是胀溺,千萬不要搞錯祈餐,下面這種闸盔,并不是環(huán),如圖3:
3遗锣、結(jié)論
由以上的內(nèi)容货裹,我們可以得到一個結(jié)論,當(dāng)堆中的引用關(guān)系圖中精偿,只要出現(xiàn)環(huán)弧圆,就會造成循環(huán)引用。
細(xì)心的童鞋肯定還會發(fā)現(xiàn)一個問題笔咽,即是不是只有A對象和B對象這種關(guān)系(B是A的屬性)才會出現(xiàn)環(huán)呢搔预,且看第二部分的探究:環(huán)的產(chǎn)生。
二叶组、環(huán)的產(chǎn)生
1斯撮、堆內(nèi)存的持有方式
仔細(xì)思考下可以發(fā)現(xiàn),堆內(nèi)存的持有方式扶叉,一共只有兩種:
方式a:將一個外部聲明的空指針指向一段內(nèi)存(例如:棧對堆的引用),如圖4:
方式b:將一段內(nèi)存(即已存在的對象)中的某個指針指向一段內(nèi)存(堆對堆的引用)帕膜,如圖5:
一中所講的B是A的屬性無疑是方式b枣氧,除去這種關(guān)系,還有幾種常見的關(guān)系也屬于方式b垮刹,比如:block對block所截獲變量的持有达吞,再比如:容器類NSDictionary,NSArray等對其包含對象的持有荒典。
2酪劫、方式a對產(chǎn)生環(huán)的影響
如圖6:
3吞鸭、方式b對產(chǎn)生環(huán)的影響
如圖7:
4、結(jié)論
方式b是造成環(huán)的根本原因覆糟,即堆對堆的引用是產(chǎn)生循環(huán)引用的根本原因刻剥。
可能有的童鞋可能說,那方式a的指針還有什么用呢滩字?當(dāng)然是有用的造虏,a的引用和b的引用共同決定了一個對象的引用計(jì)數(shù),即麦箍,共同決定這個對象何時需要dealloc漓藕,如圖8:
三.誤區(qū)
1.就是不是所有循環(huán)引用,系統(tǒng)都會提示你的挟裂,實(shí)際上享钞,大部分的循環(huán)引用系統(tǒng)都不會默認(rèn)提醒你,所以不要以為系統(tǒng)沒有提示的時候就是安全的诀蓉。
2.不是只要block里面含有self栗竖,就會導(dǎo)致循環(huán)引用
舉例:
2.當(dāng) block 本身不被 self 持有,而被別的對象持有交排,同時不產(chǎn)生循環(huán)引用的時候划滋,就不需要使用 weak self 了。最常見的代碼就是 UIView 的動畫代碼埃篓,我們在使用 UIView 的 animateWithDuration:animations 方法 做動畫的時候处坪,并不需要使用 weak self,因?yàn)橐贸钟嘘P(guān)系是:
UIView 的某個負(fù)責(zé)動畫的對象持有了 block
block 持有了 self
因?yàn)?self 并不持有 block架专,所以就沒有循環(huán)引用產(chǎn)生同窘,因?yàn)榫筒恍枰褂?weak self 了。
[UIView animateWithDuration:0.2 animations:^{
self.alpha = 1;
}];
當(dāng)動畫結(jié)束時部脚,UIView 會結(jié)束持有這個 block想邦,如果沒有別的對象持有 block 的話,block 對象就會釋放掉委刘,從而 block 會釋放掉對于 self 的持有丧没。整個內(nèi)存引用關(guān)系被解除。
另一個例子
[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.otherView.mas_centerY);
}];
block中持有了self锡移,但是self.view并沒有持有這個block呕童,因?yàn)榭吹組asonry的源碼是這樣的:
- (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夺饲,這就是為什么開發(fā)者使用Masonry進(jìn)行開發(fā),但是不加weakSelf,strongSelf也不會導(dǎo)致循環(huán)引用的原因往声。
四.日常開發(fā)中常遇到的循環(huán)引用的例子
例子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);
};
}
這里形成環(huán)的原因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();
}
這里也會產(chǎn)生循環(huán)引用浩销,因?yàn)閟tudent持有block贯涎,block持有self,self持有student撼嗓,正好三個形成圓環(huán)了柬采,所以這也是循環(huán)引用,不過你用Instrument的leak會監(jiān)測不出來的且警,但是實(shí)際上確實(shí)循環(huán)引用了
例子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;
};
}
這里是因?yàn)閟tudent持有block粉捻,block持有_block,_block持有student斑芜,依舊是形成環(huán)
例子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();
}
這里不會循環(huán)引用肩刃,因?yàn)閟tudent.study()執(zhí)行后,stu=nil(也就是說_block=nil)杏头,也就是說student持有block,block不持有_block了(因?yàn)開block = nil后,就正常釋放了).這是一種解決循環(huán)引用的方法盈包。
點(diǎn)評(使用__block解決循環(huán)引用雖然可以控制對象持有時間,在block中還能動態(tài)的控制是__block變量的值醇王,可以賦值nil呢燥,也可以賦值其他的值,但是有一個唯一的缺點(diǎn)就是需要執(zhí)行一次block才行寓娩。否則還是會造成循環(huán)引用叛氨。)
例子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棘伴,這樣就可以避免循環(huán)引用
例子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;
//開始網(wǎng)絡(luò)請求
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_downloadData = [[NSData alloc] initWithContentsOfURL:_url];
dispatch_async(dispatch_get_main_queue(), ^{
//網(wǎng)絡(luò)請求完成
[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
上面的循環(huán)引用是
1寞埠、completion handler的block因?yàn)橐O(shè)置_fetchedData實(shí)例變量的值,所以它必須捕獲self變量焊夸,也就是說handler塊保留了EOCClass實(shí)例仁连。(block持有EOCClass)
2、EOCClass實(shí)例通過strong實(shí)例變量保留了EOCNetworkFetcher阱穗,最后EOCNetworkFetcher實(shí)例對象也會保留了handler的block饭冬。(EOCClass持有block)
所以循環(huán)引用了,書本上教了有三種釋放的方法
方法一:手動釋放EOCNetworkFetcher使用之后持有的_networkFetcher揪阶,這樣可以打破循環(huán)引用
- (void)downloadData {
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
_networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
_fetchedData = data;
_networkFetcher = nil;//加上此行昌抠,打破循環(huán)引用
}];
}
//釋放對象屬性
方法二:直接釋放block。因?yàn)樵谑褂猛陮ο笾笮枰藶槭謩俞尫徘睬绻涐尫啪蜁斐裳h(huán)引用了。如果使用完completion handler之后直接釋放block即可。打破循環(huán)引用
- (void)p_requestCompleted {
if(_completionHandler) {
_completionHandler(_downloadData);
}
self.completionHandler = nil;//加上此行蕴茴,打破循環(huán)引用
}
//釋放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不就可以解決循環(huán)引用了嗎
這個問題蒋畜,公司的同事也問過我,無奈我原理理解不透徹撞叽,和他說strongSelf可以保證block里面的self可以完全執(zhí)行完block中的所有操作姻成,然后block再完全釋放≡钙澹可是當(dāng)時同事直接用weakSelf科展,跑了一次項(xiàng)目,沒什么問題糠雨,所以他覺得沒必要寫strongSelf才睹,我也找不到漏洞,只好接受了他們的觀點(diǎn)甘邀。后面看到下面的例子琅攘,才明白為何一定要用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)
重點(diǎn)就在dispatch_after這個函數(shù)里面。在study()的block結(jié)束之后松邪,student被自動釋放了坞琴。又由于dispatch_after里面捕獲的__weak的student,根據(jù)第二章講過的__weak的實(shí)現(xiàn)原理逗抑,在原對象釋放之后剧辐,__weak對象就會變成null,防止野指針锋八。所以就輸出了null了浙于。
究其根本原因就是weakSelf之后,無法控制什么時候會被釋放挟纱,為了保證在block內(nèi)不會被釋放羞酗,需要添加__strong。
在block里面使用的__strong修飾的weakSelf是為了在函數(shù)生命周期中防止self提前釋放紊服。strongSelf是一個自動變量當(dāng)block執(zhí)行完畢就會釋放自動變量strongSelf不會對self進(jìn)行一直進(jìn)行強(qiáng)引用檀轨。
所以StrongSelf是在這個時候才發(fā)揮作用
#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