循環(huán)應(yīng)用的知識整理

這個知識點(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:


圖1

接下來埃元,看一個循環(huán)引用如何影響內(nèi)存回收的抠刺,如圖2:

圖2

那么推廣開來抓谴,我們可以看圖2,是不是很像一個有向圖,而造成循環(huán)引用的根源就是有向圖中出現(xiàn)環(huán)露泊。但是胀溺,千萬不要搞錯祈餐,下面這種闸盔,并不是環(huán),如圖3:

圖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:

圖4

方式b:將一段內(nèi)存(即已存在的對象)中的某個指針指向一段內(nèi)存(堆對堆的引用)帕膜,如圖5:

圖5

一中所講的B是A的屬性無疑是方式b枣氧,除去這種關(guān)系,還有幾種常見的關(guān)系也屬于方式b垮刹,比如:block對block所截獲變量的持有达吞,再比如:容器類NSDictionary,NSArray等對其包含對象的持有荒典。

2酪劫、方式a對產(chǎn)生環(huán)的影響
如圖6:

圖6

3吞鸭、方式b對產(chǎn)生環(huán)的影響
如圖7:

圖7

4、結(jié)論
方式b是造成環(huán)的根本原因覆糟,即堆對堆的引用是產(chǎn)生循環(huán)引用的根本原因刻剥。
可能有的童鞋可能說,那方式a的指針還有什么用呢滩字?當(dāng)然是有用的造虏,a的引用和b的引用共同決定了一個對象的引用計(jì)數(shù),即麦箍,共同決定這個對象何時需要dealloc漓藕,如圖8:

圖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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市欺嗤,隨后出現(xiàn)的幾起案子参萄,更是在濱河造成了極大的恐慌,老刑警劉巖煎饼,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讹挎,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)筒溃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門马篮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人怜奖,你說我怎么就攤上這事浑测。” “怎么了歪玲?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵迁央,是天一觀的道長。 經(jīng)常有香客問我滥崩,道長岖圈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任夭委,我火速辦了婚禮幅狮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘株灸。我一直安慰自己崇摄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布慌烧。 她就那樣靜靜地躺著逐抑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪屹蚊。 梳的紋絲不亂的頭發(fā)上厕氨,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音汹粤,去河邊找鬼命斧。 笑死,一個胖子當(dāng)著我的面吹牛嘱兼,可吹牛的內(nèi)容都是我干的国葬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼芹壕,長吁一口氣:“原來是場噩夢啊……” “哼汇四!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起踢涌,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤通孽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后睁壁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體背苦,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡互捌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了行剂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疫剃。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖硼讽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情牲阁,我是刑警寧澤固阁,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站城菊,受9級特大地震影響备燃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凌唬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一并齐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧客税,春花似錦况褪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至秧均,卻和暖如春食侮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背目胡。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工锯七, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人誉己。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓眉尸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親巫延。 傳聞我的和親對象是個殘疾皇子效五,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容