22.定時器的使用總結(jié)

1.子線程中開啟定時器

具體代碼如下:

@interface ZGKTimerVC ()

@property (nonatomic, strong) NSTimer *timer;

// 要關(guān)閉的runloop, 要保持同一線程
@property (nonatomic, assign) CFRunLoopRef runloop;
// 記錄子線程
@property (nonatomic, strong) NSThread *backgroundThread;

@end

@implementation ZGKTimerVC

- (void)viewDidLoad {
    [super viewDidLoad];
    
   // 子線程開啟定時器
    [NSThread detachNewThreadSelector:@selector(startTimerOnBackgroundThread) toTarget:self withObject:nil];
    
    // 子線程上的定時器,必須要在子線程里面調(diào)用invalidate移除, 不要在主線程進行, 否則timer雖然停止了,但是runloop不會停止(每個子線程都有一個runloop, 需要找到對應(yīng)線程關(guān)閉runloop)
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (self.backgroundThread) {
            [self performSelector:@selector(cancelTimer) onThread:self.backgroundThread withObject:nil waitUntilDone:NO];
        }
    });
}

// 在子線程中執(zhí)行定時器
- (void)startTimerOnBackgroundThread{
    NSLog(@"runloop start on backgroundThread");
    
    // 子線程上的定時器,必須要在子線程里面invalidate, 不要在主線程進行, 否則timer雖然停止了,但是runloop不會停止
    [self performSelector:@selector(cancelTimer) withObject:nil afterDelay:4.0];
    
    [self startTimer];
  
    // 停止了runloop才能執(zhí)行
    NSLog(@"runloop end on backgroundThread");
}


- (void)startTimer{
    // 間隔之前先調(diào)用一次
    [self timerAction];
    
    // 方式一: 手動將timer放入runloop
    // timerWithTimeInterval:在主線程執(zhí)行timer,需要將timer手動添加到runloop中, 才會執(zhí)行
     self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    // runloop有兩種模式, 分別是default和tracking
    // 所以定時器要加到兩種模式中, NSRunLoopCommonModes
     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    
    // 方法二: 系統(tǒng)已經(jīng)將timer加入到了runloop
//    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    
// 子線程默認是關(guān)閉runloop的, 需要手動開啟
//    [[NSRunLoop currentRunLoop] run];
    CFRunLoopRun();
}

- (void)timerAction{
    static int num = 0;
    
    NSLog(@"%d %@", num++, [NSThread currentThread]);
    
    // 滿足條件后,停止當(dāng)前的運行循環(huán)
    if (num == 10) {
        // 一旦停止了運行循環(huán),CFRunLoopRun()的后續(xù)代碼能夠執(zhí)行,執(zhí)行完畢后,線程被自動銷毀
        CFRunLoopStop(CFRunLoopGetCurrent());
    }
}


- (void)cancelTimer{
    NSLog(@"cancelTimer------");
    [self.timer invalidate];
    self.timer = nil;
}

- (void)dealloc{
    NSLog(@"timerVc dealloc");
    [self cancelTimer];
}

// 打印結(jié)果
2020-07-27 16:10:25.134929+0800 03-timer[2934:193644] runloop start on backgroundThread
2020-07-27 16:10:25.135111+0800 03-timer[2934:193644] 0 <NSThread: 0x600000053680>{number = 6, name = (null)}
2020-07-27 16:10:26.140051+0800 03-timer[2934:193644] 1 <NSThread: 0x600000053680>{number = 6, name = (null)}
2020-07-27 16:10:27.135630+0800 03-timer[2934:193644] 2 <NSThread: 0x600000053680>{number = 6, name = (null)}
2020-07-27 16:10:28.140374+0800 03-timer[2934:193644] 3 <NSThread: 0x600000053680>{number = 6, name = (null)}
2020-07-27 16:10:29.139387+0800 03-timer[2934:193644] cancelTimer------
2020-07-27 16:10:29.139609+0800 03-timer[2934:193644] runloop end on backgroundThread

注意點:

1.1 runloop在子線程是默認關(guān)閉的, 將定時器加入到runloop時,需要手動開啟
1.2 停止定時器的方法有兩種, 第一種是調(diào)用定時器的invalidate方法,將定時器移出runloop, runloop在沒有輸入源timer,就會自動停止, 線程銷毀. 第二種,是調(diào)用CFRunLoopStop(CFRunLoopGetCurrent()), 直接停止runloop
1.3 顯式開啟runloop后, 會開啟一個死循環(huán), 要將runloop停止后,才能執(zhí)行后續(xù)的代碼,如: NSLog(@"runloop end on backgroundThread");
1.4 子線程上的定時器,必須要在子線程里面invalidate, 不要在主線程進行, 否則timer雖然停止了,但是runloop不會停止(每個子線程都有一個runloop, 需要找到對應(yīng)線程關(guān)閉runloop), 如: 在主線程執(zhí)行invalidate操作, timer隨便停止了, 但是runloop的run方法后面的代碼依舊沒有執(zhí)行
1.5 由于定時器是基于runloop的, 如果app存在大量運算時,定時器有時會不準確, 如果追求定時器的精確, 可以使用gcd的定時器.
1.6 創(chuàng)建timer時,self無論是正常的self還是weakSelf, 都會被timer強引用

2. 解決定時器的循環(huán)引用問題

2.1 使用帶有block的timer構(gòu)造法方法, 但是,這是iOS10推出的方法, 需要做兼容
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
2.2 通過timer的分類來實現(xiàn)解耦(模仿系統(tǒng)帶block的timer)
@implementation NSTimer (NF)

+ (NSTimer *)nf_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(block)block{
    
    return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(selectorMethod:) userInfo:[block copy] repeats:repeats];
}

+ (void)selectorMethod: (NSTimer *)timer{
    
    block block = timer.userInfo;
    
    if (block) {
        block(timer);
    }
}
2.3 使用NSProxy的消息轉(zhuǎn)發(fā)機制來解耦
NS_ASSUME_NONNULL_BEGIN

@interface ZGKProxy : NSProxy

@property (nonatomic, weak) id target;

// 為了防止綁定target,最好調(diào)用這個工廠方法創(chuàng)建對象
+ (instancetype)allocWithTarget: (id)target;

@end

NS_ASSUME_NONNULL_END

#import "ZGKProxy.h"

@implementation ZGKProxy

+ (instancetype)allocWithTarget: (id)target{
    ZGKProxy *proxy = [ZGKProxy alloc];
    proxy.target = target;
    return proxy;
}

#pragma mark - 這個函數(shù)拋出一個函數(shù)的簽名,再由后面的forwardInvocation:去執(zhí)行,為給定消息提供參數(shù)類型信息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    // sel => 調(diào)用的方法名, 即控制器的timerAction方法
    return [self.target methodSignatureForSelector:sel];
}

#pragma mark - NSInvocation封裝了NSMethodSignature, 通過invokeWithTarget:方法將消息轉(zhuǎn)發(fā)給其他對象,由被轉(zhuǎn)發(fā)的對象執(zhí)行該消息方法
- (void)forwardInvocation:(NSInvocation *)invocation{
    [invocation invokeWithTarget:self.target];
}

@end

#import "ZGKTimerVC.h"
#import "ZGKProxy.h"

@interface ZGKTimerVC ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation ZGKTimerVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"NSProxy實現(xiàn)解耦";
    self.view.backgroundColor = UIColor.whiteColor;
    
    // 使用NSProxy消息轉(zhuǎn)發(fā)機制,進行解耦
    ZGKProxy *proxy = [ZGKProxy allocWithTarget:self];
    // 如果沒有設(shè)置target,則proxy進行函數(shù)簽名的時候, 會直接crash
    /*
     Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSProxy doesNotRecognizeSelector:timerAction] called!'
     */
//    proxy.target = self;
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)timerAction{
    NSLog(@"執(zhí)行定時器任務(wù)");
}

- (void)dealloc{
    NSLog(@"%s", __func__);
    [self.timer invalidate];
    self.timer = nil;
}


@end


這里需要特別注意的是:

2.3.1 proxy必須重寫函數(shù)簽名方法methodSignatureForSelector:和消息轉(zhuǎn)發(fā)方法forwardInvocation:
2.3.2 綁定target對象, 如果沒有綁定target會直接造成crash.
2.3.3 proxy的屬性target, 必須是weak, 才能解耦

3. GCD的定時器

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,990評論 2 374