一.初识RunLoop
1.RunLoop的基本作用
- 保持程序的持续运行;
如果没有Runloop,main()函数一执行完,程序就会立刻退出。
而我们的iOS程序能保持持续运行的原因就是在main()函数中调用了UIApplcationManin函数,这个函数内部会启动主线程的RunLoop; - 处理App中的各种事件(比如触摸事件,定时器事件等);
- 节省CPU资源,提高程序性能;该做事时做事,该休息时休息。
2.RunLoop的应用范畴
- 定时器(Timer)、PerformSelector
- GCD:dispatch_async(dispatch_get_main_queue(),^{});
- 事件响应、手势识别、界面刷新;
- 网络请求;
- AutoreleasePool;
3.RunLoop在实际开发中的应用
- 使用端口或自定义输入源与其他线程进行通信;
- 在子线程上使用定时器;
- 解决NSTimer在滑动时停止工作的问题;
- 控制线程的生命周期,实现一个常驻线程;
- 在Cocoa应用程序中使用任何performSelector...方法;
- 监控应用卡顿;
- 性能优化;
- ......
二.RunLoop的数据结构
1.CFRunLoopRef
RunLoop对象的底层就是一个CFRunLoopRef结构体,它里面存储着:
- _pthread:
RunLoop与线程是一一对应关系; - _commonModes:存储着NSString对象的集合(Mode的名称);
- _commonModeItems:存储着被标记为通用模式的
Source0/Source1/Timer/Observer; - _currentMode:
RunLoop当前的运行模式; - _modes:存储着
RunLoop所有的Mode(CFRunLoopModeRef)模式;
// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;
// CFRunLoop.c
struct __CFRunLoop {
pthread_t _pthread; //与线程一一对应
CFMutableSetRef _commonModes; //存储着NSString对象的集合(Mode的名称)
CFMutableSetRef _commonModeItems; //存储着被标记为通用模式的Source0/Source1/Timer/Observer
CFRunLoopModeRef _currentMode; //RunLoop当前的运行模式
CFMutableSetRef _modes; //RunLoop所有的Mode(CFRunLoopModeRef)模式
...
};
2.CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式;- 一个
RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer; RunLoop启动时只能选择其中一个Mode,作为currentMode;- 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,切换模式不会导致程序退出;
- 不同Mode中的
Source0/Source1/Timer/Observer能分隔开来,互不影响; - 如果Mode里没有任何
Source0/Source1/Timer/Observer,RunLoop会立马退出;
// CFRunLoop.h
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
// CFRunLoop.c
struct __CFRunLoopMode {
CFStringRef _name; // mode 类型,如:NSDefaultRunLoopMode
CFMutableSetRef _sources0; // CFRunLoopSourceRef
CFMutableSetRef _sources1; // CFRunLoopSourceRef
CFMutableArrayRef _observers; // CFRunLoopObserverRef
CFMutableArrayRef _timers; // CFRunLoopTimerRef
...
};
3.RunLoop的常见模式
-
NSDefaultRunLoopMode/KCFRunLoopDefaultMode:默认模式
-
UITrackingRunLoopMode:界面追踪模式,用于UIScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响;
-
NSRunLoopCommonModes/KCFRunLoopCommonModes:该模式不是实际存在的一种模式,它只是一个特殊的标记,是同步
Source0/Source1/Timer/Observer到多个Mode中的技术方案,被标记为通用的Source0/Source1/Timer/Observer都会存放到_commonModeItems集合中,会同步这些Source0/Source1/Timer/Observer到多个Mode中。 -
NSDefaultRunLoopMode和NSRunLoopCommonModes属于Foundation框架; -
KCFRunLoopDefaultMode和KCFRunLoopCommonModes属于Core Foundation框架; -
前者是对后者的封装,作用相同。
CFRunLoopModeRef这样子设计有什么好处?RunLoop为什么会有多个Mode?
- Mode做到了屏蔽的效果,当
RunLoop运行在Mode1下面的时候,是处理不了Mode2的事件的; - 比如
NSDefaultRunLoopMode默认模式和UITrackingRunLoopMode滚动模式,滚动屏幕的时候就会切换到滚动模式,就不用去处理模式下的事件了,保证了UITableView等的滚动顺畅。
- Mode做到了屏蔽的效果,当
4.CFRunLoopSourceRef
- 在
RunLoop中有两个很重要的概念,一个是上面提到的模式,还有一个就是事件源。事件源分为输入源(Input Sources)和定时器源(Timer Sources)两种; 输入源(Input Sources)又分为Source0和Source1两种,以下__CFRunLoopSource共用体union中的version0和version1就分别对应Source0和Source1。
// CFRunLoop.h
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
// CFRunLoop.m
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
Source0和Source1的区别
-
Source0:需要手动唤醒线程,添加
Source0到RunLoop并不会主动唤醒线程,需要手动唤醒- 触摸事件处理
performSelector:onThread:
-
Source1:具备唤醒线程的能力
- 基于Port的线程间通信
- 系统事件捕捉:系统事件捕捉是由Source1来处理,然后再交给Source0处理
5.CFRunLoopTimerRef
CFRunLoopTimer和NSTimer是toll-free bridged的,可以相互转换;performSelector:withObject:afterDelay:方法创建timer并添加到RunLoop中。
// CFRunLoop.h
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
// CFRunLoop.c
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加该 timer 的 RunLoop
CFMutableSetRef _rlModes; // 所有包含该 timer 的 modeName
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable 理想时间间隔 */
CFTimeInterval _tolerance; /* mutable 时间偏差 */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable 回调入口 */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
6.CFRunLoopObserverRef
作用
CFRunLoopObserverRef用来监听RunLoop的6种活动状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 表示以上所有状态
};
- UI刷新(BeforeWaiting)
- Autorelease pool (BeforeWaiting)
定义
// CFRunLoop.h
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
// CFRunLoop.c
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加该 observer 的 RunLoop
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable 监听的活动状态 */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable 回调入口 */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
CFRunLoopObserverRef中的_activities用来保存RunLoop的活动状态。当RunLoop的状态发生改变时,通过回调_callout通知所有监听这个状态的Observer。
7.CFRunLoop函数实现:事件循环的实现机制
/**
* __CFRunLoopRun
*
* @param rl 运行的 RunLoop 对象
* @param rlm 运行的 mode
* @param seconds loop 超时时间
* @param stopAfterHandle true: RunLoop 处理完事件就退出 false:一直运行直到超时或者被手动终止
* @param previousMode 上一次运行的 mode
*
* @return 返回 4 种状态
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
{
int32_t retVal = 0;
do {
// 通知 Observers:即将处理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers:即将处理 Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理 Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判断有无 Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有 Source1,就跳转到 handle_msg
goto handle_msg;
}
// 通知 Observers:即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// ⚠️休眠,等待消息来唤醒线程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers:刚从休眠中唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被 Timer 唤醒) {
// 处理 Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (被 GCD 唤醒) {
// 处理 GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被 Source1 唤醒
// 处理 Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
// 处理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
/* 设置返回值 */
// 进入 loop 时参数为处理完事件就返回
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
// 超出传入参数标记的超时时间
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
// 被外部调用者强制停止
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
// 自动停止
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
// mode 中没有任何的 Source0/Source1/Timer/Observer
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
从该函数实现中可以得知RunLoop主要就做以下几件事情:
- _CFRunLoopDoObservers:通知
Observers接下来要做什么 - _CFRunLoopDoBlocks:处理
Blocks - _CFRunLoopDoSources0:处理
Source0 - _CFRunLoopDoSources1:处理
Source1 - _CFRunLoopDoTimers:处理
Timers - 处理GCD相关:
dispatch_async(dispatch_get_main_queue(),^{}); - _CFRunLoopSetSleeping/_CFRunLoopUnsetSleeping:休眠等待/结束休眠
- _CFRunLoopServiceMachProt->mach-msg():转移当前线程的控制权
8.CFRunLoopServiceMarchPort函数实现:RunLoop休眠的实现原理
在_CFRunLoopRun函数中,会调用_CFRunLoopServiceMachPort函数,该函数中调用了mach_msg()函数来转移当前线程的控制权给内核态/用户态。
- 没有消息需要处理时,休眠线程以避免资源占用,调用
mach_msg()从用户态切换成内核态,等待消息; - 有消息需要处理时,立刻唤醒线程,调用
mach_msg()回到用户态处理消息。
这就是RunLoop休眠的实现原理,也是RunLoop与简单的do...while循环区别:
RunLoop:休眠的时候,当前线程不会做任何事,CPU不会再分配资源;- 简单的
do...while循环:当前线程并没有休息,一直占用CPU资源;
9.RunLoop与线程的关系
RunLoop对象和线程是一一对应关系;RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value;- 如果没有
RunLoop,线程执行完任务就会退出;如果没有RunLoop,主线程执行完main()函数就退出,程序就不能处于运行状态; RunLoop创建时机:线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;RunLoop销毁时机:RunLoop会在线程结束时销毁;- 主线程的
RunLoop已经自动获取(创建),子线程默认没有开启RunLoop; - 主线程的
RunLoop对象是在UIApplcationMain中通过[NSRunLoop currentRunLoop]获取,一旦发现它不存在,就会创建RunLoop对象。
实现一个常驻线程
-
好处:经常用到子线程的时候,不用一直创建销毁,提高性能;
-
条件:该任务需是串行的,而非并发;
-
步骤:
- 获取/创建当前线程的
RunLoop; - 向该
RunLoop中添加一个Source/Port等来维持RunLoop的事件循环(如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出); - 示例代码及测试输出如下:
- 获取/创建当前线程的
// ViewController.m
#import "ViewController.h"
#import "HTThread.h"
@interface ViewController ()
@property (nonatomic, strong) HTThread *thread;
@property (nonatomic, assign, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[HTThread alloc] initWithBlock:^{
NSLog(@"begin-----%@", [NSThread currentThread]);
// ① 获取/创建当前线程的 RunLoop
// ② 向该 RunLoop 中添加一个 Source/Port 等来维持 RunLoop 的事件循环
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
// ③ 启动该 RunLoop
/*
[[NSRunLoop currentRunLoop] run]
如果调用 RunLoop 的 run 方法,则会开启一个永不销毁的线程
因为 run 方法会通过反复调用 runMode:beforeDate: 方法,以运行在 NSDefaultRunLoopMode 模式下
换句话说,该方法有效地开启了一个无限的循环,处理来自 RunLoop 的输入源 Sources 和 Timers 的数据
*/
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"end-----%@", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (!self.thread) return;
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
}
// 停止子线程的 RunLoop
- (void)stopThread
{
// 设置标记为 YES
self.stopped = YES;
// 停止 RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
- (void)dealloc
{
NSLog(@"%s", __func__);
if (!self.thread) return;
// 在子线程调用(waitUntilDone设置为YES,代表子线程的代码执行完毕后,当前方法才会继续往下执行)
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
@end
// HTThread.h
#import <Foundation/Foundation.h>
@interface HTThread : NSThread
@end
// HTThread.m
#import "HTThread.h"
@implementation HTThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
10.RunLoop与NSTimer
NSTimer是由RunLoop来管理的,NSTimer其实就是CFRunLoopTimerRef,他们之间是toll-free bridge的,可以相互转换;- 如果我们在子线程上使用
NSTimer,就必须开启子线程的NSTimer,否则定时器无法生效。
解决tableview滑动时NSTimer失效的问题
- 问题:
RunLoop同一时间只能运行在一种模式下,当我们滑动tableview/scrollview的时候RunLoop会切换到UITrackingRunLoopMode界面追踪模式下。如果我们的NSTimer是添加到RunLoop的KCFRunLoopDefaultMode/NSDefaultRunLoopMode默认模式下的话,此时是会失效的。 - 解决:我们可以将
NSTimer添加到RunLoop的KCFRunLoopCommonModes/NSRunLoopCommonModes通用模式下,来保证无论在默认模式还是界面追踪模式下NSTimer都可以执行。 NSTimer的创建方式。
如果我们是通过以下方式创建的NSTimer,是自动添加到RunLoop的默认模式下
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
我们可以通过以下方式创建NSTimer,来自定义添加到RunLoop的某种模式下
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
注意: 如果是通过timerxxx开头方法创建的NSTimer是不会自动添加到RunLoop中的,所以一定要记得手动添加,否则NSTimer不生效。
11.CFRunLoopAddTimer函数实现
CFRunLoopAddTimer()函数中会判断传入的modeName模式名称是不是kCFRunLoopCommonModes通用模式,是的话就会将timer添加到RunLoop的_commonModeItems 集合中,并同步该timer到的_commonModeItems里的所有模式中,这样无论在默认模式还是界面追踪模式下NSTimer都可以执行。
12.NSTimer和CADisplayLink存在的问题
- 不准时:
NSTimer和CADisplayLink底层都是基于RunLop的CFRunLoopTimerRef的实现,也就是说他们都依赖与RunLop。如果RunLop的任务过于繁重,会导致它们不准时。比如NSTimer每1.0秒就会执行一次任务,RunLoop每进行一次循环,就会看一下NSTimer的时间是否达到1.0秒,是的话就执行任务。但是由于RunLoop每一次循环的任务不一样,所花费的时间就不固定。假设第一次循环所花时间为 0.2s,第二次 0.3s,第三次 0.3s,则再过 0.2s 就会执行NSTimer的任务,这时候可能RunLoop的任务过于繁重,第四次花了0.5s,那加起来时间就是1.3s,导致NSTimer不准时。 - 解决方法:
使用GCD的定时器。GCD的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLop,所以它非常的准时。示例如下:
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置时间(start:几s后开始执行; interval:时间间隔)
uint64_t start = 2.0; //2s后开始执行
uint64_t interval = 1.0; //每隔1s执行
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
//设置回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%@",[NSThread currentThread]);
});
//启动定时器
dispatch_resume(timer);
NSLog(@"%@",[NSThread currentThread]);
self.timer = timer;
/*
2020-02-01 21:34:23.036474+0800 多线程[7309:1327653] <NSThread: 0x600001a5cfc0>{number = 1, name = main}
2020-02-01 21:34:25.036832+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:26.036977+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:27.036609+0800 多线程[7309:1327707] <NSThread: 0x600001a1e5c0>{number = 4, name = (null)}
*/
面试题
1.为什么NSTimer有时候不好使?
因为创建的NSTimer默认是被接入到了NSDefaultMode,所以当RunLoop的Mode变化时,当前的NSTimer就不会工作了。
2.PerformSelector的实现原理?
- 当调用NSObject的performSelecter:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop中,所以如果当前线程没有RunLoop,则这个方法会失效。
- 当调用performSelector:onThread:时,实际上其会创建一个Timer加到对应的线程中去,同样的,如果对应线程没有RunLoop该方法也会失效。
3.PerformSlector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?
- 不起作用,子线程默认没有RunLoop,也没有Timer;
- 解决办法是可以使用GCD来实现:Dispatch_after
4.AFNetworking中如何运用RunLoop?
AFURLConnectionOperation这个类是基于NSURLConnection构建的,其希望能在后台线程接收Delegate回调,为此AFNetworking单独创建了一个线程,并在这个线程中启动了一个RunLoop:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
RunLoop启动前内部必须要有至少一个Timer/Observer/Source,所以AFNetworking在[runloop run]之前先创建了一个新的NSMachPort添加进去了。通常情况下,调用者需要持有这个NSMachPort(mach_port)并在外部线程通过这个port发送消息到loop内;但此处添加port只是为了让RunLoop不至于退出,并没有用于实际的发送消息。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
当需要这个后台执行任务时,AFNetworking通过调用[NSObject performSelector:onThread:..]将这个任务扔到了后台线程的RunLoop。
5.AutoReleasePool在何时被释放?
App启动后,苹果在主线程RunLoop里面注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
-
第一个
Observer监视的事件是Entry(即将进入RunLoop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。 -
第二个
Observer监视了两个事件:BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池。
6.RunLoop的Mode
总共有5种CFRunLoopMode:
kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行;UITrackingRunLoopMode:跟踪用户交互事件(用于UIScrollView/UITableView追踪触摸滑动,保证界面滑动时不受其他Mode影响);UIInitalizationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后就不再使用;GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到;kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案;
7.RunLoop的实现机制
对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在没有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的。
RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户调用mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。即基于Port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop。
大致逻辑如下:
-
1.通知观察者
RunLoop即将启动; -
2.通知观察者即将要处理
Timer事件; -
3.通知观察者即将要处理
Source0事件; -
4.处理
Source0事件; -
5.如果基于端口的源(
Source1)准备好并处于等待状态,进入步骤9; -
6.通知观察者线程即将进入休眠状态;
-
7.将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程
- 一个基于
port的Source1的事件; - 一个
Timer到时间了; RunLoop自身的超时时间到了;- 被其他调用者手动唤醒;
- 一个基于
-
8.通知观察者线程将被唤醒;
-
9.处理唤醒时收到的事件
- 如果用户定义的定时器启动,处理定时器事件并重启 RunLoop。进入步骤 2。
- 如果输入源启动,传递相应的消息。
- 如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop。进入步骤 2
-
10.通知观察者 RunLoop 结束。
8.RunLoop 和线程
- 线程和 RunLoop 是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
- 自己创建的线程默认是没有开启 RunLoop 的
8.1 怎么创建一个常驻线程
- 为当前线程开启一个 RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个 RunLoop)
- 向当前 中添加一个 Port/Source 等维持 RunLoop 的事件循环(如果 RunLoop 的 mode 中一个 item 都没有, 会退出)
- 启动该RunLoop
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop] ;
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run] ;
}
8.2怎样保证子线程数据回来更新 UI 的时候不打断用户的滑动操作?
当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新 UI,那么就会影响当 前正在滑动的体验。
我们就可以将更新 UI 事件放在主线程的 上执行即可,这样就会等用户不再滑动页 面,主线程 RunLoop 由 切换到 时再去更新 UI
9.RunLoop的数据结构
NSRunLoop(Foundation)是CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
RunLoop相关的主要涉及五个类:
CFRunLoop:RunLoop对象CFRunLoopMode:运行模式CFRunLoopSource:输入源/事件源CFRunLoopTimer:定时源CFRunLoopObserver:观察者
9.1 CFRunLoop
由pthread(线程对象,说明RunLoop和线程是一一对应的)、
currentMode(当前所处的运行模式)、
modes(多个运行模式的集合)、
commonModes(模式名称字符串集合)、
commonModelItems(Observer,Timer,Source集合)构成
9.2 CFRunLoopMode
由name、source0、source1、currentMode、timers构成
9.3 CFRunLoopSource
分为source0和source1两种
source0:即非基于port的,也就是用户触发的事件,需要手动唤醒线程,将当前线程从内核态切换到用户态;source1:基于port的,包含一个mach_port和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件,具备唤醒线程的能力。
9.4 CFRunLoopTimer
基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer是不准确的。因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延迟,或者少执行一次)。
9.5 CFRunLoopObserver
监听以下时间点:CFRunLoopAtivity
kCFRunLoopEntry:RunLoop准备启动kCFRunLoopBeforeTimers:RunLoop将要处理一些Timer相关事件kCFRunLoopBeforeSource:RunLoop将要处理一些Source事件kCFRunLoopBeforeWaiting:RunLoop将要进行休眠状态,即将由用户态转换成内核态kCFRunLoopAfterWaiting:RunLoop被唤醒,即从内核态切换成用户态后kCFRunLoopExit:RunLoop退出kCFRunLoopAllActivities:监听所有状态
9.6 各数据结构之间的联系
- 线程和
RunLoop一一对应 RunLoop和Mode是一对多的Mode和Observer,Timer,Source也是一对多的
10.RunLoop概念
RunLoop是通过内部维护的事件循环Event Loop来对事件/消息进行管理的一个对象。
- 没有消息处理时,休眠已避免资源占用,由用户态切换成内核态(CPU-内核态和用户态)
- 有消息需要处理时,立刻被唤醒,由内核态切换到用户态
为什么main函数不会退出?
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,并执行了一段无限循环的代码(不是简单的for循环或while循环)
UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。
11.RunLoop与NSTimer
滑动UITableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时添加到kCFRunLoopDefaultMode和UITrackingRunLoopMode上。那么如何把Timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes了,Timer就被添加到多个mode上,这样即使RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件。
12.解释一下NSTimer
NSTimer其实就是CFRunLoopTimerRef,他们之间是toll-free bridge的。一个NSTimer注册到RunLoop后,RunLoop会为其重复的时间点注册好事件。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer有个属性叫做Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。
CADisplayLink是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,造成界面卡顿的感觉,在快速滑动TableView时,即使一帧也会让用户有所察觉。
13.解释一下事件响应的过程?
苹果注册了一个Source1(基于mach port的)用来接收系统事件,其回调函数为_IOHIDEventSystemClientQueueCallback()
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent事件并由SpringBoard接收。SpringBoard只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种Event,随后用mach port转发给需要的App进程。随后苹果注册的Source1就会触发回调,并调用_UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包含识别UIGesture/处理屏幕旋转/发送给UIWindow等。通常事件比如UIButton点击、touchesBegin/Move/End/Cancel事件都是在这个回调中完成的。
14.解释一下手势识别的过程?
当_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用Cancel将当前的code>touchesBegin/Move/End系列回调打断。随后系统将对应的UIGestureRecognizer标记为待处理。
苹果注册了一个Observer监测BeforeWaiting(RunLoop即将进入休眠)事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。
当有UIGestureRecognizer的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
15.利用RunLoop解释一下页面的渲染的过程?
当我们调用[UIView setNeedsDisplay]时,这时会调用当前view.layer的[view.layer setNeedsDisplay]方法。
这等于给当前的layer打上了一个脏标记,而此时并没有直接进行绘制工作。而是会到当前的RunLoop即将休眠,也就是beforeWaiting时才会进行绘制工作。
紧接着会调用[CALayer display],进入到真正绘制的工作。CALayer层会判断自己的delegate有没有实现异步绘制的代理方法displayer:,这个带俩方法是异步绘制的入口,如果没有实现这个方法,那么会继续进行系统绘制的流程,然后绘制结束。
CALayer内部会创建一个Backing Store,用来获取图形上下文,接下来会判断这个layer是否有delegate。
如果有的话,会调用[layer.delegate drawLayer:inContext:],并且会返回给我们[UIView drawRect:]的回调,让我们在系统绘制的基础之上再做一些事情。
如果没有delegate,那么会调用[CALayer drawInContext:]
以上两条分支,最终CALayer都会将位图提交到Backing Store,最后提交给GPU。至此绘制的过程结束。