iOS- Runloop浅谈

最近项目完成年前的时间也比较轻松,对一些知识就行总结。今天总结的是Runloop.
对于Runloop,在平时项目中用的多吗?为毛,有时候一个项目都看不见一个Runloop的关键词,但面试还一直不断的问?那是因为我们其实使用了它,但它并不是以NSRunloop出现的,而是以其他的一些形式出现。

我们就以最明显的主线程来引出Runloop

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

这段代码绝对见过吧,但是点进去看过它具体是怎么实现的吗?
就和网上写的方法,我们打印一下看看结果。

NSLog(@"开始");
    int num=UIApplicationMain(argc, argv, nil, appDelegateClassName);
    NSLog(@"结束");

输出为:2021-01-29 11:11:00.388412+0800 RuntimeDemo[1174:38507] 开始

没打印出结束,这说明这个函数它就一直没结束。具体为什么没执行完的原因是下面这段代码。

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

看不懂do里面的代码没关系,到do-while总能看的懂吧,这是不是就可以回答了为什么一直没结束了。因为它是个死循环啊,就算地球爆炸它也会一直执行下去的啊。
好,这算是引出Runloop了,下面我们从易到难来看看Runloop的实现和底层原理。

什么是Runloop

runloop是通过内部维护的事件循环来对事件/消息进行管理的对象。

Runloop的使用

先来个简单的代码实现

self.num=0;
    NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:NSDefaultRunLoopMode];
-(void)timebtnclick{
    
    self.num++;
    NSLog(@"---%ld",self.num);
}

这就是一个简单的runloop。上面的NSTime不用说,我们来看看添加time到runloop时的参数。
1.[[NSRunLoop currentRunLoop] :获取当前runloop,在这也没创建其他的线程和runloop,在上面提到了,app主线程本身就是一个死循环的runloop,所以获取的这个必然就是主任务。
2.time:这个就是所需的时间。至于为什么要添加time,先不着急,下面会一张图就明白了。
3.Mode:NSDefaultRunLoopMode NSRunLoopCommonModes UITrackingRunLoopMode
我们先来看上面三个model。

假如现在在uiview视图上面添加一个滑动视图,然后就会发现在滑动这个视图的时候,是不会打印出结果的。下面先来看这个图
截屏2021-01-29 下午3.10.20.png

我们设置的是默认模式NSDefaultRunLoopMode,当滑动视图的时候,它进入的是UI模式的也就是UITrackingRunLoopMode,所以它肯定就不会有打印出来。 UI模式它的调起是有UI的点击事件触发的。
如果想要滑动的时候也运行runloop,我们是不是可以这样写
self.num=0;
    NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:UITrackingRunLoopMode];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:NSDefaultRunLoopMode];

同时把time添加到了默认模式和UI模式。这个运行起来也没什么问题。但 NSRunLoopCommonModes可能更方便。

self.num=0;
    NSTimer *time=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timebtnclick) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop]addTimer:time forMode:NSRunLoopCommonModes];
Runloop应用

还是和之前一样,我们先来看它的应用然后在看底层的原理

常驻线程
子线程执行完操作以后就会立即释放。即使强引用子线程也不能给子线程添加操作。下面我们来看段代码

-(IBAction)btnclick:(UIButton *)sender{
    
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    //创建子线程并开启
    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
    self.thread=thread;
    [thread start];
    
}
-(void)show{
    
    //添加一个端口
    [[NSRunLoop currentRunLoop]addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    
//    //添加timer
//    NSTimer *timer=[NSTimer scheduledTimerWithTimeInterval:2.0f target:self selector:@selector(test) userInfo:nil repeats:YES];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //创建监听者
    CFRunLoopObserverRef observer=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
       
        switch (activity) {
                    case kCFRunLoopEntry:
                        NSLog(@"RunLoop进入");
                        break;
                    case kCFRunLoopBeforeTimers:
                        NSLog(@"RunLoop要处理Timers了");
                        break;
                    case kCFRunLoopBeforeSources:
                        NSLog(@"RunLoop要处理Sources了");
                        break;
                    case kCFRunLoopBeforeWaiting:
                        NSLog(@"RunLoop要休息了");
                        break;
                    case kCFRunLoopAfterWaiting:
                        NSLog(@"RunLoop醒来了");
                        break;
                    case kCFRunLoopExit:
                        NSLog(@"RunLoop退出了");
                        break;

                    default:
                        break;
                }
    });
    //添加监听者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    //开启runloop
    [[NSRunLoop currentRunLoop]run];
    CFRelease(observer);
}

-(void)test{
    NSLog(@"%@",[NSThread currentThread]);
}

点击屏幕开启子线程并开启Runloop,然后点击button,我们看看打印结果

2021-03-19 16:18:15.170791+0800 RuntimeDemo[37765:14285242] RunLoop进入
2021-03-19 16:18:15.171060+0800 RuntimeDemo[37765:14285242] RunLoop要处理Timers了
2021-03-19 16:18:15.171230+0800 RuntimeDemo[37765:14285242] RunLoop要处理Sources了
2021-03-19 16:18:15.171343+0800 RuntimeDemo[37765:14285242] RunLoop要休息了
2021-03-19 16:18:31.043323+0800 RuntimeDemo[37765:14285242] RunLoop醒来了
2021-03-19 16:18:31.043712+0800 RuntimeDemo[37765:14285242] RunLoop要处理Timers了
2021-03-19 16:18:31.043828+0800 RuntimeDemo[37765:14285242] RunLoop要处理Sources了
2021-03-19 16:18:31.044092+0800 RuntimeDemo[37765:14285242] <NSThread: 0x283b1d540>{number = 7, name = (null)}
2021-03-19 16:18:31.044213+0800 RuntimeDemo[37765:14285242] RunLoop退出了
2021-03-19 16:18:31.044474+0800 RuntimeDemo[37765:14285242] RunLoop进入
2021-03-19 16:18:31.044588+0800 RuntimeDemo[37765:14285242] RunLoop要处理Timers了
2021-03-19 16:18:31.044681+0800 RuntimeDemo[37765:14285242] RunLoop要处理Sources了
2021-03-19 16:18:31.044776+0800 RuntimeDemo[37765:14285242] RunLoop要休息了

发现runloop处于休眠状态。
图片下载
把setImage放到NSDefaultRunLoopMode去做,在滑动的时候并不会去调用复制图片的方法,而是等到欢动完毕切换到NSDefaultRunLoopMode下才去调用。

[self.img performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:[NSDefaultRunLoopMode]];
//上面传递过来的image
-(void)setImage:(UIImgae *)image{

}

滚动的ScrollView导致定时器失效
在界面上有一个滚动视图,如果执行一个定时器执行时间,在滚动过程中定时器会失效。这个时候,我们可以把timer注册到NSRunLoopCommonModes,[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
使用GCD创建定时器,GCD创建的定时器不会受到runloop的影响
监测卡顿
卡顿检测在这里先不说,我把它规划到性能优化中去了。
自动释放池
Runloop内部有一个自动释放池,当Runloop开启时,就会自动创建一个自动释放池,当Runloop休眠之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当runloop被唤醒执行时Timer source等新的时间会被放到新的自动释放池中,当runloop退出的时候也会被释放。
只有主线程的runloop会默认启动,意味着会自动创建自动释放池,子线程需要在线程调度方法中手动的添加自动释放池。

Runloop的底层原理

Runloop是什么?
Runloop可以理解为运行循环,是线程内一个运行时间处理和响应传入事件的一个循环。它的作用就是为了在有事件到达时唤醒线程一处理各种事件,事件处理完让进入休眠节省CPU资源。
runloop和线程的关系
线程的作用是用来执行一个或者多个任务的,在默认情况下,线程执行完之后就会销毁,那假如我不希望它销毁,该怎么办呢?这也就是我们在一开始提到的线程保活。
1.一条线程对应一个Runloop对象,每条线程都有唯一一个和他对应的Runloop对象
2.主线程的Runloop系统已经自动创建好了,子线程的Runloop需要主动创建
3.Runloop在第一获取时创建,在线程结束时销毁。
4.Runloop并不保证线程的安全。我们只能在当前线程内部操作当前项城的runloop对象,而不能再当前线程内部去操作其他线程的runloop对象。
上面说了runloop和线程的关系,下面我们来看看runloop的中的几个相关类。

runloop结构.png

这张图是不是经常见到,它很好的描述了runloop几个相关类的关系。

CFRunLoopRef:代表 RunLoop 的对象
CFRunLoopModeRef:代表 RunLoop 的运行模式
CFRunLoopSourceRef:就是 RunLoop 模型图中提到的输入源 / 事件源
CFRunLoopTimerRef:就是 RunLoop 模型图中提到的定时源
CFRunLoopObserverRef:观察者,能够监听 RunLoop 的状态改变

一个Runloop对象包含了若干个运行模式Model,而在运行模式下有包含了若干个输入源(CFRunLoopSourceRef)、定时源、观察者。
每次runloop启动时,只能指定其中一个运行模式,这个运行模式被称作当前运行模式。
如果需要切换运行模式,只能退出当前的loop,在重新指定一个运行模式。

知道了runloop的结构,我们来具体看看每一个具体是什么?又是怎么实现的。
Observer事件
是观察者,每个 Observer都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观察的时间点有下面几个:

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
};

Timer事件
延迟的performSelector,延迟的dispatch_after,基于时间的触发器,可以与NSTimer进行转换
Source事件
1.Source0事件:非基于port的处理事件,不能主动唤醒休眠中的runloop,需要手动触发。触摸屏幕时,屏幕表面的事件会先包装成Event,event先告诉source1,source1唤醒runloop,然后将事件event分发给source0,然后由source0来处理。
2.Source1事件:基于mach_port的,来自系统内核或其他进程或线程的事件,可以主动的唤醒休眠中的Runloop
block事件
费延迟的performSelector,非延迟的dispatch_after,block回调。
Main_Dispatch_Queue事件
GCD中main queue的block会被dispatch到main loop执行。
知道了上面这些知识点,我们再来看下Runloop的处理逻辑流程图。

流程图.png

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

推荐阅读更多精彩内容