Description Thread Local Data.

The pthread_getSpecific and pthread_setSpecific interfaces are used to fetch and set data from the thread’s local storage area, respectively. The same pthread_KEY_T reads different results in different threads, that is, the thread’s local storage space is isolated from each other. This is the key to thread-local storage.

(There is also a catch. We can’t think of these two interfaces in an object-oriented way. We don’t need to pass in pthread_t objects when we call pthread_getSpecific and pthread_setspecific. If we want to read data from storage in a thread, we can only execute the pthread_getSpecific function in the current thread. Similarly, we can only execute the pthread_getspecific function in the current thread.

The __CFFinalizeRunLoop function is called when each thread exits.

// Called for each thread as it exits
// called when each thread exits

CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) {
    CFRunLoopRef rl = NULL;
    
    if (data <= 1) {
        // Destruction starts when data is less than or equal to 1
        
        // static CFLock_t loopsLock = CFLockInit;
        // loopsLock is a global lock
        __CFLock(&loopsLock);
        
        // Read the current thread's Run loop object from the __CFRunLoops global dictionary
        if (__CFRunLoops) {
            // Read run loop with pthreadPointer(pthread_self()) as key
            rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self()));
            
            Retain is retained to continue using RL, which is removed from the __CFRunLoops dictionary and its reference count is reduced by 1
            if (rl) CFRetain(rl);
            CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self()));
        }
        
        __CFUnlock(&loopsLock);
    } else {
        // The initial value is PTHREAD_DESTRUCTOR_ITERATIONS-1 is 3, so the __CFFinalizeRunLoop function needs to be called twice with minus 1 to actually destroy the Run loop object
        _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void(*) (void *))__CFFinalizeRunLoop);
    }
    // We can not destroy the run loop of the main thread. We can only destroy the run loop of the child thread.
    if (rl && CFRunLoopGetMain() != rl) { // protect against cooperative threads
        // If the _counterpart exists, the sum is released
        if (NULL! = rl->_counterpart) {CFRelease(rl->_counterpart);
            rl->_counterpart = NULL;
        }
        
        // purge all sources before deallocation
        // Clear all sources before destroying run loop
        
        // Get the mode array
        CFArrayRef array = CFRunLoopCopyAllModes(rl);
        
        // Walk through the mode array to remove all sources in mode
        for (CFIndex idx = CFArrayGetCount(array); idx--;) {
            CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
            __CFRunLoopRemoveAllSources(rl, modeName);
        }
        
        // Remove all sources from common mode
        __CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes);
        CFRelease(array);
    }
    / / release the rl
    if (rl) CFRelease(rl);
}
Copy the code

Before destroying the Run loop object, remove it from the __CFRunLoops global dictionary, loop through all its modes, remove all sources in each mode, and finally destroy the Run loop object. All mode items are also released before mode destruction.


30. Analysis of run loop (CFRunLoopRun function abstract)

Under the specified conditions, the run loop exits and returns the following values:

  • kCFRunLoopRunFinishedRun loop mode without source or timer. (Also returns kCFRunLoopRunFinished when the RunLoop object is marked as being destroyed)
  • kCFRunLoopRunStoppedThe run loop is in useCFRunLoopStopThe function stops.
  • kCFRunLoopRunTimedOutThe number of seconds has passed.
  • kCFRunLoopRunHandledSourceProcessed source. This exit condition applies onlyreturnAfterSourceHandledtrueAt the right time.

You cannot specify the kCFRunLoopCommonModes constant for the mode parameter. Run loops always run in a specific mode. You can specify the Common mode only when configuring the run loop observer and only if you want the observer to run in multiple modes.

void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
    
        // Call CFRunLoopRunSpecific to start the run loop of the current thread with kCFRunLoopDefaultMode.
        // The elapsed time is passed in as 10^10 seconds (2777777 hours),
        // returnAfterSourceHandled as false,
        // Indicates that run loop does not exit after processing a source and continues processing events.
        
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0 e10.false);
        
        CHECK_FOR_FORK(a); }while(kCFRunLoopRunStopped ! = result && kCFRunLoopRunFinished ! = result); }Copy the code

The CFRunLoopRunSpecific function calls the __CFRunLoopRun function internally, Then you can put the result = __CFRunLoopRun (rl, currentMode, seconds, returnAfterSourceHandled, previousMode); This line is called as a demarcation line. If yes, check whether RL is marked as being destroyed. If yes, return kCFRunLoopRunFinished. Otherwise, CFRunLoopModeRef will be found from the _modes of RL according to modeName. If not found or CFRunLoopModeRef sources0 / sources1 / timers/block is empty, is returned directly kCFRunLoopRunFinished. And then I’m going to change _perRunData and _currentMode of rL and I’m going to record the old values, and then I’m ready, Rl _currentMode _observerMask is used to determine whether the run loop observer needs to be called to tell them that the run loop is going to kCFRunLoopEntry. The run loop is then formally started by calling the __CFRunLoopRun function.

The __CFRunLoopRun function returns, First, rL _currentMode _observerMask determines whether the run loop observer needs to be called back to tell them that run loop is going to kCFRunLoopExit. We then revert the Run loop object back to its previous _perRunData and _currentMode (which handles nesting of run loops).

The above description may not be very clear, but the code and comments below are extremely clear.

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK(a);// Take the flag value from the _cfinfo field of rL to see if RL is destroying, if so, return kCFRunLoopRunFinished directly
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    
    / / CFRunLoop lock
    __CFRunLoopLock(rl);
    
    // Call __CFRunLoopFindMode to find the run loop mode named modeName from the _modes of rL.
    // If the third parameter is false, run loop mode is not created, and NULL is returned.
    // (CFRunLoopMode lock)
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    / / if currentMode is NULL or an currentMode is empty does not contain sources0 sources1 / timers/block, then return
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        
        // If currentMode exists, CFRunLoopMode is unlocked.
        // This corresponds to the cfrunloopFindMode (rl, modeName, false) call
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        
        / / CFRunLoop unlocked
        __CFRunLoopUnlock(rl);
        
        / / return kCFRunLoopRunFinished
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    // The __CFRunLoopPushPerRunData function changes the value of each member variable of the rL _perRunData field and returns the previous _perRunData.
    // Change _perRunData inside the function to mark different run loop states.
    // (where previousPerRun is used when the following __CFRunLoopRun call returns, the current run loop object is returned to _perRunData).
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    
    // previousMode records the current run loop mode of rL, which is the previous run loop mode compared to the run loop mode obtained by the modeName passed in.
    // This previousMode is used in the following line: When the __CFRunLoopRun call returns, the current run loop object returns to the previous run loop mode.
    // return the current run loop object to its previous _perRunData state.
    CFRunLoopModeRef previousMode = rl->_currentMode;
    
    // Update rL _currentMode to run loop mode corresponding to input parameter modeName
    rl->_currentMode = currentMode;
    
    // The temporary variable result is used to record different exit reasons for the run loop when the function returns
    int32_t result = kCFRunLoopRunFinished;
    
    // If currentMode (_observerMask) contains kCFRunLoopEntry (_observerMask),
    // Tells currentMode's Run Loop Observer that a status change has occurred that the run loop is about to enter the loop.
    if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // Start the run loop. The __CFRunLoopRun function is extremely long and probably the longest function ever seen in the source code
    / / ♻ ️ ♻ ️ ♻ ️ ♻ ️ ♻ ️ ♻ ️
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // ⬆️⬆️⬆️ __CFRunLoopRun does not return. When it returns, the current run loop will exit.
    
    // Same as above kCFRunLoopEntry enters the loop callback, in this case exit the run loop callback.
    // If currentMode _observerMask contains kCFRunLoopExit,
    // That is, the Run loop Observer needs to observe the kCFRunLoopExit exit of the Run loop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    / / CFRunLoopMode unlocked
    __CFRunLoopModeUnlock(currentMode);
    
    // Destroy rL's current _perRunData and reassign previousPerRun to RL's _perRunData
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    
    // go back to _currentMode
    rl->_currentMode = previousMode;
    
    / / CFRunLoop unlocked
    __CFRunLoopUnlock(rl);
    
    // Return result
    return result;
}
Copy the code

One thing to note here is that the CFRunLoopRunSpecific function finally reassigns previousPerRun and previousMode to _perRunData and _currentMode of the run loop, They are designed to handle nested runs of run loops.

The __CFRunLoopModeIsEmpty function internally determines whether souces0/source1/timers are empty, as well as whether the rL block list contains blocks that can be executed under the specified RLM.

The __CFRunLoopDoObservers function is one of the most important functions used to call back when the run loop has changed state.

When the state of run loop will (notice will, will, will… KCFRunLoopExit is an exception, the exit callback is true after the completion of the exit callback) changes, first according to the run loop current run loop mode _observerMask contains this state change, The __CFRunLoopDoObservers function can then be called to perform a callback to the run loop state change. There are many important things we can do with this state change, which we will learn in more detail when we look at the run loop usage scenarios. (Here is a review of the previous run loop states: About to enter the Run loop, about to process the Source event, about to process the Timer event, about to sleep, about to end the sleep, And about to exit the run loop.)

Run loop Observer callback function.

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func,
                                                                          CFRunLoopObserverRef observer,
                                                                          CFRunLoopActivity activity,
                                                                          void *info) {
    // Simply call the func function with an argument
    if (func) {
        func(observer, activity, info);
    }
    
    asm __volatile__(""); // thwart tail-call optimization
}
Copy the code

The __CFRunLoopRun inner do while loop is mainly used to “keep” the run looop asleep until it needs to be woken up. Wake up only occurs when:

  1. The event arrived at the port-based input source (source1).
  2. Timers are triggered in CFRunLoopMode. (CFRunLoopMode can add multiple timers, all of which share the same _timerPort to wake up the Run loop and count the time of the next timer to be fired.)
  3. The timeout period set for run loop expires.
  4. Run loop is explicitly woken up. (Manually awakened by some other caller)

Inside the CFRunLoopWakeUp function, the Run loop object is woken up by the _wakeUpPort wakeup port of the run loop.

Inside the __CFRunLoopDoBlocks function is a list of run loop blocks, executed under the specified RLM block, the node is removed from the list after the execution of the node block, and the first and last nodes of the list are updated.

Signaled CfrunLoopSources0 is Signaled and collected from the _source0 of RLM. Execution of Source0’s Perform function, Signaled by Source0’s INFO, and waiting on CFRunLoopSourceRef become UnsetSignaled.

The __CFRunLoopDoTimers function executes the CFRunLoopTimerRef callback and updates its _fireTSR and _nextFireDate.


31. Mach_msg function

The core of the Run Loop is to ensure that threads sleep when there is no message to avoid system resource consumption, and wake up when there is a message. This mechanism of Run Loop relies entirely on the system kernel, specifically Mach in Darwin, the core component of Apple operating system. Mach, BSD, File System, Mach, and Networking are located in the Kernel and Device Drivers layer.

In Mach, everything is implemented through its own objects. Processes, threads, and virtual memory are referred to as “objects”. Unlike other architectures, Objects cannot be called directly to each other, but can communicate with each other only through messaging. “Messages” (Mach MSG) are the most basic concept in Mach. Messages are passed between two ports (Mach ports), and this is the heart of Mach IPC (inter-process communication).

Mach is at the heart of Darwin, arguably the core of the kernel, providing basic services such as interprocess communication (IPC) and processor scheduling. In Mach, communication between processes and threads is done in the form of messages (Mach MSG), Messages are passed between (or through) two Mach ports (which is why Source1 is called a Port-based Source, Because it relies on the Mach MSG to send a message to the specified Mach port to wake up the Run loop.

To send and receive messages, the mach_msg function actually calls a Mach trap, the function mach_MSg_trap, which is the equivalent of a system call in Mach. When mach_MSG_trap is called in user mode, it will trigger trap mechanism and switch to kernel mode. The mach_msg function implemented by the kernel in the kernel state does the actual work.

The core of the run loop is a mach_msg. The run loop calls this function to receive the message, and if no one else sends a port message, the kernel puts the thread into a waiting state. For example, if you run an iOS App in the simulator and pause the App while it’s still, you’ll see that the main thread call stack is stuck at mach_MSg_trap

(The mach_msg function can set the timeout parameter. If MSG is not read before timeout, the run loop of the current thread will sleep.)

Mach_msg function is used to send and receive messages, and the essence of mach_MSG_trap is called mach_MSG_trap, which is equivalent to a system call, which will trigger the switch between kernel mode and user mode.

Click the App icon, and the App will be in a static state after startup (generally, if no timer needs to be executed over and over again), then the run loop of the main thread will go to sleep. By adding CFRunLoopObserverRef to the main thread’s run loop, you can see in the callback that the last active state of the main thread’s Run loop is kCFRunLoopBeforeWaiting. Click the Pause Program Execution button at the bottom of the Xcode console. From the Debug Navigator to the left of Xcode, you can see that the call stack for the main thread stops at mach_MSG_trap.

_timerPort is a member variable of __CFRunLoopMode. Both dispatch_Source and MK build timers are supported on macOS, and only MK is supported on iOS. Here we’ll just focus on _timerPort. We’ll do this in Cocoa Foundation layer by manually creating and adding a timer NSTimer to the run loop specified run Loop mode, Also, the Core Foundation layer creates an instance of CFRunLoopTimerRef and adds it to the specified RunLoop mode of the Run loop, The internal implementation is to add an instance of CFRunLoopTimerRef to the _timers collection of the RunLoop mode. It is the _timerPort that wakes up the Run loop when the timer in the _timers collection needs to be executed. And all timers in the _timers collection of run Loop Mode share this _timerPort.

Here we can do a validation by adding a CFRunLoopOberver to the main thread and observing the state change of the main Run loop and an NSTimer that executes once every 1 second. After the program runs, you can see the following repeated print :(the code is too simple, I will not post here)

. ⏰⏰⏰ timer callback... 🎯... KCFRunLoopBeforeTimers 🎯... KCFRunLoopBeforeSources 🎯... KCFRunLoopBeforeWaiting 🎯... KCFRunLoopAfterWaiting ⏰⏰⏰ timer callback... 🎯... KCFRunLoopBeforeTimers 🎯... KCFRunLoopBeforeSources 🎯... KCFRunLoopBeforeWaiting 🎯... kCFRunLoopAfterWaiting ...Copy the code

Wake up run loop (kCFRunLoopAfterWaiting) to execute the timer’s callback, After the timer callback completes, the Run loop goes to sleep again (kCFRunLoopBeforeWaiting) and then wakes up again at the next timer trigger time, repeating indefinitely unless the timer is manually stopped.

32. Review run loop mode item (Source0 vs. Source1).

Let’s review the Source/Timer/Observer again, because it is through these Run Loop mode items that the Run loop provides external functionality.

  1. CFRunLoopSourceRef is where the event is generated. Source has two versions: Source0 and Source1.
  • Source0 contains only one callback (function pointer) and does not actively fire events. To use this, you need to call CFRunLoopSourceSignal(source), mark the source as pending, and then manually call CFRunLoopWakeUp(runloop) to wake up the Run loop to handle the event.
  • Source1 contains a mach_port and a callback (function pointer) that is used to send messages to each other through the kernel and other threads (mach_msg). This Source actively wakes up the run loop thread.

In Source0 only some callbacks are executed during this run loop, whereas in Source1 there are Mach ports that can be used to actively wake up the Run loop.

  1. CFRunLoopTimerRef is a time-based trigger that is toll-free bridged and can be mixed with NSTimer. It contains a length of time and a callback (function pointer). When it is added to the Run loop, the Run loop registers the corresponding time point, and when that time point is reached, the Run loop is woken up to execute that callback.
  2. CFRunLoopObserverRef is the Observer, and each Observer contains a callback (function pointer) that the Observer receives when the state of the Run loop changes.

33. Run loop controls the automatic release of pool push and POP.

So when does the auto release pool pop and release all the objects in the pool? There are two cases:

  • One is that we do it manually@autoreleasepool {... }The form of auto-release pool added using clang-rewrite-objc after conversion to C++ is actually
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush(a); } ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj); }void * atautoreleasepoolobj;
};

/* @autoreleasepool */ 
{ 
    // Directly build an __AtAutoreleasePool instance,
    // The constructor calls the push function of AutoreleasePoolPage to build an automatic release pool.
    __AtAutoreleasePool __autoreleasepool;
    // ...
}
Copy the code

As you can see, __autoreleasepool is wrapped in a pair of {}. When the right curly brace is released, the pool is automatically released to perform the pop operation, which can be interpreted as the following code:

void *pool = objc_autoreleasePoolPush(a);// the code in {}
objc_autoreleasePoolPop(pool);
Copy the code

In the original main function, break a breakpoint and enable Debug Workflow’s Always Show Disassembly to see the assembly code:

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        // appDelegateClassName = NSStringFromClass([AppDelegate class]);
    } // ⬅️ make a breakpoint here
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Copy the code

Since the auto-release pool didn’t put anything in the code above, the Pop was followed after the Push.

.0x101319b78 <+32>:  bl     0x101319eb8               ; symbol stub for: objc_autoreleasePoolPush
0x101319b7c <+36>:  bl     0x101319eac               ; symbol stub for: objc_autoreleasePoolPop
...
Copy the code
  • One is an automatic release pool created by run Loop. From ibireme:

The App starts, apple registered in the main thread RunLoop two Observer, the callback is _wrapRunLoopWithAutoreleasePoolHandler (). The first Observer monitors an event called Entry(about to enter Loop), which creates an automatic release pool within its callback by calling _objc_autoreleasePoolPush(). Its order is -2147483647, the highest priority, ensuring that the release pool is created before all other callbacks. The second Observer monitors two events: calling _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() while waiting (ready to sleep) torelease the old pool and create a new one; _objc_autoreleasePoolPop() is called upon Exit(about to Exit the Loop) torelease the automatic release pool. The order of this Observer is 2147483647, the lowest priority, ensuring that its release pool occurs after all other callbacks. The code that executes on the main thread is usually written inside such callbacks as event callbacks and Timer callbacks. These callbacks are surrounded by AutoreleasePool created by RunLoop, so there is no memory leak and the developer does not have to show that the Pool was created. Understand RunLoop in depth

Here we try to verify the above conclusions, in application: didFinishLaunchingWithOptions: Add a breakpoint, print Po [NSRunLoop mainRunLoop] on the console, Note that there are two CFRunloopobservers in observers in kCFRunLoopDefaultMode and UITrackingRunLoopMode of main Run loop.

"<CFRunLoopObserver 0x600001c30320 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x60000235dc20 [0x7fff80617cb0]>{type = mutable-small, count = 0, values = ()}}"

"<CFRunLoopObserver 0x600001c30280 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x60000235dc20 [0x7fff80617cb0]>{type = mutable-small, count = 0, values = ()}}"
Copy the code

The CFRunLoopObserver whose order is -2147483647 has the highest priority and will be called back before all other CFRunloopObservers, and then its activities will be 0x1, Corresponding kCFRunLoopEntry = (1 ul < < 0), namely only observe kCFRunLoopEntry state, the callback function is _wrapRunLoopWithAutoreleasePoolHandler, Add a breakpoint _wrapRunLoopWithAutoreleasePoolHandler symbols, add a breakpoint objc_autoreleasePoolPush symbols, to run the program, and in the console bt stack print function, and can actually see the following function calls:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x00000001dd971864 libobjc.A.dylib`objc_autoreleasePoolPush // push builds the automatic release pool
    frame #1: 0x00000001de78d61c CoreFoundation`_CFAutoreleasePoolPush + 16
    frame #2: 0x000000020af66324 UIKitCore`_wrapRunLoopWithAutoreleasePoolHandler + 56
    frame #3: 0x00000001de7104fc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32 // Execute the run loop Observer callback,
    frame #4: 0x00000001de70b224 CoreFoundation`__CFRunLoopDoObservers + 412
    frame #5: 0x00000001de70af9c CoreFoundation`CFRunLoopRunSpecific + 412
    frame #6: 0x00000001e090c79c GraphicsServices`GSEventRunModal + 104
    frame #7: 0x000000020af6cc38 UIKitCore`UIApplicationMain + 212
    frame #8: 0x0000000100a75b90 Simple_iOS`main(argc=1, argv=0x000000016f38f8e8) at main.m:77:12
    frame #9: 0x00000001de1ce8e0 libdyld.dylib`start + 4
(lldb) 
Copy the code

The main thread does see __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ executing the CFRunLoopObserver callback _wrapRunLoopWithAutoreleasePoolHandler function then calls the objc_autoreleasePoolPush to create large pools of automatic release.

The CFRunLoopObserver whose order is 2147483647 has the lowest priority and will be called back after all other CFRunloopObservers, and then its activities will be 0xA0 (0b10100000), Corresponding to kCFRunLoopBeforeWaiting = (1UL << 5) and kCFRunLoopExit = (1UL << 7), that is, observe two state changes of run loop about to go to sleep and run loop about to exit. The callback function is also _wrapRunLoopWithAutoreleasePoolHandler, we add a breakpoint objc_autoreleasePoolPop symbols, at this point we need to add some test code, We add an observer for the main run loop and a timer for the main run loop. After the program starts, we can see the console printing the following loop:

🎯... KCFRunLoopAfterWaiting ⏰⏰⏰ timer callback... 🎯... KCFRunLoopBeforeTimers 🎯... KCFRunLoopBeforeSources 🎯... KCFRunLoopBeforeWaiting 🎯... KCFRunLoopAfterWaiting ⏰⏰⏰ timer callback...Copy the code

Main thread into a kind of “dormant – be awakened the timer callback execution – hibernation cycle, at the moment we open the _wrapRunLoopWithAutoreleasePoolHandler breakpoint found into the program, Then open the objc_autoreleasePoolPop breakpoint and click Continue Program Execution. The breakpoint will enter the objc_autoreleasePoolPop breakpoint and print the function call stack on the console:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001dd9718f8 libobjc.A.dylib`objc_autoreleasePoolPop
    frame #1: 0x00000001de78cba0 CoreFoundation`_CFAutoreleasePoolPop + 28
    frame #2: 0x000000020af66360 UIKitCore`_wrapRunLoopWithAutoreleasePoolHandler + 116
    frame #3: 0x00000001de7104fc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
    frame #4: 0x00000001de70b224 CoreFoundation`__CFRunLoopDoObservers + 412
    frame #5: 0x00000001de70b7a0 CoreFoundation`__CFRunLoopRun + 1228
    frame #6: 0x00000001de70afb4 CoreFoundation`CFRunLoopRunSpecific + 436
    frame #7: 0x00000001e090c79c GraphicsServices`GSEventRunModal + 104
    frame #8: 0x000000020af6cc38 UIKitCore`UIApplicationMain + 212
    frame #9: 0x0000000100bc9b2c Simple_iOS`main(argc=1, argv=0x000000016f23b8e8) at main.m:76:12
    frame #10: 0x00000001de1ce8e0 libdyld.dylib`start + 4
(lldb)
Copy the code

Do see _wrapRunLoopWithAutoreleasePoolHandler objc_autoreleasePoolPop calls.

This is integrated: Entry–>push ➡️ BeforeWaiting– >pop–>push ➡️ Exit–> POP, in such an order that ensures a push and pop in each run loop.

From the above work in the Run Loop Observer, we know that for each loop, there is a pop and push, so we can get:

  1. If you add autoreleasePool manually, autoreleasePool objects in the autoreleasePool scope are released the moment they exit the pool scope.
  2. If the autoreleasePool is automatically added by the Run loop, first push a new autoreleasePool when the Run loop starts, AutoreleasePool then performs a pop operation torelease all autorelease objects in the run loop each time the loop is about to go to sleep, and pushes a new autoreleasePool to use in the next loop. In this way, the automatic release object created in each run loop is released, and then when the Run loop switches mode to exit, the last POP is executed to ensure that the push and POP of the automatic release pool appear in pairs during the run loop.

34. A brief introduction to NSTimer and NSTimer circular reference problems.

There are a lot of questions that NSTimer can ask. When my colleagues went to Tencent for an interview, they were asked how NSTimer is implemented. Here is a detailed explanation combined with run loop.

NSTimer. H provides a set of NSTimer invocation methods. The NSInvocation, SEL, and block parameters of the constructor represent the invocation of the NSTimer object. The block callback form is new in iOS 10.0 to help us avoid cyclic references to NSTimer objects and their target parameters, timerWithTimeInterval… And initWithFireDate NSTimer object is returned, we need to manually add to the current thread’s run loop, scheduledTimerWithTimeInterval… The constructed NSTimer object is added to the NSDefaultRunLoopMode mode of the current thread’s run loop by default NSRunLoopCommonModes).

Block callbacks are in the form of an API_AVAILABLE(MacOSx (10.12), ios(10.0), Watchos (3.0), TVOs (10.0)); Note: it is added after iOS 10.

The NSTimer object returned by the following five methods requires a manual call to NSRunLoop -(void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode; Adds the function to the specified mode of the specified run loop.

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
Copy the code

NSTimer objects returned by the following three methods are automatically added to the default mode of the current thread’s Run loop.

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
Copy the code

If you use scheduledTimerWithTimeInterval… It should be noted that when the mode of run loop switches to UITrackingRunLoopMode, the timer will stop calling back, and when the run loop is stopped by sliding and switches back to kCFRunLoopDefaultMode, the timer will start calling back normally. If necessary, we need to add the timer to NSRunLoopCommonModes to ensure that the mode switch of the Run loop does not affect the timer callback (the timer object will be added to the RunLoop modes of multiple common flags at the same time) In _timers).

Also note that NSTimer added to the run loop mode is held by the mode because it is added to _timers of the Run loop mode. If the mode name is nsRunLoopCommonMode, it will also be added to the _commonModeItems of the Run loop. Therefore, when the NSTimer object is no longer needed for timing, the invalidate function must be called to remove it from the _timers and _commonModeItems collections. This can be verified by printing the reference count for each timer under ARC:

// Timer is added to run loop NSDefaultRunLoopMode by default. The reference count should be 3.
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { }]; / / 3

// The starting reference count is 1
NSTimer *timer2 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { }]; / / 1
// Add timer2 to NSDefaultRunLoopMode of run loop reference count +1
// _timers held by timer2 and NSDefaultRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode]; / / 2

NSTimer *timer3 = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { }]; / / 1
[[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSDefaultRunLoopMode]; / / 2

// Add timer3 to NSRunLoopCommonModes of run loop with reference count +3
// Held by timer3, _timers of UITrackingRunLoopMode, _timers of NSDefaultRunLoopMode, and _commonModeItems of run loop
[[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes]; / / 4

// timer3 calls the invalidate function and the reference count changes back to 1
-3 after being removed from the two _timers and _commonModeItems
[timer3 invalidate]; / / 1
Copy the code

NSTimer is created to hold the target passed in:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
Copy the code

When you build or initialize an NSTimer object using the above three functions, the NSTimer object holds the target that was passed in because the NSTimer object calls back to target’s aSelector function, If the target also holds the NSTimer object, this will create a circular reference and cause a memory leak, which is a common problem when adding an NSTimer property to a ViewController. There are usually two ways to solve this problem: one is to separate the target into an object (in this object weakly reference NSTimer and the object itself as the target of NSTimer), and the controller indirectly uses NSTimer through this object; Another way to think about it is to shift the target, but you can add the NSTimer extension directly, making the NSTimer object the target, and encapsulating the action selector into a block, as shown in the following code. (Class objects are globally unique and do not need and cannot be freed.

#import "NSTimer+Block.h"

@implementation NSTimer (Block)

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)seconds repeats:(BOOL)repeats block:(void (^)(void))block {
    // Target passes in self.class, which is the NSTimer object, and the timer's callback is the NSTimer object's runBlock: function, runBlock is a class method,
    // Place the block of the callback in userInfo, and then read the userInfo from the NSTimer object in runBlock:.
    return [self initWithFireDate:date interval:seconds target:self.class selector:@selector(runBlock:) userInfo:block repeats:repeats];
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds repeats:(BOOL)repeats block:(void(^) (void))block {
    // self is an NSTimer class object
    return [self scheduledTimerWithTimeInterval:seconds target:self selector:@selector(runBlock:) userInfo:block repeats:repeats];
}

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds repeats:(BOOL)repeats block:(void(^) (void))block {
    // self is an NSTimer class object
    return [self timerWithTimeInterval:seconds target:self selector:@selector(runBlock:) userInfo:block repeats:repeats];
}

#pragma mark - Private methods

+ (void)runBlock:(NSTimer *)timer {
    // Read the block execution from the input timer object
    if ([timer.userInfo isKindOfClass:NSClassFromString(@"NSBlock")]) {
        void (^block)(void) = timer.userInfo;
        block(a); } } @endCopy the code

After iOS 10.0, Apple also provides the NSTimer constructor in block form, which we can use directly. (Are there any pre-ios 10.0 users?)

Timers cannot be paused. The invalidate function removes the use of the counter, so the invalidate method invalidates both repeated timers and one-time timers. It’s just a one-time timer that automatically calls the invalidate method when it’s done. So the only way to pause and resume a timer is to invalidate the old timer and then create a new one, and we must call the invalidate method when the timer is no longer needed.


35. NSTimer (CFRunLoopTimerRef) execution flow.

CFRunLoopTimerRef and nS Index mer can be toll-free bridged. When the timer is added to the Run loop, the Run loop will register the corresponding trigger time. When the time is up, the Run loop will wake up if it is in sleep and execute the corresponding callback function of the timer. Below we follow the CFRunLoopTimerRef source code to a complete analysis of the timer process.

CFRunLoopTimerRef () : CFRunLoopTimerRef () : CFRunLoopTimerRef ()

CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context);
Copy the code

Allocator is an allocator that allocates memory for new objects under CF. The allocator can be NULL or kCFAllocatorDefault.

FireDate is the point at which the timer first triggers a callback, followed by successive callbacks along interval intervals.

Interval is the interval between consecutive callbacks to the timer. If it is 0 or negative, the timer will fire once and then automatically expire.

Order Priority index indicating the order of callback execution for different timers in _timers of CFRunLoopModeRef. This parameter is ignored for the time being and 0 is passed.

The callback function that is invoked when the callout timer is triggered.

Context Holds the structure of the timer’s context information. This function copies the information out of the structure, so the memory pointed to by the context does not need to continue to exist after the function call. NULL if the callback function does not need the context’s information pointer to track state. The void * info field is the argument to the callout function.

The most important thing in the CFRunLoopTimerCreate function is the setting of the trigger time:

./ / # define TIMER_DATE_LIMIT 4039289856.0
// If fireDate is too large, set it to TIMER_DATE_LIMIT
if (TIMER_DATE_LIMIT < fireDate) fireDate = TIMER_DATE_LIMIT;

// Next trigger time
memory->_nextFireDate = fireDate;
memory->_fireTSR = 0ULL;

// Get the current time
uint64_t now2 = mach_absolute_time(a); CFAbsoluteTime now1 =CFAbsoluteTimeGetCurrent(a);if (fireDate < now1) {
    // Set _fireTSR to current if the first trigger time has passed
    memory->_fireTSR = now2;
} else if (TIMER_INTERVAL_LIMIT < fireDate - now1) {
    // If the gap between the first time triggered and the current time exceeds TIMER_INTERVAL_LIMIT, set _fireTSR to TIMER_INTERVAL_LIMIT
    memory->_fireTSR = now2 + __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT);
} else {
    If the first trigger time has not arrived, set the trigger time to the difference between the current time and the first trigger timememory->_fireTSR = now2 + __CFTimeIntervalToTSR(fireDate - now1); }...Copy the code

This part of the code ensures that the timer first fires correctly. Add CFRunLoopModeRef to the specified Run loop mode of the specified run loop.

The CFRunLoopAddTimer function inserts CFRunLoopTimerRef RLT into the _timer set in CFStringRef modeName mode of CFRunLoopRef RL. If modeName is kCFRunLoopCommonModes, insert RLT into the _commonModeItems of RL. And then call the RLT __CFRunLoopAddItemToCommonModes function is added to all marked as common mode of _timer, it will also add modeName to RLT _rlModes, Record RLT can be executed in that run loop mode.

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName);
Copy the code

After adding the above, the __CFRepositionTimerInMode function is called, and then __CFArmNextTimerInMode is called, Then call mk_timer_arm to register _timerPort and a time point of CFRunLoopModeRef in the system, and wait for mach_MSG to send a message to wake up the run loop in sleep to execute the timer of arrival time.

Multiple timers in the same run loop mode share the same _timerPort, which is a circular process: Register timer(mk_timer_ARM) – Receive timer(mach_msg) – Calculate the trigger time of the next callback based on multiple timers – Register timer(mk_timer_ARM).

The call stack for adding a timer with CFRunLoopAddTimer looks like this:

CFRunLoopAddTimer
__CFRepositionTimerInMode
    __CFArmNextTimerInMode
        mk_timer_arm
Copy the code

Then mach_msg receives a timer event and the call stack looks like this:

__CFRunLoopRun
__CFRunLoopDoTimers
    __CFRunLoopDoTimer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFArmNextTimerInMode
    mk_timer_arm 
Copy the code

The __CFArmNextTimerInMode function is called each time the timer registers the next callback to the timer. After the run loop is awakened through the _timerPort of the current Run Loop mode, In this run loop, the __CFRunLoopDoTimer function is called in the __CFRunLoopDoTimers function, and the _callout function of the timer that has reached the trigger time is executed. __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info); Is the _callout function that executes the timer.


36. The unpunctuality problem with NSTimer.

From the NSTimer execution process above, you can see that the timer trigger callback is completely dependent on the run loop (mk_timer is used to wake up the Run loop on both macOS and iOS). Before using NSTimer, you must register with the Run loop. However, run Loop does not call the timer at precisely the right time to save resources. If a task takes a long time to execute (for example, the source0 event in the run Loop takes too long or the timer’s own callback takes too long), Will cause the timer callback to be delayed at the next normal point in time, or too long to be ignored (the current state of execution is determined before the timer callback executes! __CFRunLoopTimerIsFiring(RLT), if the callback execution time of the timer itself is too long, the next callback is ignored. NSTimer provides a tolerance property to set tolerance, that is, the current time point has passed the trigger point of the timer, but if the elapsed time is less than tolerance, then the timer callback can still be executed normally, but with an untimely delay. The default value of tolerance is 0, and the maximum value is half of the timer interval _interval. The value of tolerance can be set according to its own situation. Or if the main thread is really not optimized, put the timer in the child thread)).

(NSTimer is not a real-time mechanism. In the case of main Run loop, NSTimer is responsible for all main thread events, such as UI operations, so that the current run loop lasts longer than the timer interval, and the timer’s next callback is delayed. This leads to the unpunctuality of the timer. The timer has a property called tolerance, which indicates the maximum error allowed when the time point is up. A delay too long will cause the timer callback to be ignored.

You can see a description of timing accuracy in Apple’s Timer documentation: Timer Programming Topics

Timing Accuracy A timer is not A real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the Timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a Timer’s firing time occurs while the run loop is in a mode that is not monitoring the timer or during a long callout, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing A repeating timer reschedules itself based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.

Timers are not real-time mechanics; It fires only if one of the run Loop modes to which the timer has been added is running and can check whether the timer’s trigger time has passed. Because a typical Run loop manages a variety of input sources, the effective resolution of the timer interval is limited to the order of 50-100 milliseconds. If the timer’s trigger time occurs when the run loop is in unmonitored timer mode or during an extended invocation, the timer will not start until the next time the loop checks the timer. As a result, the actual time the timer may trigger may be a considerable time after the scheduled trigger time.

Repeat timers reschedule themselves based on the scheduled trigger time rather than the actual trigger time. For example, if you plan to trigger the timer at a specific time and then every 5 seconds, the scheduled trigger time will always fall on the original 5-second interval, even if the actual trigger time is delayed. If the trigger time is delayed so far that one or more of the planned trigger times are exceeded, the timer fires only once during that time period; The timer is rescheduled for the next scheduled trigger time in the future.

The following code applies for a thread and starts its Run loop to observe the time of the timer callback.

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    // sleep(1);
        NSLog(@"⏰⏰⏰ timer callback...");
    }];
    
    // Execute caculate in thread after 2 seconds
    [self performSelector:@selector(caculate) withObject:nil afterDelay:2];
    
    [[NSRunLoop currentRunLoop] run];
    }];
    [thread start];
}

- (void)caculate {
    NSLog(@"👘 👘 % @", [NSThread currentThread]);
    sleep(2);
}
Copy the code

The running code will see that the first two seconds of the timer execute normally based on the printing time. Then caculate’s execution will cause the timer to be delayed by two seconds. After two seconds, the timer will continue to execute normally once per second. If you turn on the sleep(1) annotation in the timer’s callback, you see that the timer executes every two seconds.


37. PerformSelector family of functions

When calling NSObject performSelecter: afterDelay: after its internal will actually create a Timer and add to the current thread run in the loop. So if the current thread does not have a Run loop, this method will fail.

The following functions are declared under the NSDelayedPerforming category of NSObject.

@interface NSObject (NSDelayedPerforming)
/ / specified NSRunLoopMode
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
// The default is NSDefaultRunLoopMode
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
@end
Copy the code

PerformSelector: withObject: afterDelay: inModes: after the delay using the specified mode on the current thread calls the receiver (NSObject object and its subclasses) method.

ASelector: aSelector that identifies the method to call. The method should have no obvious return value (void), and should take a single argument of type ID, or no arguments.

AnArgument: The argument passed to the method when called. If the method does not accept arguments, nil is passed.

Delay: Minimum time before sending a message. Specifying a delay of 0 does not necessarily cause the selector to execute immediately. The selector is still queued in the thread’s Run loop and executed as soon as possible.

Modes: An array of strings that identify the modes associated with the timer that executes the selector. This array must contain at least one string. If you specify nil or an empty array for this argument, this method returns without executing the specified selector.

This method sets up a timer to execute aSelector messages on the run loop of the current thread. The timer configuration runs in the modes specified by the modes parameter. When the timer fires, the thread attempts to fetch the message from the Run loop and execute the selector. If run loop is running and in one of the specified modes, it succeeds; Otherwise, the timer will wait until the Run loop is in one of these modes. It will add a timer under the run loop mode of the current run loop, which can be verified by the following code:

    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"🧗 ‍ ♀ ️ 🧗 ‍ ♀ ️...");

        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"⏰⏰⏰ timer callback...");
        }];

        [self performSelector:@selector(caculate) withObject:nil afterDelay:2]; // ⬅️ breakpoint 1
        
        NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // ⬅️ breakpoint 2
        [runloop run];
    }];
    [thread start];
Copy the code

When both breakpoints are executed, print from the console via Po [NSRunLoop currentRunLoop] :

// at breakpoint 1: Po [NSRunLoop currentRunLoop]. timers = <CFArray0x28314e9a0 [0x20e729430]>{type = mutable-small, count = 1, values = (
    0 : <CFRunLoopTimer 0x28204df80 [0x20e729430]>{valid = Yes, firing = No, interval = 1, tolerance = 0, next fire date = 631096717 (14.273319 @ 16571855540445), callout = (NSTimer) [_NSTimerBlockTarget fire:] (0x1df20764c / 0x1df163018) (/System/Library/Frameworks/Foundation.framework/Foundation), context = <CFRunLoopTimer context 0x28154b900>})...// at breakpoint 2: Po [NSRunLoop currentRunLoop]. timers = <CFArray0x28314e9a0 [0x20e729430]>{type = mutable-small, count = 2, values = (
    0 : <CFRunLoopTimer 0x28204df80 [0x20e729430]>{valid = Yes, firing = No, interval = 1, tolerance = 0, next fire date = 631096717 (32.979197 @ 16571855540445), callout = (NSTimer) [_NSTimerBlockTarget fire:] (0x1df20764c / 0x1df163018) (/System/Library/Frameworks/Foundation.framework/Foundation), context = <CFRunLoopTimer context 0x28154b900>}
    1 : <CFRunLoopTimer 0x28204db00 [0x20e729430]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 631096747 (2.84795797 @ 16572578697099), callout = (Delayed Perform) ViewController caculate (0x1df1f4094 / 0x10093ab88) (/var/containers/Bundle/Application/C2E33DEA-1FB0- 48A0-AEDD2 -D13AF564389/Simple_iOS.app/Simple_iOS), context = <CFRunLoopTimer context 0x28003d4c0>})...Copy the code

Can see performSelector: withObject: afterDelay: added a timer.

If you want to run the loop in the default mode of make out a message, please use the performSelector: withObject: afterDelay: inModes: method. If you are unsure whether the current thread is given priority to the thread, you can use performSelectorOnMainThread: withObject: waitUntilDone: Or performSelectorOnMainThread: withObject: waitUntilDone: modes: method to ensure that the selector on the main thread. To cancel the line message, please use the cancelPreviousPerformRequestsWithTarget: or cancelPreviousPerformRequestsWithTarget: selector: object: method.

This method registers with the Runloop of its current context and depends on runloops that run periodically for proper execution. A common context is when scheduling queue calls, you might call this method and end up registering with a Runloop that doesn’t automatically run periodically. If such functionality is required when running on a dispatch queue, use dispatch_after and related methods to obtain the desired behavior. (Similarly, NSTimer is not always available, you can use dispatch_source instead)

🎉🎉🎉 To be continued…