1. 什么是 RunLoop?
要知道什么是 RunLoop,先要有一个概念就是线程是怎样处理事务的?
线程一般来讲,是一次只能执行一个任务,当这个任务执行完成后,线程就会退出,如果有很多任务需要去处理,就需要频繁的开启线程,会使CPU使用效率打折扣。
那么,为了避免上述这种情况,我们就需要一种能够让线程在有任务需要处理的时候去执行任务,任务执行完成后使线程不退出,而是去休眠,等再次有任务需要处理的时候,就去唤醒这个线程再次去执行新的任务的这样的机制,因此,也就引入了这次的主角: Runloop。
所以,Runloop 的概念也就是上述的这样的一种机制,简而言之就是让线程随时处理事件但不退出的机制。
应用程序启动后,会有一个主Runloop,能够使App持续运行不退出,并且能够在运行期间不断地响应用户的Event(button 的点击、触摸、view 的展示)、定时器Timer的运行。
2. iOS系统中的 RunLoop
iOS系统中,有两种 Runloop,为 CFRunloopRef 和 NSRunloop两类。
CFRunloopRef 是 CoreFoundation 框架内的,是一套纯 C 的 API。
NSRunloop 基于 CFRunloopRef 封装而成。提供了面向对象的 API。
3. main.m 中的 Runloop
我们在 main.m 文件中有这样一段代码:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
这个方法体中 UIApplicationMain 函数内部就启动了一个 Runloop,所以 UIApplicationMain 函数一直没有返回,类似于死循环,保证了程序能够持续运行。这个默认启动的 RunLoop 是跟主线程相关联的。
3.1 UIApplicationMain 函数
/**
* @param argc 系统参数
* @param argv 系统参数
* @param principalClassName 应用程序名称
* @param delegateClassName 应用程序代理名称
*/
int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName)
Apple 对 UIApplicationMain 的解释如下:
Apple文档对UIApplicationMain函数的描述如下: This function is called in the main entry point to create the application object and the application delegate and set up the event cycle.Even though an integer return type is specified, this function never returns. When users exits an iOS application by pressing the Home button, the application moves to the background.
译:在主入口点调用此函数以创建应用程序对象和应用程序委托并设置事件周期。即使指定了整数返回类型,此函数也永远不会返回。 当用户通过按“主页”按钮退出iOS应用程序时,该应用程序将移至后台。
其中:
argc、argv 系统参数,直接传给UIApplicationMain。
principalClassName 该类必须是UIApplication(或子类)。如果为nil,则用UIApplication类作为默认值.
delegateClassName 指定应用程序的代理类,该类必须遵守UIApplicationDelegate协议
UIApplicationMain 会根据传入的 principalClassName 参数,创建相应的 UIApplication 对象。
UIApplicationMain 会根据传入的 delegateClassName 参数,设置为 UIApplication 的代理对象,代理对象可以执行 Appdelegate 的代理方法。
4. Runloop 需要掌握的几个组成部分
4.1 CFRunLoopRef
CFRunLoopRef 可理解为 Runloop 在 Core Foundation的一个类或者对象。它和剩余的4个部分关系大致可以如下图概括:
上图可以看到一个 Runloop 中可以包含若干个 Mode,每个 Mode 又包含若干的 Source/Timer/Observer。
每次调用 Runloop 的主函数时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode,如果需要切换 Mode,必须退出当前 Loop,然后重新指定一个 Mode 进入。这样做的目的是为了分隔不同的 Source/Timer/Observer,使其互不影响。
4.2 CFRunLoopModeRef
Runloop 中可以包含若干个 Mode,每个 Mode 又包含若干的 Source/Timer/Observer。
CFRunLoopMode 结构体大致为:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
系统注册了5个Mode:
常用的三个:
kCFRunLoopDefaultMode: App 默认的mode,通常主线程是在这个 Mode下运行。
UITrackingRunLoopMode: 界面跟踪 Mode,例如在 UIScrollview 和 UITableView 滚动时,Runloop会出去这个 Mode状态下,保证界面滑动时不被其他 Mode影响。
kCFRunLoopCommonModes:是一个Mode 集合,是一个占位用的 Mode,不是一种真正的 Mode。
不常用的两个:
UIInitializationRunLoopMode:在 App刚启动的时刻,会处于这个 Mode下。启动完成后不再使用此 Mode。
GSEventReceiveRunLoopMode:接受系统事件的内部 Mode。
Source/Timer/Observer被统称为 Mode 的 item,如果一个Mode中一个 item 都没有,那么 Runloop会直接退出,不进入事件循环。
4.3 CFRunLoopSourceRef
CFRunLoopSourceRef可以理解为事件源,按照函数调用栈的分类可分为两类:
Source0:用户Event事件,比如UIButton的点击、触摸、手势,
Source1:基于底层 mach_port的事件,我理解的比如手机一些传感器的数据回调。
4.4 CFRunLoopTimerRef
CFRunLoopTimerRef 是基于时间的触发器,基本上说的就是 NSTimer,NSTimer 的执行会受 Mode 影响。
GCD 的定时器受 Runloop 的 Mode影响。
4.5 CFRunLoopObserverRef
CFRunLoopObserverRef 可以理解为一个观察者,当RunLoop 的状态发生改变时,通过它可以让外界知道 Runloop 的状态。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop 1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer 2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source 4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 32
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 64
kCFRunLoopExit = (1UL << 7), // 即将退出Loop 128
};
怎样使用 CFRunLoopObserverRef ?
//1.先创建一个 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
//2 .给当前RunLoop添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//3.释放Observer
CFRelease(observer);
5. 一个完整的RunLoop 到底是怎样的?
此时我需要在脑海中时刻保持这这样的一个概念,就是 RunLoop 说到底就是一个 do-while 的循环。
因为能力有限,现在借鉴大佬的一份对 RunLoop 内部代码 的整理片段:
/// 用DefaultMode启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 执行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 进入loop时参数说处理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部调用者强制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
上面的代码就是 RunLoop 循环的整个流程,与 RunLoop 对应的线程会一直停留着这个循环里,直到超时或者被手动停止,这个函数才会返回。
上面的代码如果不好理解,下面有精简版的:(当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去 (call out), 当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本)
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
如果上面的代码还不好理解,下面还有更精简版的:
SetUpThisRunLoopRunTimeoutTimer();
do {
//通知 Observers: 即将触发 Timer 回调。
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
// 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
//触发 Source0 (非基于port的) 回调。
__CFRunLoopDoSource0();
CheckIfExistMessagesInMainDispatchQueue();//看GCD有没有给过来需要去处理的任务
//通知Observers,即将进入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
//通过 mach_msg_trap 来捕获通过mach_msg传来的消息,类似于睡着的时候撑起一直耳朵
var wakeUpPort = sleepAndWaitForWakingUpPorts();
// mach_msg_trap
// Zzz...
// 获取到 mach_msg 传来的消息,通知“被唤醒了”
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//处理
if (wakeUpPort == timePort) {
//如果是被Timer唤醒的,回调Timer
__CFRunLoopDoTimer();
} else if (wakeUpPort == MainDispatchQueuePort) {
//GCD 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_();
} else {
//如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRunLoopDoSource1()
}
__CFRunLoopDoBlocks();
} while (! stop && ! timeOut);
//通知Observers,即将退出RunLoop
__CFRunLoopDoObservers(kCFRunLoopExit);
以上均为大佬所整理,借鉴过来便于自己学习和理解。
6. 如何正确开启一个NSTimer?
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。
当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
比如:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopModesWhat];
//当`NSRunLoopModesWhat` 为 `NSDefaultRunLoopMode` 时,如果有tableView滚动,那么这个 timer就会停止。
//当`NSRunLoopModesWhat` 为 `UITrackingRunLoopMode` 时,只有tableView滚动时,这个 timer的 run 才会执行。
//如果想要让 timer 不受 Mode 的切换所影响,就应像下面这样操作:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
另外: scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:
默认添加到当前runLoop中,而且是NSDefaultRunLoopMode。想要实现上面效果需要我们手动修改 Mode:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
7. AutoreleasePool 的创建和释放
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
我自己做个示意图如下:
8. RunLoop 的应用
8.1 RunLoop 的基本使用
// Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
// Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
8.2 ImageView 在 tableView停止滚动后展示图片
// 只在NSDefaultRunLoopMode模式下显示图片
// inModes:设置运行模式
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
8.3 常驻线程(一般说的是分线程) 比如 AFNetworking 的应用
- (void)allwaysRuning {
//addPort:添加端口(就是source) forMode:设置模式
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//启动RunLoop
[[NSRunLoop currentRunLoop] run];
}
在 AFNetworking中 的应用可在源码中看到:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
上面两个方法的基本原理都是在 RunLoop 开始 run 之前,添加一个 NSPort/NSMachPort,通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 Loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。因为 RunLoop 启动前必须要至少一个 item (Timer/Source/Observer)在 Mode 中,否则会退出滴。当 RunLoop 发现还有 source/timer/observer的时候,RunLoop 就不会退出.所以 AFNetworking 这里就通过给当前 RunLoop 添加一个NSMachPort,这个 port 实际上相当于添加了一个 source 事件源,这样子线程的 RunLoop 就会一直处于循环状态,等待别的线程向这个 port 发送消息,而实际上 AF 这里是没有消息发送到这个 port 的。
8.4 GCD异步调度主队列时
当GCD 在异步调度到主队列也就是执行到 dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程的 RunLoop 也就是 mainRunLoop 发送消息,然后 mainRunloop 会被唤醒,并从消息中获取到这个block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_() 里面进行这个block内容,这个逻辑仅限于 dispatch 到 主线程。
8.5 在分线程中加入 RunLoop 使 Timer执行
先来看一组代码:
...
- (void)viewDidLoad {
[super viewDidLoad];
[NSThread detachNewThreadSelector:@selector(newTreadFunction) toTarget:self withObject:nil];
}
...
- (void)newTreadFunction {
NSLog(@"----当前线程----%@",[NSThread currentThread]);
NSTimer *time = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerFunction) userInfo:nil repeats:YES];
}
...
- (void)timerFunction {
NSLog(@"Timer firing");
}
执行发现并不会打印 “Timer firing”,timer 压根就没有动起来,这是为什么呢?
因为子线程中的 RunLoop 是需要我们手动创建并 run 起来的。而创建的方法就是调用 currentRunLoop 方法。
所以我们需要将上述newTreadFunction方法修改为:
- (void)newTreadFunction {
NSLog(@"----当前线程----%@",[NSThread currentThread]);
NSTimer *time = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerFunction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
如上,就可以正常打印出Timer firing 了。
另外, Timer的创建有多种方式,如果我在 newTreadFunction 方法中,利用+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo 方式创建 Timer 的话,是不是用同样的解决方法呢?
答案是否定的,这就需要了解到 这两种方法创建 Timer的不同之处:
方式一:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
方式二:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
方式一创建的 Timer 会被主动加入到 当前 RunLoop中去,Mode 为 NSDefaultRunLoopMode,这也正是区别去方法二的地方,方法二不会将创建好的 Timer 假如到当前 RunLoop 中,所以,如果我们要想在 newTreadFunction 用方法二实现同样的效果,我们应该这样在当前 RunLoop 中 用- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode 做:
- (void)newTreadFunction {
NSTimer *time = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerFunction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
9: NSTimer 精确吗?
不精确!
首先,timer 的运行跟所处的 Mode 有关,如果是加在主线程的 timer 会默认设置为 NSDefaultRunLoopMode,这样在 Mode 切换时 timer 就会停止。
另外,由于 timer 的执行和 RunLoop 运行相关,那么可以理解为 timer 最小精度也就是 RunLoop 循环一周所需时间,大约是 3-4ms,如果需要更精确的精度,肯定是达不到的。
还有就是如果在一个 RunLoop 周期内或者说是与这个 loop相对应的线程中执行了耗时的操作,相当于线程(无论主线程还是分线程)受到阻塞,从而造成 timer不准。
一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
那么如何提高 Timer 的精确度?
第一,设置特定的 Mode,比如 RunLoopMode 为 NSRunLoopCommonModes。
第二,在分线程开启 Timer,然后到调度主线程刷新 UI。
10. NSTimer 中 invalidate 的作用
上面我们知道 Timer 之所以能够定时执行,是因为将 timer 加入到了 RunLoop 中,才使得能够时常出来 Timer 的回调。
This method is the only way to remove a timer from an NSRunLoop object.
invalidate 的作用 就是将 Timer 从 RunLoop 中移除出去,这样Timer 脱离了RunLoop ,也就停止了。
11. 如何用好一个 NSTimer?
NSTimer使用不当,容易造成循环引用,导致控制器无法正常释放,从而引起内存泄漏。
比如在使用下面两个方法时候,会传参 target,此时如果传入的是当前控制器,那么 timer 就会对这个 target 进行强引用
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
如果此时我们有个timer 的属性,就导致了当前控制器强引用了 timer,timer 又通过 target 强引用了 当前控制器,造成了互相强引用循环,dealloc 也就不会执行,控制前无法释放就会因此内存泄漏。
如何解决这种窘境呢?
方法一,就是在合理的时机停止 timer,并将timer 置为 nil,比如在 viewWillAppear 时开启,viewWillDisappear 处 调用 invalidate 停止 timer 并将 timer = nil,这种方式得看具体需求。
方法二,就是封装一个 Timer,露出开启和停止的API,例如:
//XXTimer.h文件
#import <Foundation/Foundation.h>
@interface XXTimer : NSObject
//开启定时器
- (void)startTimer;
//暂停定时器
- (void)stopTimer;
@end
//XXTimer.m文件
#import "XXTimer.h"
@implementation XXTimer {
NSTimer *_timer;
}
- (void)stopTimer{
if (_timer == nil) {
return;
}
[_timer invalidate];
_timer = nil;
}
- (void)startTimer{
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerDo) userInfo:nil repeats:YES];
}
- (void)timerDo{
NSLog(@"Timer的工作内容");
}
- (void)dealloc{
[_timer invalidate];
_timer = nil;
}
@end
//使用方法
#import "ViewController.h"
#import "XXTimer.h"
@interface ViewController ()
@property (nonatomic, strong) XXTimer *timer;
@end
@implementation ViewController
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Timer";
//创建timer
XXTimer *timer = [[XXTimer alloc] init];
self.timer = timer;
[timer startTimer];
}
- (void)dealloc {
[self.timer stopTimer];
}
上述方式中引用关系为 当前控制器强引用了 XXTimer 的对象,XXTimer 内存在 XXTimer 仅仅强引用了 NSTimer,我们在 dealloc 方法中执行 NSTimer 的销毁后,相对的XXTimer也会被销毁了。
方法三,使用系统提供的带 block 的timer 创建方法,例如:
// parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references
+ (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));
+ (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));
这种方法是在 iOS10+ 上提供的,我们在 block 中执行 timer need do what,这样避免了使用 target 传参的方式,也就避免了对 target 对象的引用。另外 timer 本身在执行时将 timer 作为参数传递给此块,以帮助避免循环引用,我们也要注意 block 的循环引用的隐患。
12. 相对精准的GCD定时器(不受runLoop的影响)
-(void)GCDTimer{
NSLog(@"%s",__func__);
//1.创建GCD中的定时器
/*
第一个参数:source的类型DISPATCH_SOURCE_TYPE_TIMER 表示是定时器
第二个参数:描述信息,线程ID
第三个参数:更详细的描述信息
第四个参数:队列,决定GCD定时器中的任务在哪个线程中执行
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
//2.设置定时器(起始时间|间隔时间|精准度)
/*
第一个参数:定时器对象
第二个参数:起始时间,DISPATCH_TIME_NOW 从现在开始计时
第三个参数:间隔时间 1.0 GCD中时间单位为纳秒
第四个参数:精准度 绝对精准0
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.设置定时器执行的任务
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCD---%@",[NSThread currentThread]);
});
//4.启动执行
dispatch_resume(timer);
}