iOS source code analysis (1)-RunLoop

iOS source code analysis (1)-RunLoop

NSRunLoop is an OC package based on CFRunLoopRef. It provides an object-oriented API, but it is not thread-safe. CFRunLoopRef is in the CoreFoundation framework. It provides a pure C function API and is thread-safe. CoreFoundation is open source ( CoreFoundation source code) Address )

image.png

Creation of Runloop

typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;/* locked for accessing mode list */
 Used to manually wake up the current runloop thread, complete by calling CFRunLoopWakeUp, CFRunLoopWakeUp will send a message to _wakeUpPort
    __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;
//For blocks added to runloop, block tasks can be added to runloop through CFRunLoopPerformBlock.
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

A RunLoop object mainly contains a corresponding thread, several modes, several sets of commonMode and commonModeItems, and a currently running mode. The function __CFRunLoopCreate to create RunLoop needs to pass in the parameter is thread, indicating that runloop and thread are inseparable.

image.png

CF does not provide a function to create a runloop externally, mainly by obtaining the RunLoop created by the main thread or the current thread,

CFRunLoopRef CFRunLoopGetMain(void) {
     static CFRunLoopRef __main = NULL;//no retain needed
    //pthread_main_thread_np() main thread
     if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np());//no CAS needed
     return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
     CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
     if (rl) return rl;
    //pthread_self() current thread
     return _CFRunLoopGet0(pthread_self());
 }

All call the _CFRunLoopGet0 function. From the source code, we can see that the storage method of runloop is a key-value pair, the key is the current thread, the value is runloop, and the thread and runloop have a one-to-one relationship. When the dictionary is empty, the main thread will be created by default. runloop, and the child thread is created when it is acquired.

_CFRunLoopGet0 function

When does the child thread create RunLoop

By viewing the stack of NSThread start], you can see that the child thread calls CFRunLoopGetCurrent to create the runloop of the current thread, so the runloop is created by obtaining the function of the main thread or the current thread

__NSThread_start_stack

RunLoop running logic

image.png

  Through the CFRunLoopRun function, we intuitively perceive that runloop is a do..while loop. As long as the result does not stop or returns to finish, the CFRunLoopRunSpecific function will be executed again and again.

Under what circumstances will exit the loop

The app stops running; the thread executes at one time; the set maximum time expires; the mode is empty;

         //If the event is processed, set the parameters to one-time execution when starting Runloop
            if (sourceHandledThisLoop && stopAfterHandle) {  
                retVal = kCFRunLoopRunHandledSource;  
           //If the maximum running time set when starting Runloop expires
            } else if (timeout) {  
                retVal = kCFRunLoopRunTimedOut;  
           //If the start Runloop is forced to stop by an external call,
            } else if (__CFRunLoopIsStopped(runloop)) {  
                retVal = kCFRunLoopRunStopped;  
           //If the modeI that starts Runloop is empty,
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {  
                retVal = kCFRunLoopRunFinished;  
            }  

Among them, the most contacted is Mode. Every time the main function of RunLoop is called, a Mode needs to be specified, mainly kCFRunLoopDefaultMode and UITrackingRunLoopMode. If you need to switch Mode, you can only exit Loop and re-specify a Mode to enter. The former is the default Runloop Mode of the system. For example, when entering the iOS program, it is in this mode by default without doing any operation. If you slide a UIScrollView type of View, the main thread will switch Runloop to UITrackingRunLoopMode. There is also a UIInitializationRunLoopMode that is entered when the program starts. mode, generally used rarely.

struct __CFRunLoopMode {
    ...
    pthread_mutex_t _lock;/* must have the run loop locked before locking this *///Object lock to ensure thread safety
    ...
    CFMutableSetRef _sources0;//source0 type CFRunLoopSource set
    CFMutableSetRef _sources1;//set of CFRunLoopSource of source1 type
    CFMutableArrayRef _observers;//observer array
    CFMutableArrayRef _timers;//timer array
    ...
};

CFRunLoop also defines a pseudo mode called kCFRunLoopCommonModes, which is not a real mode, but a collection of several modes. Adding to the source/timer/observer of CommonMode is equivalent to adding it to all the modes in it. We can use lldp po [NSRunLoop currentRunLoop]) to see from the print results that CommonMode contains the above DefaultMode and TrackingRunLoopMode:

common modes = <CFBasicHash 0x7fdaa0d00ae0 [0x1084b57b0]>{type = mutable set, count = 2,
entries =>
0: <CFString 0x10939f950 [0x1084b57b0]>{contents = "UITrackingRunLoopMode"}
2: <CFString 0x1084d5b40 [0x1084b57b0]>{contents = "kCFRunLoopDefaultMode"}
}

The timer can still work normally when sliding UIScrollView type View, you need to add the timer to CommonMode, so that it can be executed in DefaultMode or TrackingRunLoopMode.

How to judge whether mode is empty

Determine whether the mode is empty

  First judge whether source0 is empty, if it is empty to exit, then judge whether source1 is empty, if it is empty to exit, then judge whether there is a timer, so if the runloop is to run, it must have one of source or timer. There are two types of Source: Source0 and Source1. Source0 only contains a callback (function pointer), which cannot trigger events actively. When using it, you need to call the CFRunLoopSourceSignal(rlms) method to mark this Source as pending, and then manually call the CFRunLoopWakeUp(rl) method to wake up RunLoop and let it handle the event. Source1 contains a mach_port and a callback (function pointer), which are used to send messages to each other through the kernel and other threads. This type of Source can actively wake up the thread of RunLoop.

The running logic of runloop

The above runloop logic diagram ( picture source address ) clearly describes the logic flow of runloop operation. Compare it with the logic diagram that everyone has quoted.

image.png

Running NSRunloop provides three commonly used run methods by default:

     -(void)run; 
     -(BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
     -(void)runUntilDate:(NSDate *)limitDate;

The run method corresponds to the CFRunLoopRun in the above CFRunloop and will not exit unless CFRunLoopStop() is called; usually this method is used if you want to never exit RunLoop, otherwise you can use runUntilDate. runMode:beforeDate: corresponds to the CFRunLoopRunInMode(mode, limiteDate, true) method, which is executed only once and exits after execution. The runUntilDate: method is actually to set the timeout, and whether to execute the parameter setting is false is equivalent to CFRunLoopRunInMode (kCFRunLoopDefaultMode, limiteDate, false). After the execution, it will not exit. Continue the next RunLoop until timeout.

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {/* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

RunLoop and AutoreleasePool

@autoreleasepool is a __AtAutoreleasePool structure by executing clang -rewrite-objc

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

The objc_autoreleasePoolPush object is added to the automatic release pool, and the objc_autoreleasePoolPop releases the object. There is not much to expand on the specific implementation of the two functions.

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

In the result of [NSRunLoop currentRunLoop] we can see that the CFRunLoopObserver related to the automatic release pool is:

<CFRunLoopObserver>{activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler} 
<CFRunLoopObserver>{activities = 0xa0, callout = _wrapRunLoopWithAutoreleasePoolHandler}
####RunLoop's running status

The status changes of RunLoop are

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
   //about to enter the loop
    kCFRunLoopEntry = (1UL << 0),
   //is about to process the timer
    kCFRunLoopBeforeTimers = (1UL << 1),
   //about to process source
    kCFRunLoopBeforeSources = (1UL << 2),
   //about to sleep
    kCFRunLoopBeforeWaiting = (1UL << 5),
   //Just wake up, exit sleep
    kCFRunLoopAfterWaiting = (1UL << 6),
   //about to exit
    kCFRunLoopExit = (1UL << 7),
   //All activities
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

activities = 0x1 means kCFRunLoopEntry enters the loop, and activities = 0xa0 means (kCFRunLoopBeforeWaiting | kCFRunLoopExit) is ready to enter sleep and is about to exit the loop two runloop states

We can use CFRunLoopObserverCreateWithHandler() to create an observer, set the state changes and callbacks to be monitored when creating, and then use CFRunLoopAddObserver() to add an observer to the current RunLoop. When the current RunLoop state changes, the observer will execute the callback

 CFRunLoopObserverRef observer =
    CFRunLoopObserverCreateWithHandler(
                                       CFAllocatorGetDefault(),
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       ^(CFRunLoopObserverRef observer,
                                         CFRunLoopActivity activity) {
                                           if (activity==kCFRunLoopEntry) {
                                               NSLog(@"About to enter Loop:%zd", activity);
                                           }
                                           if (activity==kCFRunLoopBeforeWaiting) {
                                                NSLog(@"About to enter sleep: %zd", activity);
                                           }else{
                                               NSLog(@"RunLoop status change: %zd", activity);
                                           }
                                          
                                       });
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);

_wrapRunLoopWithAutoreleasePoolHandler call logic

By setting symbolic breakpoints on _wrapRunLoopWithAutoreleasePoolHandler, the obtained assembly code, _wrapRunLoopWithAutoreleasePoolHandler will call NSPopAutoreleasePool and NSPushAutoreleasePool, which are also two functions of objc_autoreleasePoolPush and objc_autoreleasePoolPop

_wrapRunLoopWithAutoreleasePoolHandler

  The current activities = kCFRunLoopEntry jumps to 0x1965217b4 through cmp x20, #0x1 will only execute _objc_autoreleasePoolPush() to add a sentinel object flag to the current AutoreleasePoolPage to create an automatic release pool. When activities = kCFRunLoopBeforeWaiting|kCFRunLoopBeforeWaiting|kCFRunLoopExitool will call Popc_autoreleaseExitool when it is about to enter sleep () The newly added object has been cleaned up until it encounters the sentinel object and objc_autoreleasePoolPush() adds the release object. When the RunLoop is about to exit, objc_autoreleasePoolPop() will be called to release the objects in the pool automatically.

Reference: https://cloud.tencent.com/developer/article/1438310 iOS source code analysis (1)-RunLoop-Cloud + Community-Tencent Cloud