OC底层->RunLoop

254 阅读7分钟

简介

  • 顾名思义
    • 运行循环
    • 在程序运行过程中循环做一些事情

image.png

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

加入没有RunLoop

image.png

  • 执行完第13行代码后,会即将退出程序

有了RunLoop

image.png

  • UIApplicationMain 创建了一个runLoop对象
    • 假如我们删除了这段代码 点击开启APP会立刻闪退。 image.png
  • 伪代码猜想这个 RunLoop 的原理
    • retVal一直为0 程序才不会退出
    • 让线程睡觉并等待
    • 当程序发生事件就会唤醒并处理这个事件
    • 处理完成后返回0 继续循环程序一直不会退出 image.png
  • 程序并不会马上退出,而是保持运行状态
  • RunLoop的基本作用
    • 保持程序的持续运行
    • 处理App中的各种事件(比如触摸事件、定时器事件等)
    • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
    • ......

RunLoop对象

  • iOS中有2套API来访问和使用RunLoop
    • Foundation:NSRunLoop
    • Core Foundation:CFRunLoopRef
  • NSRunLoop和CFRunLoopRef都代表着RunLoop对象

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value image.png
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
    • UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 这个我们可以理解为一开始创建主线程的时候是没有runloop的,但是他同时调用了[NSRunLoop currentRunLoop]主动获取(创建)了主线程的runloop。

获取RunLoop对象

  • Foundation

    • [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    • [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
  • Core Foundation

    • CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    • CFRunLoopGetMain(); // 获得主线程的RunLoop对象
  • 源码探究

    image.png

    • 1.runloop 从字典中根据线程作为key返回
    • 2.加入loop为空 就创建一个newloop 赋值给这个loop存入字典中

image.png

RunLoop相关的类

  • Core Foundation中关于RunLoop的5个类
    • CFRunLoopRef
    • CFRunLoopModeRef
    • CFRunLoopSourceRef
    • CFRunLoopTimerRef
    • CFRunLoopObserverRef
  • 简化有用的部分 image.png
    • _pthread : 线程 所以说一个线程一个runloop
    • CFMutableSetRef : 集合 相当于数组 但是数组是有序的,集合是无序的
    • _modes : 里面装有的数据是CFRunLoopModeRef
    • _currentMode : 当前模式
    • runloop 中有许多的mode放在modes里但是只有一个mode被认为是currentMode,猜测是从中取出一个mode已经响应后再取出别的响应。 image.png
    • _name : 模式的名称
    • 4个集合
    • _sources0 和 _source1 是 CFRunLoopSourceRef的集合对象
    • _observers 是 CFRunLoopObserverRef的集合对象
    • _timers 是 CFRunLoopTimerRef的集合对象 image.png

CFRunLoopModeRef

  • 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下运行
      • runloop的默认模式
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
      • runloop的滑动模式

CFRunLoopObserverRef

image.png

添加Observer监听RunLoop的所有状态

image.png

RunLoop的运行逻辑

image.png

  • Source0
    • 触摸事件处理
    • performSelector:onThread:
  • Source1
    • 基于Port的线程间通信
    • 系统事件捕捉
  • Timers
    • NSTimer
    • performSelector:withObject:afterDelay:
  • Observers
    • 用于监听RunLoop的状态
    • UI刷新(BeforeWaiting)
    • Autorelease pool(BeforeWaiting)
  • 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

我的类比理解 这是我的理解可以不作为参考。

  • 通俗的来讲就是说runloop相当于一台机器,机器有各种模式。
  • 各个模式里面都有4个数组,类比4个旋钮吧。4个旋钮调节是不会互相影响的,可以调节旋钮进行自定义。
  • 当你要开启机器时要先选择一种模式,作为当前模式运行机器,想要切换模式要先停止当前模式才能切换。
  • 如果你选择的模式4个旋钮值都是0,机器自动关闭,进入随眠状态等待事件的激活。
  • 第4个旋钮不仅能控制机器执行某些动作,还能监听机器运行的状态
  • runloop中循环做的事情
    • 进入某种模式,把模式先的source0、source1、timers、observer取出来执行

源码分析

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
	//通知observers : 进入loop
 	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); 
	//具体要做的事情
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	//通知obsercers:退出Loop
	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
   return result;
}


/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();
   int32_t retVal = 0;
    do {
		//通知obsercers:即将处理timers
		__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

		//通知obsercers:即将处理sources
		__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
		//通知obsercers:即将处理block

		__CFRunLoopDoBlocks(rl, rlm);

		//处理sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
			//处理block
            __CFRunLoopDoBlocks(rl, rlm);
		}

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
           
		//判断有无source1
		if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
			//如果有source1,跳转到handle_msg
           goto handle_msg;
        }
	}
	//通知Observers:即将休眠
	__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
	//开始休眠
	__CFRunLoopSetSleeping(rl);
       
	CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
 	   
	//等待别的消息来唤醒当前线程        
	__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

	//唤醒
	__CFRunLoopUnsetSleeping(rl);
	//通知observer:结束休眠 
 	__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        handle_msg:;

       if (被timer唤醒) {
			//处理timers
			__CFRunLoopDoTimers(rl, rlm, mach_absolute_time()))
       }
        else if (被GCD唤醒) {
			//处理GCD
           __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);

        } else {
		//source1唤醒
		//处理source1 
  		__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
		}
       //处理block
	__CFRunLoopDoBlocks(rl, rlm);
        
	//设置返回值
	if (sourceHandledThisLoop && stopAfterHandle) {
	    retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
	} else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
	    retVal = kCFRunLoopRunStopped;
	} else if (rlm->_stopped) {
	    rlm->_stopped = false;
	    retVal = kCFRunLoopRunStopped;
	} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
	    retVal = kCFRunLoopRunFinished;
	}
        
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);

    } while (0 == retVal);

    return retVal;
}

runLoop执行流程

image.png

RunLoop休眠的实现原理

  • 等待别的消息来唤醒当前线程 开始睡觉 :线程堵塞不会往下走了

  • 不同于 while(1)这样的线程阻塞,但是这样的线程并没有真正的休息 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

  • 这个就是内核层面的API 让线程休眠,传递消息到内核层面CPU休息,达到省电的功能。

  • 我们只能控制的是页面层面的API image.png

image.png

RunLoop在实际开中的应用

  • 控制线程生命周期(线程保活)
  • 解决NSTimer在滑动时停止工作的问题
  • 监控应用卡顿
  • 性能优化

应用1 解决NSTimer在滑动时停止工作的问题

  • 1.创建一个计时器 image.png
  • 放置一个textView image.png
  • 运行后发现当我们拖动textView的时候计时器是停止计时的
  • 原理分析
    • runloop 同一时间只能运行一种模式。
    • 定时器不在拖拽模式下,是在默认模式中工作的。
    • 如果定时器在拖拽模式下就可以运行了。
  • 将计时器放入拖拽模式中也在默认模式中 就放到NSRunLoopCommonModes
  • scheduledTimerWithTimeInterval 这个方法默认将计时器放到默认模式中
- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
    // NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
    // timer能在_commonModes数组中存放的模式下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    
//    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
//        NSLog(@"%d", ++count);
//    }];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end