I am building an app that requires JSON data. I am using a get call to a RESTful API and using and NSJSONSerialization to go through the data. In an effort to reduce start up time, I am logging the time everything takes to complete. After putting in the following log statements around this code, I noticed that this single call is taking on average exactly 5 seconds to complete. Any way that I can improve this time? I'm only getting 100 posts via JSON, so I can't imagine this is the fastest that it can move. Here is the code:
NSLog(#"Data Retrieval STARTED");
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:getURL]];
NSLog(#"Data Pull ENDED");
When I view the log, The first Log statement and the 2nd are always 5 seconds apart, so there's my test, not as accurate as Time Profiler, but I trust it. Tips are appreciated.
You should establish with curl (or similar) a baseline to compare the 5sec time to. Until then, I'd assume it's the server+transaction and nothing special related to iOS.
Anyway, no matter the delay, a remote request is too long to do synchronously while the app blocks. Instead, start by defining what amusing / useful thing your UI can do during the request (Maybe its a just spinner, or maybe its other startup tasks. Be thoughtful about this). Then create an NSURLRequest from the NSURL and pass that to an NSURLConnection using:
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
In the completion block, parse the response and restore your UI to "ready".
You can use dataWithContentsOfURL:options:error: instead. For the option parameter, pass in NSDataReadingUncached, this will improve performance if your data is being read only once and then discarded
Second Solution:
Open a connection using NSUrlConnection before hand, then when you are getting the JSON file, there is no extra time taken to open the connection first.
Related
In a nutshell, I am trying to display data from a publicly available JSON file on the WEB. The process is the following:
I initiate the download with an NSURLSessionDataTask, then I parse and display the JSON or handle errors if they occur. Here is my relevant code:
- (void) initiateDownload {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 5.0f;
sessionConfig.timeoutIntervalForResource = 20.0f;
NSURLSessionDataTask *downloadWeatherTask = [urlSession dataTaskWithRequest:urlRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *downloadError) {
if (downloadError) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self errorReceived:downloadError];
});
} else if (data) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self parseWeatherJSON:data];
});
}
}];
[downloadWeatherTask resume];
}
I have a couple of questions about this:
I am not all that familiar with thread handling. Although I added the
dispatch_sync(dispatch_get_main_queue(), ...)
to both completion blocks, and it seems to work, I am not sure this is the best way to be thread safe (before, I received all kinds of error messages and displaying the data took 10 seconds after the download has already finished). Is there a better way to handle the download and the threads or mine is an acceptable solution?
I would like the user to be able to initiate the download process manually any time he/she wants to refresh the data displayed. At first, I initialized the NSURLSessionDataTask once and made it available anywhere within the class; so I could just call resume every time a refresh is called. However, I could not find a command to re-do the download process. Once I called [downloadWeatherTask resume], I was unable to start the task from the beginning.
So, I added the task to a separate function (the one you see above) and initialize it there every time the function is called. This works fine, but I am not sure it is the best way to go. For example, is it memory safe or am I creating a new task every time the user initiates a refresh call and will eventually run out of memory?
Thank you for the answers!
A little more info: I use the latest XCode 11 and target iOS 9 and up.
NSURLSession will, by default, use a dedicated background serial queue for completion blocks (and delegate methods, should you do that). But you really want to make sure you trigger UI updates from the main queue (retrieved via dispatch_get_main_queue()). And you generally want to avoid updating properties and ivars from multiple threads (unless, they have some thread-safety built in to them, which is unusual), so dispatching the updates to those properties/ivars back to the main queue is a nice simple way to achieve thread safety.
So, bottom line, what you’re doing here is fine.
However, I could not find a command to re-do the download process.
You perform (resume) a given task only once. If you want to perform a similar request again, instantiate a new NSURLSessionDataTask.
Having followed the Ray Wenderlich Parse + Core Data + AFNetworking tutorial, it seems that he pulls JSON from Parse by creating an AFHTTPRequestOperation object using an NSMutableURLRequest and then adds that operation to a queue via enqueueBatchOfHTTPRequestOperations:(NSArray *)operations progressBlock:(void (^__strong)(NSUInteger, NSUInteger))progressBlock completionBlock:(void (^__strong)(NSArray *__strong))completionBlock.
My question is: Is it possible to process an AFHTTPRequestOperation immediately (rather than adding it to queue) so that the subsequent line of code processes once the request has been fully processed? Is this bad form?
What I am actually trying to do: If a requested object does not exist locally, then attempt to download it from Parse. If that request fails, assume (for now) that the object does not exist on Parse. If that request succeeds, then the user can now start using that object locally.
Advanced apologies if this is a dumb question as I am new to AFNetworking and any kind of data synchronization...also the few iOS developer friends I have have never used AFNetworking / Parse / any kind of data synchronization.
Most people consider it bad form, although I understand wanting to try it when you are getting started. I would advise never doing it in code you are submitting to the App Store. Some discussion about the same thing here: Synchronous AFNetworking calls
I am designing an IPhone application. User search something. We grab data from the net. Then we update the table.
THe pseudocode would be
[DoThisAtbackground ^{
LoadData ();
[DoThisAtForeground ^{
UpdateTableAndView();
}];
}];
What about if before the first search is done the user search something else.
What's the industry standard way to solve the issue?
Keep track which thread is still running and only update the table
when ALL threads have finished?
Update the view every time a thread finish?
How exactly we do this?
I suggest you take a look at the iOS Human Interface Guidelines. Apple thinks it's pretty important all application behave in about the same way, so they've written an extensive document about these kind of issues.
In the guidelines there are two things that are relevant to your question:
Make Search Quick and Rewarding: "When possible, also filter remote data while users type. Although filtering users' typing can result in a better search experience, be sure to inform them and give them an opportunity to opt out if the response time is likely to delay the results by more than a second or two."
Feedback: "Feedback acknowledges people’s actions and assures them that processing is occurring. People expect immediate feedback when they operate a control, and they appreciate status updates during lengthy operations."
Although there is of course a lot of nonsense in these guidelines, I think the above points are actually a good idea to follow. As a user, I expect something to happen when searching, and when you update the view every time a thread is finished, the user will see the fastest response. Yes, it might be results the user doesn't want, but something is happening! For example, take the Safari web browser in iOS: Google autocomplete displays results even when you're typing, and not just when you've finished entering your search query.
So I think it's best to go with your second option.
If you're performing the REST request for data to your remote server you can always cancel the request and start the new one without updating the table, which is a way to go. Requests that have the time to finish will update UI and the others won't. For example use ASIHTTPRequest
- (void)serverPerformDataRequestWithQuery:(NSString *)query andDelegate:(__weak id <ServerDelegate)delegate {
[currentRequest setFailedBlock:nil];
[currentRequest cancel];
currentRequest = [[ASIHTTPRequest alloc] initWithURL:kHOST];
[currentRequest startAsynchronous];
}
Let me know if you need an answer for the local SQLite databases too as it is much more complicated.
You could use NSOperationQueue to cancel all pending operations, but it still would not cancel the existing operation. You would still have to implement something to cancel the existing operation... which also works to early-abort the operations in the queue.
I usually prefer straight GCD, unless there are other benefits in my use cases that are a better fit for NSOperationQueue.
Also, if your loading has an external cancel mechanism, you want to cancel any pending I/O operations.
If the operations are independent, consider a concurrent queue, as it will allow the newer request to execute simultaneously as the other(s) are being canceled.
Also, if they are all I/O, consider if you can use dispatch_io instead of blocking a thread. As Monk would say, "You'll thank me later."
Consider something like this:
- (void)userRequestedNewSearch:(SearchInfo*)searchInfo {
// Assign this operation a new token, that uniquely identifies this operation.
uint32_t token = [self nextOperationToken];
// If your "loading" API has an external abort mechanism, you want to keep
// track of the in-flight I/O so any existing I/O operations can be canceled
// before dispatching new work.
dispatch_async(myQueue, ^{
// Try to load your data in small pieces, so you can exit as early as
// possible. If you have to do a monolithic load, that's OK, but this
// block will not exit until that stops.
while (! loadIsComplete) {
if ([self currentToken] != token) return;
// Load some data, set loadIsComplete when loading completes
}
dispatch_async(dispatch_get_main_queue(), ^{
// One last check before updating the UI...
if ([self currentToken] != token) return;
// Do your UI update operations
});
});
}
It will early-abort any operation that is not the last one submitted. If you used NSOperationQueue you could call cancelAllOperations but you would still need a similar mechanism to early-abort the one that is currently executing.
I have a program that progresses as follows. I call a method called getCharacteristics. This method connects to a remote server via a NSURL connection (all networking code done in another file) and when it receives a response it makes a method call back to the original class. This original class then parses the data (xml) and stores its contents as a map.
The problem I'm having is that it appears that somewhere in this transaction another thread is being spawned off.
Here is sample code showing what I'm doing:
#property map
- (void) aMethod
{
[[WebService getSingleton] callWebService: andReportBackTo: self]
Print "Ready to Return"
return map;
}
- (void) methodThatIsReportedBackToAfterWebServiceRecievesResponse
{
//Parse data and store in map
Print "Done Parsing"
}
The problem that I am running into is that map is being returned before it can be fully created. Additionally, "Ready to Return" is being printed before "Done parsing" which suggests to me that there are multiple threads at work. Am I right? If so, would a simple lock be the best way to make it work?
NSURLConnection will execute in another thread if you tell it to execute asynchronously.
In my opinion the best way to deal with this would be to write your own delegate protocol, and use delegation to return your map when the you have downloaded and parsed your data.
You could retrieve your data synchronously using NSURLConnection, but you may force the user to wait for an extended period of time especially if a connection timeout occurs. I would avoid this approach.
NSData has always had a very convenient method called +dataWithContentsOfURL:options:error:. While convenient, it also blocks execution of the current thread, which meant it was basically useless for production code (Ignoring NSOperation). I used this method so infrequently, I completely forgot that it existed. Until recently.
The way I've been grabbing data from the tubes is the standard NSURLConnectionDelegate approach: Write a download class that handles the various NSURLConnectionDelegate methods, gradually build up some data, handle errors, etc. I'll usually make this generic enough to be reused for as many requests as possible.
Say my typical downloader class runs somewhere in the ballpark of 100 lines. That's 100 lines to do asynchronously what NSData can do synchronously in one line. For more complexity, that downloader class needs a delegate protocol of its own to communicate completion and errors to its owner, and the owner needs to implement that protocol in some fashion.
Now, enter Grand Central Dispatch, and I can do something as fantastically simple as:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSData* data = [NSData dataWithContentsOfURL:someURL];
// Process data, also async...
dispatch_async(dispatch_get_main_queue(), ^(void) {
// Back to the main thread for UI updates, etc.
});
});
And I can throw that sucker in anywhere I want, right in-line. No need for a download class, no need to handle connection delegate methods: Easy async data in just a few lines. The disparity between this approach and my pre-GCD approach is of a magnitude great enough to trigger the Too Good to be True Alarm.
Thus, my question: Are there any caveats to using NSData + GCD for simple data download tasks instead of NSURLConnection (Assuming I don't care about things like download progress)?
You are losing a lot of functionality here:
Can't follow the download progression
Can't cancel the download
Can't manage the possible authentication process
You can't handle errors easily, which is really important especially in mobile development like on iPhone of course (because you often lose your network in real conditions, so it is very important to track such network error cases when developing for iOS)
and there's probably more I guess.
The right approach for that is to create a class than manages the download.
See my own OHURLLoader class for example, which is simple and I made the API to be easy to use with blocks:
NSURL* url = ...
NSURLRequest* req = [NSURLRequest requestWithURL:url];
OHURLLoader* loader = [OHURLLoader URLLoaderWithRequest:req];
[loader startRequestWithCompletion:^(NSData* receivedData, NSInteger httpStatusCode) {
NSLog(#"Download of %# done (statusCode:%d)",url,httpStatusCode);
if (httpStatusCode == 200) {
NSLog(%#"Received string: %#", loader.receivedString); // receivedString is a commodity getter that interpret receivedData using the TextEncoding specified in the HTTP response
} else {
NSLog(#"HTTP Status code: %d",httpStatusCode); // Log unexpected status code
}
} errorHandler:^(NSError *error) {
NSLog(#"Error while downloading %#: %#",url,error);
}];
See the README file and sample project on github for more info.
This way:
you still rely on the asynchronous methods provided by NSURLConnection (and as the Apple's documentation says about Concurrency Programming if an API already exists to make asynchronous tasks, use it instead of relying on another threading technology if possible)
you keep advantages of NSURLConnection (error handlings, etc)
but you also have the advantages of the blocks syntax that makes your code more readable than when using delegate methods
WWDC 2010 Session Videos:
WWDC 2010 Session 207 - Network Apps for iPhone OS, Part 1
WWDC 2010 Session 208 - Network Apps for iPhone OS, Part 2
The lecturer said
"Threads Are Evil™".
For network programming, it is strongly recommended to use asynchronous API with RunLoop.
Because, if you use NSData + GCD like the following, it uses one thread per connection.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
NSData* data = [NSData dataWithContentsOfURL:someURL];
And it's likely to use many connections and many threads. It is too easy to use GCD :-)
Then, many threads eats huge amount of memory for its stack.
Thus, you'd better to use asynchronous API as AliSoftware said.
As of OS X v10.9 and iOS 7 the preferred way is to use NSURLSession. It gives you a nice, block-based interface and features like canceling, suspending and background downloading.