iOS之RunLoop详解

96
威廉云霄
0.5 2019.03.12 16:39 字数 1164

1.RunLoop的介绍:

RunLoop即运行循环(跑圈),只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 RunLoop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,RunLoop 会进入休眠状态,有事件发生时, RunLoop 会去找对应的 Handler 处理事件。RunLoop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

2.RunLoop的基本作用:

  • 保持程序的持续运行
  • 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
  • 节省CPU资源,提高程序性能,该工作的时候工作,该休息就休息

3.iOS程序入口与RunLoop的关系:

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

由于main函数里面UIApplicationMain启动了一个RunLoop,所以UIApplicationMain函数一直没有返回,则程序并不会马上退出,而是保持持续运行状态,这个默认的RunLoop是跟主线程相关联的。

4.RunLoop对象:

  • NSRunLoop(OC中Foundation框架下)
  • CFRunLoopRef(C中Core Foundation框架下)

5.RunLoop与线程:

  • 每条线程都有唯一一个与之对应的RunLoop对象(字典的方式)。
  • 主线程的RunLoop默认已经创建并且开启了,子线程对应的RunLoop需要手动创建并开启。
  • RunLoop在第一次获取时创建,在线程结束时销毁。
  • RunLoop与线程的关系如下图


    runLoop.jpg

6.获取RunLoop对象:

    //Foundation框架下
    NSRunLoop * currentRunLoop =  [NSRunLoop currentRunLoop]; //获取当前线程的RunLoop对象
    NSRunLoop * mainRunLoop = [NSRunLoop mainRunLoop]; //获取主线程的RunLoop对象
    
    //Core Foundation框架下
    /*
    CFRunLoopRef * currentRunLoop =  CFRunLoopGetCurrent() //获取当前线程的RunLoop对象
    CFRunLoopRef * mainRunLoop = CFRunLoopGetMain() //获取主线程的RunLoop对象
    */

7.RunLoop的相关类:

  • CFRunLoopRef--RunLoop本身
  • CFRunLoopModeRef--RunLoop运行模式(CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Model,每个Model又包含若干个Source/Timer/Observer,每次RunLoop启动时,智能指定其中一个Model,这个Model被称作CurrentMode,如果需要切换Model,只能退出Loop,在重新指定一个Model进入,这样做主要是为了分隔开不同的Source/Timer/Observer,让其互不影响)
  • CFRunLoopSourceRef--RunLoop的事件(input Source)
  • CFRunLoopTimerRef --(Timer Source)
  • CFRunLoopObserverRef--监听者

CFRunLoopModeRef的详解:

  • 系统默认注册了5个Model
  • kCFRunLoopDefaultMode :App的默认model,通常主线程是在这个model下进行
  • UITrackingRunLoopMode :界面跟踪model,用于scrolView追踪触摸滑动,保证界面滑动时不受其他model影响
  • UIinitializationRunLoopMode :在刚启动App时进入的第一个Mode,启动完成后不在使用
  • GSEventReceiveRunLoopMode :接受系统事件的内部mode,通常用不到
  • kCFRunLoopCommonModes :这是一个占位mode,不是一个真正的mode

8.RunLoop的运行模式和Timer:

-(void)timer{
    //01-创建定时对象
    NSTimer * timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //02-把定时器添加到RunLoop中(切记:如果当前方法在子线程,需要手动开启当前子线程的RunLoop,并且开启,而且开启方法要在添加定时器之后,否则开启了因为没有事件就直接停止了)
    //(情况1)模式为默认:NSDefaultRunLoopMode--当滚动UItextView的时候定时器停止工作
    //[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //(情况2)界面追踪模式为:UITrackingRunLoopMode--(只有当滚动UItextView的时候定时器才工作)
    //[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    //(情况3)界面追踪模式为:NSRunLoopCommonModes(NSDefaultRunLoopMode|UITrackingRunLoopMode)--(不管有没有滚动,定时器一直工作)
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
}
-(void)run{
    NSLog(@"%@",[NSRunLoop currentRunLoop].currentMode);
}

9.CFRunLoopObserverRef的详解:

  • CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变。
  • 可以监听的时间点有以下:
     kCFRunLoopEntry = (1UL << 0), //即将进入Loop
     kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
     kCFRunLoopBeforeSources = (1UL << 2), //即将处理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
     kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
     kCFRunLoopExit = (1UL << 7), //即将退出loop
     kCFRunLoopAllActivities = 0x0FFFFFFFU //所有的状态
    
/*
     01-创建观察者
     参数说明
     第一个参数:分配存储空间
     第二个参数:要监听的状态
     第三个参数:是否持续监听
     第四个参数:0
     第五个参数:block回调,当runloop状态改变的时候会调用
     输出:
     2019-03-12 15:13:08.598453+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
     2019-03-12 15:13:08.598653+0800 SmallProgram[3374:145215] runloop即将处理Sources事件
     2019-03-12 15:13:08.598906+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
     2019-03-12 15:13:08.599048+0800 SmallProgram[3374:145215] runloop即将处理Sources事件
     2019-03-12 15:13:08.599180+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
     2019-03-12 15:13:08.599295+0800 SmallProgram[3374:145215] runloop即将处理Sources事件
     2019-03-12 15:13:08.599620+0800 SmallProgram[3374:145215] runloop即将进入到休眠
     2019-03-12 15:13:08.693981+0800 SmallProgram[3374:145215] runloop被唤醒
     2019-03-12 15:13:08.694216+0800 SmallProgram[3374:145215] runloop即将处理Timers事件
     2019-03-12 15:13:08.694340+0800 SmallProgram[3374:145215] runloop即将处理Sources
     */
    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;
        }
    });
    /*
    //02-监听runLoop的状态
     参数说明
     第一个参数:runLoop对象
     第二个参数:监听者
     第三个参数:runLoop在哪种模式下的状态(这里要写C情况下的状态)
               kCFRunLoopDefaultMode
    */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

10.RunLoop的应用:

  • (情景一)在tableView头视图上添加定时器轮播的时候,如果想要滑动tableView的时候,也要轮播,则修改runLoop的模式为NSRunLoopCommonModes
  • (情景二)定时器一直是准确运行的来执行对应的操作,则修改runLoop的模式为NSRunLoopCommonModes
  • 设置程序的常驻线程。
-(void)application{
    
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [thread start];
    self.thread = thread;
    
}
-(void)run{
    
    //01子线程的RunLoop需要手动创建并启动
    //02RunLoop启动后会选择运行模式,判断运行模式是否为空
    NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
    //(方法一)往运行模式中添加timer
    //[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeRun) userInfo:nil repeats:YES];
    //(方法二)往运行模式中添加source=port|custom|selector
    [runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    [runLoop run];
    
}
//将下面方法放到需要子线程来做任务的事件中,比如点击按钮事件
//按钮点击事件让子线程来执行任务run2
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:YES];
//这样self.thread会一直不死,来等待处理run2的事件

11.RunLoop的内部自动释放池:

  • App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
  • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  • 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。


    具体流程

本文参考:
线程、自动释放池、RunLoop的爱恨情仇https://www.jianshu.com/p/8b011b844231

iOS知识学习总结