This is a great article on Runloop, understanding Runloop in depth. I don’t have the depth to write this article, but why do I want to write it?

Runloop is a strange thing, and I have hardly used it in my work experience. When I learned it at that time, BECAUSE I didn’t know enough about the whole iOS ecosystem, many concepts gave me a headache.

So what I want to do in this article is change the causality. Forget what Runloop is, let’s start with requirements and see what Runloop can do. After you’ve implemented it once, it might be more enlightening to go back and look at some of these higher-level articles.

The code in this article is hosted at: github.com/tianziyao/R…

First, write down what Runloop is responsible for doing:

  • Ensure that the program does not exit;
  • Responsible for monitoring events, such as touch events, timer events, network events, etc.
  • Responsible for rendering all the UI on the screen, one Runloop, need to render all the changing pixels on the screen;
  • Save CPU overhead, let the program work when the work, to rest when the rest;

The guarantee that the program does not exit and listens should be easy to understand, expressed in pseudocode, which looks something like this:

/ / exit
var exit = false

/ / event
var event: UIEvent? = nil

// Event queue
var events: [UIEvent] = [UIEvent] ()// Event distribution/response chain
func handle(event: UIEvent) -> Bool {
    return true
}

// Main thread Runloop
repeat {
    // A new event occurs
    ifevent ! =nil {
        // Queue events
        events.append(event!)
    }
    // If there are events in the queue
    if events.count > 0 {
        // Process the first event in the queue
        let result = handle(event: events.first!)
        // The first event is removed
        if result {
            events.removeFirst()
        }
    }
    // Go to Discover events again -> Add to Queue -> Distribute events -> Process Events -> Remove events
    // The main thread exits until exit=true
} while exit == falseCopy the code

Render all of the UI on the screen, i.e. in a Runloop, events cause changes to the UI, which are represented by redrawing pixels.

So what does Runloop do at the application level, and where does it go? Let’s start with a timer.

The basic concept

When working with timers, you should have seen several methods of constructing timers, some of which need to be added to the Runloop, some of which need not.

In fact, even if we don’t need to manually add a timer to the Runloop, it is still in the Runloop, and the following two initialization methods are equivalent:

let timer = Timer(timeInterval: 1,
                  target: self,
                  selector: #selector(self.run),
                  userInfo: nil,
                  repeats: true)

RunLoop.current.add(timer, forMode: .defaultRunLoopMode)

///////////////////////////////////////////////////////////////////////////

let scheduledTimer = Timer.scheduledTimer(timeInterval: 1,
                                 target: self,
                                 selector: #selector(self.run),
                                 userInfo: nil,
                                 repeats: true)Copy the code

Now create a new project and add a TextView. Your ViewController should look like this:

class ViewController: UIViewController {

    var num = 0

    override func viewDidLoad(a) {
        super.viewDidLoad()
        let timer = Timer(timeInterval: 1,
                          target: self,
                          selector: #selector(self.run),
                          userInfo: nil,
                          repeats: true)
        RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
    }

    func run(a) {
        num += 1
        print(Thread.current ,num)
    }
}Copy the code

Intuitively, when the App is running, the console will print once every second, but when you scroll through the TextView, you’ll notice that printing stops, and when the TextView stops scrolling, printing continues.

What is the reason for this? When we learned about threads, the main thread has the highest priority, the main thread is also called the UI thread, and UI changes are not allowed on child threads. Therefore, UI events have the highest priority in iOS.

Runloop has the same concept. Runloop is divided into several modes:

// The App's default Mode, in which the main thread is usually run
public static let defaultRunLoopMode: RunLoopMode
// This is a placeholder Mode, not a real Mode, used to distinguish defaultMode
public static let commonModes: RunLoopMode
// Interface tracking Mode, used for ScrollView tracking touch sliding, to ensure that the interface sliding is not affected by other modes
public static let UITrackingRunLoopMode: RunLoopModeCopy the code

The timer is in defaultRunLoopMode, and the TextView is in UITrackingRunLoopMode, so they can’t go on at the same time.

Under what circumstances does this problem arise? For example, if you use a timer to do a wheel cast, the wheel cast stops while scrolling through the list below.

So now change the Mode of timer to CommonMode and UITrackingRunLoopMode and try again and see what’s interesting?

In UITrackingRunLoopMode, the Run method is executed when the TextView is scrolling, and when the TextView is stationary, The run method stops execution.

blocking

The main thread is enabled by default, and the child thread must be enabled manually. In the example above, when Mode is commonModes, the timer and the UI scroll occur simultaneously, which looks like they are happening simultaneously. However, no matter how the Runloop Mode changes, it always loops on this thread.

As you all know, there is an iron rule in iOS development that you should never block the main thread. Therefore, there is no time consuming operation in any Mode of the main thread. Now change the run method to the following:

func run(a) {
    num += 1
    print(Thread.current ,num)
    Thread.sleep(forTimeInterval: 3)}Copy the code

Apply the idea of Runloop

Now that we know how Runloop works and the modes it runs in, let’s try to solve a practical problem, TableCell content loading.

In daily development, we roughly divide the loading of TableView into two parts:

  1. The network request, cache read and write, data parsing, model construction and other time-consuming operations in the sub-thread processing;
  2. When the model array is ready, the callback main thread is refreshedTableView, populated with model dataTableCell;

Why do most of us do this? It’s really the same principle: never block the main thread. Therefore, for the sake of a smooth UI, we tried to separate time-consuming operations from the main thread to create the above solution.

However, UI operations must be done in the main thread, so what if populating TableCell with model data is also a time-consuming operation?

For example, operations like the following:

let path = Bundle.main.path(forResource: "rose", ofType: "jpg")
let image = UIImage(contentsOfFile: path ?? "")??UIImage()
cell.config(image: image)Copy the code

In this case, rose.jpg is a large image, and there are three of them on each TableCell. We could certainly read the image in the child thread and then update it, but we need to simulate a time-consuming UI operation, so we’ll do that first.

You can download the code and run it, scroll through the TableView, and the minimum FPS drops below 40. How does that happen?

As we mentioned above, Runloop is responsible for rendering the UI on the screen and listening for touch events.TableViewThe UI changes on the screen as it moves, which triggers the reuse and rendering of the Cell. Rendering of the Cell is a time-consuming operation, causing the Runloop to take longer to loop once, thus causing the UI to lag.

So how can we improve this process? Since the rendering of Cell is a time-consuming operation, it is necessary to strip the rendering of Cell out so that it does not affect the scrolling of TableView and ensure smooth UI, and then execute the rendering of Cell at an appropriate time. To sum up, it is the following process:

  1. Declare an array to store the rendering Cell code.
  2. incellForRowAtIndexPathThe agent returns the Cell directly.
  3. Listen to the Runloop loop, complete the loop, go to sleep and fetch the array code to execute;

Array storage code should be understandable, that is, an array of blocks, but how does Runloop listen?

Listening to the Runloop

We need to know when the Runloop starts and ends, as shown in the following Demo:

fileprivate func addRunLoopObServer(a) {
    do {
        let block = { (ob: CFRunLoopObserver? , ac:CFRunLoopActivity) in
            if ac == .entry {
                print("Enter the Runloop")}else if ac == .beforeTimers {
                print("Timer event about to be processed")}else if ac == .beforeSources {
                print("Source event about to be processed")}else if ac == .beforeWaiting {
                print("Runloop is going to sleep.")}else if ac == .afterWaiting {
                print("Runloop is awakened")}else if ac == .exit {
                print("Quit the Runloop")}}let ob = try createRunloopObserver(block: block)

        /// -parameter rl: specifies the Runloop to listen on
        /// -parameter observer: Runloop observer
        /// -parameter mode: specifies the mode to listen on
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), ob, .defaultMode)
    }
    catch RunloopError.canNotCreate {
        print("Runloop observer creation failed")}catch {}
}

fileprivate func createRunloopObserver(block: @escaping (CFRunLoopObserver? , CFRunLoopActivity) -> Void) throws -> CFRunLoopObserver {

    /* * allocator: Allocates space to a new object. By default, NULL or kCFAllocatorDefault is used. Activities: Sets the flag of the Runloop's running phase, when the CFRunLoopObserver is called. public struct CFRunLoopActivity : OptionSet { public init(rawValue: CFOptionFlags) public static var entry // Getting to work public static var beforeTimers // The Timers event is being processed public static var BeforeSources // is about to process Source events public static var afterWaiting // is about to sleep public static var afterWaiting // is woken up public static > > < span style = "padding-bottom: 0px; padding-bottom: 0px; padding-bottom: 0px; Priority of CFRunLoopObserver. In normal cases, 0 is used. Block: This block takes two arguments: observer: Running Run loop observe. Activity: The current running phase of runloop. Return value: new CFRunLoopObserver object. * /
    let ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true.0, block)
    guard let observer = ob else {
        throw RunloopError.canNotCreate
    }
    return observer
}Copy the code

Use Runloop hibernation

Now add a TableView to your controller and an observer of the Runloop. Your controller should now look something like this:

class ViewController: UIViewController {

    override func viewDidLoad(a) {
        super.viewDidLoad()
        addRunloopObserver()
        view.addSubview(tableView)
    }

    fileprivate func addRunloopObserver(a) {
        // Get the current Runloop
        let runloop = CFRunLoopGetCurrent(a)// Which state of the Runloop to listen on
        let activities = CFRunLoopActivity.beforeWaiting.rawValue
        // Create a Runloop observer
        let observer = CFRunLoopObserverCreateWithHandler(nil, activities, true.Int.max - 999, runLoopBeforeWaitingCallBack)
        // Register a Runloop observer
        CFRunLoopAddObserver(runloop, observer, .defaultMode)
    }

    fileprivate let runLoopBeforeWaitingCallBack = { (ob: CFRunLoopObserver? , ac:CFRunLoopActivity) in
        print("The runloop completes.")
    }

    fileprivate lazy var tableView: UITableView = {
        let table = UITableView(frame: self.view.frame)
        table.delegate = self
        table.dataSource = self
        table.register(TableViewCell.self, forCellReuseIdentifier: "tableViewCell")
        return table
    }()
}Copy the code

Now run, print the following information:

Runloop end Runloop end runloop end runloop end runloop end runloop end runloop endCopy the code

From here, we can see that starting with viewDidLoad of the controller, after several runloops, the TableView successfully appears on the screen and then goes to sleep. When we slide the screen or trigger the gyroscope, earphone, etc., the Runloop goes to work and then goes to sleep again.

The goal is to take advantage of the Runloop’s sleep time so that the user can handle Cell rendering tasks when no events are generated. At the beginning of this article, we mentioned that events like touch and network are triggered by the user, and after executing the Runloop, they go to sleep again. The proper event is the clock.

So we listen for defaultMode and need to start a clock event in the observer’s callback to keep Runloop active, but this clock doesn’t need it to do anything either, so I turned on a CADisplayLink to display FPS. For those of you who don’t know CADisplayLink, think of it as a timer that executes once every 1/60 of a second to output a number.

Implement the Runloop application

First we declare a few variables:

// whether to use Runloop optimization
fileprivate let useRunloop: Bool = false

/// Cell height
fileprivate let rowHeight: CGFloat = 120

/// The code to execute when runloop is idle
fileprivate var runloopBlockArr: [RunloopBlock] = [RunloopBlock] ()/// Maximum number of tasks in runloopBlockArr
fileprivate var maxQueueLength: Int {
    return (Int(UIScreen.main.bounds.height / rowHeight) + 2)}Copy the code

Modify the addRunloopObserver method:

// register a Runloop observer
fileprivate func addRunloopObserver(a) {
    // Get the current Runloop
    let runloop = CFRunLoopGetCurrent(a)// Which state of the Runloop to listen on
    let activities = CFRunLoopActivity.beforeWaiting.rawValue
    // Create a Runloop observer
    let observer = CFRunLoopObserverCreateWithHandler(nil, activities, true.0) {[weak self] (ob, ac) in
        guard let `self` = self else { return }
        guard self.runloopBlockArr.count! =0 else { return }
        // Whether to exit the task group
        var quit = false
        // If you do not exit and tasks exist in the task group
        while quit == false && self.runloopBlockArr.count > 0 {
            // Execute the task
            guard let block = self.runloopBlockArr.first else { return }
            // Whether to exit the task group
            quit = block()
            // Delete completed tasks
            let _ = self.runloopBlockArr.removeFirst()
        }
    }
    // Register a Runloop observer
    CFRunLoopAddObserver(runloop, observer, .defaultMode)
}Copy the code

Create the addRunloopBlock method:

/// Add code blocks to arrays, executed in Runloop BeforeWaiting
///
/// - Parameter block: <#block description#>
fileprivate func addRunloopBlock(block: @escaping RunloopBlock) {
    runloopBlockArr.append(block)
    // When scrolling fast, cells that are not displayed in time are not rendered, only cells that appear on the screen are rendered
    if runloopBlockArr.count > maxQueueLength {
       let _ = runloopBlockArr.removeFirst()
    }
}Copy the code

Finally, drop the render cell Block into the runloopBlockArr:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if useRunloop {
        return loadCellWithRunloop()
    }
    else {
        return loadCell()
    }
}

func loadCellWithRunloop(a) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell") as? TableViewCell else {
        return UITableViewCell()
    }
    addRunloopBlock { () -> (Bool) in
        let path = Bundle.main.path(forResource: "rose", ofType: "jpg")
        let image = UIImage(contentsOfFile: path ?? "")??UIImage()
        cell.config(image: image)
        return false
    }
    return cell
}Copy the code

The Demo address

Github.com/tianziyao/R…