Runtime 实际运用
runloop和线程一一对应
runloop包含多个mode, mode包含多个 mode item(sources,timers,observers)
runloop一次只能运行在一个model下:
切换mode:停止loop -> 设置mode -> 重启runloop
runloop通过切换mode来筛选要处理的事件,让其互不影响
iOS运行流畅的关键
/// 核心函数
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
/// 通知 Observers: 即将处理timer事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
/// 通知 Observers: 即将处理Source事件
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
/// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
/// 处理sources0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
/// 处理sources0返回为YES
if (sourceHandledThisLoop) {
/// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
/// 判断有无端口消息(Source1)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
/// 处理消息
goto handle_msg;
}
/// 通知 Observers: 即将进入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
/// 等待被唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// user callouts now OK again
__CFRunLoopUnsetSleeping(rl);
/// 通知 Observers: 被唤醒,结束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被Timer唤醒) {
/// 处理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被GCD唤醒) {
/// 处理gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else if (被Source1唤醒) {
/// 被Source1唤醒,处理Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
}
/// 处理block
__CFRunLoopDoBlocks(rl, rlm);
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;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
image.png
事件响应
当一个硬件事件(触摸/锁屏/摇晃/加速)发生后,首先IOKit.framework 生成一个IOHIDEvent事件并有SprintBoard接收,之后有mach_port转发给需要的App进程。
苹果注册了一个Source1来接收系统事件,通过回调函数触发Source0 (所以Event实际上是基于Source0的),调用_UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把IOHIDEvent 处理并包装成UIEvent 进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等。
手势识别
当上面的_UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用Cancel 将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的UIGestureRecognizer 标记为待处理。
苹果注册了一个Observer监测BeforeWaiting(Loop即将进入休眠)事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。
当有UIGestureRecognizer的变化(创建、销毁、状态改变)时,这个回调都会进行相应的处理。
界面刷新
当UI发生改变时(Frame变化,UIView/CALayer的结构变化)时,或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理。
苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在他的回调函数里会遍历所有待处理的UIView/CALayer来执行实际的绘制和调整,并更新UI界面。
AutoreleasePool
主线程Runloop注册了两个Observers,其回调都是_wrapRunloopWithAutoreleasePoolHandler
Observers1 监听Entry事件: 优先级最高,确保在所有的回调前创建释放池,回调内调用 _objc_autoreleasePoolPush()创建自动释放池
Observers2监听BeforeWaiting 和Exit事件: 优先级最低,保证在所有回调后释放释放池。BeforeWaiting事件:调用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()释放旧池并创建新池,Exit事件: 调用_objc_autoreleasePoolPop(),释放自动释放池
Timer不被ScrollView的滑动影响
+timerWithTimerInterval ... 创建timer
[[NSRunLoop currentRunLoop] addTimer: timer forMode:NSRunLoopCommonModes] 把timer加到当前runloop,使用占位模式
runloop run/runUntilData 手动开启子线程
使用GCD创建定时器,GCD创建的定时器不会受RunLoop 的影响。
// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
// 比当前时间晚1秒开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
});
// 启动定时器
dispatch_resume(self.timer);
GCD
dispatch_async(dispatch_get_main_queue)使用到了RunLoop
libDispatch向主线程的Runloop发送消息将其唤醒,并从消息中取得block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()里执行这个block
NSURLConnection
使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate就会不停收到事件回调。
start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。 CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
AFNetworking
使用runloop开启常驻线程
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
给 runloop 添加NSMachPort port使runloop不退出,实际并没有给这个port发消息
AsyncDisplayKit
仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。
卡顿检测
- dispatch_semaphore_t 是一个信号量机制,信号量到达、或者 超时会继续向下进行,否则等待,如果超时则返回的结果必定不为0,信号量到达结果为0。GCD信号量-dispatch_semaphore_t
通过监听mainRunloop的状态和信号量阻塞线程的特点来检测卡顿,通过kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting的间隔时长超过自定义阀值则记录堆栈信息。
FPS 检测
创建CADisplayLink对象的时候会指定一个selector,把创建的CADisplayLink对象加入runloop,所以就实现了以屏幕刷新的频率调用某个方法。
在调用的方法中计算执行的次数,用次数除以时间,就算出了FPS。
注:iOS正常刷新率为每秒60次。
@implementation ViewController {
UILabel *_fpsLbe;
CADisplayLink *_link;
NSTimeInterval _lastTime;
float _fps;
}
- (void)startMonitoring {
if (_link) {
[_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[_link invalidate];
_link = nil;
}
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
if (_lastTime == 0) {
_lastTime = link.timestamp;
return;
}
self.count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
_lastTime = link.timestamp;
_fps = _count / delta;
NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
self.count = 0;
_fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}
防崩溃处理
NSSetUncaughtExceptionHandler(&HandleException);监听异常信号SIGILL,SIGTRAP,SIGABRT,SIGBUS,SIGSEGV,SIGFPE
回调方法内创建一个Runloop,将主线程的所有Runmode都拿过来跑,作为应用程序主Runloop的替代
// 我的处理
LHLExceptionHelper *exceptionHandler = [LHLExceptionHelper new];
[exceptionHandler performSelectorOnMainThread:@selector(makeException:)
withObject:exceptionTemp waitUntilDone:
- (void)makeException:(NSException *)exception {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);
while (captor.needKeepAlive) {
for (NSString *mode in (__bridge NSArray *)allModesRef) {
if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
continue;
}
CFStringRef modeRef = (__bridge CFStringRef)mode;
CFRunLoopRunInMode(modeRef, keepAliveReloadRenderingInterval, false);
}
}
常驻线程
可以把自己创建的线程添加到Runloop中,做一些频繁处理的任务,例如:检测网络状态,定时上传一些信息等。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
NSLog(@"----------run----%@", [NSThread currentThread]);
@autoreleasepool{
/*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
[[NSRunLoop currentRunLoop] run];
}
// 方法2
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 方法3
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
NSLog(@"---------");
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
参考:
实时卡顿监控
玩转Runloop - 代码示例使用Source, Observer, Timer
一文看完Runloop
深入理解Runloop
RunLoop实战:实时卡顿监控
简单监测iOS卡顿的demo
关于dispatch_semaphore的使用