1. The RunLoop profile
1.1 What is RunLoop
Literally running a loop, it can also be translated as running a circle.
- A RunLoop is essentially an object that keeps the program running and handles various events in the program (such as touch events, timer times,selector events).
- RunLoop puts the thread to sleep when there is nothing to do. This saves CPU resources and improves program performance.
1.2 Runloops and threads
Runloops are closely related to threads. We all know that threads are used to perform one or more specific tasks. Normally, a thread will exit when it finishes its current task, and then it will not be able to continue if it needs to perform another task. We need a way for the thread to continue to perform tasks, even if the current thread has no task to perform, the thread will not exit, but wait for the next task to arrive. So we have RunLoop.
-
Each thread has a unique RunLoop object corresponding to it.
-
The main thread RunLoop object is automatically created for us and will only be destroyed at the end of the main thread, the end of the program.
-
The child thread’s Runloop object needs to be actively created and maintained. The child thread’s Runloop object is created when it is first acquired and destroyed when the child thread terminates. The created runLoop object is not enabled by default, and runLoop must be enabled manually.
-
Runloops are not thread-safe. We can only operate on Runloop objects in the current thread, not on Runloop objects in other threads.
The relevant codes are as follows:
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop] NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop [currentRunLoop run]; [NSRunLooop mainRunLoop] // Get the main thread RunLoop objectCopy the code
1.3 The main thread RunLoop principle by default
When we start a program, the system automatically calls the main.m file that was created automatically when we created the project. The main.m file looks like this:
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
Copy the code
The UIApplicationMain function internally helps us to open the main thread RunLoop. This RunLoop keeps UIApplicationMain from returning until the application exits or crashes. The main thread in the above code can be simply interpreted as the following code:
int main(int argc, char * argv[]) { BOOL isRunning = YES; Do {// perform various tasks, handle various events} while(isRunning); return 0; }Copy the code
Below is apple’s official RunLoop model
As you can see, a RunLoop is a loop in a thread in which a RunLoop constantly detects whether an event needs to be executed through Input sources and Timer sources. It then processes the received event notification thread and lets the thread rest when there are no events.
2. Related classes of RunLoop
IOS provides us with two apis to access runloops. One is NSRunLoop for the Foundation framework. NSRunloop is essentially a CFRunLoop based OC object encapsulation, so here we will explain the Core Foundation framework related to the five RunLoop classes.
- CFRunLoopRef: represents the RunLoop object
- CFRunLoopModeRef: indicates the running mode of RunLoop
- CFRunLoopSourceRef: is the event source/input source in the RunLoop model diagram above
- CFRunLoopTimerRef: is the timing source in the RunLoop model diagram above
5 CFRunLoopObserverRef: observer that listens for RunLoop status changes
The following is a detailed explanation of the relationship between the specific meanings of several categories. Let’s take a look at a diagram that shows the relationship between the five classes:
Let’s talk about how these five categories are related to each other:
A RunLoop object (CFRunLoopRef) contains several running modes (CFRunLoopModeRef). Each operating mode has several input sources (CFRunLoopSourceRef), timing sources (CFRunLoopTimerRef), and observers (CFRunLoopObserverRef).
- Only one of these modes can be specified each time RunLoop starts, and this mode is called CurrentMode.
- Knowledge requires either an input source or a timing source in each running mode.
- If you want to change the running mode, you must exit the current RunLoop and enter another running mode.
- The main purpose of this is to distinguish the Source/Timer/Observer groups from each other
Let’s take a closer look at these five categories:
2.1 CFRunLoopRef class
The CFRunLoop class is a RunLoop object class from the Core Foundation framework. We can get RunLoop objects in the following way
- Core Foundation
CFRunLoopGetCurrent(); // Get the RunLoop object of the current thread. If it is called in the child thread for the first time, it will help us create the RunLoop object
CFRunLoopGetMain(); // Get the main thread RunLoop object
2.2 CFRunLoopModeRef
The system defines various running modes by default, as follows:
- KCFRunLoopDefaultMode: The default running mode of the APP. This mode is usually used to run the main thread
- UITrackingRunLoopMode: tracking user interaction events (used in ScrollView to track touch sliding, ensuring interface sliding is not affected by other modes)
- UIInitializationRunLoopMode: enter one of the first Mode when just start the APP, start after the completion of the will no longer use
- CSEventReceiveRunLoopMode: accept system internal events (for drawing), usually in less than
- KCFRunLoopCommonMode: this is a placeholder mode, not a real runtime mode (used later)
These kCFRunLoopDefaultMode, UITrackingRunLoopMode, kCFRunLoopCommonModes is needed in our development pattern. The specific usage method is demonstrated in 2.3 CFRunLoopTimerRef combined with CFRunLoopTimerRef
2.3 CFRunLoopTimerRef
CFRunLoopTimerRef is a timing source, understood as a time-based trigger, basically NSTimer. Here is how to use CFRunLoopModeRef and CFRunLoopTimerRef together.
Drag a textView from main. Storyboard. Then try the following code:
- (void)viewDidLoad { [super viewDidLoad]; [self timer1]; } - (void) timer1 NSTimer * {/ / 1. Create a timer timer = [NSTimer timerWithTimeInterval: 2.0 target: self selector: @ the selector (run) userInfo:nil repeats:YES]; / / 2. Add a timer to the current RunLoop, specify the operation of the RunLoop mode as the default operating mode [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode]; } - (void)run { NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]); }Copy the code
When the program is running, the run method executes every two seconds, but if you drag the textView, the run method does not execute. Why is that?
The timer we created is added to the NSDefaultRunLoopMode running mode of RunLoop, But when we drag the textView, the current RunLoop will exit the current running mode and enter the UITrackingRunLoopMode running mode. The timer we created is not added to the UITrackingRunLoopMode running mode, so the run method will not The execution.
So what’s the solution?
-
Solution 1:
Add timer to UITrackingRunLoopMode. This allows the run method to be executed in both running modes. Add the following code:
[[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode]; Copy the code
-
Solution 2:
Add timer to the kCFRunLoopCommonMode running mode. As mentioned in 2.2, this mode is a placeholder mode, not a real operation mode. If you add timer to this mode, the timer is added to the run mode labeled common.
So which runtime modes will be labeled with the common tag? NSDefaultRunLoopMode and UITrackingRunLoopMode
Therefore, it is equivalent to adding timer to NSDefaultRunLoopMode and UITrackingRunLoopMode as long as it is added to the kCFRunLoopCommonMode operation mode.
Replace the code with the following:
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes]; Copy the code
In addition to the timer creation method used in the above code, there is a common method for creating a timer
[NSTimer scheduledTimerWithTimeInterval: 2.0 target: self selector: @ the selector (run) the userInfo: nil repeats: YES];Copy the code
The timer created by this method is added to NSDefaultRunLoopMode by default. If you want to add it to UITrackingRunLoopMode, you just need to get the timer object and select one of the above solutions.
Pay attention to the point
As mentioned in the previous example, we created a timer in the main thread and added it to a specific mode in the RunLoop. What’s the difference if we create a timer in the child thread? Try executing the following code:
- (void)viewDidLoad { [super viewDidLoad]; [NSThread detachNewThreadSelector:@selector(timer2) toTarget:self withObject:nil]; } - (void) timer2 {[NSTimer scheduledTimerWithTimeInterval: 2.0 target: self selector: @ the selector (run) the userInfo: nil repeats:YES]; } - (void)run { NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]); }Copy the code
You will notice that the run method is not called at all. Why is this?
This has to do with the creation and management of runloops mentioned above.
The child thread’s Runloop object needs to be actively created and maintained. The child thread’s Runloop object is created when it is first acquired and destroyed when the child thread terminates. The created runLoop object is not enabled by default, and runLoop must be enabled manually.
So we should modify the code as follows:
- (void)timer2 {//1. Get RunLoop and create NSRunLoop currentRunLoop = [NSRunLoop currentRunLoop]; / / 2. Create a timer [NSTimer scheduledTimerWithTimeInterval: 2.0 target: self selector: @ the selector (run) the userInfo: nil repeats: YES]; //3. Start the child thread RunLoop [currentRunLoop run]; }Copy the code
2.4 CFRunLoopSourceRef
CFRunLoopSourceRef is the event source (mentioned in the RunLoop model diagram)
- Previous division:
- Port-based Sources(port-based Sources)
- Custiom Input Sources(custom)
- Cocoa Peform Selector Sources
- The current division:
- Source0: non-port based (user event)
- Source1: Port-based, communicating, receiving, and distributing system events (system events) through the kernel and other threads
The first is classified by official theory, and the second is classified by calling functions in practice.
Here’s an example using source 1 in the function call stack. First we drag a button in main.storyboard and add action 2. Then add an output statement to the code in the click action and make a breakpoint
The steps are as follows:
When we click the button after running the program, we will come to this breakpoint, and then we can see the current function call stack.
As shown below:
So click events come like this:
- First the program starts and runs to main on line 18, then calls UIApplicationMain on line 17 in main, and then all the way up to the click function.
- We can see that in line 12 there is CFRunLoopDoSources0, that is, our click event belongs to sourece0, which is handled in source0.
- Source1 is used to receive and distribute system events and then distribute them to Source0 for processing.
2.5 CFRunLoopObserver
The CFRunLoopObserver is a listener that listens for state changes in the RunLoop.
You can listen at the following points:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // Approaching RunLoop kCFRunLoopBeforeTimers = (1UL << 1), // Approaching Timer kCFRunLoopBeforeSources = (1UL << 2), // Source kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), KCFRunLoopExit = (1UL << 7), RunLoop kCFRunLoopAllActivities = 0x0FFFFFFFU // Listen for all events};Copy the code
Specific use methods are as follows:
- (void)viewDidLoad { [super viewDidLoad]; [self observer]; } - (void)observer {/** @param1: how to allocate space (usually passed in the default allocation mode) @param2: what status of the RunLoop to listen on @param3: Whether to listen continuously @param4: Priority is always 0 @ param5: when the state changes of the callback * / CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler (CFAllocatorGetDefault (), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {switch (activity) {case kCFRunLoopEntry: NSLog(@" about to enter RunLoop"); break; Case kCFRunLoopBeforeTimers: NSLog(@" About to process Timer"); break; Case kCFRunLoopBeforeSources: NSLog(@" about to process Source"); break; Case kCFRunLoopBeforeWaiting: NSLog(@" about to go to sleep "); break; Case kCFRunLoopAfterWaiting: NSLog(@" just woke up from sleep "); break; Case kCFRunLoopExit: NSLog(@" exit RunLoop"); break; default: break; }}); /** @param1: RunLoop object to listen on @param2: observer @param3: running mode */ CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); }Copy the code
The printing desk information is as follows:
As you can see, RunLoop handles a large number of Source and Timer events once the program is running, goes to sleep when there is nothing left to do, that is, puts the thread to sleep, and wakes up the RunLoop when there is an event to handle again.
3. The principle of RunLoop
With all five classes understood, let’s go into detail about how RunLoop works.
Among them, we use the following logic diagram of a netizen to illustrate
Using the logic diagram above, we can illustrate a RunLoop logic given in apple’s official documentation
The sequence is as follows: First, RunLoop checks if Mode has a source/timer, and does not exit directly
- Notifies the observer that RunLoop has started (the system itself adds an observer for us)
- Notifies the observer that a Timer is about to be processed
- Notify the observer that Sourece0 is about to be processed
- Start any ready Source0
- If Soure1 is ready and in wait state, start immediately and go to step 9.
- Notifies the observer to go to sleep
- Put the thread to sleep until one of the following events occurs
- An event reaches a port-based source
- Timer start
- The time set by RunLoop has timed out
- RunLoop is awakened by an external display.
- Notifies the observer that the thread is awakened
- Handle unprocessed events
- If the user-defined timer is started, process the timer event and restart RunLoop to go to Step 2.
- If the input source is started, the corresponding message is passed.
- If RunLoop is woken up and the time has not timed out, restart RunLoop and go to Step 2
- Notifies the observer that RunLoop ends
4. Practical use of RunLoop
Now that we’ve covered the basics, let’s look at how RunLoop works in the real world.
4.1 use of NSTimer
We’ve just explained the difference between adding Timer to RunLoop’s different modes in 2.3. You can go back and see how it works if you forget.
4.2 ImageView delayed display
One of the things we might sometimes see is that we have tableViews in our interface, and each tableView has a bunch of images in its cell. And then when we scroll through the tableView, and we need to display a lot of images, we might get stuck.
Then we can use RunLoop to solve the problem. You do this by calling UIImageView’s setImage: method with performSelector and specifying the NSDefaultRunLoopMode running mode under RunLoop. The code is as follows:
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@" boy "] afterDelay:5.0 inModes:@[NSDefaultRunLoopMode]];Copy the code
We set the time for displaying the image to be five seconds later, but after the program runs we drag the textView and find that the image does not appear after five seconds, but only when we finish dragging.
This is because the operation we set to display the image is in NSDefaultRunLoopMode of RunLoop. When we drag the textView,RunLoop will switch to UITrackingRunLoopMode mode We’re going to wait until we’re done dragging and then we’re going to go back to NSDefaultRunLoopMode and set the image.
Pay attention to the point
In the application of postponing the display of the picture above, we can find that when we switch to UITrackingRunLoopMode, the set time for executing the operation does not stop the timer, so we will execute the operation as soon as we stop dragging.
So if we add a timer to the NSDefaultRunLoopMode of the RunLoop, after dragging the textView for a while, what happens to a lot of things that should be done when they stop dragging? Let’s run the following code to see what happens.
- (void)viewDidLoad { [super viewDidLoad]; //[self observer]; NSLog(@"%s", __func__); [self test2]; Test2 {} - (void) NSTimer * timer = [NSTimer timerWithTimeInterval: 2.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) { NSLog(@"test2"); }]; [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode]; }Copy the code
The effect is shown below:
I dragged the textView at 13:02:50, and then finished dragging at 13:03:14. I found that if there were a 24 second interval at that time, there should have been 12 prints, but only two prints were executed, and these two prints were almost immediately followed by each other, with no interval. Then the normal two-second printout begins again.
Therefore, we can get the logic of RunLoop. When timer is added to NSDefaultRunLoopMode of RunLoop, after switching to UITrackingRunLoopMode,RunLoop will temporarily store two operations at most, and then wait until RunLoop switches back In NSDefaultRunLoopMode, two more operations are performed next to each other.
Conclusion: So when NSTimer is added to THE NSDefaultRunLoopMode mode, it is not absolutely accurate, and when we scroll through some views, the execution will be delayed. The solution is to add a timer to the UITrackingRunLoopMode mode, or use another timer such as the GCD timer.
4.3 Background Resident threads
Thread knowledge
-
[NSThread detachNewThreadSelector:@selector(run1) toTarget:self withObject:nil] creates and automatically starts a thread to perform the task, without manually starting it
-
Threads were created to perform a specific task, and after executing a specific task, the thread would automatically die. Once a thread is dead, it cannot be restarted to continue executing tasks.
An error is reported if the thread is dead and calls start again
Use RunLoop to implement background resident threads
When we are working on a project, we may perform frequent operations in the background, time-consuming operations in child threads (such as downloading files, playing music in the background, recording user information in the background), so IT is better for me to keep the thread from dying so that I can continue to perform tasks, rather than constantly creating and destroying threads.
So what should we do?
Add a strong reference to the resident thread, then create a RunLoop in this thread, add a Sources, and then enable the RunLoop. The reason is that as long as the RunLoop does not time out, the task will continue to execute and the thread will not die.
Specific implementation process:
- Start by creating a sliver thread and adding methods to execute
- Start a RunLoop in the execution method and add a Source or Timer. If you do not add a RunLoop, the loop will exit. The general practice is to add a port, the port, because the port does not need to specify what task to do, and the timer needs to specify, we add Source or timer here just to ensure that the loop does not exit, so there is no need to specify the task, so generally choose port.
The implementation code is as follows:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"%s", __func__); [self residentThread]; } - (void)residentThread { NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil]; self.thread = thread; [self.thread start]; } - (void)run1 {NSLog(@"run1 -- %@", [NSThread currentThread]); [[NSRunLoop currentRunLoop]addPort:[NSPort port] // A RunLoop requires at least one Source or Timer. forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop]run]; NSLog(@" RunLoop -- %@", [NSThread currentThread]); }Copy the code
3. If RunLoop is not enabled, it does not print because the RunLoop never returns.
So to see if the thread can still do something else that’s not dead, let’s call PerformSelector in touchesBegan, and see if it prints.
The code is as follows:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
}
- (void)run2 {
NSLog(@"run2 -- %@", [NSThread currentThread]);
}
Copy the code
After running the code, click on the screen to see that it can print, meaning that the thread can continue to execute the task. This completes the resident thread.
5.RunLoop
1 Only the RunLoop of the child thread can be set to exit. The RunLoop of the main thread cannot exit. The following code does not make RunLoop exit.
[[NSRunLoop mainRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
Copy the code
2 When does RunLoop create and destroy the auto-release pool
Why did RunLoop create the auto-release pool in the first place? This is because a large number of variables and objects are created during a RunLoop, and most of the variables are never used again. If you don’t clean up these unused variables, your memory can fill up. So RunLoop periodically creates an automatic release pool, releases the release pool at a specific time, and creates another.
First creation: Last destruction when RunLoop is started: Creation and destruction at other times before exiting the RunLoop: The old release pool is released before RunLoop goes to sleep, and the variables in the release pool are destroyed. A new release pool is then created to store newly generated unused variables.