iOS-NSTimer真的沒有想象中的簡單:NSInvocation,NSProxy,NSRunloop居然都會用到

個(gè)人第三方庫:
UDUserDefaultsModel:以Model代替NSUserDefaults
YIIFMDB:直接操作Model進(jìn)行增刪改查,數(shù)學(xué)運(yùn)算等,且sql語句易于管理

在iOS開發(fā)當(dāng)中,無可避免的會涉及到定時(shí)任務(wù),比如在發(fā)送驗(yàn)證碼時(shí)的倒計(jì)時(shí):


驗(yàn)證碼倒計(jì)時(shí)demo.gif

小編相信每個(gè)人都遇到過這樣的需求,都很熟練的寫出代碼來了,如下:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFire:) userInfo:nil repeats:YES];

簡單是簡單,但是,這個(gè)樣子寫出來的代碼卻有兩個(gè)很大的缺陷:

1.會導(dǎo)致內(nèi)存泄漏(在@selector(timerFire:)這個(gè)方法里打印一個(gè)log,會發(fā)現(xiàn)這個(gè)log在pop之后還會打?。?/p>

2.如果滑動(dòng)ScrollView的時(shí)候,定時(shí)器卻不會走,只有松開ScrollView之后,定時(shí)器才重新走,如此會導(dǎo)致體驗(yàn)不佳

內(nèi)存泄漏不是個(gè)小事,這個(gè)樣子會導(dǎo)致很多程序上的bug。而至于滑動(dòng)ScrollView時(shí)定時(shí)器不走的缺陷可以暫時(shí)稍后。

為了解決這個(gè)bug,我們先來分析NSTimer內(nèi)存泄漏的原因:首先在Demo中,NSTimer在初始化的時(shí)候是放在對象(其實(shí)是一個(gè)ViewController的對象)方法中的,而當(dāng)前對象self又是作為NSTimer對象的一個(gè)參數(shù)存在的,為此就導(dǎo)致了一個(gè)死循環(huán),即:


NSTimer循環(huán).png

解決這個(gè)bug,必須打破self->timer->self(其中->代表強(qiáng)引用)這種循環(huán)引用,其中self->timer這一步?jīng)]法避免,只能從timer->self這里著手,讓其變成timer -- self(其中--代表弱引用)。

為此,我們需要查看NSTimer的官方文檔,同時(shí)也發(fā)現(xiàn)了如下兩個(gè)方法:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

NSInvocation是什么?用過Target-Action的人一定不陌生?,F(xiàn)在我們?nèi)シ垂俜轿臋n,第一句就已經(jīng)解釋清楚了:

NSInvocation objects are used to store and forward messages between objects and between applications, primarily by NSTimer objects and the distributed objects system

簡而言之就是:NSInvocation對象會保存并轉(zhuǎn)發(fā)一些信息,而且完全可以適用于NSTimer對象。而從其暴露的方法來看,只有"invocationWithMethodSignature:"這一個(gè)方法,不解釋了,想了解的去看官方文檔,這里直接上代碼:

    NSMethodSignature *methodSignature = [[self class] instanceMethodSignatureForSelector:@selector(timerFire:)];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    invocation.target = self;
    invocation.selector = @selector(timerFire:);
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];

然而經(jīng)過測試,內(nèi)存泄漏的bug依舊沒有解決,@selector(timerFire:)這里面的log在pop的時(shí)候依舊在打印。仔細(xì)分析一下會發(fā)現(xiàn)NSTimer導(dǎo)致的閉環(huán)依舊沒有解決,只不過是從self->timer->self演變成了self->timer->invocation->self罷了。

雖然NSInvocation并未解決,但是卻提供了一個(gè)思路:假設(shè)有一個(gè)對象objectA,其對self進(jìn)行一個(gè)弱引用,那么就會變成self->timer->objectA--self(其中->代表強(qiáng)持有,而--代表弱持有)就可以了。

在翻看大量資料之后,小編得知iOS提供了這樣一個(gè)類:NSProxy。對于NSProxy的解釋,官方文檔是這樣解釋的:

An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.

(自己翻譯吧!小編倒是認(rèn)為"stand-in"是關(guān)鍵詞。)

那么,我們根據(jù)NSInvocation的思想(主要有兩點(diǎn):1.store messages 2.forward messages)去查看NSProxy的官方文檔,發(fā)現(xiàn)有兩個(gè)方法十分類似:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;      // 類似store messages
- (void)forwardInvocation:(NSInvocation *)invocation;           // 類似forward messages,而且這里也涉及到了NSInvocation

除此之外,還要解決一個(gè)對self弱引用的問題,為此只需要給NSProxy進(jìn)行一個(gè)拓展,增加一個(gè)對對象的弱引用,繼承是最好的辦法。

繼承自NSProxy聲明一個(gè)叫NSProxyInprovement的類,并在.h當(dāng)中聲明一個(gè)weak修飾的屬性,如下面代碼:

@interface NSProxyInprovement : NSProxy

@property (nonatomic, weak) id aTarget;      // 此對象要從外部傳過來

@end

同時(shí)在NSProxyInprovement的.m中,實(shí)現(xiàn)類似NSInvocation中"store and forward messages"的兩個(gè)方法,如下面代碼:

@implementation NSProxyInprovement

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

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

@end

使用起來很簡單但是卻比較繁瑣,如下面代碼:

self.proxy = [NSProxyInprovement alloc];
self.proxy.aTarget = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxy selector:@selector(timerFire:) userInfo:userInfo repeats:YES];

經(jīng)過測試,當(dāng)pop的時(shí)候,調(diào)用了dealloc的方法,為此內(nèi)存泄漏的bug算是解決了。

接下來解決上面遺留的那個(gè)滑動(dòng)ScrollView的時(shí)候,定時(shí)器不走的缺陷。為此,我們看看官方文檔對于@selector(scheduledTimerWithTimeInterval:target:userInfo:repeats:)的解釋:

Creates a timer and schedules it on the current run loop in the default mode.

從上面的解釋當(dāng)中可以看到NSTimer還結(jié)合了NSRunloop的知識,并且mode類型是NSDefaultRunLoopMode,這就是問題所在:當(dāng)滑動(dòng)ScrollView的時(shí)候,NSRunloop的mode并不是NSDefaultRunLoopMode,而是UITrackingRunLoopMode,為此,我們需要設(shè)置一個(gè)包含既包含NSDefaultRunLoopMode又包含UITrackingRunLoopMode的mode,那就是NSRunLoopCommonModes。
完整代碼如下:

self.proxy = [NSProxyInprovement alloc];
self.proxy.aTarget = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self.proxy selector:@selector(timerFire:) userInfo:userInfo repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

但是這個(gè)樣子的話還有一個(gè)缺陷,那就是NSRunloop使用了兩次,為了改善這個(gè),我們使用NSTimer的另一個(gè)方法,完整代碼如下:

self.proxy = [NSProxyInprovement alloc];
self.proxy.aTarget = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self.proxy selector:@selector(timerFire:) userInfo:userInfo repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

詳情只需要查看NSTimer的官方文檔就可以了,官方文檔寫的很清楚。
經(jīng)過測試,當(dāng)滑動(dòng)ScrollView的時(shí)候,定時(shí)器不走的那個(gè)缺陷也修復(fù)了,完美。

不過,在小編看來,bug與缺陷雖然都修復(fù)了,但是代碼寫起來十分的繁瑣,畢竟還要引入NSProxyInprovement這個(gè)類,還要?jiǎng)?chuàng)建,傳值,十分的繁瑣,一點(diǎn)都不符合組件化開發(fā)的需求。

為此小編寫了一個(gè)十分簡單的組件放到了Github上,并且可支持Cocoapods(不要吝嗇你手里的Star)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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