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, 才能解耦