RunLoop

concept

A Runloop, as the name suggests, is essentially a do, while loop, doing something when you have something to do and sleeping when you have nothing to do. Runloop lets threads sleep when they are not processing messages to avoid resource usage and wake up as soon as a message arrives.

See apple’s official documentation (Multithreaded Programming Guide) for the description: “Run Loop is an infrastructure for managing the asynchronous arrival of events on threads…… Run Loop puts its thread to sleep when there is no event processing, which eliminates cpu-consuming cycle polling and prevents the processor itself from going to sleep and saves power. See, eliminating CPU idling is what it’s most useful for.

RunLoop and thread relationship

There is a one-to-one correspondence between threads and runloops, and the relationship is stored in a global Dictionary. There is no RunLoop when the thread is created, and if you don’t grab it, it never will. RunLoop creation occurs at the first fetch and RunLoop destruction occurs at the end of the thread.

Apple does not provide an interface for creating runloops directly, but we can get Runloop objects using CFRunLoopGetMain() and CFRunLoopGetCurrent()

RunLoop External interface

A RunLoop contains several modes, each of which contains several sources/timers/observers. Only one Mode can be specified each time RunLoop’s main function is called, and this Mode is called CurrentMode. If you need to switch Mode, you can only exit Loop and specify another Mode to enter. The main purpose of this is to separate the Source/Timer/Observer groups from each other.

Mode

A RunLoop contains multiple modes, each of which contains several sources/timers/observers. Only one Mode can be specified each time RunLoop’s main function is called, and this Mode is called CurrentMode. If you need to switch Mode, you can only exit Loop and specify another Mode to enter. This is mainly to separate the Source/Timer/Observer in different modes from each other.

KCFDefaultRunLoopMode Specifies the default Mode in which the main thread runs

UITrackingRunLoopMode Interface tracing Mode, used by ScrollView to track touch sliding, ensuring that the interface sliding is not affected by other modes

UIInitializationRunLoopMode into one of the first Mode when just start the App, start after the completion of the will no longer be used

GSEventReceiveRunLoopMode accept system internal Mode of events, usually in less than

Kcfrunloopcommonmode This is a placeholder Mode, not a real Mode

Source

Source0

That is, non-port-based, that is, user-defined events. You need to wake up the thread manually

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 Runloop to handle the event.

Source1

Source1 Mach_port-based events from the system kernel or other processes or threads that can actively wake up a dormant RunLoop. Just think of mach_port as a mechanism for sending messages between processes.

Timer

Time-based triggers are basically NSTimer. Wake up RunLoop at a preset point in time to perform a callback. Since it is runloop-based, it is not real-time (i.e. NSTimer is inaccurate). Because RunLoop is only responsible for distributing messages from the source. If the thread is currently working on a heavy task, the Timer may be delayed or executed less than once.

NSTimer is actually a CFrun Optimerref, between them is a toll free bridged. Once an NSTimer is registered with a RunLoop, the RunLoop registers events for its repeated time points. Such as 10:00, 10:10, 10:20. To save resources, RunLoop does not call back this Timer at a very precise point in time. The Timer has a property called Tolerance that indicates the maximum error allowed when the time point is up.

In iOS, NSTimer, CADisplayLink and GCD timer are common. NSTimer and CADisplayLink are implemented based on NSRunLoop, so there are errors. GCD timer only depends on the system kernel, which is more punctual than the first two.

Observer

CFRunLoopObserverRef is the Observer, and each Observer contains a callback (function pointer) that the Observer receives when the state of the RunLoop changes. The following time points can be observed:

kCFRunLoopEntry

RunLoop is ready to start

kCFRunLoopBeforeTimers

RunLoop will handle some timer-related events

kCFRunLoopBeforeSources

RunLoop will handle some Source events

kCFRunLoopBeforeWaiting

RunLoop will go to sleep, switching from user mode to kernel mode

kCFRunLoopAfterWaiting

RunLoop is woken up after switching from kernel to user mode

kCFRunLoopExit

RunLoop exit

kCFRunLoopAllActivities

Listening for all states

Runloop executes the flow

/* rl, RLM are locked on entrance and exit */ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef RLM, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){start a timer by GCD, Dispatch_source_t timeout_timer = NULL; . dispatch_resume(timeout_timer); int32_t retVal = 0; // Notify Observers: about to deal with timer events __CFRunLoopDoObservers(RL, RLM, kCFRunLoopBeforeTimers); / / notify Observers: __CFRunLoopDoObservers(rl, RLM, kCFRunLoopBeforeSources) // Handle Blocks __CFRunLoopDoBlocks(rl, RLM); Sources0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, RLM, stopAfterHandle); If (sourceHandledThisLoop) {// Process Blocks __CFRunLoopDoBlocks(rl, RLM); } / / judge whether port (Source1) if (__CFRunLoopWaitForMultipleObjects (NULL, & dispatchPort, 0, 0, & livePort, NULL)) {// Handle messages goto handle_msg; } // Notify Observers: about to enter hibernation __CFRunLoopDoObservers(rl, RLM, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll? 0 : TIMEOUT_INFINITY); // user callouts now OK again __CFRunLoopUnsetSleeping(rl); // notify Observers: astounded, ending dormancy __CFRunLoopDoObservers(rl, RLM, kCFRunLoopAfterWaiting); Handle_msg: if (awakened by timer) {// Handle Timers __CFRunLoopDoTimers(rl, RLM, mach_absolute_time()); }else if (GCD wake up){// handle GCD __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(MSG); }else if (by source1){// By source1, Source1 __CFRunLoopDoSource1(rl, RLM, RLS, MSG, MSG ->msgh_size, &reply)} // Handle block __CFRunLoopDoBlocks(rl, RLM); if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; Else if (timeout_context->termTSR < mach_absolute_time()) {retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) {__CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (RLM ->_stopped) {RLM ->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, RLM, previousMode)) {retVal = kCFRunLoopRunFinished; }}while (0 == retVal); return retVal; }Copy the code

  1. noticeObservers:Enter theLoop
  2. noticeObservers:To deal withTimers
  3. noticeObservers:To deal withSources
  4. To deal withSource0(May be processed againBlocks)
  5. If there isSourcel1, jump to the first9Step)
  6. noticeAstute is about to go dormant.
  7. Hibernate and wait for a message to wake up
  8. noticeObservers:The thread has just been awakened
    • To deal withTimer
    • To deal withGCD Async To Main Queue
    • To deal withSourcel1
  9. Based on the results of the previous execution, decide what to do
    • Back to the first2
    • exitLoop
  10. noticeObservers:exitLoop

Relationships between data structures

The relationship between RunLoop and mode is one-to-many, and the relationship between mode and timer, source, and observer is one-to-many

Relationship between RunLoop and AutoreleasePool

The main thread

After the App starts, Apple registers two observers in the main RunLoop. The callback is _wrapRunLoopWithAutoreleasePoolHandler ().

The first Observer monitors an event called Entry (about to enter Loop), whose callback creates an automatic release pool by calling objc_autoreleasePoolPush (). Its order is a 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_autoreleasePool Push () 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.

The child thread

In multithreading, since objects (class methods) may be created in child threads using methods such as convenience constructors, the release of these objects can only be placed in the auto-release pool, which needs to be added to the child thread.

Using convenience constructors and other methods to create objects that are autoreleases requires automatic release pooling

The primary runloop is enabled by default, and the child runloop is disabled by default. This means that autoReleasepool will not be created in the child thread, so we need to create an autoreleasepool in the child thread. (The class methods used in the child thread are all autoRelease, so there is no pool torelease, which means there is no way torelease later, causing a memory leak.) —- If an event is generated in the main thread, runloop will go back to create autoreleasepool. This explains why the autoreleasepool is not created in the child thread. The child thread’s runloop is closed by default, so autoreleasepool is not created automatically. You need to manually add NSThread and NSOperationQueue to create child threads you need to manually create autoReleasepool, GCD does not need to manually create autoreleasepool to create child threads, Because each GCD queue creates its own Autoreleasepool

conclusion

This should give you a better understanding of objective-C memory management. Normally, you don’t need to add autoReleasepool manually; you can use autoReleasepool that is automatically maintained by threads. Autoreleasepool is added manually in three cases, as described in the Apple documentation Using Autorelease Pool Blocks:

  1. If you’re writing a program that’s not based on a UI framework, like a command-line tool;
  2. If you write a loop that creates a lot of temporary objects;
  3. If you create a worker thread.

How to use Runloop in AFNetworking?

The AFURLConnectionOperation class is built on NSURLConnection and is expected to receive Delegate callbacks in the background thread. AFNetworking creates a separate thread for this purpose and starts a RunLoop in this thread:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
    [[NSThread currentThread] setName:@"AFNetworking"J;
    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:selfselector:@selector(networkRequestThreadEntryPoint:)
object:nil];
    [_networkRequestThread start];
    });
    return _networkRequestThread;
}
Copy the code

Before RunLoop starts, there must be at least one Timer/Observer/Source. AFNetworking creates a new NSMachPort before RunLoop run. Normally, the caller needs to hold this NSMachPort (mach_port) and send messages inside the loop through this port from the external thread; But port is added here only to keep the RunLoop from exiting, not to actually send messages.

Crash came back to life

Runloop relies on mode for circulation. Build a runloop by itself, save the mode of runloop before it crashes, and let it keep spinning around to prevent it from crashing. Save the crashed stack and upload it to the server for processing and then crash, which can also prevent it from crashing.