NSTimer
和CADisplayLink
使用不當(dāng)會(huì)造成強(qiáng)引用循環(huán)的問(wèn)題已經(jīng)是老生常談白嘁,但是一直也不放在心上(畢竟沒(méi)有給我踩到過(guò)坑)奋岁,最近在看ibreme大神的YYKit的過(guò)程中發(fā)現(xiàn)YYKit有專門來(lái)解決NSTimer
和CADisplayLink
強(qiáng)引用循環(huán)的方法编振。自己寫了個(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):
誤區(qū)
那就把NSTimer
申明為weak吧 :)
Target.m
...
@property (nonatomic, weak) NSTimer *timer;
看起來(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;
那么能不能這么寫:
__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大家在日常開發(fā)中使用較少,其實(shí)就是一個(gè)代理中間人税产,可以代理其他的類/對(duì)象怕轿,用在這里很合適。先貼一張示意圖:
完美解決強(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)有和NSTimer
和CADisplayLink
耦合,還可以用在其他地方智听;使用上對(duì)原有代碼的入侵也很小羽杰,原生的API依然正常使用。代碼放在了GitHub上到推。