This question already has answers here:
loading images from a background thread using blocks
(3 answers)
Closed 8 years ago.
I'm trying to download an image from a website and save it as a UIImage but if the user has low connection this can take forever... how can I download it in the background so the user can keep using the app in the meantime?
here is the code:
theIcon.image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:#"http://myWebsite.com/Icon.png"]]];
Use GCD.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
// do my background code
dispatch_async(dispatch_get_main_queue(), ^{
// do handling on main thread when done!
});
});
Use AFNetworking.
[imageView setImageWithURL:
[NSURL URLWithString:#"http://i.imgur.com/r4uwx.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder-avatar"]];
You can perform the selector in background thread while the main foreground thread runs.
[self performSelectorInBackground:#selector(downloadFile) withObject:nil];
- (void) downloadFile {
//download file
//you can show UIAlertView when done
}
In your - (void) downloadFile you can download this big file.
and have an activity indicator show (or not). You can have the activity Indicator become not hidden or hidden and have it startAnimating and stopAnimating will make it spin and stop. This can be referenced from the foreground and background processes.
The quick and dirty way:
NSMutableRequest* request = ... ;
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse* response, NSData* data, NSError* error) {
if (!error) {
// do something with the response data.
}
}];
This approach is sufficient for "a prove of concept", toy programs with simplistic insecure connections, Apple samples, and for hobbyists learning iOS for fun, and for samples which demonstrate anti-patterns ("How you should do it, not!").
If you want a solid approach you need to use NSURLConnection in asynchronous mode and implement the delegates - or use a third party library. ;)
Related
I have some data that I am getting from a server and then displaying in my UIViewController class. To do this, I have two classes. The UIViewController and another one named ServerCommunicator. UIViewController is the delegate for ServerCommunicator class. The serverCommunicator looks as follows:
- (void)fetchServerData:(NSString *) serverAddress{
NSURL *url = [[NSURL alloc] initWithString:serverAddress];
[NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:url] queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (error) {
[self.delegate fetchingSongsFailedWithError:error];
} else {
[self.delegate receivedSongsJSON:data];
}
}];
}
The UIViewController allocates the serverCommunicator, sets itself as delegate and then issue the fetch request.
- (void)viewDidLoad {
[super viewDidLoad];
self.songServerCommunicator = [[serverCommunicator alloc] init];
self.songServerCommunicator.delegate = self;
[self.songServerCommunicator fetchServerData:<some_server_ip>];
}
After it does that it implements the required protocol method:
- (void)receivedSongsJSON:(NSData *)data{
NSLog(#"received server response");
/* Parses the data and displays in textfield/imageview */
}
My problem is that when I do display the data received in the delegate method, it doesn't get reflected right away in the UI. It is very weird, sometimes it gets shown 20 seconds laters on its own, other times it takes like a minute. I am not sure whats going on. I know for a fact that the data was fetched right away because the logged message gets printed way before the UIView gets updated.
Thanks for any help on this.
Make sure you are on the main thread when you update the UI
Other people have pointed out the problem, but they did not provide the solution in concrete code. This is the coded solution:
- (void)fetchServerData:(NSString *) serverAddress{
NSURL *url = [[NSURL alloc] initWithString:serverAddress];
[NSURLConnection sendAsynchronousRequest:[[NSURLRequest alloc] initWithURL:url] queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
dispatch_async(
dispatch_get_main_queue(),
^void {
if (error) {
[self.delegate fetchingSongsFailedWithError:error];
} else {
[self.delegate receivedSongsJSON:data];
}
}
);
}];
}
You must understand how Grand Central Dispatch works in iOS. GCD is an abstraction layer for multithreading.
The main queue, which your UI runs on, is a serial queue. That means you can't have two blocks of code running at the same time. It would be poor user experience if you were to do a long network query on the main queue, because it would prevent any UI code from running. This would make the app appear like it is frozen to the user.
To solve the freezing UI issue, iOS gives you other queues to do work without blocking up the main queue. iOS provides ways to create your own custom queues or use pre-made global concurrent queues. The queues that NSURLConnection uses is not known to us because we don't have the Cocoa source code. But what we do know is that NSURLConnection is definitely not using the main queue because UI is not frozen while it is grabbing data from a server.
Since NSURLConnection is running on a different thread than what the main queue (UI) runs on, you cannot just update UI at any random time. This is a common problem with multithreaded programs. When multiple threads access the same block of code, instructions may be interleaved and both blocks get unexpected results. One of the unexpected results that you experienced was the 20 second delay of the UI finally being updated. To prevent interleaving, you need to run all the UI code on the same thread. You do this by enqueuing your UI update code to the end of the main queue.
The method receivedSongsJSON() is called from a callback given to [NSURLConnection sendAsynchronousRequest] which I think is being called from a background thread.
Even if the method receivedSongsJSON() is declared in your UIViewController it will be executed in background thread if it is called from one.
As #robdashnash has indicated make sure you call all the UI updating code from main thread. If you are not sure how to do that please check the documentation of Grand Central Dispatch (GCD) here.
I probably ask my question the wrong way and risk being blocked by stackoverflow completely. I have Asperger and no social skills, so I am very sorry for asking my last (?) question (because systems like these are only made for people without handicaps).
I am using GCD to load images and video from Instagram. I do this in an app that is very 'busy' with its user interface, and I want to keep that running smoothly, so I load the Instagram-media in the background.
The code below (which I probably formatted the wrong way so I apologize up front) works fine and does exactly want I want it to do. It loads images in the background (I left video's out to keep things simple) and my main UI is response while the images load. I display them between loads, and there works fine too.
However.
After 10 minutes, sometimes 20 minutes, sometimes 30 minutes and even sometimes after two hours my app gets OSSpinLockLock freezes. If I remove the code below I get no media, but the app never freezes.
I have searched for days on the web about alternative ways to do the job and to find an explanation for the OSSpinLockLock. No luck. Only thing I found was that using GCD could not result in an OSSpinLockLock. I have used all my knowledge of Instruments (which I must admit is more limited than I thought), but I cannot find a fault.
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^(void) {
[[InstagramEngine sharedEngine] getMediaAtLocation:location count:kInstagramLocationBufferSize maxId:nil withSuccess:^(NSArray* media, InstagramPaginationInfo* paginationInfo) {
if (media && media.count>0) {
for (InstagramMedia* mediaObject in media) {
NSData* data = [[NSData alloc] initWithContentsOfURL:mediaObject.standardResolutionImageURL];
if (data) {
UIImage* img = [UIImage imageWithData:data];
if (img)
[self.locationBuffer addObject:img];
data = nil;
}
}
}
} failure:^(NSError *error) {
;
}];
});
If you look at this code, do you see anything that might cause that lock? Because I most certainly don't.
self.locationBuffer is declared in the .h as
#property (nonatomic,strong) NSMutableArray* locationBuffer;
and is properly allocated and initialized (otherwise it would be rather clear what the problem was). I have also tried not to put the UIImage but the NSData in the array but that made no difference whatsoever.
On my iPad mini retina for instance the CPU-load goes to 195% and stays around that number for a very long time. Eventually, sometimes after several hours, the app crashes.
Any suggestions would be very welcome.
Edit: As I see now on the ips-file on the iPad itself (which for some mysterious reason I cannot paste into this webpage (is stackoverflow still in an experimental stage?)) I see that the iPad did spent 16.000+ seconds on NSURLConnection...
my tought is that some timeout or failure blocks your gcd queues for too much time. try rewriting that code with NSOperationQueue, that way you can stop the queue on errors or view/controller going away.
https://developer.apple.com/library/ios/documentation/Cocoa/Reference/NSOperationQueue_class/index.html
Update 1:
Here is my trial at your code, i added queue and logs, check them and also check the timeout value. This way if a request does not finish (most likely) you can trace it. All requests are serial so if one of them stops you should immediately notice it.
You can create more than one queue and access them sequentially (round robin) to have more requests simultaneously. I would not go with more than 4 queues which is also the default for most desktop internet browsers.
// keep the same queue for all request (class member?), don't put it in the same block
NSOperationQueue* queue= [[NSOperationQueue alloc] init];
// keep this in another code block from queue
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^(void) {
[[InstagramEngine sharedEngine] getMediaAtLocation:location count:kInstagramLocationBufferSize maxId:nil withSuccess:^(NSArray* media, InstagramPaginationInfo* paginationInfo) {
if (media && media.count>0) {
for (InstagramMedia* mediaObject in media) {
NSURL* url= mediaObject.standardResolutionImageURL;
NSLog(#"Start loading from %#", url);
NSURLRequest* req= [NSURLRequest requestWithURL:mediaObject.standardResolutionImageURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30]; // 30 seconds timeout
[NSURLConnection sendAsynchronousRequest:req queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSLog(#"Stop loading from %# %# %#", url, response, connectionError);
if (data != nil && connectionError == nil) {
UIImage* img = [UIImage imageWithData:data];
if (img) {
// if you are triggering some ui update run on main thread
// [self.locationBuffer addObject:img];
[self.locationBuffer performSelectorOnMainThread:#selector(addObject:)
withObject:img
waitUntilDone:NO];
}
}
}];
}
}
} failure:^(NSError *error) {
NSLog(#"Error getting media list: %#", error);
}];
});
OSSpinLocks has bug on iOS8.3+: report
,which leads to such freezes.
I think you should replace OSSpinLocks with something else to fix this in your app (maybe create your own realization of spin lock - search SO for OSSpinLock).
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
Questions asking for code must demonstrate a minimal understanding of the problem being solved. Include attempted solutions, why they didn't work, and the expected results. See also: Stack Overflow question checklist
Closed 9 years ago.
Improve this question
I have 100 URLs, I want to download all these images and save in Documents. For saving I have done, also I can do lazy loading, but I am unable to download all with minimum time and GUI should not hang.
What is suitable method to do so?
Thanks
Use SDWebImage. You can download it from below url
https://github.com/rs/SDWebImage
For load 100 images using Asynchronous request
for(int i=0; i<99; i++)
{
strImage=[[res valueForKey:#"result"] objectAtIndex:i];
if ([[strImage lowercaseString] hasSuffix:#".jpg"] || [[strImage lowercaseString] hasSuffix:#".png"])
{
//[HUD show:YES];
NSURL *url=[[NSURL alloc]initWithString:strImage];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
thumbnail.image=[UIImage imageWithData:data];
}];
}
}
A sophisticated solution may become quite complex. Downloading and saving 100 URLs is not as easy as 100 times downloading one image.
The most vexing issue that will arise in ambitious but less smart implementations are memory pressures. A third party network library will not automagically solve this problem for you.
One simple - yet viable - approach which tries to avoid memory problems at the cost of a bit performance, is executing the download and save to disk operation all sequentially. This ensures, that only one image will be handled at any time. Thus, you can make safe assumptions about the maximum memory required for this approach.
A solution may look as below:
Suppose, you have an asynchronous method which loads the image data from a given URL:
typedef void (^load_completion_t)(NSData* data, NSError* error);
- (void) loadURL:(NSURL*)url completion:(load_completion_t)completionHandler;
This method will load the whole image data into memory. This isn't really the best way, but IFF we can assume that one image always fits into memory, it becomes a simple solution.
Furthermore, suppose, there is a synchronous method which saves the data to disk:
- (void) saveData:(NSData*)data;
Now, given an array of URLs you can sequentially load and save a number of images as follows:
typedef void(^completion_t)(id result, NSError* error);
- (void) saveImagesWithURLs:(NSMutableArray*)urls
completion:(completion_t)completionHandler
{
if ([urls count] > 0) {
NSURL* url = [urls firstObject];
[urls removeObjectAtIndex:0];
[self loadURL:url completion:^(NSData* imageData, NSError*error){
if (imageData) {
[self saveData:imageData];
[self saveImagesWithURLs:urls completion:completionHandler];
}
else {
// handle error
}
}];
}
else {
// finished
if (completionHandler) {
completionHandler(#"Images saved", nil);
}
}
}
The method above is an "asynchronous loop":
The completion handler of loadURL:completion will call saveImagesWithURLs:completion:, much like a recursive invocation. However, this is NOT a recursive method: when the completion handler of method saveImagesWithURLs:completion:gets executed, saveImagesWithURLs:completion: already returned.
Given a propert which is an array of URLs:
#property (nonatomic, strong) NSArray* imageURLs;
you can invoke the asynchronous loop as shown below:
[self saveImagesWithURLs:[self.imageURLs mutableCopy]
completion:^(id result, NSError*error){
if (error) {
// handle error
}
else {
// result equals #"Images saved"
}
}];
You can call this method from the main thread. It will NOT block, because it is asynchronous. We also assume, that the completion handlers will be invoked on a private queue, and not on the main thread.
Better user AFNetworking, it will help you to download all the images asynchronously (no GUI hang)
https://github.com/AFNetworking/AFNetworking
You can use AFHTTPClient to enqueueBatchOperations and this has a completionBlock which is called when all operations are finished. Should be exactly what you're looking for.
I have some difficulties to set up the correct configuration relative to sendAsynchronousRequest:queue:completionHandler: method (NSURLConnection class).
My scenario is the following:
I set up a singleton class that manages different NSURLConnections. This singleton istance has a NSOperation Queue (called downloadQueue) that makes a request to a web server and retrieves a string path (1).
Once done, the path is used to download a file within a web server (2). Finally, when the file has been correctly downloaded, I need to update the UI (3).
I figured out only the first request: the one through which I'm able to download the path. Could you suggest me a way to perform the other two steps?
Few questions here:
the download queue (downloadQueue) is not the main one, is it possible to open a new NSURLConnection in that queue? In other words, is it correct? (See comments in code snippets)
if the previous question is correct, how can I grab the main queue and update the UI?
Here the code snippet I use to perform the first step where downloadQueue is an instance variable that can be obtain through accessor mehods (#property/#synthesized);
// initializing the queue...
downloadQueue = [[NSOperation alloc] init];
// other code here...
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[self downloadQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if([data length] > 0 && error == nil) {
// here the path (1)
// how to perform a second connection?
// what type of queue do I have to use?
}
}];
You're on the right track for performing your first download.
In the completion handler block after the first download, you're computing the URL that you'll need for a second download, right? Then you can perform that second download the same way: call +[NSURLConnection sendAsynchronousRequest:...] again with the new URL and the same queue. You can do this within the completion block for the first download.
To update the UI after the second download is done, switch to the main queue within the completion block for that download. You can do this with dispatch_async() or dispatch_sync() (in this case it doesn't matter which because you don't have further work to do on the download queue) and dispatch_get_main_queue(), or with -[NSOperationQueue addOperationWithBlock:] and +[NSOperationQueue mainQueue].
Your code should look something like this:
// init download queue
downloadQueue = [[NSOperationQueue alloc] init];
// (1) first download to determine URL for second
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[self downloadQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if([data length] > 0 && error == nil) {
// set newURLRequest to something you get from the data, then...
// (2) second download
[NSURLConnection sendAsynchronousRequest:newURLRequest queue:[self downloadQueue] completionHandler:^(NSURLResponse *newResponse, NSData *newData, NSError *newError) {
if([newData length] > 0 && newError == nil) {
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// (3) update UI
}];
}
}];
}
}];
For updating the ui, as far as I know, you have to do that on the main thread. The ui could be updated from other threads but those updates are not fully reliable. In an app that I put together that made request to a web service, I make use of dispatch_async() to get access to the main queue and then I update, in my case a table view, from that call.
dispatch_async(dispatch_get_main_queue(), ^{
//block to be run on the main thread
[self.tableView reloadData];
});
I hope this helps.
I've started to use blocks and queues heavily and they have been great. I use much less code and it is much easier to build and maintain. But I wonder about performance. In one case I am displaying a screen full of thumbnail images from a Flickr photo set. The code iterates over all items and starts a unique download queue to download each photo concurrently. It's working just fine, but I wonder if I should instead create a single static queue for downloading photos and then dispatch these download blocks to the same queue so that it can manage the blocks efficiently.
I've uploaded an example here.
http://www.smallsharptools.com/Downloads/iOS/UIImage+DownloadImage.zip
The implementation contents are also below. I appreciate any insight into better performance. (Later I'd like to handle caching for images by placing the file in the tmp folder so they are automatically cleared out periodically.)
How do you manage concurrent tasks with blocks? Do you create a static queue and dispatch blocks to the shared queue? Or does the implementation below implicitly manage all of my tasks efficiently already?
#import "UIImage+DownloadImage.h"
#implementation UIImage (DownloadImage)
+ (void)downloadImageWithURL:(NSURL *)imageURL andBlock:(void (^)(UIImage *image, NSError *error))returnImage {
dispatch_queue_t callerQueue = dispatch_get_current_queue();
dispatch_queue_t downloadQueue = dispatch_queue_create("Image Download Queue", NULL);
dispatch_async(downloadQueue, ^{
UIImage *image = nil;
NSError *error = nil;
// use the default cache policy to do the memory/disk caching
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:imageURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:15];
NSHTTPURLResponse *response = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
// 200 indicates HTTP success
if (response.statusCode != 200) {
data = nil;
// set the error to indicate the request failed
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: [NSString stringWithFormat:#"Request failed with HTTP status code of %i", response.statusCode], NSLocalizedDescriptionKey, nil];
error = [NSError errorWithDomain:#"UIImage+DownloadImage" code:response.statusCode userInfo:userInfo];
}
else if (!error && data) {
image = [UIImage imageWithData:data];
}
// image will be nil if the request failed
dispatch_async(callerQueue, ^{
returnImage(image, error);
});
});
dispatch_release(downloadQueue);
}
#end
It does seem inefficient to create a 1-element queue each time, though I would be surprised if this would show up as a hotspot during profiling.
If you search on Apple's iOS forums, you should be able to find Quinn's discussion of using NSURLConnection "raw" rather than via threads.
You're doing synchronous network activity on queues. This seems like a rather poor idea, since you're blocking threads and forcing GCD to spin up new threads to service other blocks. If you're downloading 20 images simultaneously, then you will have 20 blocked threads in your app and another handful to actually do work. Instead you should be doing asynchronous network activity on a single worker thread. There's even a piece of Apple sample code that does this, though I cannot for the life of me remember what it's called.