概要
RunLoop在iOS开发中的应用范围并没有像runtime 那样广泛,我们通过CFRuntime的源代码可知runloop跟线程的是密不可分的,一个线程一定会创建一个对应的runloop,只是主线程创建就自动run了,而子线程只会创建不会自动run。苹果线程管理 Thread Management也说了在线程中利用runloop,
此外,runloop并不是一个简单的do-while,作为OSX/iOS系统中Event Loop表现,runloop需要处理消息事件,在没有消息的时候休眠,有消息事件的时候立刻唤醒。
综上所述,从我个人所接触到知识面runloop一是处理子线程运行,二是根据runloop的不同的activities来处理问题。当然希望通过我这块砖头,引出同学们runloop应用的好玉来。
1.CFRunLoopSourceRef 事件源
在下面代码中,通过自定义子线程thread,运行结果可知hello China是不会被打印的,子线程在打印完hello world 就exit了。
{
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadFun) object:nil];
[thread start];
self.thread = thread;
[self performSelector:@selector(selectorFun) onThread:thread withObject:nil waitUntilDone:NO];
NSLog(@"hello Thread");
}
- (void)threadFun {
NSLog(@"hello world");
// _pthread_exit
}
- (void)selectorFun {
NSLog(@"hello China");
}
获取上面代码的堆栈可以看到子线程瞬间生命就结束了。类似在threadFun函数块结束的前面添加了_pthread_exit
frame #0: 0x000000010232f2b0 CoreFoundation`__CFFinalizeRunLoop
frame #1: 0x000000010232f264 CoreFoundation`__CFTSDFinalize + 100
frame #2: 0x0000000104e9f39f libsystem_pthread.dylib`_pthread_tsd_cleanup + 544
frame #3: 0x0000000104e9f0d9 libsystem_pthread.dylib`_pthread_exit + 152
frame #4: 0x0000000104e9fc38 libsystem_pthread.dylib`pthread_exit + 30
frame #5: 0x0000000101a36f1e Foundation`+[NSThread exit] + 11
frame #6: 0x0000000101ab713f Foundation`__NSThread__start__ + 1218
frame #7: 0x0000000104e9d93b libsystem_pthread.dylib`_pthread_body + 180
frame #8: 0x0000000104e9d887 libsystem_pthread.dylib`_pthread_start + 286
frame #9: 0x0000000104e9d08d libsystem_pthread.dylib`thread_start + 13
根据苹果线程管理的说法可以利用把线程放入runloop中,我们知道子线程的runloop并没有自动开启,需要我们手动开启,苹果也提供代码示例:
- (void)threadMainRoutine
{
BOOL moreWorkToDo = YES;
BOOL exitNow = NO;
NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
// Add the exitNow BOOL to the thread dictionary.
NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
[threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
// Install an input source.
[self myInstallCustomInputSource];
while (moreWorkToDo && !exitNow)
{
// Do one chunk of a larger body of work here.
// Change the value of the moreWorkToDo Boolean when done.
// Run the run loop but timeout immediately if the input source isn't waiting to fire.
[runLoop runUntilDate:[NSDate date]];
// Check to see if an input source handler changed the exitNow value.
exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
}
}
因此我们可以把我们上面的代码修改为,程序可以打印出来hello China
- (void)threadFun {
NSLog(@"hello world");
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runUntilDate:[NSDate distantFuture]];
// _pthread_exit
}
可以我们把代码修改成在界面添加一个按钮点击事件,点击事件由我们的子线程出来,同时我们删除我们的线程的selectorFun函数逻辑,发现我们触发按钮的点击事件并不会打印doSomething。
{
UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(0, 80, 50, 50)];
btn.backgroundColor = [UIColor redColor];
[self.view addSubview:btn];
[btn addTarget:self action:@selector(clicked) forControlEvents:UIControlEventTouchUpInside];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadFun) object:nil];
[thread start];
self.thread = thread;
}
- (void)threadFun {
NSLog(@"hello world");
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop runUntilDate:[NSDate distantFuture]];
// _pthread_exit
}
- (void)clicked{
[self performSelector:@selector(doSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)doSomething{
NSLog(@"doSomething");
}
因为当前runloop运行的model没有modeItem,run运行的前提条件必须保证当前model是有item( Source/Timer,二者之一,实际是不需要Observer)将代码修改为下面 :
- (void)threadFun {
NSLog(@"hello world");
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop runUntilDate:[NSDate distantFuture]];
}
RunLoop只处理两种源:输入源、时间源。而输入源又可以分为:NSPort/自定义源/performSelector,我们常用搭到的performSelector方法有:
// 主线程
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
/// 指定线程
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
/// 针对当前线程
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
/// 取消,在当前线程,和上面两个方法对应
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
而下面这些不是事件源的,相当于是[self xxx]调用
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
run运行函数主要以下3个
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
第一个run循环一旦开启,就关闭不了,并且之后的代码就无法执行。api文档中提到:如果没有输入源和定时源加入到runloop中,runloop就马上退出,否则通过频繁调用-runMode:beforeDate:方法来让runloop运行在NSDefaultRunLoopMode模式下。
第二个run运行在NSDefaultRunLoopMode模式,有超时时间限制。它实际上也是不断调用-runMode:beforeDate:方法来让runloop运行在NSDefaultRunLoopMode模式下,直到到达超时时间。调用CFRunLoopStop(runloopRef)无法停止Run Loop的运行,这个方法只会结束当前-runMode:beforeDate:的调用,之后的runMode:beforeDate:该调用的还是会继续。直到timeout。对应
CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)
第三个run比第二种方法是可以指定运行模式,只执行一次,执行完就退出。可以用CFRunLoopStop(runloopRef)退出runloop。api文档里面提到:在第一个input source(非timer)被处理或到达limitDate之后runloop退出,对应
CFRunLoopRunInMode(mode,limiteDate,true)
1.1 子线程常驻
给当前子线程的runbloop的mode 添加事件源来实现线程常驻。所有的关于这个的都会拿AF2.X的代码说明这个常驻的案例,如果同学开发iOS稍微有点年长的话或者古董代码的都会用到网络第三方库ASIHTTPRequest,也用到利用CFRunLoopAddSource 让当前网络线程常驻。
+ (NSThread *)threadForRequest:(ASIHTTPRequest *)request
{
if (networkThread == nil) {
@synchronized(self) {
if (networkThread == nil) {
networkThread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequests) object:nil];
[networkThread start];
}
}
}
return networkThread;
}
+ (void)runRequests
{
// Should keep the runloop from exiting
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
BOOL runAlways = YES; // Introduced to cheat Static Analyzer
while (runAlways) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
CFRunLoopRun();
[pool release];
}
// Should never be called, but anyway
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
1.2 程序crash 弹框提示
这个是算我真正接触到runloop的,当用户正在操作我们的APP的时候数据发生异常,程序会瞬间闪退,实际上从产品角度老说是一种非常不好的体验,而对码农来说也根本无法知道当前程序crash的堆栈信息,通过利用runloop的线程常驻方式,当程序发生异常的时候,通过异常捕获然后弹出提示框 而不是立马闪退,同时也可以让用户上传crash日志,早期我还是看到APP在使用这样的技术,现在crash收集机制越来越完善,目前来说几乎有这么使用的了。
- (void)alertView:(UIAlertView *)anAlertView clickedButtonAtIndex:(NSInteger)anIndex
{
if (anIndex == 0)
{
dismissed = YES;
}else if (anIndex==1) {
NSLog(@"ssssssss");
}
}
- (void)handleException:(NSException *)exception
{
[self validateAndSaveCriticalApplicationData];
UIAlertView *alert =
[[[UIAlertView alloc]
initWithTitle:NSLocalizedString(@"抱歉,程序出现了异常", nil)
message:[NSString stringWithFormat:NSLocalizedString(
@"如果点击继续,程序有可能会出现其他的问题,建议您还是点击退出按钮并重新打开\n\n"
@"异常原因如下:\n%@\n%@", nil),
[exception reason],
[[exception userInfo] objectForKey:UncaughtExceptionHandlerAddressesKey]]
delegate:self
cancelButtonTitle:NSLocalizedString(@"退出", nil)
otherButtonTitles:NSLocalizedString(@"继续", nil), nil]
autorelease];
[alert show];
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!dismissed)
{
for (NSString *mode in (NSArray *)allModes)
{
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
NSSetUncaughtExceptionHandler(NULL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
{
kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
}
else
{
[exception raise];
}
}
2 CFRunLoopObserverRef
iOS系统会监听主线程中runloop的的进入/休眠、退出的activities 来处理autoreleasepool,也是同学们长讨论的自动释放池在什么时候释放的问题。
<CFRunLoopObserver 0x7fb064418b50 [0x10e005a40]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e18c4c2), context = <CFArray 0x7fb0644189e0 [0x10e005a40]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x7fb064418bf0 [0x10e005a40]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e18c4c2), context = <CFArray 0x7fb0644189e0 [0x10e005a40]>{type = mutable-small, count = 0, values = ()}}
2.1 CFRunLoopObserverRef函数
通过CFRunLoopObserverRef 我们可以监测当前runloop的运行状态引用YYKit的写法:其中优先级设置为最小的32位-0x7fffffff 和最大的32位0x7fffffff
static void YYRunloopAutoreleasePoolSetup() {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopObserverRef pushObserver;
pushObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopEntry,
true, // repeat
-0x7FFFFFFF, // before other observers
YYRunLoopAutoreleasePoolObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, pushObserver, kCFRunLoopCommonModes);
CFRelease(pushObserver);
CFRunLoopObserverRef popObserver;
popObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit,
true, // repeat
0x7FFFFFFF, // after other observers
YYRunLoopAutoreleasePoolObserverCallBack, NULL);
CFRunLoopAddObserver(runloop, popObserver, kCFRunLoopCommonModes);
CFRelease(popObserver);
}
另外一种是block方式
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
2.2 利用空闲时间缓存数据
UITableView+FDTemplateLayoutCell的作者sunnyxx曾在优化UITableViewCell高度计算的那些事提到利用runloop来缓存cell的高度。
作者所说的代码如下:
但是这段代码在1.4版本之后就被去掉了,sunnyxx解释是:
2.3 检测UI卡顿
第一种方法通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值。ANREye就是在子线程设置flag 标记为YES, 然后在主线程中将flag设置为NO。利用子线程时阙值时长,判断标志位是否成功设置成NO。
private class AppPingThread: Thread {
func start(threshold:Double, handler: @escaping AppPingThreadCallBack) {
self.handler = handler
self.threshold = threshold
self.start()
}
override func main() {
while self.isCancelled == false {
self.isMainThreadBlock = true
DispatchQueue.main.async {
self.isMainThreadBlock = false
self.semaphore.signal()
}
Thread.sleep(forTimeInterval: self.threshold)
if self.isMainThreadBlock {
self.handler?()
}
self.semaphore.wait(timeout: DispatchTime.distantFuture)
}
}
private let semaphore = DispatchSemaphore(value: 0)
private var isMainThreadBlock = false
private var threshold: Double = 0.4
fileprivate var handler: (() -> Void)?
}
NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿,下面的代码片段来源iOS实时卡顿监控
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MyClass *object = (__bridge MyClass*)info;
// 记录状态值
object->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 创建信号
semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
NSLog(@"好像有点儿卡哦");
}
}
timeoutCount = 0;
}
});
第二种方式就是FPS监控,App 刷新率应该当努力保持在 60fps,通过CADisplayLink记录两次刷新时间间隔,就可以计算出当前的 FPS。
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
- (void)displayLinkTick:(CADisplayLink *)link {
if (lastTime == 0) {
lastTime = link.timestamp;
return;
}
count++;
NSTimeInterval interval = link.timestamp - lastTime;
if (interval < 1) return;
lastTime = link.timestamp;
float fps = count / interval;
count = 0;
NSString *text = [NSString stringWithFormat:@"%d FPS",(int)round(fps)];
3 CFRunLoopModeRef
每次启动RunLoop时,只能指定其中一个 Mode,这个就是CurrentMode。要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。系统默认注册了5个mode,以下两个是比较常用的:
1.kCFRunLoopDefaultMode (NSDefaultRunLoopMode),默认模式
2.UITrackingRunLoopMode, scrollview滑动时就是处于这个模式下。保证界面滑动时不受其他mode影响。
CFRunLoop对外暴露的管理 Mode 接口只有下面2个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
3.1 解决NSTime和scrollView纠葛
如果利用scrollView类型的做自动广告滚动条 需要把定时器加入当前runloop的模式NSRunLoopCommonModes
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
if (self.autoScroll) {
[self invalidateTimer];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (self.autoScroll) {
[self setupTimer];
}
}
(void)setupTimer
{
[self invalidateTimer];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:self.autoScrollTimeInterval target:self selector:@selector(automaticScroll) userInfo:nil repeats:YES];
_timer = timer;
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)invalidateTimer
{
[_timer invalidate];
_timer = nil;
}
3.2 RunLoopCommonModes
一个mode可以标记为common属性(用CFRunLoopAddCommonMode函数),然后它就会保存在_commonModes。主线程有kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 都已经是CommonModes了,而子线程只有kCFRunLoopDefaultMode。
_commonModeItems里面存放的source, observer, timer等,在每次runLoop运行的时候都会被同步到具有Common标记的Modes里。比如这样:[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];就是把timer放到commonItem里。
kCFRunLoopCommonModes是一个占位用的mode,它不是真正意义上的mode。如果要在线程中开启runloop,这样写是不对的:
[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];
上面的runMode beforeDate回调用CFrunloop的CFRunLoopRunSpecific函数,函数中回根据当前的name去查找当前的运营的mode,可是根本就不会存在CommonMode的。
3.3 TableView中实现平滑滚动延迟加载图片
顺带提一下,这个我在开发中没有用到。是利用CFRunLoopMode的特性,可以将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动UITrackingRunLoopMode这个mode时不会被加载而影响到。这个主要受到Github的RunLoopWorkDistribution影响,
其主要代码片段如下:
- (instancetype)init
{
if ((self = [super init])) {
_maximumQueueLength = 30;
_tasks = [NSMutableArray array];
_tasksKeys = [NSMutableArray array];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(_timerFiredMethod:) userInfo:nil repeats:YES];
}
return self;
}
static void _registerObserver(CFOptionFlags activities, CFRunLoopObserverRef observer, CFIndex order, CFStringRef mode, void *info, CFRunLoopObserverCallBack callback) {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopObserverContext context = {
0,
info,
&CFRetain,
&CFRelease,
NULL
};
observer = CFRunLoopObserverCreate( NULL,
activities,
YES,
order,
callback,
&context);
CFRunLoopAddObserver(runLoop, observer, mode);
CFRelease(observer);
}
static void _runLoopWorkDistributionCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
DWURunLoopWorkDistribution *runLoopWorkDistribution = (__bridge DWURunLoopWorkDistribution *)info;
if (runLoopWorkDistribution.tasks.count == 0) {
return;
}
BOOL result = NO;
while (result == NO && runLoopWorkDistribution.tasks.count) {
DWURunLoopWorkDistributionUnit unit = runLoopWorkDistribution.tasks.firstObject;
result = unit();
[runLoopWorkDistribution.tasks removeObjectAtIndex:0];
[runLoopWorkDistribution.tasksKeys removeObjectAtIndex:0];
}
}