iOS
开发过程中,RunLoop
对于我们平常开发一般很少用到,一般在定时器使用时候我们可能使用一下定时器。但是却不能否认其重要性,App
在运行过程中一直等待接收用户的事件,在没有事件触发的时候,App
没有动作,但是有事件时,他就能响应,这就是RunLoop
做的事。
我是从问题出发对RunLoop进行理解。
什么是RunLoop
- (翻译)事件循环
- 一般来说,一个线程执行任务是有起点和终点,执行完任务结束线程的生命周期。而RunLoop的则能让线程处理完任务不退出,还能随时处理任务。RunLoop的本质就是do while循环。能让线程处于 "接受消息->等待->处理" 的循环中,直到这个循环结束。
RunLoop的作用
- RunLoop可以让线程在没有消息响应的时候休眠以避免资源的浪费,在有消息时被唤醒做任务。
RunLoop和线程
- 线程的创建
1 主线程中RunLoop
默认开启,可以使用[NSRunLoop mainRunLoop]
来获取RunLoop
2 非主线程,可以调用[NSRunLoop currentRunLoop]
来获取当前线程的RunLoop
-
RunLoop
和线程是一一对应的,RunLoop
保证了线程做完一个不会立即退出,等待下一个消息让线程进入休眠,接收消息时唤醒线程处理任务。是基于线程进行操作的一个对象。
RunLoop 对外的接口
-
CoreFoundation
中RunLoop
的5个类
1CFRunLoopRef
2CFRunLoopModeRef
3CFRunLoopSourceRef
4CFRunLoopTimerRef
5CFRunLoopObserverRef
- 关系
解释: 一个RunLoop
中有多个mode,一个mode中有多个Source/Timer/Observer
。但是一个RunLoop
只能指定一个mode
,要切换mode
,只能退出当前RunLoop
,重新设置mode
。
RunLoop中的 Mode
- 三种开发中常用的mode
1 kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默认mode,通常主线程是在这个 Mode 下运行的
2 UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
ScrollView滚动的时候的模式,保证滑动的时候不受其他mode影响
3 kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
是一个mode组合,平常使用过程相当于NSDefaultRunLoopMode
和NSEventTrackingRunLoopMode
组合。
RunLoop中的 Source
-
RunLoop
中的Source是输入源事件,包括Source0
和Source1
-
Source0
:event
事件,只含有回调,需要先调用CFRunLoopSourceSignal(source)
,将这个Source
标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop
。 -
Source1
: 包含了一个mach_port
和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒RunLoop
的线程。
我们可以在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
方法中添加断点,可以看出
在button的点击事件中添加断点
// 按钮响应
- (void)click:(UIButton *)button {
NSLog(@"点击");
}
可以看出
包括KVO的回调
我们平常的大部分事件,都是通过
Source0
进行回调处理的。
RunLoop中的 Timer
Timer
即为定时源事件,包含一个时间长度和回调。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。NSTimer定时器的触发就是基于RunLoop。但是定时器并不是一定准确,NSTimer提供了一个tolerance属性用于设置宽容度,使定时器更加准确。
-
同样创建NSTimer,在timer的触发打断点
可以看出NSTimer是定时源事件。
RunLoop中的 Observer
这是观察者,但是和我们平常使用的观察者Observer
是两个概念。这里的Observer
包含了一个回调,观察的是RunLoop
中状态的改变,当状态改变,Observer就能收到通知。可观察的状态是个枚举值为
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 即将进入runloop
kCFRunLoopBeforeTimers = (1UL << 1), 即将处理timer事件
kCFRunLoopBeforeSources = (1UL << 2),即将处理source事件
kCFRunLoopBeforeWaiting = (1UL << 5),即将进入睡眠
kCFRunLoopAfterWaiting = (1UL << 6), 被唤醒
kCFRunLoopExit = (1UL << 7), runloop退出
kCFRunLoopAllActivities = 0x0FFFFFFFU ,所有状态
};
如果NSTimer在分线程中创建,会发生什么,应该注意什么?如何设计一个准确的timer?
我们要先明白NSTimer
创建的两个类方法的影响
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- 我们使用方法一创建
NSTimer
,发现不加入RunLoop
也能打印正常
_timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
- 我们使用方法二创建
NSTimer
,发现不加入runloop
不能循环打印
_timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
通过xcode的方法注释
我们可以看出,通过
scheduledTimerWithTimeInterval :
方法创建的NSTimer
会自动以defaultMode模式加入到当前的RunLoop
中。而timerWithTimeInterval :
方法需要手动调用[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]
方式加入RunLoop
。
同时我们注意到,打印出来的值不是以绝对0.1的速度调用,还是有偏差的。
- 子线程中创建NSTimer
// 将定时器的创建放在子线程中
[self performSelectorInBackground:@selector(addTimer) withObject:nil];
- (void)addTimer {
// 创建定时器,将定时器加入当前线程的RunLoop中
// _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
_timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
结果控制台啥也没打印。
原因 : 子线程的RunLoop
不是默认开启的,主线程的RunLoop
是默认开启的,需要程序手动调用run
方法
[[NSRunLoop currentRunLoop] run];
- 我们可以得出,
NSTimer
在分线程中创建,如果不主动开启RunLoop
,定时器不会调用。如果想要准确的定时器。可以使用GCD
定时器。GCD
定时器不受RunLoop
约束,比NSTimer
更加准时
当scrollView滑动时,同页面上的定时器为什么会暂停?
scheduledTimerWithTimeInterval
方式创建的NSTimer
,默认是以NSDefaultRunLoopMode加入到当前线程中,但是当页面滚动时,当前线程RunLoop
的mode会自动切换成UITrackingRunLoopMode。以NSDefaultRunLoopMode
注册的定时器是不会执行的。添加
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]
,将NSTimer
加到NSRunLoopCommonModes
中。这个模式等NSDefaultRunLoopMode
和UITrackingRunLoopMode
的结合
怎么在tableview滑动时延迟加载图片来提高流畅度?
在滑动过程中,
RunLoop
会自动切换到UITrackingRunLoopMode中,我们可以将图片的加载运行在NSDefaultRunLoopMode
模式中,这样在滑动过程中就不会进行图片的加载,提高流畅性。我们可以利用
performSelector
方法
[contentImage performSelector:@selector(sd_setImageWithURL:) withObject:[NSURL URLWithString:[NSString stringWithFormat:@"%@!360", dic[@"min"]]] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
但是这个方法在tableView这样可以复用的视图上,会出现每次滑动屏幕外的都会先加载默认图,当停止滑动时开始图片加载。当取消复用或者在UIScrollView这样的还可以。
常说的AFNetworking常驻线程保活是什么原理?
- 常驻线程,保持线程RunLoop一直在跑,一直处理事件。
为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出
//创建一个线程,
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloop) object:nil] ;
[self.thread start];
});
- (void)createRunloop{
@autoreleasepool {
/*
添加一个Source1事件的监听端口
RunLoop对象会一直监听这个端口,由于这个端口不会有任何事件到来所以不会产生影响
*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
//开启runloop
[[NSRunLoop currentRunLoop] run];
}
}
RunLoop模式的原理和使用注意点?
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source、Observer、Timer
- 每次RunLoop启动,只能指定一个Mode,这个Mode被称为CurrentMode
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入, 以使不同组之间的Source、Observer、Timer互不受影响
如果程序启动就需要执行一个耗时操作,你会怎么做?
- 新建一个子线程,开启子线程的runloop去操作耗时操作。
- 使用GCD异步执行耗时操作。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
}
RunLoop与autoreleasepool的关系
- 要知道
RunLoop
和autoreleasepool
的关系,首先要知道autoreleasepool
的内部原理。推荐几篇文章
- Objective-C Autorelease Pool 的实现原理
- 自动释放池的前世今生 ---- 深入解析 Autoreleasepool
-
黑幕背后的Autorelease
我也是参考这几个大神的文章总结
- 主要了解的知识点。
1AutoreleasePoolPage
: 看下源码中的定义。
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
其中
`magic` : 用于对当前 AutoreleasePoolPage 完整性的校验
`next` : 指向最新加入栈顶的下一个对象的位置。初始化时指向 begin()
`thread` : 指向当前线程
`parent` : 指向父结点,第一个结点的 parent 值为 nil ;
`child` : 指向子结点,最后一个结点的 child 值为 nil ;
概念了解:
-
AutoreleasePool
并没有单独的结构,而是由若干个AutoreleasePoolPage
以双向链表的形式组合而成。parent
指针为AutoreleasePoolPage
的前驱节点,child
为后继节点。 - 一个
AutoreleasePool
对应一个线程。 -
AutoreleasePoolPage
会开辟4096字节内存,当一个AutoreleasePoolPage
空间满了后,会新建一个AutoreleasePoolPage
,通过child
指针指向新的AutoreleasePoolPage
连接,之后需要autorelease
的对象就会在最新的page中添加。 - 向一个对象发送
autorelease
,就是将对象加入到栈顶的next指针指向的位置。
-
POOL_SENTINEL
(哨兵对象)
-
POOL_SENTINEL
只是 nil 的别名,代表的意义是一个AutoreleasePool
的边界。这个在objc_autoreleasePoolPush
和objc_autoreleasePoolPop
方法中有大作用。
objc_autoreleasePoolPush
- 当调用
objc_autoreleasePoolPush
,会创建一个新的AutoreleasePool
。即向当前的AutoreleasePoolPage
插入一个哨兵对象(POOL_SENTINEL
),可以理解为一个runloop开始的边界。并且返回插入的POOL_SENTINEL
的内存地址。
objc_autoreleasePoolPop
- 当调用
objc_autoreleasePoolPop
,就会向自动释放池中的对象发送 release 消息,直到POOL_SENTINEL
所在的page。
-
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 了。
注意:每个线程中都可以有多个AutoreleasePool。
RunLoop与PerformSelecter
我们可以看系统RunLoop.h
中有关于performSelector的方法。。
@interface NSObject (NSDelayedPerforming)
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
@end
我们可以通过文档来初步了解,performSelector延时调用的方法。
-
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay
。
- 方法内部设置了一个timer,运行在当前
RunLoop
中,运行模式为NSDefaultRunLoopMode
。 - 如果当前
RunLoop
为子线程,RunLoop
默认不开启,则添加performSelector
会无法执行。 - 如果运行循环在
NSDefaultRunLoopMode
下运行,则成功;否则,计时器将等待,直到运行循环处于默认模式。 - 总结的来说方法类似在运行时添加了个NSTimer,NSTimer指定了一个
SEL
,和NSTimer一样默认运行在NSDefaultRunLoopMode
,当ScrollView滚动时因为运行模式的切换,会出现无法调用定时器的情况。
- 测试
我们通过,performSelectorInBackground
在后台线程添加方法。
[self performSelectorInBackground:@selector(addTimer) withObject:nil];
在这个后台线程中执行performSelector
的延时调用方法。
- (void)addTimer {
[self performSelector:@selector(timerEvent) withObject:nil afterDelay:1];
}
- (void)timerEvent {
NSLog(@"时间回调");
}
发现控制台无法打印。
当我们主动开启runloop时,
[self performSelector:@selector(timerEvent) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];
打印
2018-07-16 10:59:38.311387+0800 runloop[41589:5309173] 时间回调
- 当然还是取消延时执行的方法。
/**
* 取消延迟执行
*
* @param aTarget 一般填self
* @param aSelector 延迟执行的方法
* @param anArgument 设置延迟执行时填写的参数(必须和上面performSelector方法中的参数一样)
*/
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;