你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 循環引用,看我就對了

循環引用,看我就對了

編輯:IOS開發基礎

我是一頭來自北方的羊,咩-咩-咩-!
談到循環引用,不知道你能想到什麼?可能是delegate為啥非得用weak修飾,可能是block為啥總是需要特殊對待,你也可能僅僅想到了一個weakSelf,因為它能幫你解決99%的關於循環引用的事情。本文中,我將談一談我對循環引用的看法。

一、循環引用的產生

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:

1280247-30a625697389536e.png圖1


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

1280247-801d65a76dd90939.png圖2


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

1280247-65383ac54997a19d.png圖3

3、結論

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

二、環的產生

1、堆內存的持有方式

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

1280247-b485805a27db556c.png圖4


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

1280247-5c35bfc550aea853.png圖5


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

2、方式a對產生環的影響

如圖6:

1280247-0dd032f824a9679d.png圖6

3、方式b對產生環的影響

如圖7:

1280247-bc44d34fb9a0474f.png圖7

4、結論

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

1280247-3c4768bf774aeb89.png圖8

三、如何干掉環

1、delegate與環

//ClassA:
@protocol ClssADelegate 
- (void)fuck;
@end
@interface ClassA : UIViewController
@property (nonatomic, strong) id  delegate;
@end
//ClassB:
@interface ClassB ()
@property (nonatomic, strong) ClassA *classA;
@end
@implementation ClassB
- (void)viewDidLoad {
    [super viewDidLoad]; 
    self.classA = [[ClassA alloc] init];  
    self.classA.delegate = self;
}

如上代碼,B強引用A,而A的delegate屬性指向B,這裡的delegate是用strong修飾的,所以A也會強引用B,這是一個典型的循環引用樣例。而解決其的方式大家也都耳熟能詳,即將delegate改為弱引用。

2、block與環

@interface ClassA ()
@property (nonatomic, copy) dispatch_block_t block;
@property (nonatomic, assign) NSInteger tem;
@end
@implementation ClassA
- (void)viewDidLoad {
    [super viewDidLoad];
    self.block = ^{
        self.tem = 1;
    };  
}

如上代碼,self持有block,而堆上的block又會持有self,所以會導致循環引用,這個例子非常好,因為xcode都能檢測出來,報出警告:[capturing self strongly in this block is likely to lead to a retain cycle],當然大部分循環引用的情況xcode是不會報警告的。解決這種循環引用的常用方式如下(這種解決方式可以解決大部分block引起的循環引用,但是有一定缺陷,且看下一節):

@interface ClassA ()
@property (nonatomic, copy) dispatch_block_t block;
@property (nonatomic, assign) NSInteger tem;
@end
@implementation ClassA
- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self
    self.block = ^{
        weakSelf.tem = 1;
    };  
}

3、結論

如上delegate和block引起的循環引用的處理方式,有一個共同的特點,就是使用weak(弱引用)來打破環,使環消失了。所以,可以得出結論,我們可以通過使用將strong(強引用)用weak(弱引用)代替來解決循環引用。

四、解決block循環引用的深入探索

1、weakSelf與其缺陷

//ClassB是一個UIViewController,假設從ClassA pushViewController將ClassB展示出來
@interface ClassB ()
@property (nonatomic, copy) dispatch_block_t block;
@property (nonatomic, strong) NSString *str;
@end
@implementation ClassB
- (void)dealloc {
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"111";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", weakSelf.str);
        });
    };
    self.block();   
}

這裡會有兩種情況:

  • 若從A push到B,10s之內沒有pop回A的話,B中block會執行打印出來111。

  • 若從A push到B,10s之內pop回A的話,B會立即執行dealloc,從而導致B中block打印出(null)。這種情況就是使用weakSelf的缺陷,可能會導致內存提前回收。

2、weakSelf和strongSelf

@interface ClassB ()
@property (nonatomic, copy) dispatch_block_t block;
@property (nonatomic, strong) NSString *str;
@end
@implementation ClassB
- (void)dealloc {
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"111";
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(self) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongSelf.str);
        });
    };
    self.block();   
}

我們發現這樣確實解決了問題,但是可能會有兩個不理解的點。

  • 這麼做和直接用self有什麼區別,為什麼不會有循環引用:外部的weakSelf是為了打破環,從而使得沒有循環引用,而內部的strongSelf僅僅是個局部變量,存在棧中,會在block執行結束後回收,不會再造成循環引用。

  • 這麼做和使用weakSelf有什麼區別:唯一的區別就是多了一個strongSelf,而這裡的strongSelf會使ClassB的對象引用計數+1,使得ClassB pop到A的時候,並不會執行dealloc,因為引用計數還不為0,strongSelf仍持有ClassB,而在block執行完,局部的strongSelf才會回收,此時ClassB dealloc。

這樣做其實已經可以解決所有問題,但是強迫症的我們依然能找到它的缺陷:

  • block內部必須使用strongSelf,很麻煩,不如直接使用self簡便。

  • 很容易在block內部不小心使用了self,這樣還是會引起循環引用,這種錯誤很難發覺。

3、@weakify和@strongify

查看github上開源的libextobjc庫,可以發現,裡面的EXTScope.h裡面有兩個關於weak和strong的宏定義。

// 宏定義
#define weakify(...) \
    ext_keywordify \
    metamacro_foreach_cxt(ext_weakify_,, __weak, __VA_ARGS__)
#define strongify(...) \
    ext_keywordify \
    _Pragma("clang diagnostic push") \
    _Pragma("clang diagnostic ignored \"-Wshadow\"") \
    metamacro_foreach(ext_strongify_,, __VA_ARGS__) \
    _Pragma("clang diagnostic pop")

// 用法
@interface ClassB ()
@property (nonatomic, copy) dispatch_block_t block;
@property (nonatomic, strong) NSString *str;
@end
@implementation ClassB
- (void)dealloc {
}
- (void)viewDidLoad {
    [super viewDidLoad];
    self.str = @"111";
    @weakify(self)
    self.block = ^{
        @strongify(self)
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", self.str);
        });
    };
    self.block();   
}

可以看出,這樣就完美解決了3中缺陷,我們可以在block中隨意使用self。

文中有兩處圖片有點小問題,nei cun:內存;xun huan yin yong:循環引用。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved