iOS-Runloop原理与应用

Runloop:运行循环-死循环

主要目的:提高性能,有事情就干,没事情休眠。
参考https://blog.csdn.net/callauxiliary/article/details/107419854

主要应用

1,保证线程一直运行,处理事件,比如触摸事件,时钟事件,都是由runloop完成。
2,优化卡顿:将一次runloop执行完的任务,放到多次runloop中执行。
3,UI滑动时计时不准确的问题,设置定时器的Mode为:NSRunLoopCommonModes。
4,需要在线程上使用performSelector*****方法(运行时方法)。例如

让UITableView、UICollectionView等延迟加载图片

[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

这算一个优化点,这里用的defaultMode,就是滚动的时候不加载图片,停止滚动加载图片

Runloop运行原理

当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。
Runloop的生命周期:在第一次获取时创建,在线程结束时销毁。

Runloop要想跑起来,它的内部必须要有一个mode,这个mode里面必须有source\observer\timer,至少要有其中的一个。
系统默认注册了5个mode

    a.kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

        b.UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

        c.UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用

        d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

        e.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。

主要分为三大步骤:

1、首先根据modeName找到对应的Mode

2、如果model里没有Source/Timer/Observer,直接返回

3、如果model有Source/Timer/Observer,就会即将进入runloop


image.png

详细步骤

简单来讲就是一个do-while循环,有输入源就唤醒,没有就处于休眠状态,官方解释链接。具体过程如下:

  1. 通知观察者开始进入 Runloop
  2. 通知观察者开始处理 Timer 事件
  3. 通知观察者将要处理非基于 port 的事件
  4. 启动准备好的事件
  5. 如果基于 port 的事件已经准备好,立即启动。并进入步骤 9
  6. 通知观察者进入休眠状态
  7. 线程进入休眠直到以下事件出现
    • 基于 port 的事件源出现
    • 定时器启动
    • 设置的时间已经超时
    • RunLoop 被唤醒
  8. 通知观察者线程将被唤醒
  9. 处理已经进入的事件
    • 如果用户自定义的计时器启动,处理事件并重启 Runloop。转到第二步
    • 输入源启动,传递消息
    • 如果 Runloop 被显示唤醒且没有超时,重启 Runloop。转到第二步
  10. 通知观察者 Runloop 已经退出

Runloop组成

两种对象:NSRunLoop 和 CFRunLoopRef。

CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

RunLoop共包含5个类,但公开的只有Source、Timer、Observer相关的三个类。
1,Timder,时钟
2,source,事件源:一切事件的来源,按照函数的调用栈分为source0:非系统内核事件,source1:系统内核事件。
3,observer,观察者,观察的runloop的循环周期。

苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

APP启动过程

1,当点击APP时,操作系统开启一条线程执行程序的main函数。这个线程就是这个程序的主线程。这个线程是一个常驻线程,因为这个线程的Runloop被开启了,不会被线程池释放。

线程和runloop 的关系

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)

Runloop的作用

1,保证线程不退出。
2,监听所有的事件,比如触摸事件,时钟事件,网络事件等。
子线程要监听事件,就必须开启子线程的runloop。

验证将时钟添加到runloop才可以实现监听

1,scheduledTimerWithTimeInterval初始化方法默认是添加到当前线程的runloop的,所以不用显示的添加。
这种方法添加是NSDefaultRunLoopMode模式,UI事件会中断响应。

-(void)test{

    [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
    
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

3,子线程结束runloop

-(void)test{
    //要放在子线程,设置runloop时间,时间过了没有执行的任务,子线程就会结束,并释放
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        //将时钟手动添加到当前的runloop,否者时钟方法不会执行
        [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
        //当为yes时,取消RunLoop循环
        while (!_finish) {
            NSLog(@"来了11");
            [[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceReferenceDate:1]];
        }
        NSLog(@"来了");
    });
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"22222");
    _finish = YES;
}

2,timerWithTimeInterval初始化方法需要手动将时钟添加到当前线程的runloop中,否则时钟不能被监听。

-(void)test{
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
    //将时钟手动添加到当前的runloop,否者时钟方法不会执行
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

runloop的三种Mode

1,当对象使用NSDefaultRunLoopMode(默认模式,一般处理网络事件和Timer事件),在runloop执行UI事件的时候,会暂停响应该对象的事件。
2,当对象使用UITrackingRunLoopMode(UI模式,一般处理UI事件),只有在runloop执行UI事件的时候,才会会暂停响应该对象的事件。
3,当对象使用NSRunLoopCommonModes(占位模式,这个不是一个真的模式,只是同时添加了上面两种模式),在runloop执行UI事件的时候,也会响应该对象的事件(最理想的结果)。

开启当前线程的runloop

子线程处理需要监听的事件(需要子线程活着一直监听的情况),可以启动子线程的runloop,让子线程成为常驻线程,避免被释放,从而可以响应监听的事件。唯一让线程不被释放的方法就去启动他的runloop。
解决NSTimer在滑动时停止工作的问题的办法
1,设置定时器的Mode为:NSRunLoopCommonModes
2,将NSTimder放到子线程并且开启runloop,name就是算是NSDefaultRunLoopMode,滑动UI也不会影响Timer。
NSTimder处理的事件一般放在子线程,并开启子线程的runloop,避免影响主线程处理事件。

-(void)test{

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timeerFunC) userInfo:nil repeats:YES];
        //将时钟手动添加到当前的runloop,否者时钟方法不会执行
        [[NSRunLoop currentRunLoop]addTimer:timer forMode: NSDefaultRunLoopMode ];
        //手动开启当前线程的runloop,这是一个死循环
        [[NSRunLoop currentRunLoop]run];
        NSLog(@"这里不会走了");
    });
}

-(void)timeerFunC{
    static int num;
    NSLog(@"%d",num);
    num++;
}

GCD实现定时器

Dispatch Source创建定时器timer,优于NSTimer
必须要声明timer属性强应用,不然会被释放

@property (nonatomic, strong) dispatch_source_t timer;
-(void)test2{
    NSLog(@"启动");
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(_timer, ^{
        NSLog(@"间隔一秒执行%@",[NSThread currentThread]);
    });
    dispatch_resume(_timer);
    NSLog(@"启动");
}

优化练习

tableview在快速滑动时,高清图片造成卡顿优化。
原因:tableview在快速滑动时,runloop必须在一次循环内,渲染所有的图片。
思路:runloop每次循环只加载一张图片,例如一个屏幕总的可以显示15张图片,那么就分15次加入到runloop中加载。相当于把一次做完的事情,分成了15次。例如监听kCFRunLoopBeforeWaiting事件,回调函数加载图片,加载完成后删除这个任务。每次加载图片后都会再次到达kCFRunLoopBeforeWaiting,还有图片任务就会继续执行。
通过CFRunLoopObserverRef监听runloop 的状态,函数指针回调,Ref是引用,指针的意思。
步骤
1,添加runloop观察者,观察runloop 的状态变化。
2,将加载图片的代码块放到数组中。
3,在观察者回调中,拿出数组中加载图片的代码执行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,875评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,569评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,475评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,459评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,537评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,563评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,580评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,326评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,773评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,086评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,252评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,921评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,566评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,190评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,435评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,129评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,125评论 2 352