I'm new to ios development.My app gets slower when i'm parsing image using json parser in ios 5.
Please could anybody help to solve this problem.
-(NSDictionary *)Getdata
{
NSString *urlString = [NSString stringWithFormat:#"url link"];
urlString = [urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSURL *url = [NSURL URLWithString:urlString];
NSData* data = [NSData dataWithContentsOfURL:url];
NSError* error;
NSDictionary* json;
if (data) {
json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
NSLog(#"json...%#",json);
}
if (error) {
NSLog(#"error is %#", [error localizedDescription]);
// Handle Error and return
// return;
}
return json;
}
Your description of the problem isn't exactly helpful. It's unclear to me if everything in your app is slow, or just certain operations; if you exprience a slow action and then it becomes fast again or if it continues to perform slowly.
Whatever, the general rule is to performan all network communication including the parsing of the answer on a separate thread, i.e. not on the main thread that is responsible for managing the user interface. That way the app remains responsive and appears to be fast.
If you can download the images separately, you can already display the result and put a placeholder where the image will appear. Later, when you have received the image you remove the placeholder and put the image there.
This line is probably the culprit.
NSData* data = [NSData dataWithContentsOfURL:url];
If you're calling this on the main thread (and because you haven't mentioned threads at all I suspect that you are) it will block everything and wait until the server has responded.
This is a spectacularly bad experience for the user :)
You need to do all of this on a background thread and notify the main thread when you're done. There's a couple of ways of doing this (NSOperation etc) but the simplest is just this :
// Instead of calling 'GetData', do this instead
[self performSelectorOnBackgroundThread:#selector(GetData) withObject:nil];
// You can't return anything from this because it's going to be run in the background
-(void)GetData {
...
...
// Instead of 'return json', you need to pass it back to the main thread
[self performSelectorOnMainThread:#selector(GotData:) withObject:json waitUntilDone:NO];
}
// This gets run on the main thread with the JSON that's been got and parsed in the background
- (void)GotData:(NSDictionary *)json {
// I don't know what you were doing with your JSON but you should do it here :)
}
Related
I am downloading a zip file from the internet and unzipping it. The process takes 15 seconds or so. I want to display some sort of time elapsed to the user. As a start I just want to update a UILabel on my _countUpView UIView when the download has finished and the unzipping begins. I believe that I need to use setNeedsDisplay on the view that holds the UILabel but I can not seem to figure out what I am doing wrong, here is my code:
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://URLtothiszipfile>/all.zip"]];
NSData *slicedata= [NSData dataWithContentsOfURL:url];
NSError *error;
NSString *isignsPath = [NSString stringWithFormat:#"%#/iSigns",docsDir];
if (![filemgr fileExistsAtPath:isignsPath]){ //Does directory already exist?
if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error:&error]){
NSLog(#"Create directory error: %#", error);
}
}
thePathAndName= [NSString stringWithFormat:#"%#/iSigns/all.zip", docsDir];
[slicedata writeToFile:thePathAndName atomically:YES];
_countUpLab.text= #"Unzipping";
[self displayCountUpNOW];
NSLog(#"Unzipping");
[SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:#"" error:&error];
if (error.code!= noErr){
NSLog(#"Unzip error");
return;
}else{
NSLog(#"Unzipped successfully");
}
And the setNeedsDisplay method is:
- (void) displayCountUpNOW {
dispatch_async(dispatch_get_main_queue(), ^{
[_countUpView setNeedsDisplay];
});
}
My label does not change to "Unzipping" until NSLog shows "Unzipped successfully", about 10 seconds after I set the label to "Unzipping".
In those 10 seconds, the unzipping occurs but a timer that I want to use to update the label every second stops executing too so I can't display the elapsed time in the label either.
Please help me understand this asynchronous environment.
Carmen
EDIT-EDIT-EDIT
The below code seems to work asynchronously and I even have my elapsed time indicator working since my timer isn't stopped. I couldn't find a method in SSZipArchive that works without a file so I left the save file in. How did I do Duncan? Thanks again, that is pretty slick!
ONE MORE QUESTION: What is the best way to know when an asynchronous request is still outstanding, by setting a global flag variable when the request is made and clearing it when the async process completes?
gotALL= 0;
_countUpLab.text= #"Downloading";
NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:#"http://URLtothiszipfile>/all.zip"]];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *slicedata, NSError *error1){
if ([slicedata length]> 0 && error1== nil){
NSString *isignsPath = [NSString stringWithFormat:#"%#/iSigns",docsDir];
if (![filemgr fileExistsAtPath:isignsPath]){ //Does directory already exist?
NSError *error2;
if (![filemgr createDirectoryAtPath:isignsPath withIntermediateDirectories:NO attributes:nil error: &error2]){
NSLog(#"Create directory error: %#", error2);
[self endCountUp];
return;
}
}
thePathAndName= [NSString stringWithFormat:#"%#/iSigns/all.zip", docsDir];
[slicedata writeToFile:thePathAndName atomically:YES];
_countUpLab.text= #"Unzipping";
NSLog(#"Unzipping");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSError *error3;
[SSZipArchive unzipFileAtPath:thePathAndName toDestination:isignsPath overwrite:true password:#"" error:&error3];
dispatch_async(dispatch_get_main_queue(), ^(void) {
if (error3.code!= noErr){
NSLog(#"Unzip error %#", error3.description);
[self endCountUp];
return;
}else{
NSLog(#"Unzipped successfully");
gotALL= 1;
[self endCountUp];
return;
}
});
});
}else if ([slicedata length]== 0 && error1== nil){
//todo yet
}else if (error1!= nil){
//todo yet
}
}];
Lots of problems with your code. First of all, Your current code will lock up the user interface until the download is complete. That is bad, even for short downloads. If there is a network problem, this can lock up the user interface for up to 2 minutes, even for a short file.
You should download the file using an async download, not synchronous.
Second of all, UI updates only get rendered to the screen when your code returns and the app visits the event loop. Code using this approach:
Set label text to "doing stuff"
Do something time-consuming
Change label text to "done with stuff"
Does not work. The text "doing stuff" never shows up in the label, because the label changes aren't drawn until your code finishes and returns, and by then, you've replaced the label text with "done with stuff."
Here's what you should do instead:
Use the NSURLConnection method sendAsynchronousRequest:queue:completionHandler:. Put the code that you want to run once the download is complete in the completionHandler block.
That method handles the background stuff for you, and then runs your completion block on the main thread once the download is complete. It's a slick system method.
It should be possible to do the unzipping from the background using dispatch_async. (I'm not familiar with the SSZipArchive library, so I'm not positive that it's thread-safe, but it should be.)
The basic idea is:
Display a "downloading" message.
Create an NSURLRequest.
Use sendAsynchronousRequest:queue:completionHandler: to submit the request asynchronously.
In the completion handler:
Change the message to "unzipping"
Save the downloaded data to disk if the unzip library requires that it be saved to a file. If the zip library can do the unzipping from memory, you can skip this step.
Use dispatch_async to unzip the file from the default priority global queue
At the end of the unzip block, use dispatch_async(dispatch_get_main_queue()) to change the label to "unzip complete" or whatever you want to say. (You have to send the message to the main queue because you can't change the UI from a background thread.)
Try and figure out how to code the above approach. If you get stuck, post what you've tried as an edit to your post and we can guide you, but it's better if you try to write this code yourself. You'll learn that way rather than copy/pasting.
I want to search the AppStore based on what a user types into search.
I have set up the following code to do this, which will amend the search on each character being entered to narrow the search.
However, as there is a request made as every character entered and these can take time to return, the UI can become unresponsive.
I would like to a) understand how I can stop the UI becoming unresponsive (I fear I am bringing the running back onto the main thread with performselectoronmainthread?), and b) would it be prudent to cancel the previous lookup as each character is entered, thus using the new narrower search, and if so how to do this?
Thanks in advance.
Update: I have tried the suggestion as made by Emilie Lessard, and whilst I can see the logic, I am unable to get this to benefit the app. See response below.
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)
-(void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)text
{
if(text.length == 0)
{
jsonResults = nil;
[self.tableView reloadData];
}
else
{
jsonResults = nil;
[self.tableView reloadData];
NSURL *searchUrl = [NSURL URLWithString:[NSString stringWithFormat:#"https://itunes.apple.com/search?term=%#&country=gb&entity=software",text]];
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:searchUrl];
[self performSelectorOnMainThread:#selector(fetchedData:)
withObject:data waitUntilDone:NO];
});
}
}
-(void)fetchedData:(NSData *)responseData{
NSError* error;
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:responseData
options:kNilOptions
error:&error];
jsonResults = [json objectForKey:#"results"];
[self.tableView reloadData];
}
You need to use dispatch_async()
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Do the computing-research
dispatch_async(dispatch_get_main_queue(), ^{
//do UI update here
});
});
By using a global queue, the UI of your app won't get blocked. Once all info has been computed/received, it is MANDATORY that you go back to the main thread (by using dispatch_async(dispatch_get_main_queue()) for all UI updates or you'll end up with hard-to-debug crashes.
I am needing to perform a very simple background check for my iOS app. It needs to just make one call to my web server and check the number it retrieves against something in my app. Is it possible to do that kind of background check? If so what can I do to put it together?
EDIT
To clarify what I mean by background: I am meaning background on the phone. When the app is not present. Is it possible to do this request in the background? Obviously with the app not being completely closed out from multitasking.
This sounds like the perfect sort of thing for NSOperationQueue.
http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues
You can write an operation and then put it on the queue when you need it.
Alternatively, and more simply, you can just do a really simple asynchronous call.
+ (NSArray *) myGetRequest: (NSURL *) url{
NSArray *json = [[NSArray alloc] init];
NSData* data = [NSData dataWithContentsOfURL:
url];
NSError *error;
if (data)
json = [[NSArray alloc] initWithArray:[NSJSONSerialization
JSONObjectWithData:data
options:kNilOptions
error:&error]];
if (error)
NSLog(#"%#", error)
return json;
}
and then put it in a simple dispatch block...
dispatch_queue_t downloadQueueA = dispatch_queue_create("updater", NULL);
dispatch_async(downloadQueueA, ^{
// stuff done here will happen in the background
NSArray * arrayOfData = [self myGetRequest: myURL];
// could be array... dictionary... whatever, you control this by returning the type of data model you want from the server formatted as JSON data
NSString * stringValue = arrayOfData[index];
dispatch_async(dispatch_get_main_queue(), ^{
// perform checking here and do whatever updating you need to do based on the data
});
});
There are many way to check your server and retrieve data.
Here my suggestion:
Create the file containing your data on the server (e.g. Data.txt)
Use NSURLRequest to create a request to Data.txt
Use connectionDidFinishLoading to get data from Data.txt
Put data from Data.txt in a NSArray
Work/compare the array and do your logic
If your server is fast and you have to get just one number, you can do it in the main tread, otherwise use:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
// your request here
});
to work in a different tread as requested.
And remember to check if internet connection and your server are available with something like Reachability and manage connection error with NSURLRequest delegate
You should be able to do that using Grand Central Dispatch: https://developer.apple.com/library/ios/documentation/Performance/Reference/GCD_libdispatch_Ref/Reference/reference.html
Take a look at this tutorial Multithreading and Grand Central Dispatch on iOS.
http://www.raywenderlich.com/4295/multithreading-and-grand-central-dispatch-on-ios-for-beginners-tutorial
I am kind of new to web service calls and threading in iOS. I have a ViewController in my app that contains a tableview control. I am populating the table with data obtained via a JSON web service. The JSON web service is called on its own thread, during which I am populating an NSArray and NSDictionary.
My array and dictionary seem like they are going out of scope since my NSLog statement is returning zero for the array count even though while in fetchedData the array is fully populated.
Can someone offer an explanation as to why my array and dictionary objects are empty outside of the thread?
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *serviceEndpoint = [NSString stringWithFormat:
#"http://10.0.1.12:8888/platform/services/_login.php?un=%#&pw=%#&ref=%#",
[self incomingUsername], [self incomingPassword], #"cons"];
NSURL *url = [NSURL URLWithString:serviceEndpoint];
dispatch_async(kBgAdsQueue, ^{
NSData *data = [NSData dataWithContentsOfURL:url];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:data waitUntilDone:YES];
});
NSLog(#"ARRAY COUNT: %d\n", [jsonArray count]);
}
-(void)fetchedData:(NSData*)responseData{
NSError *error;
jsonDict = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
jsonArray = [[jsonDict allKeys]sortedArrayUsingSelector:#selector(compare:)];
for(NSString *s in jsonArray){
NSLog(#"%# = %#\n", s, [jsonDict objectForKey:s]);
}
}
When you use dispatch_async, that bit of code doesn't block. This means your array count log statement triggers before fetchedData is called, so your dictionary and array are still empty. Look at the order of your log statements - you should see array count before your logging of the dictionary.
// Executes on another thread. ViewDidLoad will continue to run.
dispatch_async(kBgAdsQueue, ^{
NSData *data = [NSData dataWithContentsOfURL:url];
[self performSelectorOnMainThread:#selector(fetchedData:) withObject:data waitUntilDone:YES];
});
// Executes before the other thread has finished fetching the data. Objects are empty.
NSLog(#"ARRAY COUNT: %d\n", [jsonArray count]);
You'll need to finish populating your TableView after the data returns (ie in FetchData:).
The log statement within viewDidLoad SHOULD report that the array is empty as it has not been populated at that time. Calling dispatch_async causes that block of code to be run asynchronously and allows the viewDidLoad function to finish before the block does. That's why you have nothing in your array at the end of viewDidLoad.
You are trying to print count of jsonArray elements before it was populated. Here is what happens:
You prepare url
You create new thread to fetch some date. Execution of this thread may take some time (depending on connection speed and amount of data).
You are accessing jsonArray while thread is executing and fetchedData: was not called
Also, suggestion:
Don't use dataWithContentsOfURL: methods. Better take a look at some networking frameworks like AFNetworking.
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.