底层六- RunLoop

401 阅读8分钟

LLDB 中 打印 bt 就可以打印函数调用栈

RunLoop源码

深入理解RunLoop

简介

RunLoop:运行循环 概念: 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出

应用范畴:

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

如果没有运行循环的情况下

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

// 在执行了第5行代码后,程序会自动退出

如果有了运行循环

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
// 程序并不会马上退出,而是保持运行状态

// 伪代码
int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            // 睡眠中等待消息
            int message = sleep_and_wait();
            // 处理消息
            retVal = process_message(message);
        } while (0 == retVal) ;
        return 0;
    }
}

RunLoop的基本作用

保持程序的持续运行
处理App中的各种事件(比如触摸事件、定时器事件等)
节省CPU资源,提高程序性能:该做事时做事,该休息时休息
......

Runloop对象

iOS 中有两套API来访问和使用RunLoop

Foundation:NSRunLoop
Core Foundation:CFRunLoopRef
  • NSRunLoopCFRunLoopRef都代表着RunLoop对象
  • NSRunLoop是基于CFRunLoopRef的一层OC包装
  • CFRunLoopRef开源

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为keyRunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

获取RunLoop对象 Foundation

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

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

打印

NSLog(@"%p %p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);
NSLog(@"%p %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());    
NSLog(@"%@",[NSRunLoop mainRunLoop]);

// 0x60000069aa60 0x60000069aa60
// 0x600001e98d00 0x600001e98d00
// <CFRunLoop 0x600001e98d00 [0x10eeefb68]>

通过打印结果可以看出,NSRunLoop对象是基于CFRunLoop的一层封装

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

CFDictionaryGetValue 通过字典获取数据 上面的代码就等价于

loop = __CFRunLoops["pthreadPointer(t)"]

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

RunLoop底层结构

屏幕快照 2021-03-22 22.37.58.png

  • CFRunLoopRef中的集合_modes存放着CFRunLoopModelRef对象

  • CFRunLoopModelRef又包含着三个集合

    • _source0、_source1(存放着CFRunLoopSourceRef)

    • _observers (存放着CFRunLoopObserverRef)

    • _timers (存放着CFRunLoopTimerRef)

Mode(CFRunLoopModeRef)

  • CFRunLoopModeRef代表RunLoop的运行模式

  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer

  • RunLoop启动时只能选择其中一个Mode,作为currentMode

  • 如果需要切换Mode,只能退出当前Loop(从新进入,loop并没有结束),再重新选择一个Mode进入

    • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
  • 如果Mode里没有任何Source0/Source1/Timer/ObserverRunLoop会立马退出

source0
  • 处理触摸事件 (LLDBbt可以打印出完整的函数调用栈)

可以顺便看一次啊点击手机屏幕的响应步骤

source1 捕捉事件,放到eventQueue , source0处理事件

 3. CoreFoundation : __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
 4. UIKitCore : -[UIApplication sendEvent:]
 5. UIKitCore : -[UIWindow sendEvent:] 
 6. UIKitCore : - [UIWindow _sendTouchesForEvent:]
 
  • 处理
    • performSelector:onThread:withObject:waitUntilDone:
    • performSelector:onThread:withObject:waitUntilDone:modes: 事件
source1
  • 基于port的线程间通信
  • 系统事件的捕捉(触摸事件是source1捕捉,包装成source0处理)
Timer
  • NSTimer
  • performSelector:withObject:afterDelay:
Observers
  • 用于监听runLoop状态
  • UI刷新(beforeWaiting)
  • Autorelease Pool
CFRunLoopModeRef

常见的2种Mode

kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
CFRunLoopObserverRef (runLoop的六种状态)

图片 4.png

添加Observer监听RunLoop的所有状态

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // kCFRunLoopCommonModes默认包括kCFRunLoopDefaultMode、UITrackingRunLoopMode
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"kCFRunLoopBeforeTimers");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"kCFRunLoopBeforeSources");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"kCFRunLoopBeforeWaiting");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"kCFRunLoopAfterWaiting");
                break;
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit");
                break;
            default:
                break;
        }
    });
    
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
}

/*
 * 可以看出处理点击事件的时候,会进入source
 2021-03-31 15:24:14.237892+0800 01. ObserverRef[68299:1347218] kCFRunLoopBeforeSources
 2021-03-31 15:24:14.239497+0800 01. ObserverRef[68299:1347218] 123
 **/
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"123");
}

监听Mode的转变

// 创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
                break;
            }
                
            case kCFRunLoopExit: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
                break;
            }
                
            default:
                break;
        }
    });
    // kCFRunLoopCommonModes默认包括kCFRunLoopDefaultMode、UITrackingRunLoopMode
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
    
    
2021-03-31 15:38:57.780986+0800 01. ObserverRef[68562:1363675] kCFRunLoopExit - kCFRunLoopDefaultMode
2021-03-31 15:38:57.781314+0800 01. ObserverRef[68562:1363675] kCFRunLoopEntry - UITrackingRunLoopMode
2021-03-31 15:39:04.437094+0800 01. ObserverRef[68562:1363675] kCFRunLoopExit - UITrackingRunLoopMode
2021-03-31 15:39:04.437248+0800 01. ObserverRef[68562:1363675] kCFRunLoopEntry - kCFRunLoopDefaultMode
    

RunLoop的运行逻辑

图片 6.png

Mode类型
Source0触摸事件处理performSelector:onThread:
Source1基于Port的线程间通信系统事件捕捉
TimersNSTimerperformSelector:withObject:afterDelay:
Observers用于监听RunLoop的状态UI刷新(BeforeWaiting)Autorelease pool(BeforeWaiting)

图片 7.png

源码分析:

**************** 第一步 *******************
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    
    // 通知observers 进入RunLoop
	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 具体要做的事情
	result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 通知observers 退出RunLoop
	__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) {
  
    int32_t retVal = 0;
    do {
        // 通知observer,即将处理Timers
         __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        //  通知observer,即将处理Sources
         __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

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

        // 处理Source0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle) {
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
	}

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        // 判断有没有Source1
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            // 如果有Source1,就跳转到handle_msg
            goto handle_msg;
        }

        // 通知observers:即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
            
            
        // 等待别的消息来唤醒当前线程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        __CFRunLoopUnsetSleeping(rl);
        
        
        // 通知observers:如果有事件,runloop被唤醒,结束休眠,根据唤醒条件,处理事件
        __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) || sourceHandledThisLoo
        }
       
            // 处理blocks
            __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);

    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }

    return retVal;
}

************************ 第三步 ******************************
// 都会调用到类似的,很长的函数来处理问题
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);

注意GCD回到主线程的代码是依赖runLoop的,其他的GCD逻辑,并不依赖runLoop

// 依赖代码
disptach_async(dispatch_get_main_queue(), ^ {
    NSLog(@"这里是依赖runLoop的,可以用bt打印堆栈信息查看");
});
RunLoop休眠的实现原理

只有内核层面的API才可以实现真正的休眠

图片 8.png

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

补充:

GCD很多东西是不依赖RunLoop的,GCD执行流程是与RunLoop是分开的,但是GCD有一种情况是交给RunLoop处理就是线程间通信

NSTimer失效

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];

RunLoop的应用

处理NSTimerUIScrollView的冲突
线程保活

warning: 一般initWithTarget:容易造成循环引用

NSRunLooprun方法,用于开启无限循环的线程

使用懒加载的时候一定要注意的情况:当_thread是懒加载时

if (!_thread) return;

如果写成

if (self.thread) return;

会造成循环引用