前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入理解RunLoop及在开发中的应用

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

作者头像
honey缘木鱼
发布2019-03-19 17:40:37
1.3K0
发布2019-03-19 17:40:37
举报
文章被收录于专栏:娱乐心理测试娱乐心理测试

一.RunLoop定义

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

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

代码语言:javascript
复制
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

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

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

代码语言:javascript
复制
int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"启动"); 
        return 0; 
    }
}

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

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

代码语言:javascript
复制
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop];   // 获得主线程的RunLoop对象

Core Foundation:CFRunLoopRef

代码语言:javascript
复制
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain();   // 获得主线程的RunLoop对象

文档中的相关类:

代码语言:javascript
复制
CFRunLoopRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopModeRef
CFRunLoopObserverRef`

他们的关系如下图:

关系图

  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会被唤醒以执行那个回调。

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

方法一:

代码语言:javascript
复制
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
                                                     target:self
                                                   selector:@selector(backgroundThreadFire:) userInfo:nil
                                                    repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];

方法二:

代码语言:javascript
复制
[NSTimer scheduledTimerWithTimeInterval:10  target:self  selector:@selector(backgroundThreadFire:)
                                      userInfo:nil
                                      repeats:YES];
CFRunLoopModeRef mode类型

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

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

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

CFRunLoopObserverRef 观察者

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

代码语言:javascript
复制
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { 
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source 
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

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

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

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

使用 RunLoop 控制子线程保活

代码语言:javascript
复制
/** 线程对象 */
@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演示滚动时,定时器停止工作

代码语言:javascript
复制
- (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的默认模式:

代码语言:javascript
复制
  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的时候,包装一个自动释放池,临时创建了很多对象,等着我们释放,在很多优秀的开源库中,都有这个说明

代码语言:javascript
复制
- (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];
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.03.16 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CFRunLoopSourceRef 输入源
  • CFRunLoopTimerRef 定时源
  • CFRunLoopModeRef mode类型
  • CFRunLoopObserverRef 观察者
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档