解決NSTimer和CADisplayLink強(qiáng)引用循環(huán)的幾種姿勢(shì)

NSTimerCADisplayLink使用不當(dāng)會(huì)造成強(qiáng)引用循環(huán)的問(wèn)題已經(jīng)是老生常談,但是一直也不放在心上(畢竟沒(méi)有給我踩到過(guò)坑),最近在看ibreme大神的YYKit的過(guò)程中發(fā)現(xiàn)YYKit有專門來(lái)解決NSTimerCADisplayLink強(qiáng)引用循環(huán)的方法。自己寫(xiě)了個(gè)Demo發(fā)現(xiàn)使用姿勢(shì)不當(dāng)?shù)脑挘瑥?qiáng)引用循環(huán)的坑還是很容易踩到的,下面總結(jié)下幾種解決這個(gè)問(wèn)題的姿勢(shì)。

問(wèn)題根源

先上一段日常我們的使用姿勢(shì)(本文以NSTimer為例,CADisplayLink同理):

Target.m
@property (nonatomic, strong) NSTimer *timer;
...
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(printNum:) userInfo:nil repeats:YES];
...
- (void)dealloc {
    [_timer invalidate];
}

在使用諸如:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel

這些API時(shí),NSTimer出于設(shè)計(jì)考慮會(huì)強(qiáng)引用target,同時(shí)NSRunLoop又會(huì)持有持有NSTimer,使用方同時(shí)又持有了NSTimer,這就造成了強(qiáng)引用循環(huán):

Retain_Cycle.jpg

誤區(qū)

那就把NSTimer申明為weak吧 :)

Target.m
...
@property (nonatomic, weak) NSTimer *timer;
Retain_Cycle2.jpg

看起來(lái)沒(méi)問(wèn)題,但是Target的釋放依賴了NSTimer的釋放,然而NSTimer的釋放操作在Target的-delloc中,這是一個(gè)雞生蛋還是蛋生雞的問(wèn)題,依然會(huì)造成內(nèi)存泄露。

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

那換個(gè)思路能不能讓NSTimer弱引用Target,我們?cè)诜乐笲lock的引用循環(huán)的措施中不是有這么一條么:

__typeof(&*self) __weak weakSelf = self;

那么能不能這么寫(xiě):

__typeof(&*self) __weak weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: weakSelf selector:@selector(printNum:) userInfo:nil repeats:YES];

Naive,這樣子NSTimer仍然會(huì)強(qiáng)引用Target,NSTimer內(nèi)部不出意外是聲明一個(gè)__Strong的指針的,不管你在這個(gè)接口中傳遞什么類型指針,最終都會(huì)持有。

自定義Category用Block解決

網(wǎng)上有一些封裝的比較好的block的解決方案,思路無(wú)外乎是封裝一個(gè)NSTimer的Category,提供block形式的接口:

NSTimer+AZ_Helper.h

+ (NSTimer *)AZ_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats;
NSTimer+AZ_Helper.m

+ (NSTimer *)AZ_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)())inBlock repeats:(BOOL)inRepeats
{
    void (^block)() = [inBlock copy];
    NSTimer * timer = [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(__executeTimerBlock:) userInfo:block repeats:inRepeats];
    return timer;
}

+ (void)__executeTimerBlock:(NSTimer *)inTimer;
{
    if([inTimer userInfo])
    {
        void (^block)() = (void (^)())[inTimer userInfo];
        block();
    }
}

使用者只需注意block中避免強(qiáng)引用循環(huán)就好了,這種方案已經(jīng)是比較簡(jiǎn)潔的解決方案。但是也不是沒(méi)有缺點(diǎn),使用者不用使用原生的API了,同時(shí)要為NSTimer何CADisplayLink分別引進(jìn)一個(gè)Category。

GCD自己實(shí)現(xiàn)Timer

直接用GCD自己實(shí)現(xiàn)一個(gè)定時(shí)器,YYKit直接有一個(gè)現(xiàn)成的類YYTimer這里不再贅述。
缺點(diǎn):代價(jià)有點(diǎn)大,需要自己重新造一個(gè)定時(shí)器。

代理Proxy

NSProxy大家在日常開(kāi)發(fā)中使用較少,其實(shí)就是一個(gè)代理中間人,可以代理其他的類/對(duì)象,用在這里很合適。先貼一張示意圖:

Weak_Proxy.jpg

完美解決強(qiáng)引用循環(huán)問(wèn)題。下面是部分代碼:

WeakProxy.h
...
@property (nullable, nonatomic, weak, readonly) id target;
...
WeakProxy.m
...
- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[JYWeakProxy alloc] initWithTarget:target];
}

- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}
...

使用的時(shí)候只需要把self替換成[WeakProxy proxyWithTarget:self]

 _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[WeakProxy proxyWithTarget:self] selector:@selector(printNum:) userInfo:nil repeats:YES];

結(jié)語(yǔ)

筆者比較推薦WeakProxy的方式,代價(jià)很小,而且WeakProxy的功能只是一個(gè)純的代理,沒(méi)有和NSTimerCADisplayLink耦合,還可以用在其他地方;使用上對(duì)原有代碼的入侵也很小,原生的API依然正常使用。代碼放在了GitHub上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容