Runloop概述
runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?
- runloop: 从字面意思看:运行循环、跑圈,其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)事件。
- runloop和线程的关系:一个线程对应一个RunLoop,主线程的RunLoop默认创建并启动,子线程的RunLoop需手动创建且手动启动(调用run方法)。RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop。
RunLoop的作用?
- 1.保持程序运行
- 2.处理app的各种事件(比如触摸,定时器等等)
- 3.节省CPU资源,提高性能。
RunLoop内部是怎么实现的?
-
- RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。
-
- 每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
-
- 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
runloop的mode是用来做什么的?有几种mode?
- model:是runloop里面的运行模式,不同的模式下的runloop处理的事件和消息有一定的差别。系统默认注册了5个Mode:
(1)kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
(2)UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
(3)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
(4)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
(5)kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。注意iOS 对以上5中model进行了封装 NSDefaultRunLoopMode、NSRunLoopCommonModes
Runloop启动过程
1.通知观察者 run loop 已经启动
2.通知观察者将要开始处理Timer事件
3.通知观察者将要处理非基于端口的Source0
4.启动准备好的Souecr0
5.如果基于端口的源Source1准备好并处于等待状态,立即启动:并进入步骤9
6.通知观察者线程进入休眠
7.将线程置于休眠直到任一下面的事件发生
(1)某一事件到达基于端口的源
(2)定时器启动
(3)Run loop 设置的时间已经超时
(4)run loop 被显式唤醒
8.通知观察者线程将被唤醒
9.处理未处理的事件,跳回2
(1)如果用户定义的定时器启动,处理定时器事件并重启 run loop。进入步骤 2
(2)如果输入源启动,传递相应的消息
(3)如果 run loop 被显式唤醒而且时间还没超时,重启 run loop。进入步骤 2
10.通知观察者run loop 结束
为什么把NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环以后,滑动scrollview的时候NSTimer却不动了?
- NSTimer对象是在 NSDefaultRunLoopMode下面调用消息的,但是当我们滑动scrollview的时候,NSDefaultRunLoopMode模式就自动切换到UITrackingRunLoopMode模式下面,却不可以继续响应nstime发送的消息。所以如果想在滑动scrollview的情况下面还调用nstime的消息,我们可以把nsrunloop的模式更改为NSRunLoopCommonModes.
与FPS的关系
使用实例
在开发中如何使用RunLoop?什么应用场景?
常驻线程:给子线程开启一个RunLoop,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出
-
AutoreleasePool: iOS应用启动后会注册两个 Observer 管理和维护 AutoreleasePool
第一个 Observer 会监听 RunLoop 的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。
第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。
3.UITableView 与 NSTimer 冲突
4.AFNetworking内部创建了一个空的线程并启动了RunLoop,当需要使用这个后台线程执行任务时AFNetworking通过performSelector: onThread: 将这个任务放到后台线程的RunLoop中。
5.防止UITableView滚动卡顿([[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])
6.sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。
7.老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。
[beforeSource, beforeWaiting], [AfterWaitng, ...]
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;
}
});
}
AutoReleasePool(深入解析 Autoreleasepool)
苹果是如何实现Autorelease Pool的?
- Autorelease Pool作用:缓存池,可以避免我们经常写relase的一种方式。其实就是延迟release,将创建的对象,添加到最近的autoreleasePool中,等到autoreleasePool作用域结束的时候,会将里面所有的对象的引用计数器 - autorelease.
每一个自动释放池都是由一系列的 AutoreleasePoolPage 组成的,以双向链表的形式连接起来的,并且每一个 AutoreleasePoolPage 的大小都是 4096 字节
class AutoreleasePoolPage {
magic_t const magic; //对当前 AutoreleasePoolPage 完整性的校验
id *next;
pthread_t const thread; // 保存了当前页所在的线程
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
POOL_SENTINEL(哨兵对象)
- POOL_SENTINEL 只是 nil 的别名。
- 在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_SENTINEL push 到自动释放池的栈顶,并且返回这个 POOL_SENTINEL 哨兵对象
- 当方法 objc_autoreleasePoolPop 调用时,就会向自动释放池中的对象发送 release 消息,直到第一个 POOL_SENTINEL
objc_autoreleasePoolPush
- 有 hotPage 并且当前 page 不满 , 调用 page->add(obj) 方法将对象添加
- 有 hotPage 并且当前 page 已满 , 调用 autoreleaseFullPage 初始化一个新的页 , 调用 page->add(obj) 方法将对象添加
- 无 hotPage , 调用 autoreleaseNoPage 创建一个 hotPage , 调用 page->add(obj) 方法将对象添加
- page->add 添加对象 , 对next指针的一个压入操作
objc_autoreleasePoolPop
- objc_autoreleasePoolPop 传入哨兵对象
总结
自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息
使用场景
1.写基于命令行的的程序时,就是没有UI框架,如AppKit等Cocoa框架时。
2.写循环,循环里面包含了大量临时创建的对象。
3.创建了新的线程。(非Cocoa程序创建线程时才需要)
4.长时间在后台运行的任务。
第三方库使用举例
SDWebImage(SDWebImageDecoder.m文件中,3.7.0版本)
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (image == nil) { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
return nil;
}
...
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
if (anyAlpha) {
return image;
}
...
return imageWithoutAlpha;
}
}
此处操作是生成解压缩图片,该方法会在SDWebImageDownloaderOperation connection:(NSURLConnection *)connection didReceiveData:(NSData *)data方法中不停回调,该方法调用会发生在子线程,同时方法内绘制bitmap会生成大量临时对象,符合第二、第三两种情况
CocoaLumberjack gcd操作大量使用
- (void)addLogger:(id )logger withLevel:(DDLogLevel)level {
dispatch_async(_loggingQueue, ^{ @autoreleasepool {
[self lt_addLogger:logger level:level];
} });
}
- (void)removeLogger:(id )logger {
dispatch_async(_loggingQueue, ^{ @autoreleasepool {
[self lt_removeLogger:logger];
} });
}
dispatch_async使用自定义并发队列,系统并不会内部线程中添加autoreleasepool,所以手动添加autoreleasepool更好,此处如果使用globalQueue是不需要添加的,符合第三种使用条件
自动释放池什么时候释放?
- 当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池
Runloop其他博客
掘金总结 https://juejin.im/entry/599c13bc6fb9a0248926a77d
iOS-RunLoop充满灵性的死循环 https://www.jianshu.com/p/b9426458fcf6
RunLoop入门 看我就够了 https://www.jianshu.com/p/2d3c8e084205
RunLoop总结:RunLoop的应用场景(三) https://blog.csdn.net/u011619283/article/details/53483965
iOS刨根问底-深入理解RunLoop https://www.cnblogs.com/kenshincui/p/6823841.html
iOS实时卡顿监控 http://www.cocoachina.com/ios/20161101/17903.html
iOS Runloop相关面试题