Runtime 實際運用

image.png

runloop和線程一一對應
runloop包含多個mode, mode包含多個 mode item(sources,timers,observers)
runloop一次只能運行在一個model下:
切換mode:停止loop -> 設(shè)置mode -> 重啟runloop
runloop通過切換mode來篩選要處理的事件,讓其互不影響
iOS運行流暢的關(guān)鍵

image.png
/// 核心函數(shù)
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {

        /// 通知 Observers: 即將處理timer事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);

        /// 通知 Observers: 即將處理Source事件
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)

        /// 處理Blocks
        __CFRunLoopDoBlocks(rl, rlm);

        /// 處理sources0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);

        /// 處理sources0返回為YES
        if (sourceHandledThisLoop) {
            /// 處理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }

        /// 判斷有無端口消息(Source1)
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            /// 處理消息
            goto handle_msg;
        }

        /// 通知 Observers: 即將進入休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);

        /// 等待被喚醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);

        // user callouts now OK again
        __CFRunLoopUnsetSleeping(rl);

        /// 通知 Observers: 被喚醒,結(jié)束休眠
        __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 if (被Source1喚醒) {
            /// 被Source1喚醒,處理Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
        }

        /// 處理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;
        }

    } while (0 == retVal);

    return retVal;
}
image.png

事件響應

當一個硬件事件(觸摸/鎖屏/搖晃/加速)發(fā)生后,首先IOKit.framework 生成一個IOHIDEvent事件并有SprintBoard接收,之后有mach_port轉(zhuǎn)發(fā)給需要的App進程。
蘋果注冊了一個Source1來接收系統(tǒng)事件,通過回調(diào)函數(shù)觸發(fā)Source0 (所以Event實際上是基于Source0的),調(diào)用_UIApplicationHandleEventQueue() 進行應用內(nèi)部的分發(fā)。_UIApplicationHandleEventQueue() 會把IOHIDEvent 處理并包裝成UIEvent 進行處理或分發(fā),其中包括識別UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給UIWindow等。

手勢識別

當上面的_UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用Cancel 將當前的touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對應的UIGestureRecognizer 標記為待處理。
蘋果注冊了一個Observer監(jiān)測BeforeWaiting(Loop即將進入休眠)事件,這個Observer的回調(diào)函數(shù)是_UIGestureRecognizerUpdateObserver(),其內(nèi)部會獲取所有被標記為待處理的GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。
當有UIGestureRecognizer的變化(創(chuàng)建、銷毀、狀態(tài)改變)時,這個回調(diào)都會進行相應的處理。

界面刷新

當UI發(fā)生改變時(Frame變化,UIView/CALayer的結(jié)構(gòu)變化)時,或手動調(diào)用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,這個UIView/CALayer就被標記為待處理。
蘋果注冊了一個用來監(jiān)聽BeforeWaiting和Exit的Observer,在他的回調(diào)函數(shù)里會遍歷所有待處理的UIView/CALayer來執(zhí)行實際的繪制和調(diào)整,并更新UI界面。

AutoreleasePool

主線程Runloop注冊了兩個Observers,其回調(diào)都是_wrapRunloopWithAutoreleasePoolHandler
Observers1 監(jiān)聽Entry事件: 優(yōu)先級最高,確保在所有的回調(diào)前創(chuàng)建釋放池,回調(diào)內(nèi)調(diào)用 _objc_autoreleasePoolPush()創(chuàng)建自動釋放池
Observers2監(jiān)聽BeforeWaiting 和Exit事件: 優(yōu)先級最低,保證在所有回調(diào)后釋放釋放池。BeforeWaiting事件:調(diào)用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()釋放舊池并創(chuàng)建新池,Exit事件: 調(diào)用_objc_autoreleasePoolPop(),釋放自動釋放池

Timer不被ScrollView的滑動影響

+timerWithTimerInterval ... 創(chuàng)建timer
[[NSRunLoop currentRunLoop] addTimer: timer forMode:NSRunLoopCommonModes] 把timer加到當前runloop,使用占位模式
runloop run/runUntilData 手動開啟子線程
使用GCD創(chuàng)建定時器,GCD創(chuàng)建的定時器不會受RunLoop 的影響。

// 獲得隊列
    dispatch_queue_t queue = dispatch_get_main_queue();

    // 創(chuàng)建一個定時器(dispatch_source_t本質(zhì)還是個OC對象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

    // 設(shè)置定時器的各種屬性(幾時開始任務,每隔多長時間執(zhí)行一次)
    // GCD的時間參數(shù),一般是納秒(1秒 == 10的9次方納秒)
    // 比當前時間晚1秒開始執(zhí)行
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));

    //每隔一秒執(zhí)行一次
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);

    // 設(shè)置回調(diào)
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);

    });

    // 啟動定時器
    dispatch_resume(self.timer);

GCD

dispatch_async(dispatch_get_main_queue)使用到了RunLoop
libDispatch向主線程的Runloop發(fā)送消息將其喚醒,并從消息中取得block,并在回調(diào)CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()里執(zhí)行這個block

NSURLConnection

使用 NSURLConnection 時,你會傳入一個 Delegate,當調(diào)用了 [connection start] 后,這個 Delegate就會不停收到事件回調(diào)。
start 這個函數(shù)的內(nèi)部會會獲取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發(fā)的Source)。 CFMultiplexerSource 是負責各種 Delegate 回調(diào)的,CFHTTPCookieStorage 是處理各種 Cookie 的。
當開始網(wǎng)絡(luò)傳輸時,我們可以看到 NSURLConnection 創(chuàng)建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 線程是處理底層 socket 連接的。NSURLConnectionLoader 這個線程內(nèi)部會使用 RunLoop 來接收底層 socket 的事件,并通過之前添加的 Source0 通知到上層的 Delegate。

AFNetworking

使用runloop開啟常駐線程

NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];

給 runloop 添加NSMachPort port使runloop不退出,實際并沒有給這個port發(fā)消息

AsyncDisplayKit

仿照 QuartzCore/UIKit 框架的模式,實現(xiàn)了一套類似的界面更新的機制:即在主線程的 RunLoop 中添加一個 Observer,監(jiān)聽了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回調(diào)時,遍歷所有之前放入隊列的待處理的任務,然后一一執(zhí)行。

卡頓檢測

  • dispatch_semaphore_t 是一個信號量機制,信號量到達、或者 超時會繼續(xù)向下進行,否則等待,如果超時則返回的結(jié)果必定不為0,信號量到達結(jié)果為0。GCD信號量-dispatch_semaphore_t

通過監(jiān)聽mainRunloop的狀態(tài)和信號量阻塞線程的特點來檢測卡頓,通過kCFRunLoopBeforeSource和kCFRunLoopAfterWaiting的間隔時長超過自定義閥值則記錄堆棧信息。

RunLoop實戰(zhàn):實時卡頓監(jiān)控

FPS 檢測

創(chuàng)建CADisplayLink對象的時候會指定一個selector,把創(chuàng)建的CADisplayLink對象加入runloop,所以就實現(xiàn)了以屏幕刷新的頻率調(diào)用某個方法。
在調(diào)用的方法中計算執(zhí)行的次數(shù),用次數(shù)除以時間,就算出了FPS。
注:iOS正常刷新率為每秒60次。

@implementation ViewController {
    UILabel *_fpsLbe;

    CADisplayLink *_link;
    NSTimeInterval _lastTime;
    float _fps;
}

- (void)startMonitoring {
    if (_link) {
        [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        [_link invalidate];
        _link = nil;
    }
    _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsDisplayLinkAction:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)fpsDisplayLinkAction:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }

    self.count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    _fps = _count / delta;
    NSLog(@"count = %d, delta = %f,_lastTime = %f, _fps = %.0f",_count, delta, _lastTime, _fps);
    self.count = 0;
    _fpsLbe.text = [NSString stringWithFormat:@"FPS:%.0f",_fps];
}

防崩潰處理

NSSetUncaughtExceptionHandler(&HandleException);監(jiān)聽異常信號SIGILL,SIGTRAP,SIGABRT,SIGBUS,SIGSEGV,SIGFPE
回調(diào)方法內(nèi)創(chuàng)建一個Runloop,將主線程的所有Runmode都拿過來跑,作為應用程序主Runloop的替代

// 我的處理
LHLExceptionHelper *exceptionHandler = [LHLExceptionHelper new];
[exceptionHandler performSelectorOnMainThread:@selector(makeException:)
                                   withObject:exceptionTemp waitUntilDone:
- (void)makeException:(NSException *)exception {
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allModesRef = CFRunLoopCopyAllModes(runloop);

while (captor.needKeepAlive) {
    for (NSString *mode in (__bridge NSArray *)allModesRef) {
        if ([mode isEqualToString:(NSString *)kCFRunLoopCommonModes]) {
            continue;
        }
        CFStringRef modeRef  = (__bridge CFStringRef)mode;
        CFRunLoopRunInMode(modeRef, keepAliveReloadRenderingInterval, false);
    }
}

常駐線程

可以把自己創(chuàng)建的線程添加到Runloop中,做一些頻繁處理的任務,例如:檢測網(wǎng)絡(luò)狀態(tài),定時上傳一些信息等。

- (void)viewDidLoad {
    [super viewDidLoad];

    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run
{
    NSLog(@"----------run----%@", [NSThread currentThread]);
    @autoreleasepool{
    /*如果不加這句,會發(fā)現(xiàn)runloop創(chuàng)建出來就掛了,因為runloop如果沒有CFRunLoopSourceRef事件源輸入或者定時器,就會立馬消亡。
      下面的方法給runloop添加一個NSport,就是添加一個事件源,也可以添加一個定時器,或者observer,讓runloop不會掛掉*/
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

    // 方法1 ,2,3實現(xiàn)的效果相同,讓runloop無限期運行下去
    [[NSRunLoop currentRunLoop] run];
   }

    // 方法2
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

    // 方法3
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];

    NSLog(@"---------");
}

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

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

參考:
實時卡頓監(jiān)控
玩轉(zhuǎn)Runloop - 代碼示例使用Source, Observer, Timer
一文看完Runloop
深入理解Runloop
RunLoop實戰(zhàn):實時卡頓監(jiān)控
簡單監(jiān)測iOS卡頓的demo
關(guān)于dispatch_semaphore的使用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)自bireme,原地址:https://blog.ibireme.com/2015/05/18/runloop/...
    乜_啊_閱讀 1,673評論 0 5
  • RunLoop 的概念 一般來講,一個線程一次只能執(zhí)行一個任務,執(zhí)行完成后線程就會退出。如果我們需要一個機制,讓線...
    Mirsiter_魏閱讀 670評論 0 2
  • 轉(zhuǎn)自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飄金閱讀 1,065評論 0 4
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大餅炒雞蛋閱讀 1,259評論 0 6
  • 頁面上經(jīng)常彈出一些廣告,過了幾秒之后才出現(xiàn)關(guān)閉廣告按鈕(或者關(guān)閉按鈕才可點)或者剩余指定時間才給你跳過廣告(其實從...
    Yin先生閱讀 839評論 0 0

友情鏈接更多精彩內(nèi)容