I have an app which downloads some files from the server in few threads. The problems is that it is giving a heavy load to the CPU (hitting to 80%). What can be done to make it better? I made similar app on Windows with C#, and the cpu usage never goes above 5%.
EDIT: This code has been changed after getting some suggestions below. The problem now is, that the download never reaches 100% when I set [queue setMaxConcurrentOperationCount:6]. If I change the asynchronous NSURLConnection back to sendSynchronous call it works, when I change the above OperationCount to 1, also works.
This is how I add NSOperations to the queue (may be large, like 800).
int chunkId = 0;
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
chunkId = 0;
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
DownloadChunkOperation *operation = [[DownloadChunkOperation alloc] initWithDownloadObject:download
downloadFile:downloadFile downloadChunk:downloadChunk andChunkId:chunkId];
[queue addOperation:operation];
chunkId++;
}
}
#import "DownloadChunkOperation.h"
#import "Download.h"
#import "DownloadFile.h"
#import "DownloadChunk.h"
#interface DownloadChunkOperation()
#property(assign) BOOL isExecuting;
#property(assign) BOOL isFinished;
#end
#implementation DownloadChunkOperation
#synthesize download = _download;
#synthesize downloadFile = _downloadFile;
#synthesize downloadChunk = _downloadChunk;
#synthesize isFinished = _isFinished;
#synthesize isExecuting = _isExecuting;
- (id) initWithDownloadObject:(Download *)download downloadFile:(DownloadFile *)downloadFile downloadChunk:(DownloadChunk *)downloadChunk andChunkId:(uint32_t)chunkId
{
self = [super init];
if (self) {
self.download = download;
self.downloadFile = downloadFile;
self.downloadChunk = downloadChunk;
self.chunkId = chunkId;
}
return self;
}
- (void) start
{
if ([self isCancelled]) {
[self setIsFinished:YES];
[self setIsExecuting:NO];
return;
}
[self setIsExecuting:YES];
[self setIsFinished:NO];
[self.downloadChunk setChunkState:cDownloading];
downloadPath = [[NSString stringWithFormat:#"%#/%#", [self.download downloadFolder], [self.download escapedTitle]] stringByExpandingTildeInPath];
NSURL *fileURL = [[NSURL alloc] initWithString:[self.downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", [self.downloadChunk startingByte], [self.downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
// IMPORTANT! The next line is what keeps the NSOperation alive for the during of the NSURLConnection!
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
if (connection) {
NSLog(#"connection established!");
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!self.isFinished);
} else {
NSLog(#"couldn't establish connection for: %#", fileURL);
}
}
- (BOOL) isConcurrent
{
return YES;
}
- (void) connection:(NSURLConnection *)_connection didReceiveResponse:(NSURLResponse *)response
{
receivedData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// Not cancelled, receive data.
if (![self isCancelled]) {
[receivedData appendData:data];
self.download.downloadedBytes += [data length];
return;
}
// Cancelled, tear down connection.
[self setIsExecuting:NO];
[self setIsFinished:YES];
[self.downloadChunk setChunkState:cConnecting];
[self->connection cancel];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self setIsExecuting:NO];
[self setIsFinished:YES];
NSLog(#"Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *chunkPath = [downloadPath stringByAppendingFormat:#"/%#.%i", [self.downloadFile fileName], self.chunkId];
NSError *saveError = nil;
[receivedData writeToFile:chunkPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil) {
NSLog(#"Download save failed! Error: %#", [saveError description]);
}
else {
NSLog(#"file has been saved!: %#", chunkPath);
}
[self setIsExecuting:NO];
[self setIsFinished:YES];
[self.downloadChunk setChunkState:cFinished];
if ([self.download downloadedBytes] == [self.download size])
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:self.download];
}
#end
You should not create threads yourself. Use dedicated API like NSOperationQueue or even GCD directly for this purpose. They know better about hardware limits, virtual cores, etc. and support priority settings.
You shouldn't use +sendSynchronousRequest: either. Wrapping your -downloadChunk method in a dispatch call as suggested by charith won't help you improve performance, as +sendSynchronousRequest: blocks the thread until new data comes in and forces GCD to spawn new threads.
Use the asynchronous API of NSURLConnection using delegate callbacks. You can also wrap your NSURLConnection code inside a NSOperation subclass and use NSOperationQueue to manage the downloads: Using NSURLConnections
If you don't want to write the NSOperation subclass yourself, you can also use a 3rd party framework like AFNetworking.
Try with GCD blocks and global queues. This is the apple recommended way now for concurrency ex:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
[self downloadChunk:objDownload];
});
Related
I have always been nervous when it comes to blocks and GCD because my mind tells me that it looks very complex!
I am getting a crash inside a block which ideally looks alright to me:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^()
{
__block __strong HLOrdersDataProvider *ordersDataProvider = nil;
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:[^{
ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
} copy]];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
I was able to trace out the zombie object but I am not able to figure out why is this object turning out to be a zombie. Here is the snapshot from profile:
I have gone through the following guides (1, 2) to see if I can find out what I am doing wrong but I was no where near to find out what is going wrong.
Any help and reference text to what I am doing wrong will help.
Edit:
I have tried what #Jerimy has suggested and in fact my code which I have posted earlier was exactly the same as required: Declaring and initializing ordersDataProvider inside the block operation itself. But since it was crashing at the same point I tried to declare it outside the block just to see if it addresses the crash.
Below is the new code I tested:
#pragma mark -
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
__weak VTVehicleServiceNetworkManager *weakSelf = self;
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^()
{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:[fetchOrdersListTaskBlock copy]
errorBlock:^(NSError *error) {
errorBlock(error);
}];
}
The crash from Profile:
There is not much from the stack trace as well, SDMHTTPRequest is a library and am very sure there is nothing wrong there, and the HLOrdersDataProvider is the zombie object which I was able to trace out in Instruments app:
EDIT 2
Adding the interface and implementation of HLOrdersDataProvider for more details:
#interface HLOrdersDataProvider : HLDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock;
#end
#implementation HLOrdersDataProvider
-(void)performFetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
// Using SDMConnectivity
NSString *queryString = [infoDict valueForKey:#"$filter"];
NSString *appendStringForEndpoint = [kRowsetsKeyword stringByAppendingFormat:#"?%#", queryString];
[self fetchDataFromServerWithEndPointAppendString:appendStringForEndpoint
completionBlock:completionBlock
errorBlock:errorBlock];
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
return [NSString stringWithString:kRowsetsKeyword];
}
-(void)requestFinished:(SDMHttpRequest *)request
{
NSError *error = nil;
// Let's parse the response and send the results back to the caller
NSString *collectionName = [self collectionName];
NSData *responseData = [request responseData];
NSArray *entitiesArray = [self parseODataEntriesWithData:responseData
withCollectionName:collectionName
error:&error];
if (error)
[self triggerFailureBlockWithArgument:error];
else
[self triggerCompletionBlockWithArgument:entitiesArray];
}
#end
Also, HLOrdersDataProvider is inherited from HLDataProvider so below is the interface and implementation of this class too:
#import <Foundation/Foundation.h>
//#import "SDMHttpRequestDelegate.h"
#import "SDMRequestBuilder.h"
#import "SDMHttpRequest.h"
#import "SDMParser.h"
#import "HLConstant.h"
#import "HLConnectionData.h"
#interface HLDataProvider : NSObject <SDMHttpRequestDelegate>
#property (copy, atomic) CompletionBlock completionBlock;
#property (copy , atomic) ErrorBlock errorBlock;
#property (copy, atomic) CompletionBlockWithDataFetchStatus completionBlockWithDataFetchStatus;
+ (id)sharedInstance;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName;
- (NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError;
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data;
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)relationships;
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries;
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries;
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries;
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
-(NSString*)collectionName;
-(void)triggerCompletionBlockWithArgument:(id)inArg;
-(void)triggerFailureBlockWithArgument:(NSError*)inArg;
#end
#implementation HLDataProvider
+ (id)sharedInstance
{
//Subclassess will override this method
return nil;
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName
{
return [self parseODataEntriesWithData:data
withCollectionName:collectionName
error:NULL];
}
-(NSMutableArray*)parseODataEntriesWithData:(NSData*)data withCollectionName:(NSString*)collectionName error:(NSError**)outError
{
NSMutableArray *entriesArray = nil;
#try {
entriesArray = sdmParseODataEntriesXML(data,
[[[HLConnectionData metaDataDocument] getCollectionByName:collectionName] getEntitySchema],
[HLConnectionData serviceDocument]);
}
#catch (NSException *exception) {
NSLog(#"Got exception: %#", exception);
if (outError)
{
*outError = [NSError errorWithDomain:#"Vehicle Service"
code:-1001
userInfo:[NSDictionary dictionaryWithObject:exception forKey:NSLocalizedDescriptionKey]];
}
}
#finally {
}
return entriesArray;
}
- (NSMutableArray*)parseJSONEntriesWithData:(NSData*)data
{
NSError *error = nil;
id object = [NSJSONSerialization
JSONObjectWithData:data
options:0
error:&error];
NSMutableArray *resultArray = nil;
if(error) { /* JSON was malformed, act appropriately here */ }
if([object isKindOfClass:[NSDictionary class]])
{
resultArray = [NSMutableArray arrayWithObject:object];
}
else if ([object isKindOfClass:[NSArray class]])
{
resultArray = [NSMutableArray arrayWithArray:object];
}
return resultArray;
}
#pragma mark -
#pragma mark - Data Fetch - Server - SDMConnectivity
-(void)fetchDataFromServerWithEndPointAppendString:(NSString*)appendStr completionBlock:(CompletionBlock)inCompletionBlock errorBlock:(ErrorBlock)inErrorBlock;
{
self.errorBlock = inErrorBlock;
self.completionBlock = inCompletionBlock;
id<SDMRequesting> request = nil;
//NSString *clientStr = #"&sap-client=320&sap-language=EN";
NSString *urlStr =[NSString stringWithFormat:#"%#/%#",[HLConnectionData applicationEndPoint], appendStr];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[SDMRequestBuilder setRequestType:HTTPRequestType];
request=[SDMRequestBuilder requestWithURL:[NSURL URLWithString:urlStr]];
[request setUsername:kUserName];
/*Set Password in SDMRequesting object*/
[request setPassword:kPassword];
[request setRequestMethod:#"GET"];
[request setTimeOutSeconds:kTimeoutInterval];
/*set the Delegate. This class must adhere to SDMHttpRequestDelegate to get the callback*/
[request setDelegate:self];
/*Call startAsynchronous API to request object to retreive Data asynchrnously in the call backs */
[request startSynchronous];
}
-(void)updateDatabaseWithEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(void)updateDatabaseWithJSONEntries:(NSMutableArray*)scanEntries
{
//Subclasses will override this
}
-(id)parsedOdataResultFromEntries:(NSMutableArray*)entries
{
//Subclasses will override this
return nil;
}
-(void)deleteExistingEntriesFromCoredata
{
//Subclasses will override this
}
-(NSArray*)fetchEntriesFromDatabaseWithEntityName:(NSString*)entityName relationshipObjects:(NSMutableArray*)array
{
//Subclasses will overide this method
return nil;
}
#pragma mark - SDMHTTPRequestDelegate methods
- (void)requestStarted:(SDMHttpRequest*) request
{
}
- (void)requestFinished:(SDMHttpRequest*) request
{
// For service doc and metadata we instantiate HLDataProvider, so we send this raw SDMHTTPRequest object as-is. For other service agents like HLOrdersDataProvider we send the parsed information, check the subclass' implementation of -requestFinished: method
[self triggerCompletionBlockWithArgument:request];
}
-(void)triggerCompletionBlockWithArgument:(id)inArg
{
self.completionBlock(inArg);
}
- (void)requestFailed:(SDMHttpRequest*) request
{
[self triggerFailureBlockWithArgument:request.error];
}
-(void)triggerFailureBlockWithArgument:(NSError*)inArg
{
self.errorBlock(inArg);
}
- (void)requestRedirected:(SDMHttpRequest*) request
{
}
#pragma mark - Service Agent related
-(NSString*)collectionName
{
// Should be overridden by the subclasses
return nil;
}
The referenced code is very convoluted (you're doing things in a very complicated way).
First off, you should not be creating the copy of your blocks in the caller. Make the copy in the callee if necessary (ie: to copy it to heap instead of using the stack-allocated block if you are going to call it after the stack has been popped). In almost all APIs using blocks, it is not the caller's responsibility to ensure that a block is on heap.
There is no reason for your ordersDataProvider variable to be declared in the scope of fetchOrdersListTaskBlock because it is only ever used inside of fetchOrdersOperation's block.
I don't immediately see the cause of your crash, but I suspect that simplifying your code will help reveal the problem. Perhaps the issue is in HLOrdersDataProvider's initializer.
Try something like:
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
__weak VTVehicleServiceNetworkManager *weakSelf = self;
TaskBlock fetchOrdersListTaskBlock = ^{
NSBlockOperation *fetchOrdersOperation = [NSBlockOperation blockOperationWithBlock:^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
}];
[weakSelf.dataOperationQueue addOperation:fetchOrdersOperation];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}
Or better yet, re-design your class to work like this (I don't see a need for NSBlockOperation in your example):
-(void)fetchOrdersListWithInfoDict:(NSDictionary*)infoDict completionBlock:(CompletionBlock)completionBlock errorBlock:(ErrorBlock)errorBlock
{
completionBlock = [completionBlock copy];
errorBlock = [errorBlock copy];
TaskBlock fetchOrdersListTaskBlock = ^{
HLOrdersDataProvider *ordersDataProvider = [[HLOrdersDataProvider alloc] init];
[ordersDataProvider performFetchOrdersListWithInfoDict:infoDict
completionBlock:completionBlock
errorBlock:errorBlock];
};
[self fetchDataWithTaskBlock:fetchOrdersListTaskBlock
errorBlock:errorBlock];
}
Is there a way to force your application to process other events? I have a loop that is supposed to wait 10 seconds for a json response. The problem seems to be that the loop processes before the didReceiveData event can run.
JsonArray is an NSArray property.
Here is my code in Session.m:
-(BOOL)logIn
{
JsonArray = nil;
if (([Password class] == [NSNull class]) || ([Password length] == 0))
return NO;
if (([Username class] == [NSNull class]) || ([Username length] == 0))
return NO;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL_ALL_PROPERTIES]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:10.0];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
[connection start];
if (!connection)
{
return NO;
}
int time = CFAbsoluteTimeGetCurrent();
while (!JsonArray)
{
if (CFAbsoluteTimeGetCurrent() - time == 10)
{
NSLog(#"breaking loop");
break;
}
// process events here
}
if (!JsonArray) return NO;
NSLog(#"JsonArray not nil");
return YES;
}
didReceiveData handler:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(#"Data received");
NSError *e = nil;
JsonArray = nil;
JsonArray = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&e];
if (!JsonArray)
{
JsonArray = nil;
Connected = NO;
return;
}
Connected = YES;
}
Embrace the asynchronous nature of what you're trying to do. Remove the return from the login method and add a completion block instead. Store the completion block in a property (copy) and call it from connectionDidFinishLoading:.
typedef void (^MYCompletionHandler)(BOOL success);
#property (nonatomic, copy) void (^MYCompletionHandler)(bool *) completion;
- (void)loginUserWithCompletion:(MYCompletionHandler)completion {
self.completion = completion;
// start your login processing here
}
// when the login response is received
self. completion(##DID_IT_WORK##);
Your current code doesn't work because the main runloop isn't running while your hard loop is running, so the delegate methods are queued waiting to be processed. Don't try to work around it with the run loop, deal with the asynchronous nature properly.
The didReceiveData method will be called when the data has been received. If you need to process the data returned, do it in the delegate method.
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// process your data in here
}
The logIn function has to finish before didReceiveData will be called. You cannot block for a response. That's the nature of NSURLConnection.
My app is working this way :
- create an album and take pictures
- send them on my server
- get an answer / complementary information after picture analysis.
I have some issue with the sending part. Here is the code
#interface SyncAgent : NSObject <SyncTaskDelegate>
#property NSOperationQueue* _queue;
-(void)launchTasks;
#end
#implementation SyncAgent
#synthesize _queue;
- (id)init
{
self = [super init];
if (self) {
self._queue = [[NSOperationQueue alloc] init];
[self._queue setMaxConcurrentOperationCount:1];
}
return self;
}
-(void) launchTasks {
NSMutableArray *tasks = [DataBase getPendingTasks];
for(Task *t in tasks) {
[self._queue addOperation:[[SyncTask alloc] initWithTask:t]];
}
}
#end
and the SyncTask :
#interface SyncTask : NSOperation
#property (strong, atomic) Task *_task;
-(id)initWithTask:(Task *)task;
-(void)mainNewID;
-(void)mainUploadNextPhoto:(NSNumber*)photoSetID;
#end
#implementation SyncTask
#synthesize _task;
-(id)initWithTask:(Task *)task {
if(self = [super init]) {
self._task = task;
}
return self;
}
-(void)main {
NSLog(#"Starting task : %#", [self._task description]);
// checking if everything is ready, sending delegates a message etc
[self mainNewID];
}
-(void)mainNewID {
__block SyncTask *safeSelf = self;
[[WebAPI sharedClient] createNewPhotoSet withErrorBlock:^{
NSLog(#"PhotoSet creation : error")
} andSuccessBlock:^(NSNumber *photoSetID) {
NSLog(#"Photoset creation : id is %d", [photoSetID intValue]);
[safeSelf mainUploadNextPhoto:photoSetID];
}];
}
-(void)mainUploadNextPhoto:(NSNumber*) photoSetID {
//just admit we have it. won't explain here how it's done
NSString *photoPath;
__block SyncTask *safeSelf = self;
[[WebAPI sharedClient] uploadToPhotosetID:photoSetID withPhotoPath:photoPath andErrorBlock:^(NSString *photoPath) {
NSLog(#"Photo upload error : %#", photoPath);
} andSuccessBlock:^(NSString *photoPath) {
NSLog(#"Photo upload ok : %#", photoPath);
//then we delete the file
[safeSelf mainUploadNextPhoto:photoSetID];
}];
}
#end
Every network operations are done using AFNetworking this way :
-(void)myDummyDownload:(void (^)(NSData * data))successBlock
{
AFHTTPClient* _httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:#"http://www.google.com/"]];
[_httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
NSMutableURLRequest *request = [_httpClient requestWithMethod:#"GET" path:#"/" nil];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
AFHTTPRequestOperation *operation = [_httpClient HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:^(AFHTTPRequestOperation *operation, id data) {
if(dataBlock)
dataBlock(data);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Cannot download : %#", error);
}];
[operation setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{
NSLog(#"Request time out");
}];
[_httpClient enqueueHTTPRequestOperation:operation];
}
My problem is : my connections are made asynchronously, so every task are launched together without waiting fo the previous to finish even with [self._queue setMaxConcurrentOperationCount:1] in SyncAgent.
Do I need to perform every connection synchronously ? I don't think this is a good idea, because a connection never should be done this way and also because I might use these methods elsewhere and need them to be performed in background, but I cannot find a better way. Any idea ?
Oh and if there is any error/typo in my code, I can assure you it appeared when I tried to summarize it before pasting it, it is working without any problem as of now.
Thanks !
PS: Sorry for the long pastes I couldn't figure out a better way to explain the problem.
EDIT: I found that using a semaphore is easier to set up and to understand : How do I wait for an asynchronously dispatched block to finish?
If you have any control over the server at all, you should really consider creating an API that allows you to upload photos in an arbitrary order, so as to support multiple simultaneous uploads (which can be quite a bit faster for large batches).
But if you must do things synchronized, the easiest way is probably to enqueue new requests in the completion block of the requests. i.e.
// If [operations length] == 1, just enqueue it and skip all of this
NSEnumerator *enumerator = [operations reverseObjectEnumerator];
AFHTTPRequestOperation *currentOperation = nil;
AFHTTPRequestOperation *nextOperation = [enumerator nextObject];
while (nextOperation != nil && (currentOperation = [enumerator nextObject])) {
currentOperation.completionBlock = ^{
[client enqueueHTTPRequestOperation:nextOperation];
}
nextOperation = currentOperation;
}
[client enqueueHTTPRequestOperation:currentOperation];
The following code works for me, but I am not sure of the drawbacks. Take it with a pinch of salt.
- (void) main {
NSCondition* condition = [[NSCondition alloc] init];
__block bool hasData = false;
[condition lock];
[[WebAPI sharedClient] postPath:#"url"
parameters:queryParams
success:^(AFHTTPRequestOperation *operation, id JSON) {
//success code
[condition lock];
hasData = true;
[condition signal];
[condition unlock];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//failure code
[condition lock];
hasData = true;
[condition signal];
[condition unlock];
}];
while (!hasData) {
[condition wait];
}
[condition unlock];
}
I'm trying to learn how to use the NSInputStream class on the iPhone using a unit test. I can get the NSStream to read data from a file using the polling method but for some reason the delegate/event method is not working.
I've posted the relevant code below. Please ignore memory leak errors and such since I'm just trying to ensure I know how to use the NSStream class in a sandboxed environment before rolling it into my larger project.
I'm wondering if maybe I'm missing something with regards to how the run loops work?
This is the logic test that creates a streamer class to read from a file.
#import "StreamingTests.h"
#import "Streamer.h"
#implementation StreamingTests
- (void) testStream {
NSLog(#"Starting stream test.");
Streamer * streamer = [[Streamer alloc] init];
streamer.usePolling = NO;
streamer.readingStream = YES;
NSThread * readThread = [[NSThread alloc] initWithTarget:streamer selector:#selector(startStreamRead:) object:nil];
[readThread start];
while(streamer.readingStream) {
[NSThread sleepForTimeInterval:0.5];
}
[readThread cancel];
}
#end
This is a simple test helper object that reads from an NSStream. When usePolling == YES it read data and outputs the appropriate NSLog messages. However, if usePolling == NO the delegate stream event function is never called.
#implementation Streamer
#synthesize readingStream, usePolling;
- (void) startStreamRead:(NSObject*) context {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSLog(#"starting stream read.");
readingStream = YES;
/*
NSURL * url = [NSURL URLWithString:#"http://www.google.com"];
NSLog(#"Loading: %#",[url description]);
NSInputStream * inStream = [[NSInputStream alloc] initWithURL:url];
*/
NSInputStream * inStream = [[NSInputStream alloc] initWithFileAtPath:#"sample.ttc"];
if(!usePolling) {
[inStream setDelegate: self];
[inStream scheduleInRunLoop: [NSRunLoop currentRunLoop]
forMode: NSDefaultRunLoopMode];
}
[inStream open];
if(usePolling) {
while(1) {
if([inStream hasBytesAvailable]) {
uint8_t buf[1024];
unsigned int len = 0;
len = [(NSInputStream *)inStream read:buf maxLength:1024];
NSLog(#"Read: %d",len);
}
NSStreamStatus status = [inStream streamStatus];
if(status != NSStreamStatusOpen && status != NSStreamStatusOpening) {
NSLog(#"Stream not open.");
break;
}
}
readingStream = NO;
NSStreamStatus status = [inStream streamStatus];
NSError * error = [inStream streamError];
NSLog(#"Status: %d Error Desc: %# Reason: %#",(int)status,[error localizedDescription], [error localizedFailureReason]);
[pool release];
}
}
- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode {
NSMutableData * _data = nil;
NSNumber * bytesRead = nil;
NSLog(#"Event fired.");
switch(eventCode) {
case NSStreamEventHasBytesAvailable:
{
if(!_data) {
_data = [[NSMutableData data] retain];
}
uint8_t buf[1024];
unsigned int len = 0;
len = [(NSInputStream *)stream read:buf maxLength:1024];
if(len) {
[_data appendBytes:(const void *)buf length:len];
// bytesRead is an instance variable of type NSNumber.
//[bytesRead setIntValue:[bytesRead intValue]+len];
NSLog(#"Read %d bytes",len);
} else {
NSLog(#"no buffer!");
}
break;
}
case NSStreamEventEndEncountered:
{
[stream close];
[stream removeFromRunLoop:[NSRunLoop currentRunLoop]
forMode:NSDefaultRunLoopMode];
[stream release];
stream = nil; // stream is ivar, so reinit it
readingStream = NO;
break;
}
default:
{
NSLog(#"Another event occurred.");
break;
}
// continued ...
}
}
#end
Thanks in advance,
b
The reason for it should be that the run loop is blocked since the unit test is executing. You could refer to the NSRunLoop documentation where the method
runUntilDate:
might help you to run the main run loop in the thread of execution of the unit test like this:
[[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
This lets the run loop run for 1 second giving it time to process part of your file. It should be noted that this does not provide a reliable way for unit testing (since the time interval might differ depending on run loop size) and may then be unsuitable. By giving your unit an interface that could be used to check the status of the input stream read operation (with a reading finished state) such as
-(BOOL)hasFinishedReadingFile
the unit test could repeatedly execute the run loop until the above method returns TRUE and the file is read completely.
Addition: This question on stackoverflow also deals with the problem in a different way.
I've got the problem when I tried to do asynchronous requests to server from background thread. I've never got results of those requests. Simple example which shows the problem:
#protocol AsyncImgRequestDelegate
-(void) imageDownloadDidFinish:(UIImage*) img;
#end
#interface AsyncImgRequest : NSObject
{
NSMutableData* receivedData;
id<AsyncImgRequestDelegate> delegate;
}
#property (nonatomic,retain) id<AsyncImgRequestDelegate> delegate;
-(void) downloadImage:(NSString*) url ;
#end
#implementation AsyncImgRequest
-(void) downloadImage:(NSString*) url
{
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:url]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:20.0];
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (theConnection) {
receivedData=[[NSMutableData data] retain];
} else {
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[delegate imageDownloadDidFinish:[UIImage imageWithData:receivedData]];
[connection release];
[receivedData release];
}
#end
Then I call this from main thread
asyncImgRequest = [[AsyncImgRequest alloc] init];
asyncImgRequest.delegate = self;
[self performSelectorInBackground:#selector(downloadImage) withObject:nil];
method downloadImage is listed below:
-(void) downloadImage
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
[asyncImgRequest downloadImage:#"http://photography.nationalgeographic.com/staticfiles/NGS/Shared/StaticFiles/Photography/Images/POD/l/leopard-namibia-sw.jpg"];
[pool release];
}
The problem is that method imageDownloadDidFinish is never called. Moreover none of methods
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse*)response
are called. However if I replace
[self performSelectorInBackground:#selector(downloadImage) withObject:nil];
by
[self performSelector:#selector(downloadImage) withObject:nil];
everything is working correct. I assume that the background thread dies before async request is finished it job and this causes the problem but I'm not sure. Am I right with this assumptions? Is there any way to avoid this problem?
I know I can use sync request to avoid this problem but it's just simple example, real situation is more complex.
Thanks in advance.
Yes, the thread is exiting. You can see this by adding:
-(void)threadDone:(NSNotification*)arg
{
NSLog(#"Thread exiting");
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(threadDone:)
name:NSThreadWillExitNotification
object:nil];
You can keep the thread from exiting with:
-(void) downloadImage
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
[self downloadImage:urlString];
CFRunLoopRun(); // Avoid thread exiting
[pool release];
}
However, this means the thread will never exit. So you need to stop it when you're done.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
CFRunLoopStop(CFRunLoopGetCurrent());
}
Learn more about Run Loops in the Threading Guide and RunLoop Reference.
You can start the connection on a background thread but you have to ensure the delegate methods are called on main thread. This cannot be done with
[[NSURLConnection alloc] initWithRequest:urlRequest
delegate:self];
since it starts immediately.
Do this to configure the delegate queue and it works even on secondary threads:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:urlRequest
delegate:self
startImmediately:NO];
[connection setDelegateQueue:[NSOperationQueue mainQueue]];
[connection start];
NSURLRequests are completely asynchronous anyway. If you need to make an NSURLRequest from a thread other than the main thread, I think the best way to do this is just make the NSURLRequest from the main thread.
// Code running on _not the main thread_:
[self performSelectorOnMainThread:#selector( SomeSelectorThatMakesNSURLRequest )
withObject:nil
waitUntilDone:FALSE] ; // DON'T block this thread until the selector completes.
All this does is shoot off the HTTP request from the main thread (so that it actually works and doesn't mysteriously disappear). The HTTP response will come back into the callbacks as usual.
If you want to do this with GCD, you can just go
// From NOT the main thread:
dispatch_async( dispatch_get_main_queue(), ^{ //
// Perform your HTTP request (this runs on the main thread)
} ) ;
The MAIN_QUEUE runs on the main thread.
So the first line of my HTTP get function looks like:
void Server::get( string queryString, function<void (char*resp, int len) > onSuccess,
function<void (char*resp, int len) > onFail )
{
if( ![NSThread isMainThread] )
{
warning( "You are issuing an HTTP request on NOT the main thread. "
"This is a problem because if your thread exits too early, "
"I will be terminated and my delegates won't run" ) ;
// From NOT the main thread:
dispatch_async( dispatch_get_main_queue(), ^{
// Perform your HTTP request (this runs on the main thread)
get( queryString, onSuccess, onFail ) ; // re-issue the same HTTP request,
// but on the main thread.
} ) ;
return ;
}
// proceed with HTTP request normally
}