iOS循環引用

在iOS開發中,循環引用是個老生常談的問題.delegate爲啥使用weak修飾,block爲何須要weakSelf或strongSelf?經過閱讀他人的文章並結合本身理解來闡述一下本身對循環引用的理解,如有不足但願你們指出.程序員

內存的基礎知識

首先須要先了解內存中的分區:棧區、堆區、靜態區(全局區).具體職責劃分以下:bash

  • 棧區(stack):
    • 存放局部變量,先進後出,一旦出了做用域就會被銷燬,函數跳轉地址,現場保護等
    • 程序猿不須要管理棧區變量的內存
    • 棧區的地址從高到低分配
  • 堆區(heap):
    • 堆區的內存分配使用的是alloc;
    • 須要程序猿管理內存
    • ARC的內存管理,是編譯器在編譯的時候自動添加retain,release,autorelease;
    • 堆區的地址是從低到高分配
  • 全局區/靜態區(staic) :
    • 包括2個部分:未初始化和初始化; 也是說,在內存中是放在一塊兒的,好比:int a;未初始化, int a = 10 初始化的,二者都在全局區/靜態區
    • 常量區:常量字符串及時放在這裏的
    • 代碼區:存放app代碼

如圖所示: 多線程

能夠看見在上面的說明中,只有堆區是須要程序員管理的,而循環引用也與此有關,因此咱們通常只須要關注堆區內存就能夠了,即循環引用致使堆中的內存沒法正常回收.那麼內存的回收又和咱們iOS的回收機制有關,也就是你們都知道的引用計數:app

  • 對堆裏面的一個對象發送release消息來使其引用計數減一;
  • 查詢引用計數表,將引用計數爲0的對象dealloc; 用比較經典的圖來講明一下:
    iOS與OS X多線程和內存管理插圖
  1. 第一我的進入辦公室,「須要照明的人數」加1,計數值從0變爲1,所以須要開燈;
  2. 以後每當有人進入辦公室,「須要照明的人數」就加1。如計數值從1變成2;
  3. 每當有人下班離開辦公室,「須要照明的人數」加減1如計數值從2變成1;
  4. 最後一我的下班離開辦公室時,「須要照明的人數」減1。計數值從1變成0,所以須要關燈。

"對象"就至關於上圖中的燈,而"持有對象"就至關於圖中的人.第一個進來打開燈的人至關於進行了一個alloc操做,建立了對象這塊內存,並使引用計數變爲1.以後進來的人就至關於持有這個對象,使引用計數+1(retain),離開的人就使該對象引用計數-1(release).只要辦公室裏面還有人在(還有人持有這個對象),這個"燈"就不會關(dealloc).最後一我的走了,那麼燈就關了,這塊內存也就被成功釋放了. 過程如圖所示 函數

iOS與OS X多線程和內存管理插圖

正常的內存釋放過程

正常的內存釋放過程是這樣的,B對象是A對象的一個屬性,也就是A持有B,如今要釋放掉A了,給A發一個release消息,這個時候A的引用計數變爲0,就要走dealloc方法,在dealloc方法裏面會給A持有的全部對象發送一條release消息,固然包括B,也就是[B release].而後B的引用計數也變爲0,執行dealloc.這樣A和B就都釋放掉了,沒有形成任何內存問題,內存正確回收. atom

正常回收的圖

循環引用的產生

那麼何時會形成循環引用呢,顧名思義,就是互相持有,造成一個閉環,致使誰也沒法正確釋放.如圖所示: spa

造成循環引用

形成循環引用的過程是這樣的:想要讓A釋放,須要B給A發送release消息,由於此時B持有A,但B只有在dealloc的時候會發送release消息,要讓B執行dealloc方法,就須要A發送release消息給B,要讓A發送release消息給B就須要A執行dealloc方法,要讓A執行dealloc方法又須要B給A發送release消息...這樣循環往復,都在等對方給本身發送release消息,形成誰也沒法dealloc,內存也就沒法釋放.就像兩我的都拽着對方的手說你先鬆我才鬆同樣,誰都不願先鬆,而後就這樣一直拽着對方直到天荒地老. 這種感受就像下面這張圖同樣: 線程

循環引用的例子與解決方案

1.delegate
//ClassA:
@protocol ClssADelegate <NSObject>
- (void)doNothing;
@end
@interface ClassA : UIViewController
@property (nonatomic, strong) id <ClssADelegate> delegate;
@end

//ClassB:
@interface ClassB ()<ClassADelegate>
@property (nonatomic, strong) ClassA *classA;
@end
@implementation ClassB
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.classA = [[ClassA alloc] init];  
    self.classA.delegate = self;
}
複製代碼

在上面的代碼中,classB持有classA,而classA中delegate屬性使用strong強引並指向了self(classB),因此classA經過delegate持有了classB.這樣就形成了循環引用.你們可能都知道該如何解決這個問題,那就是使用weak替代strong.這也是爲何delegate一般都用weak修飾的緣由.這裏順便簡單說一下weak吧. weak是弱引用,用weak描述修飾或者所引用對象的計數器不會加一,而且會在引用的對象被釋放的時候自動被設置爲nil,大大避免了野指針訪問壞內存引發崩潰的狀況,另外weak還能夠用於解決循環引用.3d

2.Block
@interface ClassA ()
@property (nonatomic, copy) Block block;
@property (nonatomic, assign) NSInteger num;
@end
@implementation ClassA
- (void)viewDidLoad {
    [super viewDidLoad];
    self.block = ^{
        self.num = 1;
    };  
}
複製代碼

在上面的代碼中的block是存在於堆內存中,classA持有block,而堆內存中的block中又持有了self,這樣就形成了循環引用.若是是棧中的block就不會形成這種問題,以下所示:指針

void (^block)(void) = ^{
        self.num = 1;
    };
 block();
複製代碼

要解決Block形成的這種循環引用,經常使用的解決方式是使用WeakSelf,以下:

@interface ClassA ()
@property (nonatomic, copy) Block block;
@property (nonatomic, assign) NSInteger num;
@end
@implementation ClassA
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self
    self.block = ^{
        weakSelf.num = 1;
    };  
}
複製代碼

在上面的兩個例子中能夠看出,使用weak弱引用替代strong強引用來讓環消失是很是有效的方式,在大多數狀況下用這種方法就能夠了,但在某些狀況下仍是有缺陷的

3.weak-strong dance

有一種場景就是在block執行過程,self被釋放掉了,這個時候若是去訪問self的話就會發生錯誤.代碼以下:

#import "ControllerB.h"

@interface ControllerB ()
@property (nonatomic,copy) void (^block)(void);
@property (nonatomic, strong) NSString *str;
@end

@implementation ControllerB

- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"test";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", weakSelf.str);
        });
    };
    self.block();
}
複製代碼
  • ControllerA push到ControllerB中,若是在4秒沒有pop回去的話,B中的block會打印出test.不然會打印出(null).這種狀況是由於內存提早回收,也就是須要用到self的時候,self已經置爲nil了.

那麼這個時候就須要在block強引用self,直到block執行再釋放掉self.代碼以下:

#import "ControllerB.h"

@interface ControllerB ()
@property (nonatomic,copy) void (^block)(void);
@property (nonatomic, strong) NSString *str;
@end

@implementation ControllerB

- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"test";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(self) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongSelf.str);
        });
    };
    self.block();
}
複製代碼

strongSelf是個局部變量,存在於棧中,而棧中內存系統會自動回收,也就是在block執行結束後回收,不會形成循環引用.同時strongSelf使ControllerB的引用計數加1,致其在pop後不會立馬執行dealloc銷燬str屬性,由於此時strongSelf持有了ControllerB,4秒事後,block執行並打印str,局部變量strongSelf被系統回收,其持有的ControllerB也會執行dealloc方法.

@weakify和@strongify

以前用RAC的時候看見裏面的宏定義@weakify和@strongify,以爲很是高明.這樣的話不只很方便,並且防止不當心在block中使用self形成的循環引用. 那麼上面的ControllerB的代碼就能夠改爲這樣:

#import "ControllerB.h"

@interface ControllerB ()
@property (nonatomic,copy) void (^block)(void);
@property (nonatomic, strong) NSString *str;
@end

@implementation ControllerB

- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"test";
     @weakify(self)
    self.block = ^{
    @strongify(self)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", self.str);
        });
    };
    self.block();
}
複製代碼

這樣就能夠隨意的在block中使用self了

相關文章
相關標籤/搜索