iOS 漲薪: Run Loop 面試題

Run Loopgit

運行循環github

app 程序只有不停地運行, 才能不斷響應用戶的操做

Run Loop 兩大功能:數組


  • 睡眠中,等待消息
  • 處理消息

從睡眠中 -> 處理消息, 須要一個喚醒的過程數據結構


一、 講講 RunLoop, 項目中有用到嗎?app

appDelegate, 是一個 RunLoop 對象ide

RunLoop 的基本做用:oop

保持程序的持續運行性能

節省 CPU 的資源,提升程序的性能 ( 沒有事情,就請休眠,不要功耗。有事情,就處理)ui

Screen Shot 2020-07-18 at 5.08.02 PM.png


二、 RunLoop 內部實現邏輯?this

RunLoop 裏面有不少種模式,他在運行的過程當中,只會選擇一種來運行

Modes, 就是 RunLoop 平時要作的事情

Screen Shot 2020-07-18 at 3.42.00 PM.png

  • Core Foundation 中關於 RunLoop 的 5 個類:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef


CFRunLoopModeRef

  • CFRunLoopModeRef 表明 RunLoop 的運行模式
  • 一個 RunLoop 包含若干個 Modes, 每一個 Mode 又包含若干個 Source0 / Source1 / Timer / Observer
  • RunLoop 啓動時,只能選擇其中一個 Mode, 做爲 currentMode
  • 若是須要切換 Mode, 只能退出當前 Loop, 再從新選擇一個 Mode 進入

不一樣組的 Source0 / Source1 / Timer / Observer 能分割開來,互不影響

  • 若是 Mode 裏沒有任何 Source0 / Source1 / Timer / Observer , RunLoop 會馬上退出

  • __CFRunLoop 的數據結構
struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;            /* locked for accessing mode list */
    __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    
    
    
    pthread_t _pthread;
    uint32_t _winthread;
    
    
    
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    
    
    CFRunLoopModeRef _currentMode;
    

    // 這裏有一個集合
    // 裝的是一些 CFRunLoopModeRef 
    CFMutableSetRef _modes;
    // _modes 裏面有不少 mode ( CFRunLoopModeRef )
    // 其中有一個 mode 是 _currentMode ( 當前模式 )
    
    
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

進入 mode

CFRunLoopModeRef 的數據結構

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    
    
    
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    
    
    
    // 這兩個集合,裝的是 CFRunLoopSourceRef 對象
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    // 對應平時的事情:
    // 包括點擊事件,刷新 UI 事件,performSelector
    
    
    
    
    // 這個數組,裝的是 CFRunLoopObserverRef 對象
    CFMutableArrayRef _observers;
    // 監聽器
    
    
    // 這個數組,裝的是 CFRunLoopTimerRef 對象
    CFMutableArrayRef _timers;
    // 對應計時器, 定時器
    
    
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

三、 RunLoop 和線程的關係?

  • 每條線程,都有惟一的一個,與之對應的 RunLoop 對象

runloops[thread] = runloop

  • RunLoop 保存在一個全局的 Dictionary 裏,線程做爲 Key, RunLoop 做爲 Value
  • 線程剛建立時,並無 RunLoop 對象, RunLoop 會在第一次獲取它時,建立
  • RunLoop 會在線程結束時,銷燬

線程都沒有了,runloop 也就沒有意義了

子線程,要什麼 runloop?

沒有 runloop , 就是命令行,調用一次就完結

有了 runloop, 能夠反覆休眠、喚醒、處理消息


  • 主線程的 RunLoop 已經自動獲取 ( 建立 ),子線程默認沒有開啓 RunLoop

子線程中,獲取一下 currentRunLoop, 就建立開啓了 RunLoop

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    
    // 從這裏拿
    return _CFRunLoopGet0(pthread_self());
}

進入詳情

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        
        
     // 從字典裏面,獲取 runloop 對象
    // __CFRunLoops 字典
    // pthreadPointer(t) ,鍵 key
    
    
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    // runloop 對象 loop 不存在,就新建
    if (!loop) {
            // 字典中,設置
            //   __CFRunLoops  字典
            //   pthreadPointer(t),鍵 key
            //   newLoop, 值  value
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

線程之間通訊 (交互),通常經過端口 port 的形式

四、 timer 和 runloop 的關係?

五、 程序中添加每 3 秒響應一次的 NSTimer, 當拖動 tableView 時 timer 可能沒法響應,要怎麼解決?

CFRunLoopModeRef

常見的 2 種 Mode:

  • kCFRunLoopDefaultMode ( NSDefaultRunLoopMode ) : App 的默認 Mode, 一般主線程是在這個 Mode 下運行
  • UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滾動,保證界面滾動時,不受其餘 Mode 的影響

切換 Mode:

是在 while 的循環中進行,至關於一次 goto, 或者 continue

六、 RunLoop 是怎麼響應用戶操做的,具體流程是什麼樣的?

RunLoop 的運行邏輯

Screen Shot 2020-07-22 at 2.34.59 AM.png


七、 說說 RunLoop 的幾種狀態

切換 RunLoop 的模式,須要 RunLoop 退出和從新進入

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
  kCFRunLoopEntry = (1UL << 0),
  // 即將進入 Loop
  
  
  
  kCFRunLoopBeforeTimers = (1UL << 1),
  // 即將處理 Timer
  
  
  kCFRunLoopBeforeSources = (1UL << 2),
  // 即將處理 Source
  
  
  kCFRunLoopBeforeWaiting = (1UL << 5),
  // 即將進入休眠
  
  
  kCFRunLoopAfterWaiting = (1UL << 6),
  // 剛從休眠中喚醒
  // 喚醒
  
  
  
  kCFRunLoopExit = (1UL << 7),
  // 即將退出 Loop
  
  
  kCFRunLoopAllActivities = 0x0FFFFFFFU
};
RunLoop 的 Before 什麼,就是即將處理什麼

八、 runloop 的 mode 做用是什麼?

source 0
處理觸摸 ( 點擊 )事件
performSelector: onThread: (把方法放在子線程,去執行)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    
    NSLog(@"%s", __func__);
   // 斷點, bt 一下 
    
    
}

點擊一下,進入斷點

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1

// 咱們的 source code
  * frame #0: 0x0000000104d2eed4 RunLooper`-[ViewController touchesBegan:withEvent:](self=0x00007fe015509870, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000016998c0) at ViewController.m:43:1
    frame #1: 0x00007fff48cb94e2 UIKitCore`forwardTouchMethod + 323
    frame #2: 0x00007fff48cb938e UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
    frame #3: 0x00007fff48cc82ab UIKitCore`-[UIWindow _sendTouchesForEvent:] + 622
    frame #4: 0x00007fff48cca311 UIKitCore`-[UIWindow sendEvent:] + 4501
    frame #5: 0x00007fff48ca4755 UIKitCore`-[UIApplication sendEvent:] + 356
    frame #6: 0x00007fff48d2f552 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 7628
    frame #7: 0x00007fff48d32716 UIKitCore`__handleEventQueueInternal + 6584
    
    
    // 處理事件
    frame #8: 0x00007fff48d28fb9 UIKitCore`__handleHIDEventFetcherDrain + 88
    
    
    // source 0
    frame #9: 0x00007fff23da0d31 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #10: 0x00007fff23da0c5c CoreFoundation`__CFRunLoopDoSource0 + 76
    
    
    
    // source 0
    frame #11: 0x00007fff23da0434 CoreFoundation`__CFRunLoopDoSources0 + 180
    frame #12: 0x00007fff23d9b02e CoreFoundation`__CFRunLoopRun + 974
    frame #13: 0x00007fff23d9a944 CoreFoundation`CFRunLoopRunSpecific + 404
    frame #14: 0x00007fff38ba6c1a GraphicsServices`GSEventRunModal + 139
    frame #15: 0x00007fff48c8b9ec UIKitCore`UIApplicationMain + 1605
    frame #16: 0x0000000104d2f182 RunLooper`main(argc=1, argv=0x00007ffeeaecfd30) at main.m:18:12
    frame #17: 0x00007fff51a231fd libdyld.dylib`start + 1

source 1

  • 基於 port 的線程間通訊
  • 系統事件捕捉

(例如: 在屏幕上點一下,產生一個點擊事件,
這個點擊事件,一開始,屬於 source 1,
再包裝成 source 0 ,來處理 )
屏幕點擊, source 1 捕捉,分發到 source 0 來處理

Timers

  • NSTimer
  • performSelector: withObject: afterDelay:

Observers

  • 用於監聽 RunLoop 的狀態
  • UI 刷新 ( before waiting )

監聽器一旦監聽到, runLoop 要休眠了,
他就會在 runLoop 休眠以前,
刷新 UI 界面

  • 自動釋放池, autoReleasePool

runLoop 休眠以前, 清理一下自動釋放池


使用的代碼, 目前最新 1153

代碼 github

相關文章
相關標籤/搜索