Summary of the underlying principles of iOS - RunLoop

interview questions

  1. Talk about RunLoop, is it useful in the project?
  2. The internal implementation logic of RunLoop?
  3. The relationship between Runloop and thread?
  4. The relationship between timer and Runloop?
  5. An NSTimer that responds every 3 seconds is added to the program. When the tableview is dragged, the timer may not respond. What should I do?
  6. How does the Runloop respond to user operations, and what is the specific process?
  7. Talk about several states of RunLoop?
  8. What is the mode function of Runloop?

1. Introduction to RunLoop

Run loop, do some things in a loop while the program is running. If there is no Runloop program, it will exit immediately. If there is a Runloop program, it will run all the time and wait for the user's input operation all the time. RunLoop can run itself when needed, and stop and rest when there is no operation. Fully save CPU resources and improve program performance.

2. Basic functions of RunLoop:

  1. To keep the program running continuously , a main thread will be opened as soon as the program starts, and a RunLoop corresponding to the main thread will run as soon as the main thread is started. RunLoop ensures that the main thread will not be destroyed, which also ensures the continuous operation of the program.
  2. Handle various events in the App (such as: touch events, timer events, Selector events, etc.)
  3. Save CPU resources and improve program performance . When the program is running, when nothing is done, RunLoop will tell the CPU that there is nothing to do now, and I am going to rest, then the CPU will release its resources to do other things When there is something to do, RunLoop will immediately get up and do it. Let 's take a brief look at the internal operation principle of RunLoop through a picture in the API.
    The internal working principle of RunLoop
    It can be seen from the picture that RunLoop will hand over to the corresponding processor for processing when it receives Input sources or Timer sources during the lap run. When no event message comes in, RunLoop rests. Here is just a brief understanding of this picture. Next, let's learn about the RunLoop object and some of its related classes to gain a deeper understanding of the RunLoop running process.

3. Where to start RunLoop

The Runloop is started in the UIApplicationMain function, and the program will not exit immediately, but will remain running. Therefore, every application must have a runloop. We know that as soon as the main thread is started, it will run a RunLoop corresponding to the main thread, so the RunLoop must be opened in the entry main function of the program.

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

Enter UIApplicationMain

UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);

We find that it returns an int number, then we make some modifications to the main function

int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"开始");
        int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        NSLog(@"结束");
        return re;
    }
}

Running the program, we found that it will only print the start, but not the end, which means that in the UIApplicationMain function, a RunLoop related to the main thread is opened, so that UIApplicationMain will not return and is always running, which ensures the program's run continuously . Let's see the source code of RunLoop

// 用DefaultMode启动
void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

We found that RunLoop is indeed implemented by judging the value of result while do while. Therefore, we can think of RunLoop as an infinite loop. If there is no RunLoop, the UIApplicationMain function will return directly after execution, and no program will continue to run.

4. RunLoop object

Fundation framework (based on CFRunLoopRef encapsulation) NSRunLoop object

CoreFoundation CFRunLoopRef object

Because the Fundation framework is a layer of OC encapsulation based on CFRunLoopRef, here we mainly study the source code of CFRunLoopRef

How to get the RunLoop object

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

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

Five. The relationship between RunLoop and threads

  1. Each thread has a unique RunLoop object corresponding to it
  2. RunLoop is stored in a global Dictionary, thread as key, RunLoop as value
  3. The RunLoop of the main thread has been automatically created, and the RunLoop of the child thread needs to be actively created
  4. RunLoop is created when it is first acquired and destroyed when the thread ends

View the above correspondence through the source code

// 拿到当前Runloop 调用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

// 查看_CFRunLoopGet0方法内部
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);
	// 根据传入的主线程获取主线程对应的RunLoop
	CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
	// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
	CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
	CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 从字典里面拿,将线程作为key从字典里获取一个loop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
    if (!loop) {  
	CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
	loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	
	// 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
	if (!loop) { 
	    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;
}

As can be seen from the above code, there is a one-to-one correspondence between threads and RunLoop, and the relationship is stored in a Dictionary. Therefore, when we create a sub-thread RunLoop, we only need to obtain the RunLoop object of the current thread in the sub-thread. [NSRunLoop currentRunLoop];If we do not obtain it, the sub-thread will not create the RunLoop associated with it, and can only obtain its RunLoop inside a thread. When the RunLoop [NSRunLoop currentRunLoop];method is called, it will first look at the dictionary to see if there is a RunLoop that is relatively used for storing child threads. If there is, it will directly return the RunLoop. If not, it will create one and store the corresponding child thread in the dictionary. When the thread ends, the RunLoop is destroyed.

6. RunLoop structure

Through the source code we find the __CFRunLoop structure

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;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

In addition to some record attributes, mainly look at the following two member variables

CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;

CFRunLoopModeRef is actually a pointer to the __CFRunLoopMode structure. The source code of the __CFRunLoopMode structure is as follows

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];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    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 */
};

Mainly view the following member variables

CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;

From the above analysis, we know that CFRunLoopModeRef represents the running mode of RunLoop. A RunLoop contains several Modes, and each Mode contains several Source0/Source1/Timer/Observer. When RunLoop starts, only one of the Modes can be selected as the currentMode.

What do Source1/Source0/Timers/Observer represent?

1. Source1 : Port-based inter-thread communication

2. Source0 : touch event, PerformSelectors

Let's verify it with the code

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"点击了屏幕");
}

The stack information is printed after the breakpoint. When the stack information printed in the xcode tool area is incomplete, the complete stack information can be printed through the "bt" command on the console. It can be found from the stack information that the touch event will indeed trigger the Source0 event.

touchesBegan stack info

Verify the performSelector stack information in the same way

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});

It can be found that PerformSelectors also triggers the Source0 event

performSelector stack info

In fact, when we trigger an event (touch/lock screen/shake, etc.), an IOHIDEvent event is generated by IOKit.framework, and IOKit is Apple's hardware driver framework, which performs the abstract encapsulation of the underlying interface and interacts with the system to transmit hardware Induced events, and specially handle user interaction devices, consisting of IOHIDServices and IOHIDDisplays. Among them, IOHIDServices is dedicated to handling user interaction. It encapsulates events into IOHIDEvents objects, and then forwards them to the required App process with the mach port, and then Source1 It will receive IOHIDEvent, and then call back __IOHIDEventSystemClientQueueCallback(), trigger Source0 in __IOHIDEventSystemClientQueueCallback(), and Source0 trigger _UIApplicationHandleEventQueue(). So the touch event is seen in Source0.

3. Timers : timers, NSTimer

Verify by code

[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
    NSLog(@"NSTimer ---- timer调用了");
}];

print full stack info

Timer battle information

4. Observer : Listener, used to monitor the status of RunLoop

7. Detailed explanation of RunLoop related classes and functions

Through the above analysis, we have a general understanding of the internal structure of RunLoop, and then we will analyze the related classes of RunLoop in detail. The following are the 5 classes of RunLoop in Core Foundation

CFRunLoopRef - get current RunLoop and main RunLoop

CFRunLoopModeRef - RunLoop operation mode, only one can be selected, and different operations can be performed in different modes

CFRunLoopSourceRef - event source, input source

CFRunLoopTimerRef - Timer time

CFRunLoopObserverRef - Observer

1. CFRunLoopModeRef

CFRunLoopModeRef represents the running mode of RunLoop. A RunLoop contains several Modes, and each Mode contains several Sources, Timers, and Observers. Each time RunLoop starts, only one of the Modes can be specified. This Mode is called CurrentMode. If you need to switch the Mode, only You can exit RunLoop, and then re-specify a Mode to enter. This is mainly to separate Source, Timer, and Observer of different groups, so that they do not affect each other. If there is no Source0/Source1/Timer/Observer in Mode, RunLoop will exit immediately as shown in the figure:

Schematic diagram of CFRunLoopModeRef
Note: There can be multiple Sources (event sources, input sources, port-based event sources such as keyboard touches, etc.) in a Mode, Observer (observer, observe the current RunLoop running state) and Timer (timer event source). But there must be at least one Source or Timer, because if Mode is empty, RunLoop will run to empty mode without idling, and will exit immediately.

There are 5 Modes registered by the system by default:

RunLoop has five operating modes, of which there are two common ones: 1.2

1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode 

Switch between Modes

We must have encountered it in our development. When we use NSTimer to slide UIScrollView every time we perform something, NSTimer will pause. When we stop sliding, NSTimer will resume again. Let's take a look at it through a piece of code.

Comments in the code are also important, showing the process of our exploration

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
    // 加入到RunLoop中才可以运行
    // 1. 把定时器添加到RunLoop中,并且选择默认运行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 当textFiled滑动的时候,timer失效,停止滑动时,timer恢复
    // 原因:当textFiled滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了
    
    // 2. 当我们将timer添加到UITrackingRunLoopMode模式中,此时只有我们在滑动textField时timer才会运行
    // [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // 3. 那个如何让timer在两个模式下都可以运行呢?
    // 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer
    // 3.2 使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签
    //0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
    //2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
    // 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
-(void)show
{
    NSLog(@"-------");
}

It can be seen from the above code that NSTimer does not work because of the mode switching, because if we use the timer in the main thread, the Mode of RunLoop is kCFRunLoopDefaultMode, that is, the timer belongs to kCFRunLoopDefaultMode, then when we slide the ScrollView, the RunLoop's Mode is kCFRunLoopDefaultMode. Mode will switch to UITrackingRunLoopMode, so the timer in the main thread will no longer work, and the called method will no longer be executed. When we stop sliding, the Mode of RunLoop switches back to kCFRunLoopDefaultMode, so NSTimer works again.

For the same reason, there is also the display of ImageView. Let's look at the code directly and won't repeat it.

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSLog(@"%s",__func__);
    // performSelector默认是在default模式下运行,因此在滑动ScrollView时,图片不会加载
    // [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
    // inModes: 传入Mode数组
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];

Using GCD can also create timers, and to be more precise let's look at the code

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //1.创建一个GCD定时器
    /*
     第一个参数:表明创建的是一个定时器
     第四个参数:队列
     */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
    // 局部变量,让指针强引用
    self.timer = timer;
    //2.设置定时器的开始时间,间隔时间,精准度
    /*
     第1个参数:要给哪个定时器设置
     第2个参数:开始时间
     第3个参数:间隔时间
     第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
     GCD的单位是纳秒 所以要*NSEC_PER_SEC
     */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    
    //3.设置定时器要执行的事情
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"---%@--",[NSThread currentThread]);
    });
    // 启动
    dispatch_resume(timer);
}

2. CFRunLoopSourceRef event source (input source)

Source is divided into two

Source0: Non-Port-based events for user-triggered events (clicking the button or clicking the screen)

Source1: Port-based sending messages to each other through the kernel and other threads (related to the kernel)

Touch events and PerformSelectors will trigger the Source0 event source, which has been verified in the previous article, so I won't go into details here.

3. CFRunLoopObserverRef

CFRunLoopObserverRef is an observer that can monitor the state change of RunLoop

Let's look at the code directly, add a listener to RunLoop, and monitor its running status

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
     //创建监听者
     /*
     第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
     第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
     第三个参数 Boolean repeats:YES:持续监听 NO:不持续
     第四个参数 CFIndex order:优先级,一般填0即可
     第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
     */
     /*
     所有事件
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
     kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
     kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
     kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
     kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
     kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
     kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop进入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要处理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要处理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒来了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;
                
            default:
                break;
        }
    });
    
    // 给RunLoop添加监听者
    /*
     第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
     第二个参数 CFRunLoopObserverRef observer 监听者
     第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     /*
     CF的内存管理(Core Foundation)
     凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
     GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
     */
    CFRelease(observer);
}

Let's look at the output

The listener monitors the running status of RunLoop
As can be seen from the above, Observer is indeed used to monitor the status of RunLoop, including wake-up, rest, and processing of various events.

Eight. RunLoop processing logic

At this time, we will analyze the processing logic of RunLoop, and it will be much simpler and clearer. Now look back at the processing logic of the official document RunLoop, and have a new understanding of the processing logic of RunLoop.

Official document RunLoop processing logic

Source code analysis

The following source code only retains the main process code

// 共外部调用的公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

// 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    // 通知Observers : 进入Loop
    // __CFRunLoopDoObservers内部会调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函数
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 核心的Loop逻辑
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 通知Observers : 退出Loop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    return result;
}

// 精简后的 __CFRunLoopRun函数,保留了主要代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        // 通知Observers:即将处理Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 
        
        // 通知Observers:即将处理Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        // 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 处理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 处理Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        // 如果有Sources1,就跳转到handle_msg标记处
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            goto handle_msg;
        }
        
        // 通知Observers:即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        
        // 进入休眠,等待其他消息唤醒
        __CFRunLoopSetSleeping(rl);
        __CFPortSetInsert(dispatchPort, waitSet);
        do {
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
        
        // 醒来
        __CFPortSetRemove(dispatchPort, waitSet);
        __CFRunLoopUnsetSleeping(rl);
        
        // 通知Observers:已经唤醒
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
        if (被Timer唤醒的) {
            // 处理Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }
        else if (被GCD唤醒的) {
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else { // 被Sources1唤醒的
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
        }
        
        // 执行Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
        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;
}

In the above source code, the corresponding event processing function will also call lower-level functions, and the internal call is the function that actually handles the event. It can also be verified by printing all the stack information from the above bt.

__CFRunLoopDoObservers 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

__CFRunLoopDoBlocks 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__

__CFRunLoopDoSources0 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

__CFRunLoopDoTimers 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

GCD 调用 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__

__CFRunLoopDoSource1 内部调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

RunLoop processing logic flow chart

At this point, if we rearrange the RunLoop processing logic according to the source code, it will be very clear

RunLoop processing logic

9. RunLoop exit

  1. The main thread destroys RunLoop and exits
  2. There are some Timers, Sources, and Observers in Mode. These ensure that RunLoop is not idling and is running when Mode is not empty. When Mode is empty, RunLoop will exit immediately.
  3. We can set when to stop when we start RunLoop
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>

10. RunLoop application

1. Resident thread

The role of resident threads: We know that when the tasks in the sub-threads are executed, they are destroyed, so if we need to open a sub-thread that will always exist during the running of the program, then we will face a problem, how to To keep the child thread alive forever, a resident thread is used: open a RunLoop for the child thread Note: the child thread will be released immediately after the operation is completed, even if we use a strong reference to refer to the child thread so that the child thread will not be released, You can't add operations to the child thread again, or start it again. The code for the child thread to open RunLoop, first click the screen to open the child thread and open the child thread RunLoop, and then click the button.

#import "ViewController.h"

@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
   // 创建子线程并开启
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
    self.thread = thread;
    [thread start];
}
-(void)show
{
    // 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。
    // 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来
    NSLog(@"%s",__func__);
    // 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入
    // 添加Source [NSMachPort port] 添加一个端口
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    // 添加一个Timer
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];    
    //创建监听者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"RunLoop进入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"RunLoop要处理Timers了");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"RunLoop要处理Sources了");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"RunLoop要休息了");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"RunLoop醒来了");
                break;
            case kCFRunLoopExit:
                NSLog(@"RunLoop退出了");
                break;
            
            default:
                break;
        }
    });
    // 给RunLoop添加监听者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 2.子线程需要开启RunLoop
    [[NSRunLoop currentRunLoop]run];
    CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
    NSLog(@"%@",[NSThread currentThread]);
}
@end

Note: To create a RunLoop related to a child thread, you can create it in the child thread, and there must be at least one Timer or one Source in the RunLoop to ensure that the RunLoop will not exit due to idling, so it is added directly when it is created. Source, or just adding a listener, the running program will crash

2. Autorelease pool

Timer and Source are also some variables, which need to occupy part of the storage space, so they need to be released. If they are not released, they will keep accumulating, and the occupied memory will become larger and larger, which is obviously not what we want. So when will it be released, and how will it be released? There is an automatic release pool inside RunLoop. When RunLoop is turned on, an automatic release pool will be created automatically. When RunLoop will release the contents of the automatic release pool before resting, a new empty automatic release pool will be recreated. When it is awakened and starts running the lap again, new events such as Timer and Source will be placed in the new automatic release pool, and will also be released when RunLoop exits . Note: Only the main thread's RunLoop will start by default. This means that the automatic release pool will be created automatically, and the child thread needs to manually add the automatic release pool in the thread scheduling method.

@autorelease{  
      // 执行代码 
}

NSTimer, ImageView display, PerformSelector, etc. have been examples above, so I won't repeat them here.

Check yourself at last

The answers to the interview questions at the beginning of the article can be found in the article, so I won't repeat them here.

reference

Apple official documentation

CFRunLoopRef source code


This article draws on the articles of many predecessors. If there is anything wrong, please correct me. Welcome everyone to exchange and learn xx_cc.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325377558&siteId=291194637