-->

Mac application window stops updating

2020-07-26 14:57发布

问题:

I am writing a Mac application (target 10.9+, using Xcode 6 Beta 3 on Mavericks) in Swift where I have a number of NSTextFields (labels) updating several times per second for extended periods of time by modifying their .stringvalue from a background thread. This seems to work well for a varying duration of time (anywhere between five minutes to 2 hours), but then the application window seems to stop updating. The text stops updating, hovering over the 'stoplight' controls on the upper-left does not show the symbols, and clicking in text boxes, etc., does not highlight the box/show the I-beam. However, my indeterminate progress wheel DOES continue to spin, and when I resize/minimize/zoom the window, or scroll in an NSScrollView box, the window updates during the movement.

My first guess was that some sort of window buffer was being used instead of a live image, so I tried to force an update using window.update(), window.flushWindowIfNeeded(), and window.flushWindow(), all to no avail. Can someone please tell me what's going on, why my window stops updating, and how to fix this problem?

回答1:

Your problem is right here:

I have a number of NSTextFields (labels) updating several times per second for extended periods of time by modifying their .stringvalue from a background thread.

In OSX (and iOS), UI updates must occur in the main thread/queue. Doing otherwise is undefined behavior; sometimes it'll work, sometimes it won't, sometimes it'll just crash.

A quick fix to your issue would be to simply use Grand Central Dispatch (GCD) to dispatch those updates to the main queue with dispatch_async like:

dispatch_async(dispatch_get_main_queue(), ^{
    textField.stringValue = "..."
});

The simplified version of what that does is it puts the block/closure (the code between {}) in a queue that the default run loop (which runs on the main thread/queue) checks on each pass through its loop. When the run loop sees a new block in the queue, it pops it off and executes it. Also, since that's using dispatch_async (as opposed to dispatch_sync), the code that did the dispatch won't block; dispatch_async will queue up the block and return right away.

Note: If you haven't read about GCD, I highly recommend taking a look at this link and the reference link above (this is also a good one on general concurrency in OSX/iOS that touches on GCD).

Using a timer to relieve strain on your UI

Edit: Several times a second really isn't that much, so this section is probably overkill. However, if you get over 30-60 times a second, then it will become relevant.

You don't want to run in to a situation where you're queueing up a backlog of UI updates faster than they can be processed. In that case it would make more sense to update your NSTextField with a timer.

The basic idea would be to store the value that you want displayed in your NSTextField in some intermediary variable somewhere. Then, start a timer that fires a function on the main thread/queue tenth of a second or so. In that function, update your NSTextField with the value stored in that intermediary variable. Since the timer will already be running on the main thread/queue, you'll already be in the right place to do your UI update.

I'd use NSTimer to setup the timer. It would look something like this:

var timer: NSTimer?

func startUIUpdateTimer() {
    // NOTE: For our purposes, the timer must run on the main queue, so use GCD to make sure.
    //       This can still be called from the main queue without a problem since we're using dispatch_async.
    dispatch_async(dispatch_get_main_queue()) {
        // Start a time that calls self.updateUI() once every tenth of a second
        timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target:self, selector:"updateUI", userInfo: nil, repeats: true)
    }
}

func updateUI() {
    // Update the NSTextField(s)
    textField.stringValue = variableYouStoredTheValueIn
}

Note: as @adv12 pointed out, you should think about data synchronization when you're accessing the same data from multiple threads.

Note: you can also use GCD for timers using dispatch sources, but NSTimer is easier to work with (see here if interested).

Using a timer like that should keep your UI very responsive; no need to worry about "leaving the main thread as empty as possible". If, for some reason, you start losing some responsiveness, simply change the timer so that it doesn't update as often.


Update: Data Synchronization

As @adv12 pointed out, you should synchronize your data access if you're updating data on a background thread and then using it to update the UI in the main thread. You can actually use GCD to do this rather easily by creating a serial queue and making sure you only read/write your data in blocks dispatched to that queue. Since serial queues only execute one block at a time, in the order the blocks are received, it guarantees that only one block of code will be accessing your data at the same time.

Setup your serial queue:

let dataAccessQueue = dispatch_queue_create("dataAccessQueue", DISPATCH_QUEUE_SERIAL)

Surround your reads and write with:

dispatch_sync(dataAccessQueue) {
    // do reads and/or writes here
}