How to perform all other tasks once the asynchronous dispatch queue finishes execution - objective-c

I have a PDFDocument where it has some actions on pages like delete,crop,rotate etc.
So when I click on the delete button and click on save(current thread : main thread)
-(void)save
{
// -- process deleted pages first
for( NSString* deletedPageId in self.deletedPageIdList )
{
[self.pdfCoordinator removePageWithId:deletedPageId];
}
// -- wait for all pages to delete before continuing
[self.pdfCoordinator waitUntilAllOperationsAreFinished];
// few lines of code after this should run only when the above code finishes its execution
// code to save the changes in the pdf to managedObject context
}
The code for removePageWithId:
- (void) removePageWithId:(NSString*)pageId
{
NRMPDFOperation *op = [[NRMPDFOperation alloc] initWithNRMPDF:self
pageId:pageId
selector:#selector(removePageOpImpl:)
args:nil];
[self addOperation:op];
[op release];
}
above code creates an operation and adds it to the operation queue for each deletion of the page
code for removePageOpImpl:
- (NSError *)removePageOpImpl:(NRMPDFOperation *)op
{
NSError* error = [self loadDocument];
if( !error )
{
NSUInteger index = [self pageIndexForId:[op pageId]];
if( index < [self pageCount] )
{
[[self pdfDocument] removePageAtIndex:index];
[[self mutablePageIdList] removeObjectAtIndex:index];
[self updatePageLabelsFromIndex:index];
[self updateChangeCount:NSChangeDone];
self.contentsChanged = YES;
}
else
{
// TODO: error
}
}
return error;
}
In the removePageOpImpl: method the line of code
[[self pdfDocument] removePageAtIndex:index]; internally executing some tasks on main thread(but we are making the main thread to wait until this operation finishes).which causes the deadlock.
I tried to execute the code inside removePageOpImpl: in an asynchronous dispatch queue to avoid the deadlock.below is the code for that
- (NSError *)removePageOpImpl:(NRMPDFOperation *)op
{
NSError* error = [self loadDocument];
if( !error )
{
NSUInteger index = [self pageIndexForId:[op pageId]];
if( index < [self pageCount] )
{
dispatch_async(dispatch_get_main_queue(), ^{
[[self pdfDocument] removePageAtIndex:index];
[[self mutablePageIdList] removeObjectAtIndex:index];
[self updatePageLabelsFromIndex:index];
[self updateChangeCount:NSChangeDone];
self.contentsChanged = YES;
});
}
else
{
// TODO: error
}
}
return error;
}
Now I am out from the deadlock. But another issue is by putting the above code into asynchronous block the code which should run after this tasks is executing before this, because of that my app is not behaving as expected.
Code inside waitUntilAllOperationsAreFinished method
- (void) addOperation:(NRMPDFOperation*)operation
{
[_operationSet addObject:operation];
[[self operationQueue] addOperation:operation];
}
- (void) waitUntilAllOperationsAreFinished
{
[[self operationQueue] waitUntilAllOperationsAreFinished];
}
- (NSOperationQueue *)operationQueue
{
if( !_operationQueue )
{
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
_operationQueue = queue;
}
return _operationQueue;
}
This is how the original Save method looks like :
- (void)saveDocumentWithDelegate:(id)delegate didSaveSelector:(SEL)didSaveSelector contextInfo:(void *)contextInfo
{
// TODO: don't just ignore saveOperation
__block BOOL success = YES;
__block NSError *error = nil;
/* write back field changes */
if ([item hasChangesInEditFieldsetFor:#"values"] )
{
//Some code
}
if( self.isPDFEdited )
{
// -- process deleted pages first
for( NSString* deletedPageId in self.deletedPageIdList )
{
[self.itemPDFCoordinator removePageWithId:deletedPageId];
}
// -- wait for all pages to delete before continuing
[self.itemPDFCoordinator waitUntilAllOperationsAreFinished];
// -- replace the search text any pages we deleted
if( [self.deletedPageIdList count] )
{
[self.item setValue:[self.editPDF string] forKeyPath:#"dict.values._searchText"];
}
NSMutableDictionary* originalRotations = [NSMutableDictionary dictionaryWithCapacity:
[self.itemPDFCoordinator pageCount]];
for( NSString* pageId in self.itemPDFCoordinator.pageIdList )
{
NSInteger rotation = [[self.itemPDFCoordinator pageForId:pageId] rotation];
[originalRotations setObject:[NSNumber numberWithInteger:rotation] forKey:pageId];
}
// -- now process actions on remaining pages (crop, rotate, and convert to b&w)
BOOL didCropAnyPages = NO;
NSMutableArray* convertToBwJobs = [NSMutableArray array];
for( NSString* pageId in [self.pageActionDict allKeys] )
{
NSArray* actions = [self.pageActionDict objectForKey:pageId];
for( NSDictionary* action in actions )
{
NSNumber* rotationNumber = [action objectForKey:#"rotation"];
NSValue* cropRectVal = [action objectForKey:#"cropRect"];
NSNumber* convertToBlackAndWhite = [action objectForKey:#"convertToBlackAndWhite"];
if( rotationNumber )
{
[self.itemPDFCoordinator rotateByDegrees:[rotationNumber integerValue]
forPageWithID:pageId];
}
else if( cropRectVal )
{
[self.itemPDFCoordinator setNormalizedBounds:[cropRectVal rectValue]
forBox:kPDFDisplayBoxCropBox
forPageWithID:pageId];
// -- set a flag so we know to recrop the entire document
didCropAnyPages = YES;
}
else if( [convertToBlackAndWhite boolValue] )
{
NSUInteger pageIndex = [self.itemPDFCoordinator pageIndexForId:pageId];
NRMJob* job = [NRMAppJobFactory convertToBlackAndWhiteJobForItem:self.item
pageIndex:pageIndex];
[convertToBwJobs addObject:job];
}
}
}
// -- reapply crop box to any cropped pages
if( didCropAnyPages )
{
[self.itemPDFCoordinator applyCropBoxToAllPages];
}
[self.itemPDFCoordinator waitUntilAllOperationsAreFinished];
for( NRMJob* job in convertToBwJobs )
{
if( ![[self.masterDocument docjob] addJob:job forItem:self.item error:&error] )
[NSApp presentError:error];
else
[job waitUntilDone];
}
// -- make sure document attributes are updated
NSDictionary *docDict = [self.itemPDFCoordinator documentAttributes];
NSDictionary *newDict = [(NRMItem *)item updateDocumentAttributes:docDict];
if (![newDict isEqualToDictionary:docDict])
[self.itemPDFCoordinator setDocumentAttributes:newDict];
[self.itemPDFCoordinator waitUntilAllOperationsAreFinished];
// -- check if we need to reprocess any pages
for( NSString* pageId in self.itemPDFCoordinator.pageIdList )
{
NSInteger oldRotation = [[originalRotations objectForKey:pageId] integerValue];
NSInteger newRotation = [[self.itemPDFCoordinator pageForId:pageId] rotation];
if( oldRotation != newRotation )
{
// -- if it's an image page and we already have OCR data for it, we should reanalyze
NSUInteger pageIndex = [self.itemPDFCoordinator pageIndexForId:pageId];
BOOL isPageImage = [self.itemPDFCoordinator isImagePageAtIndex:pageIndex DPI:NULL];
if( isPageImage && [item OCRDataForPageIndex:pageIndex] )
{
NRMJob* job = [NRMAppJobFactory reprocessPageJobForItem:self.item
pageIndex:pageIndex];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(reanalyzeJobFinished:)
name:kNRMJobFinishedNotification
object:job];
success = [[self.masterDocument docjob] addJob:job forItem:self.item error:&error];
if( !success )
{
if( error )
[self presentError:error];
}
//goto bail;
}
}
}
//Force Save of PDF to Disk
[self.itemPDFCoordinator setManagedObjectContext:[item managedObjectContext]];
[self.itemPDFCoordinator saveChanges];
}
if( success )
{
[self updateChangeCount:NSChangeCleared];
if( self.isPDFEdited )
{
// !CLEARSPLIT! please do not remove this comment
[self.item setValue:nil forKeyPath:#"dict.values._splitId"];
if( ![self loadPDFForItem:item error:&error] )
goto bail;
}
}
bail:
if( error )
[self presentError:error];
if( delegate )
{
/* signature:
- (void)document:(NSDocument *)document didSave:(BOOL)didSaveSuccessfully contextInfo:(void *)contextInfo;
*/
objc_msgSend( delegate, didSaveSelector, self, (error ? NO : YES), contextInfo );
}
}
can anyone suggest me how can I get out of this issue.

Rather than calling waitUntilAllOperationsAreFinished (which blocks a thread), I'd suggest specifying an additional completion operation, which you can make dependent upon the other operations finishing:
-(void)save {
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
// whatever you want to do when it's done
}];
for (NSString* deletedPageId in self.deletedPageIdList) {
NSOperation *operation = [self.pdfCoordinator removePageWithId:deletedPageId];
[completionOperation addDependency:operation];
}
// add this completionOperation to a queue, but it won't actually start until the other operations finish
[queue addOperation:completionOperation];
}
Clearly this assumes that you change removePageWithId to return the operation it added to the queue:
- (NSOperation *)removePageWithId:(NSString*)pageId {
NRMPDFOperation *operation = [[NRMPDFOperation alloc] initWithNRMPDF:self
pageId:pageId
selector:#selector(removePageOpImpl:)
args:nil];
[self addOperation:operation];
[operation release];
return operation;
}
This way, you're not blocking any thread waiting for the operations to finish, but simply specify what to do when they do finish.

Related

NSURLSessionDownloadTask in NSOperation crashes on cancel

I'm trying to create a DownloadOperation subclass of NSOperation to download data asynchronously. Everything seemed to be working fine until I tried to add cancelling support. Basically, the completion handler of the operation's NSURLSessionDownloadTask seems to be called after the operation has been released. It will crash with EXC_BAD_ACCESS at the line weakSelf.state = kFinished.
The full sample project is here: https://github.com/angstsmurf/DownloadOperationQueue. Press Command+. after running to crash.
#import "DownloadOperation.h"
typedef enum OperationState : NSUInteger {
kReady,
kExecuting,
kFinished
} OperationState;
#interface DownloadOperation ()
#property NSURLSessionDownloadTask *task;
#property OperationState state;
#end
#implementation DownloadOperation
// default state is ready (when the operation is created)
#synthesize state = _state;
- (void)setState:(OperationState)state {
#synchronized(self) {
if (_state != state) {
[self willChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
_state = state;
[self didChangeValueForKey: #"isExecuting"];
[self didChangeValueForKey: #"isFinished"];
}
}
}
- (OperationState)state {
#synchronized (self) {
return _state;
}
}
- (BOOL)isReady { return (self.state == kReady); }
- (BOOL)isExecuting { return (self.state == kExecuting); }
- (BOOL)isFinished { return (self.state == kFinished); }
- (BOOL)isAsynchronous {
return YES;
}
- (instancetype)initWithSession:(NSURLSession *)session downloadTaskURL:(NSURL *)downloadTaskURL completionHandler:(nullable void (^)(NSURL * _Nullable, NSURLResponse * _Nullable, NSError * _Nullable))completionHandler {
self = [super init];
if (self) {
__unsafe_unretained DownloadOperation *weakSelf = self;
// use weak self to prevent retain cycle
_task = [[NSURLSession sharedSession] downloadTaskWithURL:downloadTaskURL
completionHandler:^(NSURL * _Nullable localURL, NSURLResponse * _Nullable response, NSError * _Nullable error) {
/*
if there is a custom completionHandler defined,
pass the result gotten in downloadTask's completionHandler to the
custom completionHandler
*/
if (completionHandler) {
completionHandler(localURL, response, error);
}
/*
set the operation state to finished once
the download task is completed or have error
*/
weakSelf.state = kFinished;
}];
}
return self;
}
- (void)start {
/*
if the operation or queue got cancelled even
before the operation has started, set the
operation state to finished and return
*/
if (self.cancelled) {
self.state = kFinished;
return;
}
// set the state to executing
self.state = kExecuting;
NSLog(#"downloading %#", self.task.originalRequest.URL.absoluteString);
// start the downloading
[self.task resume];
}
-(void)cancel {
[super cancel];
// cancel the downloading
[self.task cancel];
}
#end
As pointed out in the comments by Scott Thompson, the correct keyword to use for the weakSelf variable is __weak, not __unsafe_unretained.

Microsoft MSAL ObjC - Trying to acquire token Interactively for multiple scopes

I am trying to acquire token Interactively for multiple scopes, Policy and RMS scopes using Objective C. I am not sure whether I am doing it right or wrong, the way I am trying to get it as below.
I have written a method "aquireToken" where I am calling the function twice one for Policy scope and other RMS scope and updating result in NSDictionary.
There is a flag which is being updated inside the "completionBlock = ^(MSALResult *result, NSError *error)". But its value is not reflected in the caller "aquireToken" function.
The code snippet is as below:
- (void) aquireToken
{
policyTokenResult = false;
rmsTokenResult = false;
NSError *error = nil;
MSALPublicClientApplication *application = [self createPublicClientApplication:&error];
[self retrieveTokens:application forScopes:scopesPolicy isPolicy:true];
if (policyTokenResult)
{
[self retrieveTokens:application forScopes:scopesRMS isPolicy:false];
}
for (NSString* key in resultMap) {
id value = resultMap[key];
// id object = [resultDict objectForKey:key];
NSLog(#"%# = %#", key, value);
// do stuff
}
}
- (void)retrieveTokens:(MSALPublicClientApplication*) application
forScopes: (NSArray<NSString *> *) scopes
isPolicy: (BOOL) isPolicy
{
NSError *error = nil;
MSALAccount* userAccount = nil;
for (MSALAccount *account in [application allAccounts:&error])
{
if([[account.username uppercaseString] isEqualToString:[authID uppercaseString]])
{
NSLog(#"Account Found: \t%#", account.username);
userAccount = account;
break;
}
}
MSALCompletionBlock completionBlock;
__block __weak MSALCompletionBlock weakCompletionBlock;
weakCompletionBlock = completionBlock = ^(MSALResult *result, NSError *error)
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!error)
{
if (isPolicy)
{
[resultMap setObject:result.accessToken forKey:#"PolicyAccessToken"];
[resultMap setObject:result.account.username forKey:#"UserId"];
authID = result.account.username;
policyTokenResult = true;
}
else
{
[resultMap setObject:result.accessToken forKey:#"RMSAccessToken"];
rmsTokenResult = true;
}
if(policyTokenResult && rmsTokenResult)
{
[resultMap setObject:#"" forKey:#"ResultStatusSuccess"];
}
return;
}
if ([error.domain isEqualToString:MSALErrorDomain] && error.code == MSALErrorInteractionRequired)
{
[self acquireTokenInteractive:application scopes:scopes isPolicy:isPolicy completionBlock:weakCompletionBlock];
return;
}
});
};
if(userAccount)
{
[self acquireTokenSilent:application scopes:scopes forAccount:userAccount isPolicy:isPolicy completionBlock:completionBlock];
}
else
{
[self acquireTokenInteractive:application scopes:scopes isPolicy:isPolicy completionBlock:completionBlock];
}
}
- (void) acquireTokenSilent: (MSALPublicClientApplication *) application
scopes: (NSArray<NSString *> *) scopes
forAccount: (MSALAccount *) userAccount
isPolicy: (BOOL) isPolicy
completionBlock: (MSALCompletionBlock) completionBlock
{
MSALSilentTokenParameters *silentParams = [[MSALSilentTokenParameters alloc] initWithScopes:scopes account:userAccount];
[application acquireTokenSilentWithParameters:silentParams completionBlock:completionBlock];
}
- (void) acquireTokenInteractive: (MSALPublicClientApplication *) application
scopes: (NSArray<NSString *> *) scopes
isPolicy: (BOOL) isPolicy
completionBlock: (MSALCompletionBlock)completionBlock
{
MSALInteractiveTokenParameters *interactiveParams = [[MSALInteractiveTokenParameters alloc] initWithScopes:scopes];
[interactiveParams setPromptType:MSALPromptTypeSelectAccount];
interactiveParams.completionBlockQueue = dispatch_get_main_queue();
[application acquireTokenWithParameters:interactiveParams completionBlock:completionBlock];
}

CKQueryOperation working with big batch

I have problem with creating CKQuery operation with big batch of data. My query works with 100 results but after more results query fail, because one thread is bad dispatched or something (libdispatch.dylib`dispatch_group_leave:
) i am lost... any idea?
+ (void) fetchAnswersWithRecordId:(CKRecordID *)recordId completionHandler:(CloudKitCompletionHandler)handler {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"ANSrecordID == %#", recordId];
CKQuery *query = [[CKQuery alloc] initWithRecordType:ckAnswers predicate:predicate];
CKQueryOperation *operation = [[CKQueryOperation alloc] initWithQuery:query];
CKQueryOperation * __weak weakSelf = operation;
operation.resultsLimit = 300;
NSMutableArray *tmp = [[NSMutableArray alloc] init];
operation.recordFetchedBlock = ^(CKRecord *record) {
[tmp addObject:record];
};
operation.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error) {
if (!handler)
return;
NSArray *array = [NSArray arrayWithArray:tmp];
if(cursor != nil) {
CKQueryOperation *newOperation = [[CKQueryOperation alloc] initWithCursor:cursor];
newOperation.recordFetchedBlock = weakSelf.recordFetchedBlock;
newOperation.completionBlock = weakSelf.completionBlock;
[[self publicCloudDatabase] addOperation:newOperation];
} else {
NSLog(#"Results: %lu", [array count]);
dispatch_async(dispatch_get_main_queue(), ^{
handler(array, error);
});
}
};
[[self publicCloudDatabase] addOperation:operation];}
I think your issue lies with the __weak operation and the way you create an operation inside another operation. Here is an example (in swift) of how I do something similar i.e. get additional results, but in a fetch and not a query. Note the use of a instance variable to initialize first time and the use of semi-recursion through GCD dispatch_aync:
private func _fetchRecordChangesFromCloud() {
if !_fetching {
// this is the first and only time this code is called in GCD recusion
// we clean the caches we use to collect the results of the fetch
// so we can then save the record in the correct order so references can be created
_fetchedModifiedRecords = []
_fetchedDeletedRecordIDs = []
// mark fetching has started
_fetching = true
}
let operation = CKFetchRecordChangesOperation(recordZoneID: _customRecordZoneID, previousServerChangeToken: _serverChangeToken)
operation.recordChangedBlock = { (record: CKRecord?) in
if let record = record {
println("Received record to save: \(record)")
self._fetchedModifiedRecords.append(record)
}
}
operation.recordWithIDWasDeletedBlock = { (recordID: CKRecordID?) in
if let recordID = recordID {
println("Received recordID to delete: \(recordID)")
self._fetchedDeletedRecordIDs.append(recordID)
}
}
operation.fetchRecordChangesCompletionBlock = {
(serverChangeToken: CKServerChangeToken?, clientChangeToken: NSData?, error: NSError?) -> Void in
if let error = error {
println("Error in fetching record changes: \(error)")
// try again next sync
self._fetchAfterNextSuccessfullSync = true
self._fetching = false
return
}
// fetched records successfuly
println("fetched records successfuly")
if let serverChangeToken = serverChangeToken {
self._serverChangeToken = serverChangeToken
}
if operation.moreComing {
// we need to create another operation object and do it again
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
self._fetchRecordChangesFromCloud()
}
} else {
// we are finally done
// process the fetched records
self._processFetchedRecords()
// save all changes back to persistent store
self._saveBackgroundContext()
// we are done
self._fetching = false
}
}
self._privateDatabase.addOperation(operation)
}
FYI, dispatch_async() is not necessary, this is a memory management issue. The following works properly for a multi-batch fetch:
//
__block CKQueryOperation* enumerateOperationActive = nil;
//
NSPredicate* predicate = [NSPredicate predicateWithValue:TRUE];
CKQuery* query = [[[CKQuery alloc] initWithRecordType:#"myType" predicate:predicate] autorelease];
CKQueryOperation* enumerateOperation = [[[CKQueryOperation alloc] initWithQuery:query] autorelease];
// DEBUG: fetch only 1 record in order to "force" a nested CKQueryOperation cycle
enumerateOperation.resultsLimit = 1;
enumerateOperation.recordFetchedBlock = ^(CKRecord* recordFetched)
{
// ...
};
enumerateOperation.queryCompletionBlock = ^(CKQueryCursor* cursor, NSError* error)
{
if (error)
{
// ...
}
else
{
if (cursor)
{
CKQueryOperation* enumerateOperationNested = [[[CKQueryOperation alloc] initWithCursor:cursor] autorelease];
// DEBUG: fetch only 1 record in order to "force" a doubly-nested CKQueryOperation cycle
enumerateOperationNested.resultsLimit = 1;
enumerateOperationNested.recordFetchedBlock = /* re-used */ enumerateOperationActive.recordFetchedBlock;
enumerateOperationNested.queryCompletionBlock = /* re-used */ enumerateOperationActive.queryCompletionBlock;
// CRITICAL: keep track of the very last (active) operation
enumerateOperationActive = enumerateOperationNested;
[database addOperation:enumerateOperationNested];
}
}
};
//
// CRITICAL: keep track of the very last (active) operation
enumerateOperationActive = enumerateOperation;
[database addOperation:enumerateOperation];
NOTE: if you attempt to access (the original) enumerateOperation.queryCompletionBlock instead of (the very last) enumerateOperationActive.queryCompletionBlock, the operation will never complete.
I liked the recursive solution on the main thread. Below is my solution in Objective C. I'm using a class level var: _recordArray, allocated ahead of time.
- (void) readRecords_Resurs: (CKDatabase *) database query: (CKQuery *) query cursor: (CKQueryCursor *) cursor {
// Send either query or cursor
CKQueryOperation *operation;
if (query != nil) operation = [[CKQueryOperation alloc] initWithQuery: query];
else operation = [[CKQueryOperation alloc] initWithCursor: cursor];
operation.recordFetchedBlock = ^(CKRecord *record) {
[_recordArray addObject:record];
};
operation.queryCompletionBlock = ^(CKQueryCursor *cursor, NSError *error) {
if (cursor == nil || error != nil) {
// Done
dispatch_async(dispatch_get_main_queue(), ^{ [self readRecordsDone: error == nil ? nil : [error localizedDescription]]; });
}
else {
// Get next batch using cursor
dispatch_async(dispatch_get_main_queue(), ^{ [self readRecords_Resurs: database query: nil cursor: cursor]; });
}
};
[database addOperation: operation]; // start
}
- (void) readRecordsDone: (NSString *) errorMsg {
}
My solution is a category that uses two operations but both use the same blocks, and you can supply how many results per request you'd like.
#interface CKDatabase (MH)
/* Convenience method for performing a query receiving the results in batches using multiple network calls. Best use max 400 for cursorResultsLimit otherwise server sometimes exceptions telling you to use max 400. Even using CKQueryOperationMaximumResults can cause this exception. */
- (void)mh_performCursoredQuery:(CKQuery *)query cursorResultsLimit:(int)cursorResultsLimit inZoneWithID:(CKRecordZoneID *)zoneID completionHandler:(void (^)(NSArray /* CKRecord */ *results, NSError *error))completionHandler;
#end
#implementation CKDatabase(MH)
- (void)mh_performCursoredQuery:(CKQuery *)query cursorResultsLimit:(int)cursorResultsLimit inZoneWithID:(CKRecordZoneID *)zoneID completionHandler:(void (^)(NSArray /* CKRecord */ *results, NSError *error))completionHandler{
//holds all the records received.
NSMutableArray* records = [NSMutableArray array];
//this block adds records to the result array
void (^recordFetchedBlock)(CKRecord *record) = ^void(CKRecord *record) {
[records addObject:record];
};
//this is used to allow the block to call itself recurively without causing a retain cycle.
void (^queryCompletionBlock)(CKQueryCursor *cursor, NSError *error)
__block __weak typeof(queryCompletionBlock) weakQueryCompletionBlock;
weakQueryCompletionBlock = queryCompletionBlock = ^void(CKQueryCursor *cursor, NSError *error) {
//if any error at all then end with no results. Note its possible that we got some results,
// and even might have got a cursor. However if there is an error then the cursor doesn't work properly so will just return with no results.
if(error){
completionHandler(nil,error);
}
else if(cursor){
CKQueryOperation* cursorOperation = [[CKQueryOperation alloc] initWithCursor:cursor];
cursorOperation.zoneID = zoneID;
cursorOperation.resultsLimit = cursorResultsLimit;
cursorOperation.recordFetchedBlock = recordFetchedBlock;
cursorOperation.queryCompletionBlock = weakQueryCompletionBlock; // use the weak pointer to prevent retain cycle
//start the recursive operation and return.
[self addOperation:cursorOperation];
}
else{
completionHandler(records,nil);
}
};
//start the initial operation.
CKQueryOperation* queryOperation = [[CKQueryOperation alloc] initWithQuery:query];
queryOperation.zoneID = zoneID;
queryOperation.resultsLimit = cursorResultsLimit;
queryOperation.recordFetchedBlock = recordFetchedBlock;
queryOperation.queryCompletionBlock = queryCompletionBlock;
[self addOperation:queryOperation];
}
#end

NSURLSession downloadTask not releasing memory

Due to the customer can't implement just few downloads in his server in a short time and backgroundDownloadTaks were very inconsistent when there are so much files (500-1000 downloads) I decide to use NSURLDownloadTask without background NSURLSession.
It works quite well with a large amount of files, but there is an inconvenience. Memory usage is always growing untill I get a memory warning. When I get it I cancel pending tasks and free NSURLCache but memory is not released so when you resume the downloads you get the same memory warning.
I'm not using cancelWithResumeData for cancelling the tasks.
This is my code
- (void) startDownloadFiles:(NSMutableArray*)arrayFiles
{
if([[UIDevice currentDevice] isMultitaskingSupported])
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (!self.session)
{
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.HTTPMaximumConnectionsPerHost = 5;
sessionConfiguration.timeoutIntervalForRequest = 0;
sessionConfiguration.timeoutIntervalForResource = 0;
sessionConfiguration.requestCachePolicy = NSURLCacheStorageNotAllowed;
self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
//Resetting session
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
for (NSURLSessionTask *_task in downloadTasks)
{
[_task cancel];
}
[self.session resetWithCompletionHandler:^{
for (id<FFDownloadFileProtocol> file in self.selectedCatalogProducto.downloadInfo.arrayFiles)
{
if (cancel)
break; //Do not create more taks
if (![file isDownloaded])
[self startDownloadFile:file];
}
}];
}];
});
}
}
- (void) startDownloadFile:(id<FFDownloadFileProtocol>)file
{
if (![file isDownloading])
{
if ([file taskIdentifier] == -1
&& ! cancel)
{
NSURLSessionDownloadTask *task = [self.session downloadTaskWithURL:[file downloadSource]];
if (task)
{
[file setDownloadTask:task];
[file setTaskIdentifier:[file downloadTask].taskIdentifier];
[[file downloadTask] resume];
}
else
{
NSLog(#"Error creando tarea para descargar %#", [file downloadSource]);
}
}
}
}
#pragma mark - Auxiliar Methods
-(id<FFDownloadFileProtocol>)getFileDownloadInfoIndexWithTaskIdentifier:(unsigned long)taskIdentifier
{
for (id<FFDownloadFileProtocol> file in self.selectedCatalogProducto.downloadInfo.arrayFiles)
{
if (file.taskIdentifier == taskIdentifier) {
return file;
}
}
return nil;
}
#pragma mark - NSURLSessionDownloadTaskDelegate
- (void) URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
if (totalBytesExpectedToWrite == NSURLSessionTransferSizeUnknown) {
NSLog(#"Unknown transfer size");
}
else
{
// Locate the FileDownloadInfo object among all based on the taskIdentifier property of the task.
id<FFDownloadFileProtocol> file = [self getFileDownloadInfoIndexWithTaskIdentifier:downloadTask.taskIdentifier];
// Calculate the progress.
file.downloadProgress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite;
// [[NSOperationQueue mainQueue] addOperationWithBlock:^{
// NSLog("%# ; %f", [file fileName], [file downloadProgress]);
// }];
}
}
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
id<FFDownloadFileProtocol> file = [self getFileDownloadInfoIndexWithTaskIdentifier:downloadTask.taskIdentifier];
if (file)
{
NSError *error;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *destinationURL = [[NSURL fileURLWithPath:tempPath] URLByAppendingPathComponent:[file fileName]];
if ([fileManager fileExistsAtPath:[destinationURL path]]) {
NSError *delError = nil;
[fileManager removeItemAtURL:destinationURL error:nil];
if (delError)
{
NSLog(#"Error borrando archivo temporal en %#", [destinationURL path]);
}
}
BOOL success = [fileManager copyItemAtURL:location
toURL:destinationURL
error:&error];
if (success) {
// Change the flag values of the respective FileDownloadInfo object.
file.isDownloading = NO;
file.isDownloaded = YES;
// Set the initial value to the taskIdentifier property of the file object,
// so when the start button gets tapped again to start over the file download.
}
else
{
NSLog(#"Unable to copy temp file to %# Error: %#", [destinationURL path], [error localizedDescription]);
}
if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateActive)
{
indexFile++;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self numFilesDownloaded:indexFile];
}];
}
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
id<FFDownloadFileProtocol> file = [self getFileDownloadInfoIndexWithTaskIdentifier:task.taskIdentifier];
if (error != nil
&& error.code != -999)
{
//No se ha producido error o se ha cancelado la tarea bajo demanda
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(#"Download: %#. \n Downlonad completed with error: %#", [task.response.URL absoluteString], [error localizedDescription]);
if (!cancel)
{
NSString *alertBody = #"Se ha producido un error en la descarga, por favor reanúdela manualmente";
if ([error.domain isEqualToString:#"NSPOSIXErrorDomain"] && (error.code == 1) )
{
alertBody = #"Se ha interrumpido la descarga debido a que su iPad está bloqueado por código. Por favor reanude la descarga manualmente y evite que el iPad se bloquee";
}
// Show a local notification when all downloads are over.
UILocalNotification *localNotification = [[UILocalNotification alloc] init];
localNotification.alertBody = alertBody;
[[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];
[self errorDownloading:error.localizedDescription];
}
}];
}
else if (file)
{
NSLog(#"%# download finished successfully.", [[file downloadSource] absoluteString]);
file.taskIdentifier = -1;
// In case there is any resume data stored in the file object, just make it nil.
file.taskResumeData = nil;
file.downloadTask = nil;
}
else if (cancel)
{
NSLog(#"Tarea cancelada");
}
if (self.selectedCatalogProducto.downloadInfo.arrayFiles.count == indexFile
&& !cancel)
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
if (!complete)
{
complete = YES;
[self downloadComplete];
}
}];
}
task = nil;
}
#pragma mark - Memory warning
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
if (_isDownloading)
{
[self storeCatalogProductInfo:self.selectedCatalogProducto andDownloadInfo:YES];
[self stopDownloading];
}
[[NSURLCache sharedURLCache] removeAllCachedResponses];
[self.session.configuration.URLCache removeAllCachedResponses];
}
And these are two snapshot of memory usage
Memory usage increasing when files are downloading
Download tasks are stopped but memory is not released
Why can't I release memory?
Thanks for any help provided
You need to call the invalidateAndCancel method on your NSURLSession instance when you're done using it, otherwise it will leak memory.

Objective-C crash with zombie object

I've made an implementation for an JSON-RPC (a little bit modified) Server/Client in objective-c with the GCDAsyncSocket library. but the app crashes on responding to an request. without debugging for zombies i'm getting this error:
JSONRPCTestServer(1301,0x7fff7f887960) malloc: *** error for object 0x10014db10: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
the screenshot in xcode shows the error is in the completeCurrentRead method of the GCDAsyncSocket library.
when debugging for zombies it logs this:
2013-02-04 14:36:16.430 JSONRPCTestServer[1367:603] *** -[__NSArrayI release]: message sent to deallocated instance 0x1005b6fd0
and instruments shows this:
as this happens when a response to the rpc-call is of type nsarray i'd guess its this array that is causing the error. the rpc-method is:
-(NSArray *)testArray:(GCDAsyncSocket *)sock {
return [NSArray arrayWithObjects:#"test1",#"test2", nil];
}
The JSON-RPC header is:
#import <Foundation/Foundation.h>
#import "JSONRPCMethod.h"
#import "JSONRPCResponse.h"
#import "JSONRPCError.h"
#import "JSONRPCArgument.h"
#import "JSONRPCRequest.h"
#import "GCDAsyncSocket.h"
#class GCDAsyncSocket;
#protocol JSONRPCResponseDelegate <NSObject>
#optional
-(void)rpcSocket:(GCDAsyncSocket*)sock returnedValue:(id)retVal forMethod:(NSString*)m id:(id)i;
-(void)rpcSocket:(GCDAsyncSocket*)sock returnedError:(JSONRPCError*)err forMethod:(NSString*)m id:(id)i;
-(void)rpcReturnedValue:(id)retVal forMethod:(NSString*)m id:(id)i;
-(void)rpcReturnedError:(JSONRPCError*)err forMethod:(NSString*)m id:(id)i;
#end
#interface JSONRPC : NSObject {
NSMutableArray *supportedMethods;
GCDAsyncSocket *mainSocket;
NSMutableArray *connectedSockets;
NSMutableArray *responseDelegates;
BOOL isServer;
}
+(JSONRPC*)sharedConnection;
-(BOOL)startServer:(NSUInteger)port;
-(BOOL)connectToServer:(NSString*)host port:(NSUInteger)port;
-(BOOL)addMethod:(JSONRPCMethod*)method;
-(void)removeMethod:(JSONRPCMethod*)method;
-(void)removeMethodsWithTarget:(id)target;
-(void)sendRequest:(JSONRPCRequest*)req toSocket:(GCDAsyncSocket*)sock;
-(void)sendRequest:(JSONRPCRequest*)req;
-(void)sendNotification:(JSONRPCRequest*)req toSocket:(GCDAsyncSocket*)sock;
-(void)sendNotification:(JSONRPCRequest*)req;
-(void)sendNotification:(JSONRPCRequest*)req toSocketsWithUserData:(id)userData;
#end
.m is:
#import "JSONRPC.h"
#import "GCDAsyncSocket.h"
#define kGeneralReadTimeout -1.0
#define kGeneralWriteTimeout -1.0
#implementation JSONRPC
- (id)init
{
self = [super init];
if (self) {
isServer = NO;
supportedMethods = [[NSMutableArray alloc] init];
mainSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
connectedSockets = [[NSMutableArray alloc] init];
responseDelegates = [[NSMutableArray alloc] init];
}
return self;
}
+ (JSONRPC *)sharedConnection {
static JSONRPC *sharedSingleton;
#synchronized(self)
{
if (!sharedSingleton)
sharedSingleton = [[JSONRPC alloc] init];
return sharedSingleton;
}
}
-(BOOL)startServer:(NSUInteger)port {
// Now we tell the socket to accept incoming connections.
// We don't care what port it listens on, so we pass zero for the port number.
// This allows the operating system to automatically assign us an available port.
isServer = YES;
NSError *err = nil;
if ([mainSocket acceptOnPort:port error:&err]) {
} else {
DDLogError(#"Error while starting JSON-RPC Server: %#",err);
return NO;
}
DDLogInfo(#"Started JSON-RPC Server on port %hu",[mainSocket localPort]);
return YES;
}
-(BOOL)connectToServer:(NSString *)host port:(NSUInteger)port {
NSError *err = nil;
mainSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];
[mainSocket connectToHost:host onPort:port error:&err];
if(err != nil) {
DDLogError(#"Couldn't connect to host %#:%lu (Error: %#)",host,port,err);
return NO;
}
return YES;
}
-(BOOL)addMethod:(JSONRPCMethod *)method {
for (JSONRPCMethod *meth in supportedMethods) {
if([meth.name isEqualToString:method.name]) {
return NO;
}
}
[supportedMethods addObject:method];
return YES;
}
-(void)removeMethod:(JSONRPCMethod *)method {
[supportedMethods removeObject:method];
}
-(void)removeMethodsWithTarget:(id)target {
NSMutableArray *toRemove = [[NSMutableArray alloc] init];
for (JSONRPCMethod *meth in supportedMethods) {
if(meth.target == target) {
[toRemove addObject:meth];
}
}
[supportedMethods removeObjectsInArray:toRemove];
}
-(void)sendRequest:(JSONRPCRequest *)req toSocket:(GCDAsyncSocket*)sock {
[responseDelegates addObject:req];
[req setIdentifier:[NSNumber numberWithUnsignedInteger:[responseDelegates count]-1]];
[self sendPackage:[req dictionary] toSocket:sock];
}
-(void)sendRequest:(JSONRPCRequest *)req {
[self sendRequest:req toSocket:mainSocket];
}
-(void)sendNotification:(JSONRPCRequest *)req toSocket:(GCDAsyncSocket*)sock{
[req setIdentifier:nil];
[self sendPackage:[req dictionary] toSocket:sock];
}
-(void)sendNotification:(JSONRPCRequest *)req {
[self sendNotification:req toSocket:mainSocket];
}
-(void)sendNotification:(JSONRPCRequest *)req toSocketsWithUserData:(id)userData {
NSMutableArray *matchingSockets = [[NSMutableArray alloc] init];
for (GCDAsyncSocket*sock in connectedSockets) {
if(sock.userData == userData) {
[matchingSockets addObject:sock];
}
}
if(matchingSockets.count == 0)
return;
[req setIdentifier:nil];
NSData *pkgData = [self writableDataFromDictionary:[req dictionary]];
for (GCDAsyncSocket*sock in matchingSockets) {
[sock writeData:pkgData withTimeout:kGeneralWriteTimeout tag:0];
}
}
#pragma mark Socket Delegate
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket {
[connectedSockets addObject:newSocket];
[newSocket readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
DDLogVerbose(#"socket:didConnectToHost:%# port:%hu", host, port);
[sock readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
DDLogVerbose(#"socketDidDisconnect:%#", err);
if(isServer)
[connectedSockets removeObject:sock];
}
-(void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
// So, we've received something from the client
// As we have to cut out the last 0x00 for JSONSerialization it has to be longer than 1 byte
if(data.length > 1) {
// Shorten out that 0x00
data = [data subdataWithRange:NSMakeRange(0, data.length-1)];
// Try to serialize
NSError *err;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&err];
DDLogVerbose(#"Dict: %#",dict);
if(err != nil) {
// The package isn't json
JSONRPCResponse *response = [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest]];
[self sendPackage:[response dictionary] toSocket:sock];
} else {
JSONRPCResponse *response = [self handleDictionary:dict fromSocket:sock];
if(response != nil) {
[self sendPackage:[response dictionary] toSocket:sock];
}
}
}
[sock readDataToData:[GCDAsyncSocket ZeroData] withTimeout:kGeneralReadTimeout tag:0];
}
-(JSONRPCResponse*)handleDictionary:(NSDictionary*)dict fromSocket:(GCDAsyncSocket*)sock {
// Check if the "id" is of a correct value/type
id identifier = [dict valueForKey:#"id"];
if(!(identifier == nil || [identifier isKindOfClass:[NSNumber class]])) {
return [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest]];
}
// Handle the package
NSString *methodName = [dict valueForKey:#"method"];
id errorValue = [dict valueForKey:#"error"];
id resultValue = [dict valueForKey:#"result"];
if([methodName isKindOfClass:[NSString class]]) {
// We have a string as method
DDLogInfo(#"Method: %#, object: %#",methodName,[dict valueForKey:#"params"]);
for (JSONRPCMethod *method in supportedMethods) {
if([method.name isEqualToString:methodName]) {
id result = nil;
if(isServer == YES) {
// It is a server and the method needs to know from where the call comes
result = [method invoke:[dict valueForKey:#"params"] fromSocket:sock];
} else {
// It is a client and we don't need to know where the call is from. it can only be the server.
result = [method invoke:[dict valueForKey:#"params"]];
}
if([result isKindOfClass:[JSONRPCError class]]) {
return [JSONRPCResponse responseWithError:result id:identifier];
} else if(result != nil) {
return [JSONRPCResponse responseWithResult:result id:identifier];
} else {
return nil;
}
}
}
} else if(resultValue != nil) {
// We have a response from our partner
//DDLogInfo(#"Result: %#",resultValue);
NSUInteger responseDelegateId = [identifier unsignedIntegerValue];
if(responseDelegateId < [responseDelegates count]) {
JSONRPCRequest *originalRequest = [responseDelegates objectAtIndex:responseDelegateId];
if(originalRequest.sender == nil) {
return nil;
}
#try {
SEL selector;
if(isServer) {
selector = #selector(rpcSocket:returnedValue:forMethod:id:);
} else {
selector = #selector(rpcReturnedValue:forMethod:id:);
}
NSMethodSignature *signature = [originalRequest.sender methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:originalRequest.sender];
[invocation setSelector:selector];
NSUInteger startArg = 2;
if(isServer) {
[invocation setArgument:&sock atIndex:startArg];
startArg++;
}
NSString *method = [originalRequest method];
id orgId = [originalRequest identifier];
[invocation setArgument:&resultValue atIndex:startArg];
[invocation setArgument:&method atIndex:startArg+1];
[invocation setArgument:&orgId atIndex:startArg+2];
[invocation invoke];
}
#catch (NSException *exception) {
DDLogWarn(#"Couldn't find a response: %#",exception);
}
}
} else if(errorValue != nil) {
// We have a string as method
DDLogInfo(#"Error: %#",errorValue);
} else {
return [JSONRPCResponse responseWithError:[JSONRPCError invalidRequest] id:identifier];
}
return nil;
}
-(void)sendPackage:(NSDictionary *)dict toSocket:(GCDAsyncSocket *)sock {
NSData *answerData = [self writableDataFromDictionary:dict];
[sock writeData:answerData withTimeout:kGeneralWriteTimeout tag:0];
}
-(NSData*)writableDataFromDictionary:(NSDictionary*)dict {
NSMutableData *answerData = [[NSMutableData alloc] init];
// Serialize the answer
NSError *err = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict options:0 error:&err];
if(err != nil) {
// Log
DDLogError(#"JSON-RPC had an internal error while converting the answer to JSON. The answer-dictionary is: %#",dict);
// Form answer manually
jsonData = [NSData dataWithBytes:"{\"error\":{\"code\":-32700,\"message\":\"Parse error\"}}"
length:49];
}
// Format the answer
[answerData appendData:jsonData];
[answerData appendData:[GCDAsyncSocket ZeroData]];
return answerData;
}
i just don't know how to fix this. why do i keep getting an error with an nsarray but when i return a nsnumber it works? how can i fix this?