RunLoop,即运行循环,你非要叫它跑圈,也不是不可以
这哥们到底是什么?
我们知道,一个程序的入口函数,也就是在main.m文件中
程序启动首先就会走到这里,这里有个自动释放池,一般在创建大量临时变量的时候会手动创建释放池,这里将整个程序运行期间都置于这个释放池内,程序运行结束之时,自动释放池结束,内存回收。这个自动释放池内,就一句代码,这是什么意思?进入UIApplicationMain
函数内可以发现,这个函数返回值是int
型。那我们换种写法
运行起来,会发现第十六行并不会执行,代码停留在了第十五行,不会再继续往下执行,也就是说UIApplicationMain
函数一直没有返回。我们的程序一直是在主线程上执行的,我们打印下看看
NSLog(@"thread=%@",[NSThread currentThread]);
2018-03-30 22:53:12.821131+0800 RunLoopDemo[74265:4182972] thread=<NSThread: 0x604000066180>{number = 1, name = main}
的确,当前就是在主线程,这是必然的。整个项目运行期间,主线程都不会退出,一旦退出,程序肯定就挂了。究其原因,是因为UIApplicationMain
函数内开启了一个死循环,用来保住主线程的生命,程序处于运行状态,使得整个项目运行期间,主线程都不会退出。这个死循环,其实就是RunLoop,RunLopp就是个死循环。这个默认启动的RunLoop
是跟主线程相关联的。RunLoop
这么设计,它的作用是什么呢?
- 保证线程不退出,即程序的持续运行
- 监听事件,询问是否有事件发生,从事件队列里取出事件并处理(网络、触摸、时钟等事件)
- 节省
CPU
资源,提高程序性能:该做事时做事,该休息时休息
那就写个代码玩一下试试看
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
}
- (void)timerMethod {
static int a = 0;
a++;
NSLog(@"a=%zd",a);
}
一个很简单的时钟事件,这个时候你会发现方法并没有走。上面已经提到,RunLoop
用来监听事件,需要将timer
加到RunLoop
中去
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
注意RunLoop无法手动创建,只能获取,不过其实第一次获取就是创建。这里面有个mode
属性,暂时写默认,这时候运行程序,正常运行。这时候我们往控制器上拖个textView, 运行
问题出来了,启动后时钟事件正常运行,但是一旦拖动textView,时钟事件就停止打印,不拖动textView,时钟事件又正常运行。这是什么情况?
上面我们将timer
丢给RunLoop
的时候,使用的模式mode
是NSDefaultRunLoopMode
默认模式。
iOS中有两套API来访问和使用RunLoop对象,分别是OC语言中的Foundation
框架(NSRunLoop
)和C语言中的Core Foundation
框架(CFRunLoopRef
),NSRunLoop
是基于CFRunLoopRef
的一层OC
包装
RunLoop其实共五种模式
NSDefaultRunLoopMode
UITrackingRunLoopMode
NSRunLoopCommonModes
NSConnectionReplyMode
NSModalPanelRunLoopMode
NSDefaultRunLoopMode
模式就是RunLoop
的默认模式,而UITrackingRunLoopMode
模式则是UI
模式,UI
模式只能被触摸事件唤醒,所以优先级最高。这里第三种模式NSRunLoopCommonModes
严格来说不是一种模式,而是一种占位模式,既能起到默认模式的效果又能起到UI
模式的效果。第四种和第五种模式分别是在刚启动进入App
时进入的第一个Mode
,启动后就不再使用,和接受系统时间的内部Mode
,作为开发者这两种模式基本不会使用到,
Core Foundation
中关于RunLoop
有五个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopRef
对应的就是NSRunLoop
,CFRunLoopModeRef
代表上面说到的RunLoop
的几种运行模式,CFRunLoopSourceRef
是事件源,或者说是输入源,现在事件源分为两种,非基于Port
的Source0
和基于Port
的Source1
,基于Port
是指 通过内核和其他线程通信,接收、分发系统事件,CFRunLoopTimerRef
是基于事件的触发器,相当于NSTimer
,CFRunLoopObserverRef
是观察者,能够监听RunLoop
的状态改变,可以监听的时间点有以下几个
kCFrunLoopEntry
(1UL << 0) 即将进入LoopkCFRunLoopBeforeTimers
(1UL << 1) 即将处理TimerkCFRunLoopBeforeSources
(1UL << 2) 即将处理SourcekCFRunLoopBeforeWaiting
(1UL << 5) 即将进入休眠kCFRunLoopAfterWaiting
(1UL << 6) 刚从休眠中唤醒kCFRunLoopExit
(1UL << 7) 即将退出LoopkCFRunLoopAllActivities
所有状态
上面我们是把时钟加到默认模式下,现在我们加在UI
模式看下效果
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
再次运行程序
问题又出来了,当我们拖到textView
时,时钟事件可以正常打印,但是一旦停止就不再打印了。这是因为,时钟是加到RunLoop
的UI
模式下的,而UI
模式只有在触摸屏幕的时候才会唤醒,而没有触摸屏幕的时候RunLoop
则处于休眠状态,所以才会出现此问题。怎么才能解决这个问题呢?将这个时钟既加到默认模式下,又加到UI
模式下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
这要添加两次,而🍎肯定考虑到了这点,所以有了NSRunLoopCommonModes
这种占位模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
再次运行,会发现上述问题都得到了解决,拖拽不再影响时钟方法的执行了。简单画个图
这是🍎官网的图
上面提到,RunLoop
的作用之一就是监听事件并执行,事件可以有很多种,比如时钟事件、监听事件、网络事件等等。UI
模式下的这些事件必须通过触摸才能唤醒RunLoop
去执行,执行完之后RunLoop就处于休眠状态,循环往复。而默认模式下,只要有这些事件,就会及时通知RunLoop
去执行。
继续深究,如果这个事件是个耗时操作会怎样?我们在timerMethod
这个时间下模拟一下
[NSThread sleepForTimeInterval:1.0];
我让它每次睡个2秒钟再执行
问题又出来了,你会发现UI
被卡住了,因为当前是在主线程上执行,每次都强迫它睡2秒,这绝壁会卡住。既然这样,那就让这个耗时操作放在子线程去执行
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"111");
}];
[thread start];
}
- (void)timerMethod {
NSLog(@"222");
[NSThread sleepForTimeInterval:2.0];
static int a = 0;
a++;
}
这时候运行会发现111尽管打印了,但是222没有打印,这说明timerMethod
方法并没有执行,这是什么原因?这时候我们自定义一个FN_Thread
@implementation FN_Thread
- (void)dealloc {
NSLog(@"dealloc");
}
把NSThread
替换成FN_Thread
FN_Thread *thread = [[FN_Thread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"111");
}];
[thread start];
再次运行,会发现打印完111,接着走进了FN_Thread
中的dealloc
方法,这说明这个线程对象thread
被释放了,既然被释放了,那这个线程上的timeMethod
方法自然就不会执行了。那我们强引用这个线程对象试一下,加上对应代码
@property (nonatomic, strong) FN_Thread *thread;
_thread = thread;
再次运行,会发现FN_Thread
类中的dealloc
方法不走了,但是timeMethod
方法依然没有执行。线程对象没有释放,当前线程上的时钟方法又没有走,这又是什么原因?再加一个方法看一下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"thread=%@",_thread);
[_thread start];
}
运行,触摸屏幕的时候再次开启这个线程,开启前打印一下
这个线程对象还在,但是开启会崩
这是因为,这个线程已经挂了,已经被释放了,肯定无法被开启,但是这个OC
对象thread
还在,因为被我们强引用了。强引用线程对象,并不会阻止线程的释放。所以会出现上述情况。事实上,子线程block
内的代码执行完线程就会被释放,要想线程不被释放。就必须让线程有执行不完的任务。那我们可以手动让线程一直在执行,在block
内加如下代码
while (true) { }
这时候会发现程序可以正常运行了。但这并不是正确的解决方法。正确的姿势应该是将事件加到RunLoop
中,然后开启它,因为
每个线程都对应一个
RunLoop
,但是除了主线程,所有子线程的RunLoop
默认都没有开启,需要手动开启在NSLog(@"111");
代码前加上
[[NSRunLoop currentRunLoop] run];
再次运行,会发现时钟方法timeMethod
终于可以正常运行了,但是你会发现NSLog(@"111");
这句代码又没有走了?这是因为NSRunLoop
是个死循环,下面的代码根本不会执行。好在,RunLoop
还提供给了一个指定线程运行时间的方法[[NSRunLoop currentRunLoop] runUntilDate:];
,毕竟一开启就停不下来,这操作很难接得住
这里就让线程存活指定时间感受一下
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
再次运行,一切都很明了了
这里如果想强制退出子线程,使用[NSThead exit]
,不过并不建议这么做
有必要强调一下,如果在主线程调用[NSThead exit]
会怎样?
你会发现程序并不会挂掉,但是主线程卡死了,子线程仍然在继续。其实在CPU
眼里,主线程和其他子线程一样,一样可以强退
iOS里有个peformSelector
方法,我们看一下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
FN_Thread *thread = [[FN_Thread alloc] initWithBlock:^{
NSLog(@"touch");
}];
[thread start];
[self performSelector:@selector(performMethod) onThread:thread withObject:nil waitUntilDone:NO];
}
- (void)performMethod {
NSLog(@"performMethod");
}
这时候你会发现performMethod
方法根本就没走,performSelector
就是线程间的通信,performMethod
是在子线程thread
上执行的,thread
线程的block
代码走完线程就挂了,所以不会走进performMethod
方法,需要加上下面这句代码保住子线程的生命
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
这里的performSelector
事件就是上面提到的RunLoop
处理的Source
事件源。如果是performSelectorOnMainThread
方法就不需要开启RunLoop
,因为主线程的RunLoop
在程序启动之时就已经默认开启了
NSTimer
作为定时器有时候并不是十分精准的,可能会受到RunLoop
的运行模式的影响,再加上其本身的精度有限, 可以使用GCD
的定时器作为代替,精度更高,且不会受到RunLoop
的影响,因为其是基于C语言的
int count = 0;
- (void)GCDTimer {
// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue(); // dispatch_get_global_queue(0, 0);
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
self.timer = timer;
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(timer, start, interval, 0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"count=%zd",count);
count++;
});
// 启动定时器
dispatch_resume(timer);
// 取消定时器
//dispatch_cancel(self.timer);
}
上面提到了RunLoop
中的Timer
和Source
,还有一个Observer
,监听者,是用来监听RunLoop
所处的状态,OC
中无法创建,只能使用C语言
- (void)addOberser {
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"activity = %zd", activity);
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
}
可以看到RunLoop
的运行逻辑,每次都是从睡眠中唤醒,处理一些事件,最后处于休眠状态。监听状态变化的block
中可以根据自己需求定义一些处理事件
在项目中经常会遇到加载图片的需求,现在一般都使用SDWebImage
库,如果不使用这个库怎么处理?
主要逻辑就是将一个个图片下载任务放入队列,下载成功存入内存缓存,然后写入沙盒,下次再次显示时先从内存缓存里获取,获取不到则从沙盒获取,依然获取不到则进行下载任务。其实使用SDWebImage
库也依然会进行这些操作。
如果,这些下载的图片非常的大,分辨率比较高,你会发现这时候滑动的时候会有明显卡顿,是因为即便下载是在子线程,但是在渲染的时候图片会解压缩,这是在主线程进行的,是个耗时的操作,图片分辨率高的情况下卡顿尤其明显。这些下载下来的图片是在一次RunLoop
循环的时候渲染显示,如果一次RunLooop
循环只处理一张图片,卡顿就会得到有效解决,这需要用到Observer
来监听RunLoop
的状态,RunLoop
在处理完事件会进入休眠状态,那就每次监听在RunLoop
进入休眠状态前处理这些图片,直到没有图片处理然后进入休眠状态
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly
,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!