• 发文
  • 评论
  • 微博
  • 空间
  • 微信

浅析RunLoop原理及其应用

EAWorld 2019-10-28 14:01 发文

转载本文需注明出处:微信公众号EAWorld,违者必究。

引言:

一个APP的启动与结束都是伴随着RunLoop循环往复的,不断的循环、不断的往复。当线程被杀掉、APP退出后被系统以占用内存为由杀掉,RunLoop就消失了。但平时开发中很少见到RunLoop,为何它如此神秘?本文跟大家分享一下RunLoop的相关知识。  

目录:

1、RunLoop的概念  

2、RunLoop与线程的关系  

3、RunLoop的常用模式

4、RunLoop的应用  

1.RunLoop的概念  

将英文拆解不难理解其实RunLoop表示一直在运行着的循环或者从上面的定义源码中可以看出就是一个do..while..循环。当启动一个iOS APP时主线程启动与其对应的RunLoop也已经开启。如果不杀掉APP则APP一直运行,就是因为RunLoop循环一直为开启状态保证主线程不会被摧毁。这也是RunLoop的作用之一保证线程不退出。RunLoop在循环过程中监听事件,当前线程有任务时,唤醒当当线程去执行任务,任务执行完成以后,使当前线程进入休眠状态。当然这里的休眠不同于我们自己写的死循环(while(1);),它在休眠时几乎不会占用系统资源,当然这是由操作系统内核去负责实现的。

UIApplicationMain()函数方法会默认为主线程设置一个NSRunLoop对象,这个循环会随时监听屏幕上由用户触摸所带来的底层消息并将其传递给主线程去处理,当点击一个button事件的传递从图上的调用栈可以看出。(监听的范围还包含时钟/网络)RunLoop循环与While循环的区别在于,RunLoop会在没有事件发生时进入休眠状态从而不占用CPU消耗,有事件发生才会去找对应的 Handler 处理事件,而While则会一直占用。在 Cocoa 程序的线程中都可以通过代码NSRunLoop *runloop = [NSRunLoop currentRunLoop];来获取到当前线程的Runloop对象。

RunLoop共有两套API接口 :1. Foundation框架NSRunLoop 2. Core Foundation框架CFRunLoopRef。NSRunLoop和CFRunLoopRef都代表着RunLoop对象,它们是等价的,可以互相转换。

NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)。

2.RunLoop与线程之间的关系

RunLoop和线程是相辅相成的,一个Runloop对应着一条唯一的线程,可以这样说RunLoop是为了线程而生,没有线程,它也没有存在的必要。RunLoop是线程的基础架构部分, Cocoa 和 CoreFundation 都提供了RunLoop对象方便配置和管理线程的 RunLoop。每个线程,包括程序的主线程( main thread )都有与之相对应的 RunLoop对象。上图从 input source 和 timer source 接受事件,然后在线程中处理事件都是由RunLoop推动完成。

注意:开一个子线程创建runloop,不是通过alloc init方法创建,而是直接通过调用currentRunLoop方法来创建,它本身是一个懒加载的。在子线程中,如果不主动获取Runloop的话,那么子线程内部是不会创建Runloop的。

3.RunLoop的常用模式

RunLoop 的模式有五种。图上列出了其中两种分别是 NSDefaultRunLoopMode(默认模式) 和 UITRackingRunLoopMode(UI模式) 、NSRunLoopCommonModes(占位模式)。其实占位模式不是一个真正的模式,它相当于上面两种模式之和。苹果公开提供的 Mode 有两个NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes)。

4.RunLoop的应用

例如创建一个比较常见的注册页面,里面用NSTimer来自处理常见的验证码倒计时,每秒处理一下,如果NSTimer添加到的是默认模式的RunLoop这时候注册页面有一个展示注册协议的UITextView当用户滑动UITextView时验证码的倒计时是停止的,这是因为主线程的RunLoop模式是UI模式这个时候RunLoop循环是优先处理UI模式的任务而忽略了默认模式的计时器。此时解决上面的问题就需要用到NSRunLoopCommonModes(占位模式),这个模式相当于把NSTimer在两种模式下都添加了,这就不难理解为什么NSRunLoopCommonModes是一个复数形式了。这个模式下滑动UITextView或停止的时候RunLoop是在UITRacking和default模式下切换的(从打印日志中可以看出)。如果觉得NSTimer设置RunLoop模式很复杂可以尝试用GCD的Timer用法很简便。

RunLoop在TableView中的应用(解决滑动卡顿问题)。

如图代码展示,当加载高清大图渲染屏幕,而此时不得不在主线程操作,会引起滑动的卡顿。

tableview 在加载 cell 时如果遇到多个耗时操作会有点卡顿。将耗时操作放到 DefaultMode 里只能解决滑动时流畅,但是停止时需要加载耗时,仍然会有卡顿的感觉。正确方法是采用 RunLoop 监听,将多个耗时操作分开执行,在每次 RunLoop 唤醒时去做一个耗时任务。

阻塞原因:kCFRunLoopDefaultMode时候 多张图片(特别是高清大图)一起加载(耗时)loop不结束无法BeforeWaiting(即将进入休眠) 切换至UITrackingRunLoopMode来处理等候的UI刷新事件造成阻塞。

解决办法:每次RunLoop循环只加载一张图片 这样loop就会很快进入到BeforeWaiting处理后面的UI刷新(UITrackingRunLoopMode 优先处理)或者没有UI刷新事件继续处理下一张图片。

RunLoop 监听添加Observer (监听RunLoop的beforeWaiting)当处理完一张图片即将进入到beforeWaiting时处理数组里的tasks,这些任务就在callback里面做处理。

callBack拿到task处理了一部分就进入到了休眠 比如拿到18个任务只处理了7个就不处理了。

此处添加Timer是让RunLoop一直处于活跃状态 保证即使处理完所有task还是一直活跃状态。

注意:当CFRunLoopAddObserver(runloop, observer , kCFRunLoopDefaultMode); 添加到观察者时模式为kCFRunLoopDefaultMode 这样的的话只能监听到一般模式的BeforeWaiting,即不滑动的时候。所以图上的加载只在拖动结束时,而拖动UI时无任何加载。如下图:

所以这里可以再次优化,将模式改为kCFRunLoopCommonModes,这样的话滑动或者不滑动都可以加载图片渲染屏幕,而且是在不影响屏幕流畅性的基础上。如以下GIF:

源码:

#import "ViewController.h"
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (nonatomic, strong) NSTimer *timer;@property (nonatomic, strong) NSMutableArray *tasks;@property (nonatomic, assign) NSInteger maxTaskNumber;
@end
void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){    //C语言与OC的交换用到桥接 __bridge    //处理控制器加载图片的事情    ViewController *VC = (__bridge ViewController *)(info);    if (VC.tasks.count == 0) {        return;    }    void(^task)() = [VC.tasks firstObject];    task();    [VC.tasks removeObject:task];    NSLog(@"COUNT:%ld",VC.tasks.count);    }
@implementation ViewController
- (void)viewDidLoad {    [super viewDidLoad];            [self addRunloopOvserver];        self.maxTaskNumber = 18;    self.tasks = [NSMutableArray array];            [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];    }-(void)timerMethod{    }-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{    ViewController2 *vc2 = [ViewController2 new];    [self presentViewController:vc2 animated:YES completion:^{            }];}
- (void)addRunloopOvserver{    //获取当前的RunLoop    CFRunLoopRef runloop = CFRunLoopGetCurrent();    //上下文 (此处为C语言 对OC的操作需要上下文)将(__bridge void *)self 传入到Callback    CFRunLoopObserverContext context = {0, (__bridge void *)self, &CFRetain, &CFRelease};    //创建观察者 监听BeforeWaiting 监听到就调用回调callBack    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);    //添加观察者到当前runloop kCFRunLoopDefaultMode可以改为kCFRunLoopCommonModes    CFRunLoopAddObserver(runloop, observer , kCFRunLoopCommonModes);    //C语言中 有create就需要release    CFRelease(observer);}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{    return 30000;}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identity" forIndexPath:indexPath];    NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode);    //以下两个循环的UI操作在必须放在主线程,但是弊端就是太多图片的处理会阻塞tableview的滑动流畅性    for (int i = 1; i < 4; i++) {        UIImageView *imageView = [cell.contentView viewWithTag:i];        [imageView removeFromSuperview];    }    for (int i = 1; i < 4; i++) {    /*     阻塞模式    */    //        CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15;    //        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)];    //        [cell.contentView addSubview:imageView];    //        imageView.tag = i;    //        imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];
               //阻塞原因:kCFRunLoopDefaultMode时候 多张图片一起加载(耗时)loop不结束无法BeforeWaiting(即将进入休眠) 切换至UITrackingRunLoopMode来处理等候的UI刷新事件造成阻塞    //解决办法:每次RunLoop循环只加载一张图片 这样loop就会很快进入到BeforeWaiting处理后面的UI刷新(UITrackingRunLoopMode 优先处理)或者没有UI刷新事件继续处理下一张图片            /*         流畅模式        */        //下面只是把任务放到数组 不消耗性能        void(^task)() = ^{            CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15;            UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)];            [cell.contentView addSubview:imageView];            imageView.tag = i;            imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];        };        [self.tasks addObject:task];        //保证只拿最新的18个任务处理        if (self.tasks.count > self.maxTaskNumber) {            [self.tasks removeObjectAtIndex:0];        }    }    return cell;}
- (void)didReceiveMemoryWarning {    [super didReceiveMemoryWarning];}

(左右滑动查看全部代码)

推荐阅读

OC观察者模式之KVO的使用与思考

我对敏捷软件测试的理解与实践

基于Redis实现Spring Cloud Gateway的动态管理

关于作者:热河,普元移动端开发工程师,互联网技术爱好者,专注于iOS开发。目前参与Mobile 8.0项目的开发,主要接触RN技术的应用,黏合前端代码与iOS底层之间的交互。

关于EAWorld:微服务,DevOps,数据治理,移动架构原创技术分享。长按二维码关注!

在看点这里

声明:本文为OFweek维科号作者发布,不代表OFweek维科号立场。如有侵权或其他问题,请及时联系我们举报。
2
评论

评论

    相关阅读

    暂无数据

    EAWorld

    微服务,DevOps,数据治理,...

    举报文章问题

    ×
    • 营销广告
    • 重复、旧闻
    • 格式问题
    • 低俗
    • 标题夸张
    • 与事实不符
    • 疑似抄袭
    • 我有话要说
    确定 取消

    举报评论问题

    ×
    • 淫秽色情
    • 营销广告
    • 恶意攻击谩骂
    • 我要吐槽
    确定 取消

    用户登录×

    请输入用户名/手机/邮箱

    请输入密码