A. Runloop is introduced
1. What is Runloop
The literal running loop is an object that provides an entry function. The program will enter do… A while loop, which handles events. It is not a normal do-while loop, which uses CPU resources temporarily, and runloop goes to sleep when there is no message processing.
2. The Runloop
- Keep the program running
- Handles various events in your app: touches, timers, performSelector, and so on
- Save CPU resources, provider performance
3.Runloop and thread relationship
- Apple doesn’t allow you to create runloops directly, it only provides two auto-fetching functions:
CFRunLoopGetMain()
: Gets the main running loop.CFRunLoopGetCurrent()
: Gets the current running loop. - Runloop and thread one – to – one relationship.
- You can only operate on the RunLoop of the current thread, not on runloops of other threads.
- The RunLoop object is created when the RunLoop is first fetched, and destroyed at the end of the thread.
- The RunLoop object for the main thread is created automatically, while the RunLoop object for the child thread needs to be acquired actively, because the child thread does not have a RunLoop when it is created. If you do not acquire the RunLoop object, it will never be generated.
CFRunLoopGetCurrent() : CFRunLoopGetCurrent() :
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
Copy the code
Get the RunLoop, call _CFRunLoopGet0, and the current thread (pthread_self()) is passed in as an argument.
static pthread_t kNilPthreadT = { nil, nil }; CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { //1. Nil, set to the main threadif(pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFSpinLock(&loopsLock); 3.__CFRunLoops is a static global variable of type CFMutableDictionaryRef that stores the mapping between threads and runloopsif(! __CFRunLoops) {4. If __CFRunLoops is empty __CFSpinUnlock(&loopsLock); / / 5. Create a mutable dictionary CFMutableDictionaryRef CFMutableDictionaryRef dict = CFDictionaryCreateMutable (kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); //8. Assign dict to __CFRunLoopsif(! OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFSpinLock(&loopsLock); CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFSpinUnlock(&loopsLock); //10. There is no runloopif(! CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFSpinLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if(! __CFRunLoops CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newloop); loop = newLoop; } // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFSpinUnlock(&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; }Copy the code
- Global static variable of type CFMutableDictionaryRef
__CFRunLoops
, thread is key, corresponding torunloop
forvalue
Stored in the__CFRunLoops
Threads and runloops are one-to-one.
2. The Runloop structure
- CFRunLoopRef
- CFRunLoopModeRef// Running mode
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
- CFRunLoopModeRef// Running mode
1.CFRunLoopRef: Runloop object
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 forruns of the run loop pthread_t _pthread; // Uint32_t _winthread; CFMutableSetRef _commonModes; // Two modes (kCFRunloopDefaultMode and UITrackingMode) CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; //// Mode CFMutableSetRef _modes running under the current loop; // // run all modes (CFRunloopModeRef class) struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFTypeRef _counterpart; };Copy the code
2.CFRunLoopModeRef: indicates the running mode
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
Copy the code
A RunLoop object may contain more than one Mode, and only one Mode(CurrentMode) can be specified each time the main function of RunLoop is called. You can override the specified and switch modes. The main purpose is to separate different sources, timers, and observers so that they do not affect each other.
There are five modes under RunLoop:
- KCFRunLoopDefaultMode: The default mode in which the main thread runs
- UITrackingRunLoopMode: tracking user interaction events (used in ScrollView to track touch sliding, ensuring interface sliding is not affected by other modes)
- UIInitializationRunLoopMode: in the first to enter the first Mode when just start the App, start after the completion of the will no longer be used
- GSEventReceiveRunLoopMode: accept system internal event, is usually not used
- Kcfrunloopcommonmode: pseudo mode, which is not a real operating mode, but a combination of kCFRunLoopDefaultMode and UITrackingRunLoopMode.
In the project, the following scenario: There is an infinite loop of banner in the page. When the user slides on the interface, the banner timer does not work. There are two modes in the main thread RunLoop: kCFRunLoopDefaultMode and UITrackingRunLoopMode. The default is defaultMode, but when sliding UIScrollView RunLoop switches mode to TrackingRunLoopMode and Timer does not execute. If you want to keep the timer alive while sliding, you can use CommonMode to do this.
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Copy the code
3.CFRunLoopSourceRef
- Source0: not based on Port. Contains only one callback (function pointer) and cannot actively fire events. To use this, call CFRunLoopSourceSignal(source), mark the source as pending, and then manually call CFRunLoopWakeUp(runloop) to wake up the Runloop to handle the event. Touch event processing and performSelector: onThread: will trigger Source0.
- Source1: Port-based, receives and distributes system events by communicating with other threads through the kernel. Contains a mach_port and a callback (function pointer) that is used to send messages to each other through the kernel and other threads. This Source actively wakes up the RunLoop thread. Port-based thread communication and system event capture are completed by Source1. When Source1 captures the system time, it will be put in the queue and then packaged as Source0 for processing.
4.CFRunLoopTimerRef
CFRunLoopTimerRef is the timing source, which you can simply think of as NSTimer. It contains a point in time and a callback (function pointer). When added to a RunLoop, the RunLoop registers the corresponding point in time, and when the time is up, the RunLoop performs the corresponding point in time callback. NSTimer and performSelector: withObject: afterDelay: by its handle.
5.CFRunLoopObserverRef
CFRunLoopObserverRef is an observer that listens for the status of RunLoop.
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Copy the code
- KCFRunLoopEntry: the RunLoop is about to enter
- KCFRunLoopBeforeTimers: The Timer is to be processed
- KCFRunLoopBeforeSources: Source is about to be processed
- KCFRunLoopBeforeWaiting: is about to go to sleep
- KCFRunLoopAfterWaiting: Is about to wake up from sleep
- KCFRunLoopExit: is about to exit the RunLoop
- KCFRunLoopAllActivities: Listens for all state changes
6. CFRunLoopRef CFRunLoopModeRef CFRunLoopSourceRef, CFRunLoopTimerRef, CFRunLoopObserverRef relations
3. Explore the source code of RunLoop logic process
The Runloop starts with CFRunLoopRun.
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do{result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10,false);
CHECK_FOR_FORK();
} while(kCFRunLoopRunStopped ! = result && kCFRunLoopRunFinished ! = result); }Copy the code
The next step is to call CFRunLoopRunSpecific:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) returnkCFRunLoopRunFinished; __CFRunLoopLock(rl); CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName,false); / / if you don't find | | mode not registered any event, in the end, not into the loopif (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
returndid ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); CFRunLoopModeRef previousMode = rl->_currentMode; Rl ->_currentMode = currentMode; Int32_t result = kCFRunLoopRunFinished;if(currentMode->_observerMask & kCFRunLoopEntry ) /// 1. Notifying Observers: RunLoop is about to enter loop. __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds,returnAfterSourceHandled, previousMode);
if(currentMode->_observerMask & kCFRunLoopExit ) /// 10. Notify Observers: RunLoop is about to exit. __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl);return result;
}
Copy the code
The core code __CFRunLoopRun is too long and only the core code is posted here:
/// 2. Notify Observers: RunLoop is about to trigger a Timer callback.if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
if(rlm->_observerMask & kCFRunLoopBeforeSources) /// 3. Notify Observers: RunLoop is about to trigger a Source0 (non-port) callback. __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // Execute the added block __CFRunLoopDoBlocks(rl, RLM); /// 4. RunLoop triggers the Source0 (non-port) callback. BooleansourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {/// execute the added block __CFRunLoopDoBlocks(rl, RLM); } // If there is no Sources0 event handling and no timeout, poll isfalse// if there is a Sources0 event processing or timeout, poll is bothtrue
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // The first time do.. The WHIL loop does not go to this branch because the didDispatchPortLastTime initialization istrue
if(MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) {#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINIMSG = (mach_MSG_header_t *)msg_buffer; /// 5. If there is a port based Source1 in ready state, process the Source1 directly and jump to the message.if(__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {// If a message is received, Goto step 9 and start processing MSG goto handle_msg; }#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}
didDispatchPortLastTime = false; /// 6. Notify Observers: RunLoop threads that are about to enter sleep.if(! poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); // Set RunLoop to sleep __CFRunLoopSetSleeping(rl);Copy the code
msg = (mach_msg_header_t *)msg_buffer; /// 7. Call mach_msg and wait for the message to accept mach_port. The thread will sleep until it is awakened by one of the following events. /// • A port-based Source event. __CFRunLoopServiceMachPort() ¶waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
Copy the code
/// 8. Notify Observers: threads of RunLoop have just been awakened.if(! poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);Copy the code
Conclusion:
- 1. Notify the observer that RunLoop has started.
- 2. Notify the observer that the timer is about to start.
- 3. Notify the observer of any non-port-based sources that are about to start.
- 4. Start any prepared non-port-based sources (Source0).
- 5. If the port-based source (Source1) is ready and in the waiting state, go to Step 9.
- 6. Notify the observer that the thread has gone to sleep.
- 7. Put the thread to sleep until any of the following events occur. An event reaches a port-based source timer and starts. The time set by RunLoop has timed out. RunLoop is woken up.
- 8. Notify the observer that the thread will wake up.
- 9. Handle unhandled events. If the user-defined timer starts, process the timer event and restart RunLoop. Go to Step 2. If the input source is started, the corresponding message is passed. If RunLoop is awakened and the time has not expired, restart the RunLoop. Enter Step 2.
- 10. Notify the observer of the end of RunLoop.
4. Runloop applications
Almost all functions of the main thread are called from one of six functions:
-
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
Used to report external changes to the current state of the RunLoop, many of the mechanisms in the framework are triggered by the RunLoopObserver, such as CAAnimation
-
CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
Message notifications, non-delayed Perform, non-delayed Dispatch calls, block callbacks, KVO
Application: block ` ` ` void (^ block) (void) = ^ {NSLog (@ "123"); }; block(); ` ` `Copy the code
-
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"hello word"); });
-
CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
A delayed Perform, delayed dispatch call
[the self performSelector: @ the selector (the fire) withObject: nil afterDelay: 1.0];Copy the code
-
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
Handle App internal events and manage (trigger) by App itself, such as UIEvent and CFSocket. Normal function calls, system callsCopy the code
-
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
Managed by RunLoop and kernel and driven by Mach port such as CFMachPort and CFMessagePortCopy the code
Runloop with GCD
-
The runLoop timeout is implemented using dispatch_source_t in the GCD
-
Execute asynchronous tasks on GCD MainQueue
Runloop uses GCD. When dispatch_async(dispatch_get_main_queue(), block) is called, libDispatch sends a message to the main runloop and the runloop wakes up. Get the block from the message and execute it in the callback CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(). But this logic is limited to dispatches to the main thread, dispatches to other threads are still handled by libDispatch.
Runloop and automatic release pool
Apple has registered two ‘observers’ in the main RunLoop: The first Observer monitors an event called Entry(about to enter the Loop) that creates an automatic release pool in its callback. Its order is -2147483647, the highest priority, ensuring that the release pool is created before all other callbacks. The second Observer monitors two events: BeforeWaiting(ready to go to sleep) and Exit(about to Exit Loop), BeforeWaiting(ready to go to sleep) calls _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() torelease old pools and create new ones; _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 UI refresh
When in operation the UI, such as changing the Frame, update the UIView/CALayer level, or manually invoked the UIView/CALayer setNeedsLayout/setNeedsDisplay method, The UIView/CALayer is marked to be processed and submitted to a global container. Apple registers an Observer to listen for BeforeWaiting(about to go to sleep) and Exit (about to Exit Loop) events, which are called back to execute. Iterate over all uiViews /CAlayer to be processed to perform the actual drawing and adjustment, and update the UI interface.
Incident response
Apple registered a Source1 (based on the Mach port) to the receiving system, the callback function as __IOHIDEventSystemClientQueueCallback (). When a hardware event (touch/lock/shake, etc.) occurs, an IOHIDEvent event is first generated by IOKit. Framework and received by SpringBoard. SpringBoard only receives events such as buttons (lock screen/mute etc.), touch, acceleration and proximity sensor, and then forwards them to the App process using Mach port. Then apple registered the Source1 will trigger the callback, and call the _UIApplicationHandleEventQueue () distribution within the application. _UIApplicationHandleEventQueue () will wrap IOHIDEvent process, and as a UIEvent processing or distribution, including identifying UIGesture/processing screen rotation/send UIWindow, etc. Usually click event such as a UIButton, touchesBegin/Move/End/Cancel events are completed in the callback.
How to Handle Gestures
When the above _UIApplicationHandleEventQueue () to identify a gesture, the first call will Cancel the current touchesBegin/Move/End series callback to interrupt. The system then marks the corresponding UIGestureRecognizer as pending. Apple registered a Observer monitoring BeforeWaiting (Loop entering hibernation) events, the callback function Observer is _UIGestureRecognizerUpdateObserver (), Internally it gets all GestureRecognizer that has just been marked for processing and executes the GestureRecognizer callback. This callback is handled when there are changes to the UIGestureRecognizer (create/destroy/state change).
How to handle the Timer
NSTimer is essentially a CFRunLoopTimerRef. It’s toll-free bridged between them. Once an NSTimer is registered with a RunLoop, the RunLoop registers events for repeated times. To save resources, the RunLoop does not call back the Timer at exactly the right time. The Timer has a property called Tolerance that indicates the maximum error allowed when the time point is up.
MeInterval tolerance API_AVAILABLE(MacOS (10.9), ios(7.0), Watchos (2.0), TVOs (9.0));Copy the code
The NSTimer and performSEL methods are essentially a wrapper around the CFRunloopTimerRef.
How do I handle performSelector
When calling NSObject performSelecter: afterDelay: after its internal will actually create a Timer and add to the current thread RunLoop. So if the current thread does not have a RunLoop, this method will fail. When the performSelector: onThread: when, in fact, it will create a Timer is added to the corresponding thread, in the same way, if the corresponding thread no RunLoop this method will fail.
Resident child thread
To keep the thread running for a long time, add a RunLoop to the child thread and give the RunLoop an item to prevent the RunLoop from exiting automatically.
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
Copy the code
Caton monitoring
The main principle of stuttering is to add an observer to the main thread RunLoop. Detect whether the time between Source about to be processed (kCFRunLoopBeforeSources) and going to sleep (kCFRunLoopBeforeWaiting) is too long. If the time spent is greater than a certain threshold, it is considered to be stalled, and the corresponding stack call information can be output at this time.