趁热打个铁,火烧眉毛想记录新东西了html
通常来说,一个线程只能执行一次任务,执行完线程就会退出。若是咱们须要这样一个机制,让线程能随时处理事件而不退出,一般的逻辑代码以下:安全
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
复制代码
这种模型一般叫作Event Loop
。这个模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以免占用资源,在有消息到来时当即被唤醒。 因此,RunLoop实际是一个对象,该对象管理了其须要处理的事件和消息,并提供了入口函数来处理上面的Event Loop
的逻辑。线程执行这个函数后,就会一直处在函数内部“接收消息->等待->处理”的循环中,直到接收到退出消息(如quit),函数结束。bash
OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。 CFRunLoopRef 是在 Core Foundation 框架内的,它提供了纯 C 函数的 API,全部这些 API 都是线程安全的。它是基于pthread的。 NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,可是这些 API 不是线程安全的。服务器
苹果不容许直接建立 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对象(主线程除外)。app
在建立一个iOS程序后,会自动生成main.m文件,以下框架
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
其中UIApplicationMain
函数内部为主线程开启了RunLoop,逻辑代码如Event Loop
模型所示。 下图为苹果官方给出的RunLoop模型图。 async
Core Foundation框架下有关于RunLoop的5个类,以下:函数
他们的关系以下图: oop
咱们能够经过以下API来获取Core Fundation中的CFRunLoopRef。
//获取主线程的RunLoop
CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
//获取当前线程的RunLoop
CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
复制代码
系统定义了多种运行模式:
- (void)viewDidLoad {
[super viewDidLoad];
//将定时器加入到默认运行模式中(一旦用户交互就不会响应)
NSTimer *timer1 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode];
//将定时器加入到交互运行模式中(一旦中止交互就不会响应)
NSTimer *timer2 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInTrackingMode) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
//将定时器加入到伪模式中(不管是否交互均可以响应)
NSTimer *timer3 = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runInCommonMode) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes];
}
- (void)runInDefaultMode {
NSLog(@"我只有在默认模式下运行!");
}
- (void)runInTrackingMode {
NSLog(@"我只有在交互模式下运行!");
}
- (void)runInCommonMode {
NSLog(@"我在默认模式和交互模式下都能运行!");
}
复制代码
咱们观察到,没有作操做时timer1
能正常运行,而timer2
无响应,用户操做后timer1
中止运行,而timer2
正常运行,与此同时timer3
始终都能运行,这是为何呢?缘由以下:
NSDefaultRunLoopMode
模式下,因此timer1
此时能稳定2秒运行。NSDefaultRunLoopMode
模式,并切换到UITrackingRunLoopMode
工做,因此timer1
不能继续工做,转而该模式下的timer2
开始工做。NSRunLoopCommonModes
不是一个真正的模式,并不是须要停止其余模式再切换,只是使得能够在标记了Common Modes的模式下运行,也就是NSDefaultRunLoopMode
和NSRunLoopCommonModes
,因此timer3
能一直工做。这里咱们能够看下CFRunLoopMode 和 CFRunLoop 的大体结构:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
复制代码
一个Mode能够将本身标记为“Common”属性(将本身的ModelName添加到RunLoop中的_commonModes中)。每当RunLoop发生变化时,RunLoop会将_commonModeItems中的Source/Observer/Timer同步到全部标记了“Common”的Mode中。
另外,说到NSTimer,咱们平时使用的如下方法,是自动添加到了RunLoop对象的NSDefaultRunLoopMode
模式下,因此一旦交互是没法响应的。
[NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runInDefaultMode) userInfo:nil repeats:YES];
复制代码
例如咱们点击一个按钮,拦截它的响应事件的函数调用栈,能够看到
CFRunLoopObserverRef是观察者,用来监听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
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听所有状态改变
};
复制代码
咱们能够经过以下代码来监听RunLoop状态的改变
//建立监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd", activity);
});
//添加到当前的RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
//添加完后释放
CFRelease(observer);
复制代码
这里监听了全部的状态,打印日志能够看到RunLoop的状态不断的改变,最终会变成32,也就是立刻会进入休眠状态。
注:上面的 Source/Timer/Observer 被统称为 mode item,一个 item 能够被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会重复生效的。若是一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
根据苹果在文档里的说明,RunLoop 内部的逻辑大体以下:
注:RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,若是没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,而后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
{
/// 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);
}
复制代码
AutoreleasePool用于在代码块结束时释放全部在代码块中建立的对象,最重要的使用场景就是临时建立了大量的对象,例如在循环中建立对象,能够在循环体内使用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 了。
苹果注册了一个Source1(基于mach port)来接收系统事件,若是发生硬件事件(触摸/锁屏/摇晃等),Source1会触发回调__IOHIDEventSystemClientQueueCallback() ,函数内而后触发Source0,Source0再经过_UIApplicationHandleEventQueue() 分发到应用内部。 _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。一般事件好比 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。 注:网上对于点击事件是Source0仍是Source1触发有争议,在 __IOHIDEventSystemClientQueueCallback 处下一个 Symbolic Breakpoint能够看到,确实是如上述逻辑。
当上面的_UIApplicationHandleEventQueue() 接收到一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。 苹果注册了一个Observer来监听BeforeWaiting(即将进入休眠),其回调函数内部会获取全部刚才标记了未处理的手势,并触发它们的回调。 当有 UIGestureRecognizer 的变化(建立/销毁/状态改变)时,这个回调都会进行相应处理。
当在操做 UI 时,好比改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。 苹果注册了一个Observer来监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,并在回调函数里遍历全部待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
NSTimer实际也就至关于CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在很是准确的时间点回调这个Timer。Timer 有个属性叫作 Tolerance (宽容度),标示了当时间点到后,允许有多少最大偏差。 若是某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就好比等公交,若是 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。 CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不同,其内部实际是操做了一个 Source)。若是在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 类似),形成界面卡顿的感受。在快速滑动TableView时,即便一帧的卡顿也会让用户有所察觉。
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会建立一个 Timer 并添加到当前线程的 RunLoop 中。因此若是当前线程没有 RunLoop,则这个方法会失效。 当调用 performSelector:onThread: 时,实际上其会建立一个Source0加到对应的线程去,一样的,若是对应线程没有 RunLoop 该方法也会失效。()
RunLoop底层会用到GCD的东西,GCD的实现也用到了RunLoop,好比dispatch_async()函数。 当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其余线程仍然是由 libDispatch 处理的。
例如咱们须要在cell上展现分时图,那么在滚动的时候若是有一堆的分时图须要重复的清空再计算绘制,就有可能形成卡顿。 首先cell复用分时图须要使用到两个方法clearTimeLine
和refreshTimeLine
,咱们能够利用PerformSelector
调用refreshTimeLine
将其放在主线程的NSDefaultRunLoopMode
下,这样避免滚动时还会触发绘图操做,减小计算和绘制,提升性能,同时也减小了内存占用。
若是在实际开发中有大量的耗时操做须要在后台完成,频繁的新建子线程并非好的方案,咱们能够选择让这条线程常驻内存。
- (void)viewDidLoad {
[super viewDidLoad];
//强引用子线程,初始化该线程并启动
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
//经过performSelector来在子线程中处理耗时操做,避免重复建立
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run {
//开启当前线程的RunLoop,此处添加port是避免RunLoop退出
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
复制代码
因为咱们绝大多数操做都是基于非port通讯的,也就是source0,因此咱们能够经过使用子线程来检测RunLoop中kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
两个状态之间的时间来判断这一轮操做是否卡顿,并把当前线程的堆栈信息存储到文件中,在某个合适的时机上传到服务器。
大体步骤以下:
kCFRunLoopBeforeSources
和kCFRunLoopBeforeWaiting
两个状态。kCFRunLoopBeforeSources
记录更新时间,而且记录状态为NO,用于定时器区分状态;在kCFRunLoopBeforeWaiting
时将状态置为YES。优化点:
相关代码
#import "ViewController.h"
static CGFloat lagTimeInterval = 0.5;
@interface ViewController ()
//监听子线程
@property (nonatomic, strong) NSThread *monitorThread;
//是否进入休眠
@property (nonatomic, assign) BOOL isBeforeWaiting;
//即将处理source0的时间
@property (nonatomic, strong) NSDate *beforeSource0Time;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//建立监听子线程,打开其RunLoop
_monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(openRunLoop) object:nil];
[_monitorThread start];
//添加定时器
[self performSelector:@selector(startMonitorTimer) onThread:_monitorThread withObject:nil waitUntilDone:NO];
//主线程RunLoop添加观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopBeforeSources:
{
_beforeSource0Time = [NSDate date];
_isBeforeWaiting = NO;
}
break;
case kCFRunLoopBeforeWaiting:
{
_isBeforeWaiting = YES;
}
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
//打开子线程的RunLoop对象
- (void)openRunLoop {
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
}
}
//添加定时器到子线程的RunLoop中
- (void)startMonitorTimer {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5*lagTimeInterval repeats:YES block:^(NSTimer * _Nonnull timer) {
//若是_isBeforeWaiting状态为YES,表示主线程RunLoop即将进入休眠
if(!_isBeforeWaiting) {
//获取当前时间与记录时间的差值
NSTimeInterval timeInterval = [[NSDate date] timeIntervalSinceDate:_beforeSource0Time];
//若是大于卡顿时间,则打印出来
if(timeInterval >= lagTimeInterval) {
NSLog(@"##############卡了");
[self logStack];
} else {
NSLog(@"##############没卡");
}
}
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)logStack {
NSLog(@"%@", [NSThread callStackSymbols]);
}
@end
复制代码