iOS 進階 - 記憶體管理(八) -- 迴圈引用

深海謝先生發表於2020-10-27

迴圈引用

三種型別迴圈引用

  1. 自迴圈引用
  2. 相互迴圈引用
  3. 多迴圈引用
自迴圈引用

假如有一個物件,內部強持有它的成員變數obj,
若此時我們給obj賦值為原物件時,就是自迴圈引用

自迴圈引用

相互迴圈引用

物件A內部強持有obj,物件B內部強持有obj,
若此時物件A的obj指向物件B,同時物件B中的obj指向物件A,就是相互引用

相互迴圈引用

多迴圈引用

假如類中有物件1…物件N,每個物件中都強持有一個obj,
若每個物件的obj都指向下個物件,就產生了多迴圈引用
多迴圈引用

常見迴圈引用
  1. 代理
  2. Block
  3. NSTimer
  4. 大環引用
如何破除迴圈引用
  1. 避免產生迴圈引用
    在使用代理時,兩個物件,一個強引用,一個弱引用,避免產生相互迴圈引用
  2. 在合適的時機手動斷環

具體方案

  1. __weak
  2. __block
  3. __unsafe_unretained 用這個的關鍵字修飾的物件也沒有增加引用計數,和__weak在效果上是等效的
__weak破解

物件B會強持有A,物件A弱引用B

__weak破解

__block破解

__block在ARC和MRC中是不同的
MRC下,__block修飾物件不會增加其引用計數,避免了迴圈引用
ARC下,__block修飾物件會被強引用,無法避免迴圈引用,需手動解環

__unsafe_unretained破解
  1. 修飾物件不會增加其引用計數,避免了迴圈引用
  2. 如果被修飾的物件在某一時機被釋放,會產生懸垂指標,再通過這個指標去訪問原物件的話,會導致記憶體洩露,所以一般不建議用__unsafe_unretained去解除迴圈引用
迴圈引用示例
NSTimer

NSTimer
假如VC中有個廣告欄,需要1S中滾動一次播放下一個廣告,
我們會把這個廣告欄的UI物件作為VC的成員變數,由VC進行強持有
因為廣告欄每隔1S需要滾動播放,則廣告欄中會新增成員變數NSTimer並強引用,
當分配定時回撥事件之後,NSTimer會對廣告欄的Target進行強引用,就產生了相互迴圈引用

如果把物件對NSTimer的強引用改為弱引用,是無法解決問題的,原因如下圖:
在這裡插入圖片描述因為當NSTimer被分配之後,會被當前執行緒的Runloop進行強引用,
如果物件以及NSTimer是在主執行緒建立的,就會被主執行緒的Runloop持有這個NSTimer,
所以即使我們在廣告欄中通過弱引用來指向NSTimer,但是由於主執行緒中Runloop常駐記憶體通過對NSTimer的強引用,
再通過NSTimer對物件的強引用,仍然對這個物件產生了強引用,
此時即使VC頁面退出,去掉VC對物件的引用,
當前廣告欄仍然有被Runloop的間接強引用持有,這個物件也不會被釋放,此時就產生了記憶體洩露

解決方法:NSTimer分重複定時器和非重複定時器
非重複定時器:

在定時器的回撥方法中去呼叫[NSTimer invalid]同時將NSTimer置為nil,
可以將Runloop對NSTimer的強引用解除掉,同時NSTimer也解除了對物件的強引用。

重複定時器:

重複定時器:不能在定時器的回撥方法中去呼叫[NSTimer invalid]以及將NSTimer置為nil操作,此時的解決方案是:
左側是Runloop對NSTimer的強引用,右側是VC對物件的強引用,
可以在NSTimer和物件中間新增一箇中間物件,然後由NSTimer對中間物件進行強引用,
同時中間物件分別對NSTimer和廣告欄物件進行弱引用,那麼對於重複物件而已,
噹噹前VC退出之後,VC就釋放了對廣告欄物件的強引用,
當下次定時器的回撥事件回來的時候,可以在中間物件當中,判斷當前中間物件所持有的弱引用廣告欄物件是否被釋放了,
實際上就是判斷中間物件當中所持有的weak變數是否為nil,
如果是nil,然後呼叫[NSTimer invalid]以及將NSTimer置為nil,
就打破了Runloop對NSTimer的強引用以及NSTimer對中間物件的強引用
這個解決方案是利用了:當一個物件被釋放後,它的weak指標會自動置為nil

中間物件TimerWeakObject的實現

//NSTimer的NSTimer類別
#import <Foundation/Foundation.h>
@interface NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)repeats;
 
@end
 
 
 
#import "NSTimer+WeakTimer.h"
@interface TimerWeakObject : NSObject
@property (nonatomic, weak) id target;//中間物件的弱引用指標
@property (nonatomic, assign) SEL selector;//定時器到時之後的一個回撥方法
@property (nonatomic, weak) NSTimer *timer;//中間物件的弱引用指標
 
- (void)fire:(NSTimer *)timer;
@end
 
@implementation TimerWeakObject
/*對它所持有的target進行判斷,若target存在,判斷它是否響應選擇器,
  如果響應則執行對應的回撥方法
  否則就把timer置為無效,就可以達到Runloop對Timer強引用的釋放,同時Timer也會對弱引用物件進行釋放
*/
- (void)fire:(NSTimer *)timer
{
    if (self.target) {
        if ([self.target respondsToSelector:self.selector]) {
            [self.target performSelector:self.selector withObject:timer.userInfo];
        }
    }
    else{
        [self.timer invalidate];
    }
}
 
@end
 
//分類中的具體實現
@implementation NSTimer (WeakTimer)
+ (NSTimer *)scheduledWeakTimerWithTimeInterval:(NSTimeInterval)interval
                                         target:(id)aTarget
                                       selector:(SEL)aSelector
                                       userInfo:(id)userInfo
                                        repeats:(BOOL)repeats
{
/*
   建立中間物件,把我們傳進分類中的aTarget和aSelector指派給中間物件,
   然後呼叫系統的NSTimer方法去建立NSTimer,
   同時指定Timer的回撥事件是中間物件的fire方法,
  然後再fire方法中再對實際物件回撥方法進行呼叫
*/
    TimerWeakObject *object = [[TimerWeakObject alloc] init];
    object.target = aTarget;
    object.selector = aSelector;
    object.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:object selector:@selector(fire:) userInfo:userInfo repeats:repeats];
    
    return object.timer;
}
 
@end

相關文章