深入了解RunLoop及在开发中的应用

作者 : 开心源码 本文共5635个字,预计阅读时间需要15分钟 发布时间: 2022-05-12 共93人阅读

一.RunLoop定义

RunLoop:运行循环,简单的说就是解决线程事件和管理线程的一种机制。当子线程的事件结束时,runloop将会自动休眠,app主线程中的runloop处于一直唤醒状态。当客户触发事件时,runloop通知线程执行事件内容。

二.线程与RunLoop的关系
1.每条线程都有唯一的一个与之对应的RunLoop对象,没有线程,也就没有RunLoop存在的必要。
2.RunLoop在第一次获取时创立,在线程结束时销毁;只能在一个线程的内部获取其 RunLoop(主线程除外)。
3.主线程的RunLoop系统默认启动,子线程的RunLoop需要主动开启;
有时候我们感觉自己在实际开发中很少用到RunLoop,其实在我们每次建立项目的时候,就已经使用上了RunLoop。
在程序的启动入口 main 函数中有这样一段熟习的代码:

int main(int argc, char * argv[]) {    @autoreleasepool {        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));    }}

UIApplicationMain 函数内部就启动了一个与主线程相关联的 RunLoop。
当我们点击运行,系统运行 UIApplicationMain 函数,系统进入了:主线程 main 的运行循环。RunLoop 使得主线程一直处在运行循环中。

我们可以这样测试下UIApplicationMain的作用,用下面代码代替:

int main(int argc, char * argv[]) {    @autoreleasepool {        NSLog(@"启动");         return 0;     }}

结果: 程序打印出“启动”后,就直接关闭了,控件与其余程序有关的都没有执行,界面空白,这说明了在 UIApplicationMain 函数中,开启了一个和主线程相关的 RunLoop,让 UIApplicationMain 不会返回,一直在运行中,也就保证了程序的持续运行。这就是为什么App程序启动之后能够持续运行在前端的起因。

三. RunLoop 对象和相关类
iOS中有2套API来访问和使用RunLoop:
Foundation:NSRunLoop

[NSRunLoop currentRunLoop]; // 取得当前线程的RunLoop对象[NSRunLoop mainRunLoop];   // 取得主线程的RunLoop对象

Core Foundation:CFRunLoopRef

CFRunLoopGetCurrent(); // 取得当前线程的RunLoop对象CFRunLoopGetMain();   // 取得主线程的RunLoop对象

文档中的相关类:

CFRunLoopRefCFRunLoopSourceRefCFRunLoopTimerRefCFRunLoopModeRefCFRunLoopObserverRef`

他们的关系如下图:

关系图

  1. 一个RunLoop包含若干个Mode,而每个Mode又包含若干个Source/Timer/Observer。
  2. RunLoop每次只能指定一种Mode。而且假如需要切换 Mode,只能退出当前 Loop。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
  3. 假如一个 mode 中一个 “Source/Timer/Observer” 都没有,则 RunLoop 会直接退出,不进入循环。
CFRunLoopSourceRef 输入源

是事件产生的地方,函数调用栈上Source有两个版本:Source0 和 Source1。

  • Source0:非基于端口port,例如触摸,滚动,selector选择器等客户触发的事件;(只包含了一个回调函数,它并不能主动触发事件)
  • Source1:基于端口port,少量系统事件; (包含了一个 mach_port 和一个回调函数,被用于通过内核和其余线程相互发送消息。能主动唤醒 RunLoop 的线程)
CFRunLoopTimerRef 定时源

基于时间的触发器,与NSTimer可混用。
包含了一个时间长度和一个回调函数。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

创立定时器源有两种方法,

方法一:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0                                                     target:self                                                   selector:@selector(backgroundThreadFire:) userInfo:nil                                                    repeats:YES];    [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];

方法二:

[NSTimer scheduledTimerWithTimeInterval:10  target:self  selector:@selector(backgroundThreadFire:)                                      userInfo:nil                                      repeats:YES];
CFRunLoopModeRef mode类型

事实上CFRunLoopModeRef 类并没有对外暴露,而假如在Xcode中查看CFRunLoopRef,可以看到CFRunLoopModeRef 类,通过 CFRunLoopRef 的接口进行了封装。
CFRunLoopModeRef有5种形式:

kCFRunLoopDefaultMode 默认模式,通常主线程在这个模式下运行;UITrackingRunLoopMode 界面跟踪Mode,用于追踪Scrollview触摸滑动时的状态;kCFRunLoopCommonModes 占位符,带有Common标记的字符串,比较特殊的一个mode;UIInitializationRunLoopMode 刚启动App时进入的第一个Mode,启动后不在使用;GSEventReceiveRunLoop 内部Mode,接收系事件。 

从关系图,我们可以知道 RunLoop 一次只能指定一种 Mode,且能够让不同组的 Source/Timer/Observer 互不影响.

CFRunLoopObserverRef 观察者

RunLoop的观察者,能够监听RunLoop的状态改变。
每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化,可以观察到不同时刻的状态有以下几个:

/* Run Loop Observer Activities */typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入LoopkCFRunLoopBeforeTimers = (1UL << 1), // 即将解决 TimerkCFRunLoopBeforeSources = (1UL << 2), // 即将解决 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop};

四、在实际开发中的应用
(1). 控制线程生命周期(线程保活)
在项目中,有时我们需要创立子线程,由于假如把所有的事情都放在主线程中去做,就会阻塞住主线程。导致APP 看起来很卡。这个时候即可以开启一个子线程,把耗时的操作放到子线程中。子线程做完事情以后,就会销毁。有时我们不希望子线程大量的创立和销毁,即可以使用 RunLoop 控制子线程的生命周期。

-(void)tapBtn{    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(todo) object:nil];    [thread start];}-(void)todo{    NSLog(@"执行此方法");    NSLog(@"%@",  [NSThread currentThread]);}

每一次点击按钮的时候,线程执行完方法,直接释放掉了,下一次直接创立了一个新的线程

使用 RunLoop 控制子线程保活

/** 线程对象 */@property(strong,nonatomic)  NSThread *thread;@end@implementation MyPurseViewController- (void)viewDidLoad {    [super viewDidLoad];        UIButton *btn = [[UIButton alloc]initWithFrame: CGRectMake(50, 150, 200, 80)];    btn.backgroundColor = [UIColor redColor];    [btn addTarget:self action:@selector(tapBtn) forControlEvents:UIControlEventTouchUpInside];    [btn setTitle:@"点击测试线程" forState:UIControlStateNormal];    [self.view addSubview:btn];        _thread = [[NSThread alloc]initWithTarget:self selector:@selector(todo) object:nil];    [_thread start];    // Do any additional setup after loading the view.}-(void)tapBtn{    NSLog(@"%@",  [NSThread currentThread]);}-(void)todo{    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];    [[NSRunLoop currentRunLoop] run];}

(2). 处理NSTimer在UIScrollView滑动时中止工作的问题
默认情况下,在滚动 tableView、UIScrollView 的时候,NSTimer会中止工作,这是由于在滚动时,RunLoop 会进入另一个Mode 模式UITrackingRunLoopMode 下,在该模式下,定时器就会中止,当不在滚动 UITextView , 定时器会重新开始。 RunLoop 同一时间,只能运行一种模式。
例如:UIScrollView+ NSTimer演示滚动时,定时器中止工作

- (void)viewDidLoad {    [super viewDidLoad];    UIScrollView *scrollView = [[UIScrollView alloc]initWithFrame:CGRectMake(0, 0, kScreenWidth, kScreenHeight)];    [self.view addSubview:scrollView];    scrollView.backgroundColor = [UIColor redColor];    scrollView.contentSize = CGSizeMake(0, 3000);        static int count = 0;        [NSTimer  scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {            NSLog(@"该方法第%d次",++count);        }];}

从后端打印日志看出,当我们在手机屏幕上滑动时,定时器不工作,日志不打印,放开手后,定时器重新工作,开始打印。(应用最常见的应该为轮播图自动播放时)

边滚动,定时器边工作,我们即可以用NSRunLoop的默认模式:

  static int count = 0;     NSTimer *timer =    [NSTimer  scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {            NSLog(@"该方法第%d次",++count);        }];    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

(3). 监控应用卡慢
有时我们在滑动列表时,感觉很卡,特别是列表上有很多图片要显示时,如何处理卡慢呢,由于我们现在加载图片用的SDWebImage,源码中已经解决了该问题,所以有时我们滑动列表时很顺畅。

(4). 性能优化

一个RunLoop对应一个线程
建议每一次启动RunLoop的时候,包装一个自动释放池,临时创立了很多对象,等着我们释放,在很多优秀的开源库中,都有这个说明

- (void)viewDidLoad {    [super viewDidLoad];    _thread = [[NSThread alloc] initWithTarget:self                                      selector:@selector(todo)                                        object:nil];    [_thread start];}- (void) todo{//    该方法默认不加入RunLoop中,使用schedule可以    @autoreleasepool {        NSTimer *timer = [NSTimer timerWithTimeInterval:0.3                                                 target:self                                               selector:@selector(test2)                                               userInfo:nil                                                repeats:YES];        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];        [[NSRunLoop currentRunLoop] run];    }}

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 深入了解RunLoop及在开发中的应用

发表回复