如何優(yōu)雅的處理循環(huán)引用(retain cycle)

什么是循環(huán)引用?

顧名思義, 就是幾個對象某種方式互相引用, 形成了"環(huán)"。由于 Objective-C 內(nèi)存管理使用引用計數(shù)的架構(gòu), 而并不是 GC(garbage collector), 而在 ARC(自動引用計數(shù)) 下所有 OC 對象的內(nèi)存都交由系統(tǒng)統(tǒng)一管理。在 ARC 下 retain、rerlease、autorelease、dealloc 都無法被調(diào)用, 因為 ARC 要分析何處應該自動調(diào)用內(nèi)存管理方法, 如果手動調(diào)用的話會干擾其工作。更多關于內(nèi)存管理的內(nèi)容我會在之后的文章解答。

兩個或兩個以上對象彼此強引用而形成循環(huán)應用


  兩個或兩個以上對象彼此強引用而形成循環(huán)應用

循環(huán)引用中只剩一個對象還引用產(chǎn)生循環(huán)引用的某個對象

  
  循環(huán)引用中只剩一個對象還引用產(chǎn)生循環(huán)引用的某個對象
  

  
移除此引用后 ABCD 四個對象所造成的循環(huán)引用就泄露了

  
  移除此引用后 ABCD 四個對象所造成的循環(huán)引用就泄露了
  
那么在 ARC 下經(jīng)常產(chǎn)生循環(huán)引用的就只有三種情況了:

Delegate:

在聲明 delegate 的時候, 使用 retain、strong、copy 等強引用屬性關鍵字修飾時, 會導致代理方擁有被代理方的引用, 被代理方又通過 delegate 擁有了代理方的引用, 這樣就造成了循環(huán)引用。
  解決方式就是在 ARC 下將關鍵字改為 weak 即可。

Block:

有幾種我們常見的 block 的使用:

1、類方法不會造成循環(huán)引用, 因為類不會持有對象

[UIView animateWithDuration:2 animations:^{
        
}];

2、self 并沒有對 block 進行引用, 只是 block 對 self 單方面引用, 所以沒有造成循環(huán)引用

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
});

3、在某個作用域內(nèi)創(chuàng)建對象并且的 block 回調(diào)調(diào)用 self, self 沒有持有該對象, 沒有造成循環(huán)引用

TestObject *test = [[TestObject alloc] init];
[test somethingBlock:^{
    [self doSomething];
}];

4、self 強引用了 object, object 又強引用了 block, 而在 block 回調(diào)里又調(diào)用了 self, 導致 block 強引用 self, 造成循環(huán)引用, 導致 self 無法被釋放

[self.object somethingBlock:^{
        [self doSomething];
}];

通常會做如下處理:

// 弱引用 self 這個對象
__weak ViewController *weakSelf = self;
[self.object somethingBlock:^{
    // 捕獲 weakSelf 這個引用(由于 __weak 修飾的都是在棧內(nèi), 有可能被系統(tǒng)釋放, 導致 block 內(nèi)使用 weakSelf 調(diào)用的代碼無效)
    __strong ViewController *strongSelf = weakSelf;
    [strongSelf doSomething];
};

也可以根據(jù)不同應用場景做不同的處理:

  • 當 object 不再使用時可以主動置為 nil, 從而打破循環(huán)引用。 如果 block 聲明為屬性, 也可以將屬性主動置為 nil, 也可打破循環(huán)引用。
[self.object somethingBlock:^{
    [self doSomething];
    self.object = nil;
}];
  • 如果某類內(nèi)部將 block 作為私有屬性保存并使用, 當 block 后續(xù)不會再被使用到時, 可以主動將置為 nil, 從內(nèi)部打破循環(huán)引用。

下面是某類的具體實現(xiàn), 內(nèi)部有一私有屬性將 block 捕獲, 使用 somethingBlock 做一系列事情后, 將 block 回調(diào)。

#import "TestObject.h"

@interface TestObject ()

@property (nonatomic, copy) void(^somethingBlock)(void);

@end

@implementation TestObject

- (void)somethingBlock:(void(^)(void))block {
    _somethingBlock = block;
    // 使用 _somethingBlock 做一些事情
    !_somethingBlock ? : _somethingBlock();
    _somethingBlock = nil;
}

@end

于是是使用此類的代碼就可以這樣寫:

[self.object somethingBlock:^{
    [self doSomething];
}];

NSTimer:

當使用 NSTimer 定時器時, 定時器會強引用 target, 等自身失效時再釋放此對象。執(zhí)行完相關任務后, 沒有循環(huán)的定時器會自動失效, 但是如果需要循環(huán)的定時器, 則需要調(diào)用 - (void)invalidate; 使定時器失效。
  由于定時器會保留目標對象, 所有循環(huán)執(zhí)行任務的時候通常會導致循環(huán)引用, 先看下面代碼:

@interface RepeatTimer ()

- (void)startTimer;
- (void)stopTimer;

@end

@implementation RepeatTimer {
    NSTimer *_repeatTimer;
}

- (id)init {
    return [super init];
}

- (void)dealloc {
    [_repeatTimer invalidate];
}

- (void)startTimer {
    _repeatTimer = [NSTimer scheduledTimerWithTimeInterval:5
                                              target:self
                                            selector:@selector(doSomething)
                                            userInfo:nil
                                             repeats:YES];
}

- (void)stopTimer {
    [_repeatTimer invalidate];
    _repeatTimer = nil;
}

- (void)doSomething {
    
}

當使用者創(chuàng)建了 RepeatTimer 的對象并且調(diào)用 - (void)startTimer 后, startTimer 內(nèi)部實現(xiàn)將 RepeatTimer 的對象自身傳入 NSTimer, 使得 NSTimer 保留了此對象, 而 RepeatTimer 內(nèi)部有持有了 NSTimer 的對象, 造成了循環(huán)引用, 只有當使用者調(diào)用 - (void)stopTimer 時, 才可以打破循環(huán)引用。
  除非使用該類的代碼完全在你的掌控之中, 否則沒有辦法保證其他在開發(fā)人員一定會調(diào)用 - (void)stopTimer 方法, 所以這并不是一個很好的解決方案。此外如果想在系統(tǒng)回收該類時令定時器無效也是沒有用的, 因為 NSTimerRepeatTimer 在相互引用, 所以 RepeatTimer 的對象絕對不會被釋放。 當指向 RepeatTimer 實例的最后一個外部引用移走之后, 除了 NSTimer 再無其它類在對其保持引用, 也就是說該實例已經(jīng)"丟失"了, 并永遠不會被釋放。

  • 可以添加一個中介者target來綁定selector, 之后在 dealloc中釋放該 timer 即可:
@interface ViewController ()

@property (nonatomic, strong) id target;
@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _target = [NSObject new];
    
    IMP imp = class_getMethodImplementation([self class], @selector(doSomething));
    class_addMethod([_target class], @selector(doSomething), imp, "v@:");
    
    _timer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                              target:_target
                                            selector:@selector(doSomething)
                                            userInfo:nil
                                             repeats:YES];
}

- (void)dealloc {
    [_timer invalidate];
    _timer = nil;
}
  • 也可以創(chuàng)建一個NSProxy虛類的對象去解決這個問題:
@interface TimerProxy : NSProxy

@property (nonatomic, weak) id target;

@end

@implementation TimerProxy

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

@end

在使用的地方將其alloc, 綁定代理的對象target即可

@interface ViewController ()

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TimerProxy *timerProxy;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _timerProxy = [TimerProxy alloc];
    _timerProxy.target = self;
    
    _timer = [NSTimer scheduledTimerWithTimeInterval:5.0
                                              target:_timerProxy
                                            selector:@selector(doSomething)
                                            userInfo:nil
                                             repeats:YES];
}

- (void)dealloc {
    [_timer invalidate];
    _timer = nil;
}

上面代碼利用消息轉(zhuǎn)發(fā)來斷開NSTimer對象與視圖之間的引用關系。

  • 當然為 NSTimer 添加一個 category, 增加一個帶有 block 的方法來解決此問題更為直觀:
@interface NSTimer (RepeatBlockTimer)

+ (NSTimer *)scheduledMyTimerWithTimeInterval:(NSTimeInterval)timeInterval
                                      repeats:(BOOL)repeats
                                        block:(void(^)(void))block;

@end

@implementation NSTimer (RepeatBlockTimer)

+ (NSTimer *)scheduledMyTimerWithTimeInterval:(NSTimeInterval)timeInterval
                                      repeats:(BOOL)repeats
                                        block:(void(^)(void))block {
    return [self scheduledTimerWithTimeInterval:timeInterval
                                         target:self
                                       selector:@selector(blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
}

+ (void)blockInvoke:(NSTimer *)timer {
    void (^block)(void) = timer.userInfo;
    !block ? : block();
}

@end

上面代碼將定時器所執(zhí)行的任務封裝成 block, 在調(diào)用定時器的時候作為 userInfo 的參數(shù)傳進去 , 傳入時將 block 拷貝的堆上, 否則稍后執(zhí)行它的時候, 該 block 可能已經(jīng)無效。定時器現(xiàn)在的 targetNSTimer 的對象, 這是個單例, 所以不需要關心定時器是否會保留它。 不過此處依然有循環(huán)引用, 不過因為類對象是不需要回收的, 所以不考慮。
  然后在之前 - (void)stopTimer 里做如下修改:

- (void)startTimer {
    __weak typeof(self) weakSelf = self;
    _repeatTimer = [NSTimer scheduledMyTimerWithTimeInterval:5
                                  repeats:self
                                    block:^(void) {
        RepeatTimer *strongSelf = weakSelf;
        [strongSelf doSomething];
    }];
}

使用 __weak 定義一個弱引用指向 self, 在 block 內(nèi)部捕獲這個引用。這樣做的好處是保證 self 不會被定時器所引用, 保證實例(也就是捕獲的引用)在執(zhí)行期間持續(xù)存活。
  這樣在外部指向 RepeatTimer 的引用為0時, 該實例對象就會被回收, 同時會停止定時器循環(huán)所做的操作。
  不過 iOS 在 10.0 以后系統(tǒng)已經(jīng)提供了此方法:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

在使用時只需下面這樣就可以了

- (void)startTimer {
    __weak typeof(self) weakSelf = self;
    _repeatTimer = [NSTimer scheduledTimerWithTimeInterval:5 repeats:self block:^(NSTimer *timer) {
        RepeatTimer *strongSelf = weakSelf;
        [strongSelf doSomething];
    }];
}
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 30,187評論 8 265
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,619評論 1 32
  • OC語言基礎 1.類與對象 類方法 OC的類方法只有2種:靜態(tài)方法和實例方法兩種 在OC中,只要方法聲明在@int...
    奇異果好補閱讀 4,506評論 0 11
  • 第一條 總則:為嚴明紀律,獎懲分明,調(diào)動員工工作積極性,提高工作效率和經(jīng)濟效率;本著公平競爭,公正管理的原則,進一...
    西風冽閱讀 9,714評論 0 6
  • 立春,是二十四節(jié)氣之首。 立春是中國民間重要的傳統(tǒng)節(jié)之一?!傲ⅰ笔恰伴_始”的意思,自秦代以來,中國就一直以立春作為...
    聿靈隨筆閱讀 1,226評論 0 1

友情鏈接更多精彩內(nèi)容