RunLoop

作者: 六横六竖亚 | 来源:发表于2020-08-12 10:19 被阅读0次

    RunLoop的核心,主要是涉及到用户态和内核态的切换(mach_msg())。

    基本作用

    保持程序运行(main()的UIApplicationMain函数中会启动主线程的Runloop)

    处理事件(触摸、Timer)

    节省CPU资源,提高性能(切换到内核态,休眠线程,等待事件/消息)

    CFRunloopRef 对象

    typedef struct __CFRunloop *CFRunLoopRef;

    __CFRunloop属性:pthread-与线程一一对应;commonModes-模式名称的字符串集合;commonModeItems-通用模式下的事件源:Source01、Timer、Observer(Items一个都没有则直接退出Runloop);currentMode-当前模式;modes-Runloop中所有mode(default,tracking,common等)

    Runloop对象的获取

    _CFRunLoopGetMain & _CFRunLoopGetCurrent

    __CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&lock) 自旋锁; 

    第一次进入,初始化loopsDic,并为主线程创建一个Runloop(_CFRunLoopCreate()); 

    CFDictionaryGetValue(loopsDic,thread),根据thread获取CFRunloopRef;

    取不到就创建一个,CFDictionarySetValue,并注册回调销毁线程时顺便销毁对应的Runloop;

    OSSpinUnLock(&lock)自旋锁结束;}

    CFRunLoopModeRef 模式

    一个Runloop包含几个mode(但每次只能启动一个,所以互不影响),一个mode包含name,source0/1,timer、observer等若干事件源和监听,mode中没有事件源Runloop会马上退出。

    tracking下不去处理default中的source,做到了屏蔽效果,保证了trackingmode下,滚动的顺畅。

    CommonModes:所有mode都可以标记为Common,每当Runloop内容发生变化时,自动将_commonModeItems同步到所有带有标记的Mode下(预置的default和track已被标记为common)。

    例:把timer加入CommonModeItems自动更新同步到default和track,解决滚动时NSTimer不回调的问题。

    管理mode只有两个方法:addCommonMode(只能增加不能删除)和RunInMode。

    mode中的事件源和活动状态

    CFRunLoopSourceRef 输入源(结构体union中的version0/1分别对应Source0/1)

    Source0,只有一个回调指针(添加0到Runloop不会自动唤醒线程需手动wakeup,如触摸事件?、performSeletor: thread等);

    Source1,有一个mach-port和一个回调指针(基于Port的线程通信、系统事件捕捉)

    CFRunLoopTimerRef 定时器源

    和NSTimer是toll-free-bridge桥接的,包含一个时间长度和一个回调,加入到Runloop时会注册对应的时间点,到点唤醒;performSeletor: afterDelay(本质也是创建一个NSTimer加到Runloop中)。

    CFRunLoopObserverRef 活动状态

    __CFRunLoopObserver结构体中的_activities(CFOptionFlags,6种状态如下)保存了当前mode的活动状态,状态发生改变时,通过结构体中的_callout回调(CFRunLoopObserverCallBack)通知状态的观察者。

    Entry 即将进入

    BeforeTimers //即将处理Timers

    BeforeSource //即将处理Sources

    BeforeWaiting //即将入休眠(UI刷新和AutoreleasePool执行的触发状态)

    AfterWaiting //刚从休眠中唤醒

    Exit //即将退出

    mode管理item的接口有6个,add和remove各3个分别对应以上3个事件源。CFRunLoopAddSource/Timer/Obsever(rl, item, modeName),如果modeName没有则会创建一个。

    RunLoop的内部逻辑:事件循环机制

    主线程Runloop的启动过程  通过设置断点,LLDB中输入指令bt查看调用栈,发现UIApplicationMain函数中调用了CFRunLoopSpecific(RunLoop的入口)。

    CFRunLoopSpecific的实现

    → currentMode = __CFRunLoopFindMode(rl, modeName, false)(找本次mode,找不到或mode中没有任何事件则直接Finished)

    → __CFRunLoopDoObservers(rl, currentModel, Entry)(即将进入的通知)

    → result = __CFRunLoopRun(rl, currentModel, seconds, returnAfterSourceHandled, previousMode)

    → __CFRunLoopDoObservers(rl, currentMode, Exit)(即将退出的通知)

    __CFRunLoopRun(rl, mode, seconds, handler, previousMode)的实现

    → __CFRunLoopDoObservers(rl, rlm, BeforeTimers)、(rl, rlm, BeforeSources); __CFRunLoopDoBlocks(rl, rlm);(发出即将处理Tmers,Sources的通知,响应Blocks)

    → __CFRunLoopDoSources0,__CFRunLoopDoBlocks(处理Sources0,并响应blocks)

    → 判断如果有Source1则goto handle_msg(处理source1的逻辑)

    → __CFRunLoopDoObservers(rl, rlm, BeforeWaiting)(发出即将进入休眠的通知)→ __CFRunLoopSetSleeping(rl); 

    → __CFRunLoopServiceMachPort(正式进入休眠,并等待消息来唤醒线程)

    注:休眠时,其中调用了mach_msg函数从用户态切换到了内核态,等待消息,避免CUP资源占用。有消息处理时,立刻唤醒线程,调用mach_msg切回用户态。

    handle_msg的实现:

    → int retVal = 0,do-while循环

    → __CFRunLoopDoTimers,__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__,__CFRunLoopDoSource1(被谁唤醒就处理谁)

    → __CFRunLoopDoBlocks(处理Block)

    → __CFRunLoopModeIsEmpty ? retVal = kCFRunLoopRunFinished;(根据判断条件,标记kCFRunLoopRunHandledSource=4、TimedOut=3、Stopped=2、Finished=1等返回值,return retVal (!=0))

    总结:__CFRunLoopRun方法内部的逻辑,先发出即将处理事件源Timer/Sources的通知,doBlocks,处理Source0和Source1(如果有,跳转handle_msg),发出即将进入休眠的通知,machPort转移控制权到内核态,并sleep进入休眠。最后Afterwaiting从休眠中唤醒的通知,开始新一轮。

    Runloop与线程

    1、与线程一一对应,保存在全局的Dict中,线程(pthread)为key,runloop为value;

    2、runloop为线程保活否则线程执行完任务就会退出(主线程执行完main()程序也会退出);

    3、runloop创建时机:线程刚创建时没有,第一次CFDictionaryGetValue获取它的时候创建(主线程在UIApplicationMain函数中通过[NSRunLoop currentRunLoop]自动创建,子线程默认没有开启runloop);

    4、销毁时机:线程结束时销毁。创建一个线程(子线程)并执行Test任务,[thread start]后线程会被销毁;runloop注册时会同时注册销毁的回调,当线程被销毁时执行。

    5、常驻子线程:为子线程创建Runloop(addPort+addSource添加事件来维持循环→[run] / [runMode beforeDate]启动,注意,run和runUntilDate会永久运行,stop也无法停止),runMode:beforeDate可以用stop退出:在performSelector: onThread中,执行CFRunLoopStop(CFRunLoopGetCurrent());。

    添加Source事件源

    CFMessagePortRef localPort = CFMessagePortCreateLocal(nil, CFSTR("com.example.app.port.server"), Callback, nil, nil);

    CFRunLoopSourceRef runLoopSource = CFMessagePortCreateRunLoopSource(nil, localPort, 0);

    CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);

    CF方法:创建port,创建source,addSource到currentRunLoop

    NS方法:[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; // 子线程,别忘了手动run/runMode

    或使用信号量的方式

    dispatch_semaphore_t sem;

    sem = dispatch_semaphore_create(0);

    dispatch_semaphore_signal发送信号;

    while(true)循环内dispatch_semaphore_wait(sem, -1)等待信号,并执行:

    dispatch_block_t block = [actions firstObject]; if (block) { [actions removeObject: block]; block(); }

    6、GCD:dispatch_async(main)时,libDispatch会向主线程的RunLoop发消息,RunLoop会被唤醒并从消息中取得要执行的任务的block,并在回调中执行这个block。

    注:此逻辑只限于dispatch到主线程,其他线程仍是libDispatch自己处理的。

    Runloop与NSTimer

    NSTimer 其实就是 CFRunLoopTimerRef,他们之间是toll-free bridged(等价桥接)的。

    原理:在重复的时间点注册事件,RunLoop为了节省资源不会非常准确的回调(Tolerance宽容度,允许的时间误差)。

    1、子线程上使用定时器:[[NSRunLoop currentRunLoop] run],手动启动子线程Runloop(invalidate时也要在子线程);

    2、解决NSTimer在滚动时停止工作的问题:NSTimer默认加入DefaultMode,而滚动时的TrackingMode下,不会处理DefaultMode的消息,所以应设置为CommonMode,同步到所有mode包括Default和Tracking。

    3、performSelector:afterDelay会内部创建一个Timer并加到当前线程的runloop中;onThread也会内部创建一个Timer并加到线程中。两种方法在当前线程没有启动Runloop时都会失效(如在未创建和启动Runloop的子线程中调用,回到1)

    4、NSTimer的不准时:当RunLoop的任务繁重时,每次循环都会查看是否到达Timer设置的时间点,当循环时间过长时会导致超过Timer设置的时间,则不会延后执行回调而是等待下一个时间点。可以用GCD的定时器,不依赖Runloop直接与系统内核挂钩(dispatch_source_create→dispatch_source_set_timer→dispatch_source_set_event_handler→dispatch_resume)

    注1:scheduledTimerWithTimeInterval自动添加到默认的mode下;timerWithTimeInterval不会添加到runloop中,需要手动添加并指定common模式,以避免滚动mode下停止工作的问题。

    注2:NSTimer、CADisplayLink、dispatch_source_t的区别和优劣。

    注3:CADisplayLink,和屏幕刷新率一致的定时器(内部实际是操作了一个Source),也需要添加到RunLoop,如果两次刷新之间有耗时任务,则会造成页面丢帧卡顿。

    AutoreleasePool

    主线程的RunLoop中注册了两个Observer:1、监听kCFRunLoopEntry,调用objc_autoreleasePoolPush创建自动释放池,优先级较高,保证在所有其他回调前;2、监听了kCFRunLoopBeforeWaiting,进入休眠前先调pop释放旧池再调push创建新池、和kCFRunLoopBeforeExit,调用pop释放自动释放池优先级低,保证在其他所有回调后。

    主线程的代码一般是在事件/Timer回调内的,所以会被自动创建的autoreleasePool环绕,不会出现内存泄漏。但有些情况下比如for循环中创建大量临时对象,需要手动添加@autoreleasePool在下一次RunLoop前就处理这些对象。注:enumerateObjectsUsingBlock的容器循环内,默认添加了pool。

    事件响应和手势识别

    IOKit捕捉到事件后处理成IOHIDEvent通过mach port发送给SpringBoard,再通过mach port发送给当前前台运行的app,触发了app主线程RunLoop的Source1监听并触发Source0的回调,拿到了事件后,在回调内部封装为UIEvent(手势的封装和识别也在这一步),然后调用UIApplication的sendEvent传递给UIWindow,开始了事件的传递链,寻找最佳响应者。

    事件的Source0回调中,处理事件时如果识别成Gesture手势,则标记手势对象为待处理,监听到BeforeWating事件时,会获取所有标记待处理的手势执行手势回调,每当手势变化时,都会进行update。

    UI的绘制和刷新

    更改frame、UI层级,或手动调用setNeedsLayout/Display后,UIView/CALayer将被标记为待处理并提交到全局容器中,在BeforeWaiting和Exit回调中执行一个callback函数,方法会遍历所有待处理的UI并执行绘制和调整,更新页面。

    网络请求

    网络请求接口的四层封装

    1、CFSocket:最底层的接口,只负责Socket通信

    2、CFNetwork:基于CFSocket的封装

    3、NSURLConnection:基于CFNetwork的封装,面向对象,AFNetworking在这层

    4、NSURLSession:iOS7新增,表面和3并列,底层实际还是用到了3部分功能,AFNetwork2,Alamofire在这层

    NSURLConnection的工作原理:[connection start],start函数会获取CurrentRunLoop,并在Default里CFRunLoopAddSource加4个Source0(参见RunLoop与线程中的5、创建常驻线程)。CFMultiplexerSource负责各种请求的回调,CFHTTPCookieStorage负责处理Cookie。

    网络开始传输时,NSURLConnection创建了com.apple.CFSocket.private和com.apple.NSURLConnectionLoader两个新线程,CFSocket处理底层Socket连接发个Source1事件,NSURLConnectionLoader接收这个Source1(基于mach Port),并通过之前添加的Source0通知到上层delegate,唤醒回调线程的RunLoop,执行实际的回调。

    AFNetwork

    AFURLConnectionOperation:基于NSURLConnection,单独创建了一个线程并启动了一个RunLoop,以便在后台线程接收Delegate回调。

    相关问题

    RunLoop的基本作用

    主线程RunLoop保持程序运行;处理事件/Timer;通过调用mach_msg转移线程控制权切换内核态,进入休眠,节省CPU资源。

    Runloop对象的数据结构

    一个RunLoop对象有多个mode,如defaultMode、trackMode和commonMode标记,每个mode可以处理多个source和Timer,回调多个Observer。

    整个RunLoop机制的运行过程,哪些事件源可以唤醒RunLoop

    Entry通知,RunLoop运行(source和timer通知,处理blocks,处理source0.1,然后handle_msg内执行dowhile循环,被谁唤醒处理timer,GCD,Source1),Exit通知

    获取RunLoop的两个方法和实现;管理Mode的两个方法和用处;管理ModeItem的6个方法和用处CFRunLoopGetMain/Current:从线程-RunLoop的字典中获取,获取不到就创建一个,并注册跟随线程销毁的回调。主线程会在UIApplicationMain中获取主线程RunLoop默认开启。

    addCommonMode/runInMode方法:添加自定义mode和将事件或Timer放入指定mode。

    CFRunLoopAddSource/Timer/Obsever(rl, item, modeName),如果modeName没有则会创建一个。

    RunLoop和线程的关系及生命周期,主线程创建RunLoop的过程,如何实现一个常驻子线程(线程保活)

    与线程一一对应;主线程RunLoop会在Main函数时通过currentRunLoop创建并开启;

    在子线程内addPort / addSource维持事件循环,run开启后常驻  或  使用信号量的方式:while(true)循环内执行dispatch_semaphore_wait(sem, -1)等待信号,收到信号执行block。

    NSTimer的原理和劣势;有哪些添加NSTimer的方法,分别有什么问题;能在子线程上使用NSTimer吗;PerformSelector的原理

    NSTimer和CFRunLoopTimerRef是toll-bridge的,注册时间点每次RunLoop时检查,为了性能有一定的宽容度,有耗时任务时将不会准时回调;scheduled开头的timer添加方法会加到defaultMode下,track时不回调,而timerWithTimeInterval不会加到RunLoop中,需要手动添加[[NSRunLoop mainRunLoop] addTimer: timer forMode:NSRunLoopCommonModes];。PerformSelector的onThread和afterDelay都是添加一个timer到runLoop中,子线程中不会默认启动。

    主线程中是如何通过RunLoop添加AutoreleasePool的,子线程呢

    监听Entry调用Push创建,BeforeWaiting的回调中Pop并再push一个新的,于是主线程代码中的对象被pool包围,不会内存泄露。子线程中,iOS7前需要手动开启,后来会通过自动创建hotPoolPaged把对象加入page。

    RunLoop和UI刷新的关系(绘制计算和提交渲染的时机)

    被标记需要刷新的UI会在BeforeWaiting/Exit的回调中,CPU计算处理提交GPU渲染。

    RunLoop是如何接收系统事件(识别手势),并开启传递链的

    IOKit,SpringBoard,分发给APP,通过mach port触发主线程Source1并执行Source0回调,内部处理成UIEvent,调用UIApplication的sendEvent发给UIWindow开始事件的传递。

    相关文章

      网友评论

          本文标题:RunLoop

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