说到NSRunLoop我们要说点什么
目前正是找工作、换工作的黄金时间,对于iOS开发从业者来说 面试绕不过去的就是runLoop,那么我们说到runLoop我们应该说些什么呢
答题从以下几方面入手
1、runLoop是什么
2、runLoop的作用是什么
3、runLoop和线程的关系
4、runLoop之Modes
5、modes之items(source0,source1,timer、observer)
6、runLoop的应用
1、runLoop是什么
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* 获取modeList的锁 */
__CFPort _wakeUpPort; // 唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData;
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
首先runloop是一个运行循环,它实际上也是一个对象、这个对象提供了一个入口函数,使程序进入一个do..while的循环中,循环处理各种任务
2、runLoop的作用是什么
1)由于是运行循环,他保持程序持续的运行,即便没有待处理的任务 也不退出程序(可利用这点防止程序崩溃)
2)处理App中的各种事件 包括触摸、滑动、performSelector等
3)节省cpu资源,提高程序的性能,使cpu该做事做事,该休息休息
3、runLoop和线程的关系
1)线程和runLoop是一一对应的关系,
2)主线程runloop程序启动后自动创建,子线程默认不创建runloop,直到在子线程中第一次获取runloop时,才创建runloop,同时runloop存放在一个可变字典中 字典的key-value 分别为pthread-runloop
4、runLoop之Modes
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // timer开启后为true
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
}
每个runloop包含多个mode,但是 同时一个runloop只能在一中mode下运行,runloop在运行时,是在不断切换mode的,mode的分类
kCFRunLoopDefaultMode:APP的默认mode 通常主线程是在这个model下运行
UITrackingRunLoopMode: 界面跟踪mode,用于scrollview追踪触摸滑动,保证页面滑动时,不受其他mode的影响
UIInitializationRunLoopMode:当app刚启动时,进入的第一个mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
GSEventReceiveRunLoopMode:接收系统事件的内部mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode,用并不是一种真正的model,也可以认为是一种混合模式
5、modes之items(source0,source1,timer、observer)
一个mode持有的source、timer、observer,都是集合类型所以说每个model,可以持有多个source、timer、observer
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* 不可变 */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* 不可变 */
CFTimeInterval _tolerance; /* 可变 */
uint64_t _fireTSR;
CFIndex _order;
CFRunLoopTimerCallBack _callout;
CFRunLoopTimerContext _context;
};
运行的时候 会将相应的item添加到对应的mode中,即调用CFSetAddValue方法,之后再在运行循环时会调用CFRunLoopDoBlocks方法,判断当前model后,执行相应的block回调
CFRunLoopSourceRef: 包括source0 和 source1
source0,处理app内部事件,app自己负责管理,uiEvent,CFSocket,仅包含一个函数指针
底层调用 创建source0源,将source0加入当runloop相应的mode中、执行signal信号,标记待执行,执行CFRunLoopWakeUp唤醒runloop,处理相应的事件,取消移除源CFRunLoopRemoveSource
source1,包含一个mach_port和一个回调指针,一般用于通过内核和其他线程进行通讯
CFRunLoopTimerRef:timer的底层是一个CFRunLoopTimerRef,这个timer是受runloop的mode模式影响的,
创建CFRunLoopTimerRef 添加到相应的当前runloop,若是子线程的runloop 需要调用CFRunLoopRun自行开启,否则 timer是不会调用的
CFRunLoopObserverRef:观察者,观察runloop所处的状态
初始化CFRunLoopObserverContext,
创建 CFRunLoopObserverRef,配置回调方法
添加观察者当当前的runloop
6、runLoop的应用
1)线程保活
[[NSRunLoop currentRunLoop] addPort:[NSort port] forMode:NSDefaultRunLoopMode];
while (self.keepAlive) {
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
退出线程时
self.keepAlive = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
2)主线程卡顿检测
在主线程穿件观察者,在子线程中观察observer的状态,
主线程的操作是 在 kCFRunLoopBeforeSource 和 kCFRunLoopBeforeWaiting分别记录时间,判断时间差 超过某一个阈值即认为发生了卡顿,此时可以获取堆栈信息,并记录上传,以供分析
@interface Minitor ()
@property(nonatomic, strong) NSThread *monitorThread;
@property(nonatomic, strong) NSDate *startDate;
/// 是否正在执行任务
@property(nonatomic, assign, getter=isExcuting) BOOL excuting;
@end
@implementation Minitor {
CFRunLoopObserverRef _observer;
CFRunLoopTimerRef _timer;
}
+ (instancetype)shareInstatance {
static Minitor *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[Minitor alloc] init];
instance.monitorThread = [[NSThread alloc] initWithTarget:self selector:@selector(moniterThreadEntryPoint) object:nil];
[instance.monitorThread start];
});
return instance;
}
+ (void)moniterThreadEntryPoint {
@autoreleasepool {
[[NSThread currentThread] setName:@"Monitor"];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
}
}
- (void)statrMonitor {
if (_observer) {
NSLog(@"已经创建了监听");
return;
}
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
[self performSelector:@selector(addTimerToMonitorThread) onThread:self.monitorThread withObject:nil waitUntilDone:NO modes:@[NSRunLoopCommonModes]];
}
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
Minitor *monitor = (__bridge Minitor*)info;
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
monitor.startDate = [NSDate date];
monitor.excuting = YES;
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
monitor.excuting = NO;
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
#pragma mark 定时器
- (void)addTimerToMonitorThread {
if (_timer) {
return;
}
CFRunLoopRef curentRunLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
_timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.01, 0.01, 0, 0, &runloopTimerCallBack, &context);
CFRunLoopAddTimer(curentRunLoop, _timer, kCFRunLoopCommonModes);
}
static void runloopTimerCallBack(CFRunLoopTimerRef timer, void *info) {
Minitor *monitor = (__bridge Minitor*)info;
if (!monitor.isExcuting) {
// kCFRunLoopBeforeWaiting
return;
}
NSTimeInterval excuteTime = [[NSDate date] timeIntervalSinceDate:monitor.startDate];
NSLog(@"定时器:当前线程%@,主线程执行时间:%f秒", [NSThread currentThread], excuteTime);
if (excuteTime >= 0.00001) {
NSLog(@"卡顿了 %f 秒", excuteTime);
[monitor handleStackInfo];
}
}
- (void)handleStackInfo {
NSArray *callStackSymbls = [NSThread callStackSymbols];
[callStackSymbls enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%@", obj);
}];
}
@end
3)给程序赋予一次回光返照的机会
参开链接传送门
4)大型列表 加载图片时,即图片下载或是显示 根据runloop当前为 空闲状态,才开始下载或是显示图片,以免显示图片造成页面卡顿