1. What is RunLoop

A RunLoop is a RunLoop, which is also an object, and provides entry functions to do a while loop, which ensures that the running program does not exit.

We know that the symbolic statement of the end of an application is return. In the main function of the iOS application, return and execute a UIApplicationMain function, as shown in the following figure:

Why can the application still receive and process messages if it has already returned? Shouldn’t that be the end of the program? Our agents in AppDelegate application: didFinishLaunchingWithOptions: method to add breakpoints, and bt to print the stack information, explore to the following contents:

The execution process of the program, first dyld to load the application, execute the main function, start RunLoop, load GCD…… This shows that the application performs a series of initializations during startup. RunLoop also comes from the CoreFoundation framework, part of which is open source, including RunLoop. In its source code, the implementation of RunLoop Run is indeed a do while loop. See below:

We know that RunLoop relates to threads and provides a mechanism for handling messages. A search for Thread in Apple’s official developer Documentation Archive includes a description of RunLoop:

This also shows that RunLoop and thread are closely related.

2. The role of the RunLoop

To summarize what RunLoop does:

  • Keep the program running
  • To deal withAPPEvents (touches, timers,performSelector)
  • savecpuResources, improve the performance of the program: do what you need to do, rest when you need to
  1. Keep the program running

    This is easy to understand. RunLoop is started when UIApplicationMain is created by main. If RunLoop is not started, the application will exit.

  2. Handle various events in the APP

    In apple’s official documentation, there’s this image:

    A RunLoop is a loop that a thread uses to handle running events and response events. These events include port event sources, screen touch events, performSelector, Timer, and so on.

    I don’t think we’ve seen RunLoop very much in the development of upper-level apps, and I personally think RunLoop encapsulation is at its best. RunLoop is used to handle events. Here are some examples:

    • Timer

      Handle Timer events, corresponding to __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__, as shown below:

    • performSelector

      Handles the performSelector event, which also corresponds to __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__, as shown below:

    • GCD

      Events are processed in the queue, corresponding to __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__, as shown below:

    These events are named __CFRUNLOOP_IS_. Check the source code, which provides different response methods for different events, as shown in the following figure:

    Summary of event handling callback methods:

    • blockApplication:__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
    • calltimer:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
    • The responsesource0:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
    • The responsesource1:__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
    • GCDThe home side columns:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    • observerSource:__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
  3. Save CPU resources and improve program performance

    How can RunLoop save CPU resources and improve application performance? See below:

    As you can see in the figure above, once the application is started, it stays running, but the CPU usage is always 0%. We know that a RunLoop is essentially a do while loop. What if we start a loop? Compare!

    As you can see from the graph above, the CPU usage is always high, so it can be concluded that RunLoop provides a loop that is different from a normal loop, running only when something needs to be done and resting when nothing needs to be done! Thus it can save CPU resources and improve program performance. Then how this function is realized will be analyzed below.

3. Relationship between RunLoop and thread

As mentioned above, runloops and threads are closely related and have a one-to-one correspondence. So how did their relationship come about?

// Get main RunLoop NSLog(@"%@", CFRunLoopGetMain()); // Get the current RunLoop NSLog(@"%@", CFRunLoopGetCurrent());Copy the code

We usually print the main RunLoop and the current RunLoop in one of the two ways above. See CFRunLoop source code for its implementation:

Read the above source code, it is easy to see that the RunLoop is obtained through the thread. So how is the relationship between threads and runloops established? The _CFRunLoopGet0 function is used to implement the loop0 function.

_CFRunLoopGet0

  1. Maintained aCFMutableDistionaryRefThe dictionary__CFRunLoops, dictionary defaults toNULL;
  2. ifCFRunLoopsIf no, create oneCFMutableDistionaryRefDictionary, and initialize the main thread by defaultRunLoop;
  3. Will create theRunLoopInto theCFMutableDistionaryRefIn the dictionary, in the dictionary__CFRunLoopsIn, the thread iskey.RunLoopforvalue;
  4. Fetch by threadRunLoopWhen, in order tokey-valueMethod to get the corresponding from the dictionaryRunLoop;
  5. ifRunLoopIf no, create onenewLoop, with the thread askey.RunLoopforvalueAnd stored in the__CFRunLoops;
  6. Returns the corresponding of the threadRunLoop.

Runloops for the main thread are created by default, while runloops for child threads are lazily loaded and created as needed. Runloops are one-to-one with threads and stored in a dictionary.

  • The child threadRunLoopCase analysis

    GFThread * gfThread = [[GFThread alloc] initWithBlock:^{ NSLog(@"running...."); [NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {NSLog (@ "helloc timer... % @".  [NSThread currentThread]); }]; }]; gfThread.name = @"Hello.thread"; [gfThread start];Copy the code

    GFThread is a custom thread that inherits from NSThread and overwrites the dealloc method. In this case, an NSTimer is used in the child thread. What happens when you run this code?

    The thread life cycle ends, but the NSTimer task is not executed. This is because NSTimer relies on runloops. The main thread’s RunLoop is enabled by default, while the child thread’s RunLoop is lazy and needs to be enabled manually.

    To modify the above example, start RunLoop for the child thread. See below:

    So how do you end NSTimer? First, we need to clarify a relationship: threads correspond to runloops, and NSTimer depends on runloops. According to this idea, the following modifications can be made:

    The thread can be controlled by external variables. If the thread exits, the corresponding RunLoop will stop running. NSTimer relies on the RunLoop and will not run.

4.RunLoop data structure

There are five important classes involved in RunLoop:

  1. CFRunLoopRunLoopobject
  2. CFRunLoopMode– Five operating modes
  3. CFRunLoopSource– Input source/event sourceSource0andSource1
  4. CFRunLoopTimer– The timing source, which isNSTimer
  5. CFRunLoopObserver– Observer, for listeningRunLoop
  • CFRunLoop

    The underlying RunLoop object is CFRunLoop, and NSRunLoop is the encapsulation of the OC layer. We can get the RunLoop of the current thread in two ways:

        // c/c++
        CFRunLoopRef lp     = CFRunLoopGetCurrent();
        // OC
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    Copy the code

    So how is CFRunLoop defined at the bottom?

        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;
        };
    Copy the code

    CRRunLoop includes lock _lock, wake port _wakeUpport for source1, associated thread _pthread, current mode _currentMode, etc. A set of modes is also maintained, so from the data structure we can conclude that RunLoop and mode are one-to-many. The _commonModes attribute is also included. CommonMode is a pseudo-mode.

  • CFRunLoopMode

    You can get the list of currentMode and mode for the current thread RunLoop using the following code:

        CFRunLoopRef lp     = CFRunLoopGetCurrent();
        CFRunLoopMode mode  = CFRunLoopCopyCurrentMode(lp);
        NSLog(@"mode == %@",mode);
    
        CFArrayRef modeArray= CFRunLoopCopyAllModes(lp);
        NSLog(@"modeArray == %@",modeArray);
    Copy the code

    Run the above code and get the following result:

    CurrentMode is kCFRunLoopDefaultMode, and the current thread RunLoop contains three modes: UITrackingRunLoopMode, GSEventReceiveRunLoopMode, kCFRunLoopDefaultMode.

    • This example describes how to switch mode

      The following case is introduced to understand the mode switching process, as shown below:

      RunLoop adds a Timer in DefaultMode. The program normally runs in DefaultMode, but when scrolling through the view, it switches to UITrackingMode and the Timer event is no longer triggered. When the scrolling view is stopped, DefaultMode is switched back to and timer resumes.

      How to solve this problem? We can add Timer to commonMode, why is it not affected by view scrolling when we put Timer in commonMode? Below will answer!

    So how is mode defined at the bottom?

        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 */
        };
    Copy the code

    The __CFRunLoopMode source code includes four sets, _sources0, _sources1, _observers, and _timers, which are commonly known as events. Therefore, we can draw the conclusion that CFRunLoopMode and sourses, timer and Observer are also one-to-many relationships.

    NSRunLoopMode can be found by searching in Developer Document. The system maintains 5 modes in total, as shown in the following figure:

    • kCFRunLoopDefaultModeThe default running mode, usually the main thread is in thisModeRun under
    • UITrackingRunLoopModeInterface trackingModeforScrollViewSuch view, track touch sliding, ensure that the interface is not affected by other slidesModeThe influence of
    • UIInitializationRunLoopModeHe had just startedAppWas the first one to enterModeIs not in use after the startup is complete
    • GSEventReceiveRunLoopModeAccept system time internallyModeUsually not
    • kCFRunLoopCommonModesIs a pseudo-mode that can be marked asCommonModesRun in the mode of,RunLoopWill automatically_commonModeItemsIn thesource,observe,timerSynchronize to one that has tagsModeIn the water.

To sum up, the following diagram can be obtained:

  • RunLoopwiththreadOne to one
  • RunLoopwithModeMore than a pair of
  • Modeandsource,timer,observerOne to many

5.RunLoop event handling mechanism

In the second section above, we showed that RunLoop handles various events in the APP (touches, timers, performSelector). That is, blocks, timers, source0, source1, GCD, and observers all rely on RunLoop. So how do these events get added to RunLoop? How does the bottom layer handle these events?

1. Add transactions

The source code provides methods for adding transactions (events), which are added to the corresponding mode, as shown below:

  • Block transaction addition

    When there is a block transaction, RunLoop calls the CFRunLoopPerformBlock method to store the block transaction in the corresponding mode, as shown below:

    In this process, the mode judgment processing is firstly carried out to determine which mode the transaction needs to be placed in. If the mode or block is empty, the transaction is released. Otherwise, a block_item is created, which is a linked list structure that stores one block and the address of the next node.

  • Adding timer transactions

    When there is a timer transaction, RunLoop calls the CFRunLoopAddTimer method to store the block transaction in the corresponding mode, as shown below:

    Here tomodeMake a judgment to determine whether it iscommonModesIf yes, it will be initialized_commonModeItemsSet, and willtimerTransactions are added to the collection. Otherwise find the correspondingmodeAnd then call__CFRepositionTimerInModeMethods,timerAdded to the_timersIn the collection.

The CFRunLoopAddObserver and CFRunLoopAddSource processes are similar and are not detailed here.

2.RunLoopcycle

By setting a breakpoint at the start of the program, we can see that the system first calls the CFRunLoopRunSpecific method to start the RunLoop. See below:

Follow the RunLoop process. Find the CFRunLoopRunSpecific method implementation in the source code, as shown below:

Two observers are registered here, the first Observer watching for an Entry event (about to enter the Loop) and the second Observer watching for an Exit event (about to Exit the Loop).

Enter the __CFRunLoopRun method, which is implemented as shown below:

The loop state is used to control retVal. During the loop, the state is judged, such as whether it is TimedOut, Stopped, or Finished, to determine whether RunLoop should be destroyed.

This part of the code is the core flow of RunLoop. The key points are marked and summarized as follows:

3. Transaction processing

In the previous two sections, we looked at the process of adding transactions (events) and the process of RunLoop loop processing. In the following sections, we will focus on how RunLoop handles transactions during the loop process.

In the do while loop above, there are entrances for processing transactions: __CFRunLoopDoBlocks, __CFRunLoopDoTimers, __CFRunLoopDoSources0, __CFRunLoopDoSource1.

  • Processing block transactions

    The __CFRunLoopDoBlocks source code is shown below:

    When analyzing the process of adding block transactions, it is mentioned that block transactions are stored in the form of a linked list, and all block transactions are iterated through the _next pointer during transaction processing.

    Block execution logic:

    • Transaction joinedmodeAnd the currentRunLoopthemodeequal
    • The currentmodeiscommonModes
    • By calling the callback function__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__Perform a task
      static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline));
      static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) {
          if (block) {
              block();
          }
          asm __volatile__(""); // thwart tail-call optimization
      }
      Copy the code
  • Handling timer transactions

    The __CFRunLoopDoTimers source code is shown below:

    During this process, the timer transaction to be executed is retrieved from _timers of the current mode, stored in the timers array, and then executed by calling the __CFRunLoopDoTimer method. The implementation principle of __CFRunLoopDoTimer is shown below:

    In this process, the Timer status is judged and the function __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ is called to complete the transaction.

The __CFRunLoopDoSources0 process is not described in detail, but eventually the __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ function is called.

The following flow chart can be concluded:

6. RunLoop AutoreleasePool

AutoreleasePool is created and released

  • 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: _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() are called when waiting (ready to sleep) to free 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.

That is, AutoreleasePool is created before a RunLoop event starts (push), and AutoreleasePool is released before a RunLoop event ends (POP). Autorelease objects in AutoreleasePool are added in a RunLoop event and released when AutoreleasePool is released.

7. RunLoop summary

RunLoop is an object that manages events and messages through loops maintained within the system. RunLoop is actually a do… A while loop, which starts when there are tasks and sleeps when there are no tasks. The essence is to receive and send messages via the mach_msg() function.

  • RunLoop’s relation to threads:

    1. RunLoopThe function is to manage threads, when threadsRunLoopAfter this function is enabled, the thread will be in a hibernation state after the execution of the task, waiting to accept new tasks, and will not exit.
    2. Only the main threadRunLoopIs enabled by default, other threadsRunLoopIt needs to be manually enabled. So when the program starts, the main thread keeps running and never exits.
  • There are five important classes involved in RunLoop:

    1. CFRunLoopRunLoopobject
    2. CFRunLoopMode– Five operating modes
    3. CFRunLoopSource– Input source/event sourceSource0andSource1
    4. CFRunLoopTimer– The timing source, which isNSTimer
    5. CFRunLoopObserve– Observer, for listeningRunLoop
  • CFRunLoopMode – Five operating modes

    • kCFRunLoopDefaultModeThe default running mode, usually the main thread is in thisModeRun under
    • UITrackingRunLoopModeInterface trackingModeforScrollViewSuch view, track touch sliding, ensure that the interface is not affected by other slidesModeThe influence of
    • UIInitializationRunLoopModeHe had just startedAppWas the first one to enterModeIs not in use after the startup is complete
    • GSEventReceiveRunLoopModeAccept system time internallyModeUsually not
    • kCFRunLoopCommonModesIs a pseudo-mode that can be marked asCommonModesRun in the mode of,RunLoopWill automatically_commonModeItemsIn thesource,observe,timerSynchronize to one that has tagsModeIn the water.
  • CFRunLoopSource – Event source

    • Source1Based on:mach_portHandle events from the system kernel or other processes, such as clicking on the phone screen
    • Source0: based on thePortApplication layer events need to be manually flagged as pending or manually woken upRunLoop
    • For example: aAPPStanding still in the foreground, the user clicksAPPInterface, screen surface events are first wrapped intoEventtellsource1(based onmach_port),source1Wake up theRunLoopThe eventEventDistributed to thesource0By thesource0To deal with.
  • CFRunLooTimer – Timing source

    NSTimer, which wakes up RunLoop at a preset point in time to perform a callback. Because it is based on RunLoop, it is not real-time. (The Timer is inaccurate because RunLoop is only responsible for distributing source messages. If the thread is currently working on a heavy task, the Timer may be delayed or executed less than once.

  • CFRunLoopObserver – Observer

    Used to listen for the point-in-time event CFRunLoopActivity.

    • KCFRunLoopEntery RunLoopReady to start
    • kCFRunLoopBeforeTimers RunLoopAre going to deal with someTimerRelated events
    • kCFRunLoopBeforeSources RunLoopAre going to deal with someSourceThe event
    • kCFRunLoopBeforeWaiting RunLoopHibernation is about to take place, switching from user state to kernel state
    • kCFRunLoopAfterWaiting RunLoopWake up, that is, switch from kernel to user mode
    • kCFRunLoopExit RunLoopexit
    • kCfRunLoopAllActivitiresListening for all states
  • Relationships between data structures

    • RunLoopandthreadIt’s a one-to-one relationship
    • RunLoopandRunLoopModeIt’s a one-to-many relationship
    • RunLoopModeandRunLoopSourceIt’s a one-to-many relationship
    • RunLoopModeandRunLoopTimerIt’s a one-to-many relationship
    • RunLoopModeandRunLoopObserverIt’s a one-to-many relationship
  • Why is main able to persist and never exit?

    UIApplication is called inside main, and the main thread RunLoop is started inside UIAPPlicationMain. It can handle messages, quickly switch from kernel to user mode, and immediately wake up processing. When there is no message processing, Switch from user mode to kernel mode to enter the wait state to avoid resource occupation. So main can always exist and never exit.