阅读 155

RunLoop

  • (一)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运行的时候,当有到sourcesTimer时就会交给对应的方法去处理,当没有事件消息传入时,RunLoop就会进入休眠状态。这时CUP就会将资源释放出来,提高程序性能。

  • (三)RunLoop组成

一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个SourceTimerObserver 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作CurrentMode

  • (四)RunLoop五种模式

    • 1.NSDefaultRunLoopModeApp的默认Mode,通常主线程是在这个Mode下运行
    • 2.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    • 3.NSRunLoopCommonModes:这是一个占位用的Mode,作为标记NSDefaultRunLoopModeUITrackingRunLoopMode用,并不是一种真正的Mode
    • 4.UIInitializationRunLoopMode:在刚启动 App时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
    • 5.GSEventReceiveRunLoopMode:系统内核模式,处理内核事件
  • (五)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,解决办法:这时系统为我们提供了第三种modelNSRunLoopCommonModes,这是一个占位用的Mode,作为标记NSDefaultRunLoopModeUITrackingRunLoopMode用,所以不管在那种模式下,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.那我们该如何释放这条线程呢?

提供三种方法:如果先要一条线程保活,RunLoopTimer/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)}
复制代码

这是又系统帮我们封装的基于SourceTimer,它不受线程和UI的影响,精度高于NSTimer

  • (七)RunLoop中observer

CFRunLoopobserver可以用函数指针监听监听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的运行

文章分类
iOS
文章标签