循環應用的知識整理

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


圖1

接下來,看一個循環引用如何影響內存回收的,如圖2:

圖2

那么推廣開來,我們可以看圖2,是不是很像一個有向圖,而造成循環引用的根源就是有向圖中出現環。但是,千萬不要搞錯,下面這種,并不是環,如圖3:

圖3

3、結論
由以上的內容,我們可以得到一個結論,當堆中的引用關系圖中,只要出現環,就會造成循環引用。
細心的童鞋肯定還會發現一個問題,即是不是只有A對象和B對象這種關系(B是A的屬性)才會出現環呢,且看第二部分的探究:環的產生。

二、環的產生

1、堆內存的持有方式
仔細思考下可以發現,堆內存的持有方式,一共只有兩種:
方式a:將一個外部聲明的空指針指向一段內存(例如:棧對堆的引用),如圖4:

圖4

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

圖5

一中所講的B是A的屬性無疑是方式b,除去這種關系,還有幾種常見的關系也屬于方式b,比如:block對block所截獲變量的持有,再比如:容器類NSDictionary,NSArray等對其包含對象的持有。

2、方式a對產生環的影響
如圖6:

圖6

3、方式b對產生環的影響
如圖7:

圖7

4、結論
方式b是造成環的根本原因,即堆對堆的引用是產生循環引用的根本原因。
可能有的童鞋可能說,那方式a的指針還有什么用呢?當然是有用的,a的引用和b的引用共同決定了一個對象的引用計數,即,共同決定這個對象何時需要dealloc,如圖8:

圖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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容