-
(一)RunLoop简介
RunLoop:运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。
-
(二)RunLoop作用
1.保证线程不退出
当用户点击icon时系统就会调用这个应用程序,默认帮我们开启一条主线程(常驻线程),会调用的main函数,因为这条线程上面的RunLoop被开启了,所以程序不会退出。
void CFRunLoopRun(void) {
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
2.负责监听所有事件(UI事件,时钟,网络)
RunLoop运行的时候,当有到sources和Timer时就会交给对应的方法去处理,当没有事件消息传入时,RunLoop就会进入休眠状态。这时CUP就会将资源释放出来,提高程序性能。
-
(三)RunLoop组成
一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source、Timer、Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作CurrentMode。
-
(四)RunLoop五种模式
- 1.
NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行 - 2.
UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响 - 3.
NSRunLoopCommonModes:这是一个占位用的Mode,作为标记NSDefaultRunLoopMode和UITrackingRunLoopMode用,并不是一种真正的Mode - 4.
UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode - 5.
GSEventReceiveRunLoopMode:系统内核模式,处理内核事件
- 1.
-
(五)RunLoop之Timer
1.首先写一个NSTimer的定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(fire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
- (void)fire{
static int num;
NSLog(@"%d",num++);
}
输出:
MMRunLoopViewController.m:(112): 0
MMRunLoopViewController.m:(112): 1
MMRunLoopViewController.m:(112): 2
没有任何问题,
Timer正常运行
2.我们在页面上添加一个UIScrollerView或者UITextView,当我们滑动页面时,会发现没有输出,停止滚动,定时器又会开始了。这是为什么呢?原因就是上面我们提到的5中model的区别。
我们将Timer添加到`NSDefaultRunLoopMode`默认模式下,当前线程在默认`model`下执行,不会发生问题,当我们滑动`ScrollerView`,`model`就会切换为`UITrackingRunLoopMode`,所以`Timer`不会执行,当我们停止滑动,又会切换到`NSDefaultRunLoopMode`下,Timer继续打印。
3,解决办法:这时系统为我们提供了第三种model即NSRunLoopCommonModes,这是一个占位用的Mode,作为标记NSDefaultRunLoopMode和UITrackingRunLoopMode用,所以不管在那种模式下,Timer都会执行。
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(fire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.问题看似解决了,但是会不会引发其他的什么问题呢? 我们在Timer的实现方法中加入耗时操作
- (void)fire{
[NSThread sleepForTimeInterval:1.0];//模拟耗时操作
static int num;
NSLog(@"%d",num++);
}
我们发现定时器执行的时候,我们在滑动ScrollerView时会发生卡顿。Timer添加NSRunLoopCommonModes中一旦有耗时操作就会阻塞UI。这就是为什么苹果帮我们实现的Timer,为什么添加在NSDefaultRunLoopMode默认model下了。那么有耗时操作怎么办呢?提到耗时操作,第一个想起的就是子线程,接下来,让我们看一下RunLoop与线程的关系吧。
-
(六)RunLoop与线程的关系
1.把这个Timer放到子线程中
MMThread *thread = [[MMThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(fire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSLog(@"开辟子线程");
}];
[thread start];
MMThread继承与NSThread,里面重写了析构函数
- (void)dealloc
{
NSLog(@"MMThread 析构,当前作用域释放");
}
输出:
开辟子线程
MMThread 析构,当前作用域释放
Timer并没有执行,对thread强引用也不可以,怎么办呢。
2.解决办法:要想当前的线程不被释放,就必须要它一直有任务执行,我们可以要它的RunLoop跑起来
MMThread *thread = [[MMThread alloc] initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(fire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"开辟子线程");
}];
[thread start];
输出:
当前线程-<MMThread: 0x600000036d00>{number = 7, name = (null)}
0
当前线程-<MMThread: 0x600000036d00>{number = 7, name = (null)}
1
当前线程-<MMThread: 0x600000036d00>{number = 7, name = (null)}
2
没有打印
开辟子线程,说明[[NSRunLoop currentRunLoop] run]是一个死循环,后面没有走。这时我们滑动ScrollerView就不存在卡顿现象了。
3.那我们该如何释放这条线程呢?
提供三种方法:如果先要一条线程保活,RunLoop和Timer/Source缺一不可
3.1.停止RunLoop,可以做一个标记
@property (nonatomic, assign) BOOL isFinish;//初始化设置为NO
MMThread *thread = [[MMThread alloc] initWithBlock:^{
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(fire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
//结束线程方法一:添加标记 让RunLoop在0.0001秒后结束
while (!_isFinish) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
NSLog(@"线程当前作用域结束,子线程释放");
}];
[thread start];
调用
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_isFinish = YES;
}
输出:
当前线程-<MMThread: 0x600003dfc280>{number = 6, name = (null)}
0
当前线程-<MMThread: 0x600003dfc280>{number = 6, name = (null)}
1
当前线程-<MMThread: 0x600003dfc280>{number = 6, name = (null)}
2
线程当前作用域结束,子线程释放
MMThread 析构,当前作用域释放
3.2.让`Timer`失效,当`Timer`定义一个全局的,然后手动调用失效。
当前线程-<MMThread: 0x600003dfc280>{number = 7, name = (null)}
0
当前线程-<MMThread: 0x600003dfc280>{number = 7, name = (null)}
1
当前线程-<MMThread: 0x600003dfc280>{number = 7, name = (null)}
2
Timer失效,线程立刻释放 3.3.线程直接退出,runLoop就释放了,在fire中实现。
- (void)fire{
//退出当前线程,暴力,不建议
[NSThread exit]
4.如果我们在touchesBegan中退出主线程会发生什么
4.1主线程退出,应用程序不会退出
4.2子线程不会受影响,继续打印
4.3再次触发UI事件不会有响应,报错信息:`Attempting to wake up main runloop, but the main thread as exited. This message will only log once`
4.4主线程和子线程没有实质上的分别,只是主线程是由系统帮我们开启的,子线程是有自己开启的
-
(六)RunLoop之Source
Source:事件源(输入源),按函数调用栈分为Source0,source1所有事件都是一个source
Source0:非系统内核事件,用于用户主动触发的事件(点击button 或点击屏幕)
Source1:系统内核事件,通过内核和其他线程相互发送消息
系统会帮我们封装一个GCDTimer,它就是基于Source
//全局并发队列dispatch_get_global_queue(0, 0); 参数一:优先级 参数二,预留参数
dispatch_source_t GCDTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
self.GCDTimer = GCDTimer;
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
dispatch_source_set_timer(GCDTimer, 1.0 * NSEC_PER_SEC, 0, 0);
dispatch_source_set_event_handler(GCDTimer, ^{
NSLog(@"---%@",[NSThread currentThread]);
});
dispatch_resume(GCDTimer);
输出:
---<NSThread: 0x600002d9e3c0>{number = 3, name = (null)}
---<NSThread: 0x600002d9e3c0>{number = 3, name = (null)}
---<NSThread: 0x600002da0ec0>{number = 5, name = (null)}
---<NSThread: 0x600002da0ec0>{number = 5, name = (null)}
---<NSThread: 0x600002d48b80>{number = 6, name = (null)}
这是又系统帮我们封装的基于
Source的Timer,它不受线程和UI的影响,精度高于NSTimer。
-
(七)RunLoop中observer
CFRunLoop中observer可以用函数指针监听监听RunLoop的几种状态
kCFRunLoopEntry = (1UL << 0),//即将进入循环
kCFRunLoopBeforeTimers = (1UL << 1),// 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),//即将退出循环
kCFRunLoopAllActivities = 0x0FFFFFFFU//所有状态
应用场景举例:优化页面卡顿
模拟卡顿:全部高清大图,cell不重用
@property (nonatomic, strong) NSMutableArray *tasks;//装任务的数组
@property (nonatomic, assign) NSUInteger maxQueueLength;//最多加载图片数
_maxQueueLength = 24;//每篇显示的图片
_tasks = [NSMutableArray array];
//CFRunLoop添加观察者
[self addRunLoopObserver];
//保持runloop一直运转
NSTimer *timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(fire) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
- (void)fire{
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath];
[self addTask:^{//添加耗时操作
for (int i = 0; i < 5; i++) {
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(10+(ScreenWidth/5)*i, 10, ScreenWidth/5-20, 80)];
imageView.backgroundColor = [UIColor redColor];
imageView.image = [UIImage imageNamed:@"timg.jpg"];
[cell addSubview:imageView];
}
}];
return cell;
}
//添加一个添加任务的方法
- (void)addTask:(RunLoopBlock)task{
//添加数据到数组
[self.tasks addObject:task];
if (self.tasks.count > self.maxQueueLength) {
[self.tasks removeObjectAtIndex:0];
}
}
//2.观察到runloop将要休眠时会调用这个方法
void taskCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
// NSLog(@"observer:%@\nactivity:%lu\ninfo:%@",observer,activity,info);
//处理控制器加载图片
MMRunLoopTableViewController *VC = (__bridge MMRunLoopTableViewController *)info;
if (VC.tasks.count == 0) {
return;
}
//拿出任务执行,完事后删除
RunLoopBlock task = VC.tasks[0];
task();
[VC.tasks removeObjectAtIndex:0];
}
//1.添加RunLoop的观察者
- (void)addRunLoopObserver{
//1.获取当前的runLoop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
//.定义一个上下文,获取self
CFRunLoopObserverContext context = {
0,
(__bridge void *)self,
&CFRetain,
&CFRelease,
NULL
};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, 0, &taskCallBack, &context);
CFRunLoopAddObserver(runLoop, observer , kCFRunLoopCommonModes);
CFRelease(observer);
}
首先给RunLoop添加一个观察者,监听RunLoop进入休眠时机->会触发taskCallBack回调,将加载图片的任务放到tasks数组中,触发回调时会按顺序执行tasks中的任务。
-
(八)常驻线程
线程保活:有执行不完的任务
- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
这个我们可以借鉴一下AFNetworking的实现,虽然现在已经被废弃。
参数Port:要加入的端口。
参数mode:运行循环模式
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
开启了一个线程,同时开启Runloop,并添加了一个port事件维系Runloop的运行