I have 3 UITableViews each when asked for a cell, async loads an image in a NSOperation. This is working perfectly until I insert cells, at which point cellForRow gets called and the operation is created and added to the queue.
The operations in the queue however are not processed for another few seconds, sometimes several seconds. I have another queue that is doing some image processing and saving the image to the disk; I suspect both these queues are competing for disk I/O, but I'm not sure. Ideally, when a image is processed, it should be loaded by the cell next. But it seems like the processing is done first and cells never get a chance to load, even though I experimented with the queue/thread priority. Can anyone with experience give me guidance?
Related
I was wondering how someone would load multiple UI elements asynchronously. In my case, I have a UIViewController that has a UISegmentControl. Each segment has different UI aspects to load.
For example seg1 shows one image, seg2 shows and image and some text, etc.
If I load all the UI elements before showing the UIViewController then there is a noticeable lag. So I would want to load seg2...n asynchronously to make the UI feel more responsive.
Would I need to load everything via
DispatchQueue.main.async {
// load UI
}
For each seg I want to load in the background? Or can I load these elements on another thread and not take up the main thread? I know you're not supposed to update the UI on background threads... But will using the main thread still block the UI if I use async code?
You can't make UI changes on a background thread. Nearly all UIKit calls must be made on the main thread. Therefore you're likely out of luck.
Instead what you should do is do the time consuming number-crunching (downloading and parsing data for example) on a background thread and then use DispatchQueue.main.async() to install the data into your views on the main thread once the time consuming work is done.
If it's the UI setup that is introducing the lag and it's not possible to speed it up then you may be out of luck.
I wonder how the process associated with NSOperation mainQueue doesn’t affect UITableView's scrolling.
The main reason of the sluggish scroll in my code is that each cell loading is slow, because each cell has to fetch images from the server.
That’s why I used to create another thread using GCD to handle the fetching process given by each cell. Meanwhile, the main thread handles exclusively tableview’s scroll, which is what I've understood to ensure fast scrolling.
But, in recent days, I’ve noticed a sample code is using NSOperation.
I imitated the part so that the tableview cell is loaded using NSOperation mainQueue.
I expected it would tax the tableview’s scroll performance, because the main thread handles both cell loading and tableview scrolling as I understand.
But, surprisingly, it doesn’t. The performance of both cell loading and tableview’s scrolling is perfect.
I still wonder how this can be.
I guess I’m either mistaken or I miss something.
Would you let me know what it is?
All iOS UI animations (including scrolling) are performed on separate UI thread. So as long as UITableView has all required data (i.e. you provide empty cell until image is loaded) it won't be blocked by any sort of main thread activity.
Execution of the animations occurs on a secondary thread so as to
avoid blocking the current thread or your application’s main thread.
https://developer.apple.com/.../AnimatingViews.html
The iOS application's main thread consists of two elements:
• The Run Loop
• The Main Queue
The main dispatch queue is a globally available serial queue that
executes tasks on the application’s main thread. This queue works with
the application’s run loop (if one is present) to interleave the
execution of queued tasks with the execution of other event sources
attached to the run loop. Because it runs on your application’s main
thread, the main queue is often used as a key synchronization point
for an application.
Source
The code you have in your View Controllers is ran on the run loop (not sure about this one). That's why you're not allowed to execute any long lasting tasks.
You can though, delegate these tasks to the Main Queue (and that's what you're doing). When you do that, the OS interleaves the execution between the run loop and the main queue (loading the images). That's why scrolling the view is still pretty smooth.
Why don't we do all async tasks on the main queue then? you might ask. Because the main queue has to share thread processor time with the run loop, operations will be executed slower than on a background thread. It's not something that you can observe in that example, though.
I faced a problem. My application downloading images from server and showing in a UITableView For downloading the images I am using dispatch_async.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// downloading image from server
dispatch_async(dispatch_get_main_queue(), ^{
// update UI for showing downloaded image
});
});
Now the problem is if user scroll up and down very fast then multiple dispatch_async get fired for the same image. I have image id. Is there any way to check whether a dispatch_async get fired or not for that image or not by image id?
You'd have to set up your own mechanism for keeping track of whether there were any pending requests for that image (e.g. a dictionary keyed by the image URL absolute string or something like that). The thing is, it's not as simple as that, as the cell that now needs to be updated may be different than (or in addition to) the cell for which it was originally requested. This gets ugly quickly.
But I'd step back and ask whether this is the root problem, or a symptom of a broader problem. For example, if the user scrolls down relatively quickly to the 100th row. Is that image request backlogged behind the previous 99 rows? When you're in that sort of situation, the problem you describe starts to become serious, because if you now scroll quickly back up to the top before the other 99 images have finished downloading, you'll be issuing requests to get them again.
But, if you change your asynchronous image retrieval to cancel requests for cells that are not visible, suddenly this serious problem becomes somewhat academic. Plus, you have the added benefit that the image for the 100th row that the user rapidly scrolled to is not backlogged behind all of the other image requests, so it appears quite quickly.
This means that you want to employ cancelable asynchronous requests. This would therefore suggest using NSURLSession (or delegate-based NSURLConnection if you have to support OS versions that predate NSURLSession). Also, this would generally make one lean towards operation queues and subclassed asynchronous NSOperation subclass rather than GCD queue suggested by your question (to make them cancelable, asynchronous, and also to constrain the degree of concurrency). And then you have to then implement the code that makes use of all of this cancellation logic that you've so industriously created (e.g. UIImageView category or some image request manager that your UITableViewDataSource and/or UITableViewCell subclass uses).
Also, you want to make sure that you employ a cache mechanism (possibly both to memory and persistent storage). This way, if you've already retrieved a particular image, when you scroll back to that row, the image is ready for you to retrieve it, not requiring another network call.
I know that you have said that you want to do this yourself, but this is a non-trivial amount of work to do properly. This is why I (and others) suggest you consider checking out one of the UIImageView categories that allow you to asynchronously retrieve an image. See the UIImageView categories provided by SDWebImage or AFNetworking. If you're properly dequeuing cells, these achieve very responsive UI while retrieving the images asynchronously. They effectively cancel requests for cells that are reused, thereby prioritizing visible cells. They also cache results resulting in good performance as you scroll back.
For that you should store in some place the downloaded images or mark if there is a current operation for them. So you will be doing basically a image cache mechanism. I will advise that instead to do that you can use a well tested library for that like SDWebImage that precisely deal with download images and cached them to memory or disk.
Keep it simple: use NSURLSession rather than rolling your own attempt at asynchronous URL fetches. Rely on its caching to avoid repeat downloads where the previous download completed. Then all you have to worry about is repeat requests for downloads that are currently ongoing.
The easiest thing is probably just to keep two dictionaries: URL to NSURLSessionDataTask and URL to array of completion handlers.
When a cell wants a URL it calls into the handler for those two dictionaries, supplying a completion handler.
If there's no data task for that URL, the handler creates one and starts it, also creating a dictionary containing just that completion handler. Both are put into their respective dictionaries.
If there is a data task for that URL, the completion handler is added to the array in the second dictionary.
Tasks are created with a completion block that calls the handler, which upon receipt will remove the task from the first dictionary and call everything in the array in the second dictionary, then removing the array from there.
Given that you're view related, the handler permits external calls from the main queue only. It will ensure that the work subsequent to a completed data task is also performed on the main queue. That means you'll have no concurrency issues.
How would I constantly refresh a background image of my UIImageView, while also still listening for touches and input? What I am looking for is the iOS Objective C equivalent (in java) of creating a new thread apart from the main thread, and having that thread be devoted to updating the background picture as fast as possible. Thanks!
It depends on where the new images are coming from. If they are a cycle of known existing images in your app bundle, just make this an animated image view (or an animated image) and the cycle will happen automatically.
If you need to run code e.g. to get out to the network periodically, then just start a repeating NSTimer. It calls you on the main thread, but your main thread code will be very brief indeed, and networking takes place asynchronously (unless you mess that up deliberately). Just make sure that when you actually set the image, you step out to the main thread, as you must never touch the interface in any way except on the main thread.
I have an iOS application that has an NSTimer which fires every 5 seconds. This then posts a notification, telling several controllers that they must now recalculate some data and then update their UI.
When this all happens on the main thread, scrollviews can become jittery as the data is processed.
Using GCD, I have wrapped the code called when a notification is posted:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Code here
dispatch_async(dispatch_get_main_queue(), ^(){
// Reload tableviews and UI
};
};
Unfortunately, this leads in certain cases to several threads accessing the same instance variables. This causes my application to crash as sometimes it ends up mutating an array being enumerated in another thread.
Wrapping the code in a synchronised block prevents this.
When the user scrolls the tableview, several methods are called to calculate the height etc, on the main thread. The code within the background thread is working on the code needed by the main thread. As such, this can usually further cause crashes
I have tried setting the variables to 'atomic', but this doesn't solve the issue. Ideally, I don't want to have the main thread waiting on the background thread, but I am not sure how to best resolve this issue given that they need the same data?
This is pretty classic multithreaded programming issues. There are a number of ways to solve it with basic locks (#synchronized blocks), reader/writer locks, etc but the problem is often that you can't control when the user is going to scroll or take other action. If you #synchronize, you have to do it anywhere that data is touched, including your UITableView data source methods. That can lead to stuttering if the background processing happens to be in the middle of something.**
Personally, I would use an immutable snapshot mechanism.
Have the background thread produce the results, then include just the data the UI needs to display in the notification data as an immutable snapshot (copy). That way the background thread never modifies the data the UI is currently reading for display. How you would implement this is highly dependent on how much data you are talking about and the form it takes, but the safe way would be to have copies of your classes with readonly properties. Alternatively, you can use a "frozen" flag. Make a copy, then set frozen = YES on the copy. The UI thread will only ever see "frozen" or readonly objects coming from the background thread.
The benefit is the UI never causes the background thread to stall and there are no locks required. The downside is increased memory usage, though if the amount of data is large you can use copy-on-write mechanisms to allow the background thread and UI thread to share the data, even though logically the UI thread has a completely separate copy.
** Note: In most applications, you don't have this sort of continuous background processing going on so those apps can use simpler mechanisms. This is typically a form of message passing where the background thread finishes its task and "passes the message" to the UI thread (passes the results). At that point the background work is finished so there is no concurrent modification happening.