iOS迴圈引用

Lucky_Xu發表於2018-09-22

在iOS開發中,迴圈引用是個老生常談的問題.delegate為啥使用weak修飾,block為什麼需要weakSelf或strongSelf?通過閱讀他人的文章並結合自己理解來闡述一下自己對迴圈引用的理解,若有不足希望大家指出.

記憶體的基礎知識

首先需要先了解記憶體中的分割槽:棧區、堆區、靜態區(全域性區).具體職責劃分如下:

  • 棧區(stack):
    • 存放區域性變數,先進後出,一旦出了作用域就會被銷燬,函式跳轉地址,現場保護等
    • 程式猿不需要管理棧區變數的記憶體
    • 棧區的地址從高到低分配
  • 堆區(heap):
    • 堆區的記憶體分配使用的是alloc;
    • 需要程式猿管理記憶體
    • ARC的記憶體管理,是編譯器在編譯的時候自動新增retain,release,autorelease;
    • 堆區的地址是從低到高分配
  • 全域性區/靜態區(staic) :
    • 包括2個部分:未初始化和初始化; 也是說,在記憶體中是放在一起的,比如:int a;未初始化, int a = 10 初始化的,兩者都在全域性區/靜態區
    • 常量區:常量字串及時放在這裡的
    • 程式碼區:存放app程式碼

如圖所示:

iOS迴圈引用

可以看見在上面的說明中,只有堆區是需要程式設計師管理的,而迴圈引用也與此有關,所以我們一般只需要關注堆區記憶體就可以了,即迴圈引用導致堆中的記憶體無法正常回收.那麼記憶體的回收又和我們iOS的回收機制有關,也就是大家都知道的引用計數:

  • 對堆裡面的一個物件傳送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就都釋放掉了,沒有造成任何記憶體問題,記憶體正確回收.

正常回收的圖

迴圈引用的產生

那麼什麼時候會造成迴圈引用呢,顧名思義,就是互相持有,形成一個閉環,導致誰也無法正確釋放.如圖所示:

形成迴圈引用

造成迴圈引用的過程是這樣的:想要讓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,記憶體也就無法釋放.就像兩個人都拽著對方的手說你先鬆我才鬆一樣,誰都不肯先鬆,然後就這樣一直拽著對方直到天荒地老. 這種感覺就像下面這張圖一樣:

iOS迴圈引用

迴圈引用的例子與解決方案

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還可以用於解決迴圈引用.

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了

相關文章