iOS NSTimer的注意事项

目录

  • NSTimer传参
  • NStimer在UITableViewCell中

NSTimer传参

需求:搜索要自动联想搜索,即等待1s后自动搜索

实现:用NSTimer实现:

var searchDelayer:NSTimer?
func searchBar(searchBar: UISearchBar!, textDidChange searchText: String!) {
    searchDelayer?.invalidate()
    searchDelayer = nil

    searchDelayer = NSTimer.scheduledTimerWithTimeInterval(1.5, target: self, selector: Selector("doDelayedSearch:"), userInfo: searchText, repeats: false)

}

func doDelayedSearch(text:String){
...
}

直接调用doDelayedSearch()方法会程序会挂掉,控制台打印以下数据:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFTimer copyWithZone:]: unrecognized selector sent to instance 0x7f9c622ae7e0'
*** First throw call stack:
(
0 CoreFoundation 0x000000010c05b3e5 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x000000010ba42967 objc_exception_throw + 45
2 CoreFoundation 0x000000010c0624fd -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 CoreFoundation 0x000000010bfba7ec forwarding + 988
4 CoreFoundation 0x000000010bfba388 _CF_forwarding_prep_0 + 120
5 CoreFoundation 0x000000010bf32935 CFStringCreateCopy + 229
6 libswiftFoundation.dylib 0x000000010dc41314 _TF10Foundation24_convertNSStringToStringFCSo8NSStringSS + 116
7 MapCode 0x000000010a1a567e TToFC7MapCode17MapViewController15doDelayedSearchfS0_FSST + 62
8 Foundation 0x000000010b5fce94 __NSFireTimer + 83
9 CoreFoundation 0x000000010bfc34d4 CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION + 20
10 CoreFoundation 0x000000010bfc3095 __CFRunLoopDoTimer + 1045
11 CoreFoundation 0x000000010bf863cd __CFRunLoopRun + 1901
12 CoreFoundation 0x000000010bf859f6 CFRunLoopRunSpecific + 470
13 GraphicsServices 0x000000010ecfd9f0 GSEventRunModal + 161
14 UIKit 0x000000010c96b990 UIApplicationMain + 1282
15 MapCode 0x000000010a1b3fee top_level_code + 78
16 MapCode 0x000000010a1b402a main + 42
17 libdyld.dylib 0x000000010f9d7145 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

这是因为NStimer的回调方法只能是不带参数的方法或者是带参数但是参数本身只能是NStimer的方法;
改成这样就不会闪退了:

func doDelayedSearch(timer: NSTimer) {
let searchText = timer.userInfo as String
// ...
}

stackoverFlow地址


还有个方法更加方便,用延迟调用实现:

[[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(doDelayedSearch:) object:text];
[self performSelector:@selector(doDelayedSearch:) withObject:text afterDelay:1.5];

但是这个方法一定要保证两个参数的传值一致!

NStimer在UITableViewCell中

需求:在tableViewCell中添加计时器
iOS NSTimer的注意事项_第1张图片
原型.png

实现:

如果使用NSTimer的简便构造方法时:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)time target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
tableview滑动时timer就显示暂停,原因是timer的这个简便构造方法把timer加入了NSRunLoopDefaultMode上,而tableview在滑动时只会处理UITrackingRunLoopModeRunLoop并没有处理timer事件。
解决的办法是将timer绑定到NSRunLoopCommonModes上,通过以下方法构造timer:

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

通过以下方法将构造的timer绑定到NSRunLoopCommonModes上:

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

这样timer在tableview滑动中就不会暂停了。

当然这样做也不是100%能解决问题,有人反映timer计时不准确。 可以进行如下优化:

计时器只有在 runLoop 的一次循环中被检查,所以如果在上次循环中做了什么耗时的操作,那么计时器就被延后执行了。

    __block typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        weakSelf.timer = [NSTimer timerWithTimeInterval:1.0 target:weakSelf selector:@selector(applyCheckCodeTimer:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    });

解释:
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain()CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
 
if (!loopsDic) {
    // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
    loopsDic = CFDictionaryCreateMutable();
    CFRunLoopRef mainLoop = _CFRunLoopCreate();
    CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
 
/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
 
if (!loop) {
    /// 取不到时,创建一个
    loop = _CFRunLoopCreate();
    CFDictionarySetValue(loopsDic, thread, loop);
    /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
    _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
 
OSSpinLockUnLock(&loopsLock);
    return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

从上面的代码可以看出,线程和RunLoop之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

你可能感兴趣的:(iOS NSTimer的注意事项)