RunLoop

作者: 越天高 | 来源:发表于2020-10-30 09:15 被阅读0次

01基本认识

什么是RunLoop
顾名思义
运行循环
在程序运行过程中循环做一些事情
当我一启动IOS程序的时候,他内部就会创建一个RunLoop对象,循环做一些事情

  • 应用范畴
    定时器(Timer)、PerformSelector
    GCD Async Main Queue
    事件响应、手势识别、界面刷新
    网络请求
    AutoreleasePool

如果没有RunLoop程序执行完main函数就会退出,
为了保证程序一直在运行之后,他就引入了RunLoop,当我们执行main函数调用UIApplicationMain的时候,就会创建RunLoop对象。
他内部大概是这个样的

 int retVal = 0;
    do {
        //睡眠中等待消息
        int message = sleep_and_wait();
        //处理消息
        retVal = process_message(message)
    } while (retVal == 0);

当执行完main之后,程序不会马上退出,而是保持运行状态
RunLoop的基本作用
保持程序的持续运行
处理App中的各种事件(比如触摸事件、定时器事件等)
节省CPU资源,提高程序性能:该做事时做事,该休息时休息

02获取RunLoop对象

IOS没有RunLoop会马上退出,执行main返回0,程序立即退出。
RunLoop有两个,一个是OC的一个是C语言的

    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    CFRunLoopRef runloopR = CFRunLoopGetCurrent();

iOS中有2套API来访问和使用RunLoop
Foundation:NSRunLoop
Core Foundation:CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装
CFRunLoopRef是开源的
https://opensource.apple.com/tarballs/CF/

Runloop与线程的关系
1.每条线程都有唯一的一个与之对应的RunLoop对象
2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
3. 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
4.RunLoop会在线程结束时销毁
5. 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

    NSLog(@"%p--%p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
    0x600001e6cc00--0x600001e6cc00
    NSLog(@"%p--%p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
 0x600000660000--0x600000660000

打印结果不一样是因为NSRunLoop是对CFRunLoop的包装,他里面存放着CFRunLoop,一个线程对应唯一一个runloop
通过查看源码,我们可以看出当我们执行CFRunLoopGetCurrent-> CFRunLoopGet0->里面有个CFGetDictionaryValue取RunLoop,根据一个thread的key,如果一开始runloop对象不存在,他就创建一个runloop,然后把它添加到字典之中

03CFRunLoopModeRef

如果要寻做一些事,仅仅有这个RunLoop对象是不行,他还要以来其他的对象

Core Foundation中关于RunLoop的5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

CFRunLoop的结构


CFRunLoop

可以看出,他和线程是一一对应的关系,他有一个集合存放着他的mode,这里面只有一个mode是currentMode,
mode的结构


mode

source0和source1里面装的CFRunLoopSourceRef这种对象
observers里面装的是CFRunLoopObserverRef这种对象

内存结构
  • CFRunLoopModeRef代表RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode,作为currentMode
  • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
  • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
  • 常见的2种Mode
    kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

04CFRunLoopModeRef的成员

通过控制台bt可以打印函数调用栈

 Source0
    触摸事件处理
    performSelector:onThread:
    
    Source1
    基于Port的线程间通信
    系统事件捕捉,比如点击屏幕产生的事件就是他捕捉到的,然后再交给source0处理
    
    Timers
    NSTimer
    performSelector:withObject:afterDelay:
    
    Observers
    用于监听RunLoop的状态
    UI刷新(BeforeWaiting),当前听器间听到runloop要睡觉了,他就会进行一次UI页面刷新
比如设置颜色,当你值完代码,他先保留住,一旦监听到睡觉,就是刷新
    Autorelease pool(BeforeWaiting)//他在什么时候释放对象,一旦监听到要睡觉了,就清理一下自动释放的对象

05CFRunLoopObserverRef

Observer

CFRunLoopObserverRef

监听runloop的状态Observer,没有OC的API我们只能通过CF的函数形式添加Observer
手动创建一个Observer监听runloop的状态

void observerCall(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),
        kCFRunLoopBeforeTimers = (1UL << 1),
        kCFRunLoopBeforeSources = (1UL << 2),
        kCFRunLoopBeforeWaiting = (1UL << 5),
        kCFRunLoopAfterWaiting = (1UL << 6),
        kCFRunLoopExit = (1UL << 7),
        kCFRunLoopAllActivities = 0x0FFFFFFFU
    };
    //监听状态
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
            
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
                
            break;

        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;

        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;

        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    //创建一个observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observerCall, NULL);
    //添加一个observer
    //kCFRunLoopCommonModes
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    
}

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

监听模式的改变,可以拖一个scrollView拖动一下改变的runloop的mode

  //监听状态
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry---%@",CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit---%@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
            break;
        default:
            break;
    }
//log
 kCFRunLoopExit---kCFRunLoopDefaultMode
2020-10-26 21:56:57.104392+0800 RuntimeIsa[1573:89315] kCFRunLoopEntry---UITrackingRunLoopMode
2020-10-26 21:56:57.315033+0800 RuntimeIsa[1573:89315] kCFRunLoopExit---UITrackingRunLoopMode
2020-10-26 21:56:57.315199+0800 RuntimeIsa[1573:89315] kCFRunLoopEntry---kCFRunLoopDefaultMode

06答疑

循环做把mode里面source拿出来处理,
source里面的东西是不固定的

07执行流程图

在IOS我们表面上能看到的Runloop就是applicationMain
要想看到他具体做了什么事情,就要分析源码,但是源码太过于抽象,纯c。可以先看下总结图片。他大概就是不断的切换mode,然后处理不同模式喜爱的source0 ,source1,timer等
handleport就是source1 可能是别的线程发过来的信息


Runloop工作流程
运行逻辑

08-源码分析

通过打断点,在控制台输入bt指令,来打印省略的步骤可以查看到他调用了一个CFRunLoopSpecific的函数->CFRunloopRun这个函数里面就是他在做的处理,就是一个do-while循环

__CFRunLoopRef  rl,  CFRunLoopModeRef rum
int32_t retVal = 0;

do
{
    //通知Obervers,即将处理Timers
__CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeTimers);
    //通知Obervers,即将处理Sources
__CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeSources);  
//处理Blocks
__CFRunLoppDoBlocks(rl,rlm)

//处理source0
if (__CFRunloopDoSource0(rl,rlm,stopAfterHandle))
{
//处理block;
}

}while(0 == retVal);

//判断有无source1,如果有就跳到,handle_msg

//通知observer即将进入休眠
__CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeSleeping);
__CFRunLoopSetSleeping(rl);

//等待别的消息来唤醒当前线程
__CFRunLoopServiceMachPort

handle_msg:
if(被time唤醒)
{
//处理timers
}else if(被GCD唤醒)
{
//处理GCD
}else
{//被source1唤醒,
//处理source1
}
//处理block

//设置返回值

如果返回值retVal还是 == 0;
那么就会回到开头从新循环

09调用细节

我们平时所做的UI界面刷新,timer定时器的监听,自动释放池都是疣runloop来做控制的,它主要做这几件事件,1 doObservers, 2 doBlocks 3. doSource03. doSource01 4doTimers5,处理gcd 6 休眠,这些函数里面还会调用一个__CFRUNLOOP_IS_CALLING_TO开头的函数

  • GCD有自己的逻辑,他很多东西不依赖RunLoop,他有一种情况会交给runloop处理
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //处理子线程的逻辑
        
        dispatch_async(dispatch_get_main_queue(), ^{
           //回到住线程刷新界面
        });
        
    });

01、通知Observers:进入Loop
02、通知Observers:即将处理Timers
03、通知Observers:即将处理Sources
04、处理Blocks
05、处理Source0(可能会再次处理Blocks)
06、如果存在Source1,就跳转到第8步
07、通知Observers:开始休眠(等待消息唤醒)
08、通知Observers:结束休眠(被某个消息唤醒)
01> 处理Timer
02> 处理GCD Async To Main Queue
03> 处理Source1
09、处理Blocks
10、根据前面的执行结果,决定如何操作
01> 回到第02步
02> 退出Loop
11、通知Observers:退出Loop

10-休眠的细节

开始睡觉意味着当前线程不做事情了,相当于阻塞了,这个阻塞和我们所写的while(1)(并没有休眠,线程还是在做事情)这种不一样,他是线程休息了什么代码也不执行了
通过调用mach_msg来调用内核层面的API实现线程的休眠的目的
当我们在用户态执行了mach_msg函数之后,他会进入内核态,调用内核态的mach_msg的实现,来达到休眠的目的。没消息就让线程休眠,有消息就唤醒线程。回到用户态处理消息,通过状态切换真正达到了让线程休息


状态切换

runloop是怎么响应用户的操作的?
首先它是由source1来把用户事件捕捉,点击。source1会把这个事件,包装成事件队列,放到事件队列里面去。事件队列又是在source0里面处理的

说说runloopd的几种状态
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities =
runloop的mode作用是什么?
常见的2种Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
mode可以将不同的source0/source1/t/o隔离开来。他们之间相互隔离不受影响

11NSTimer失效

runloop实际开发中的应用,
控制线程生命周期(线程保活),有时候我们不希望子线程运行完就销毁,
解决NSTimer在滑动时停止工作的问题
监控应用卡顿
性能优化

默认下的NSTimer我们一拖拽UI控件,他就会停止运行,因为我们在拖拽的时候,runloop切换了mode,定时器是在默认模式下工作的

//用这个方法创建的定时器是加到默认模式下的
    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer)
    {
        NSLog(@"%i", ++count);
    }];

相关文章

网友评论

      本文标题:RunLoop

      本文链接:https://www.haomeiwen.com/subject/ijdzmktx.html