I have some special functions in my project, they will execute cross many threads, such as childContext perform block or AFNetwork response block:
(void)my_function {
dispatch_async(dispatch_get_main_queue(), ^{
NSManagedObjectContext *childContext = [[[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType] autorelease];
childContext.parentContext = self.managedObjectContext;
[childContext performBlock:^{
[self.operationManager POST:URL parameters:nil block:^(AFHTTPRequestOperation *operation, id responseObject) {
//Do something
[childContext performBlock:^{
//Do something
}];
}];
}];
});
}
Now I want to execute them one by one (including all blocks in function). After reading a couple of other answers, some of the Apple documentation, I get some answers:
1. NSRecursiveLock
I can add NSRecursiveLock for each function, but the issue is that I can't lock/unlock cross thread.
(void)my_function {
[MyLock lock]; // Lock here
dispatch_async(dispatch_get_main_queue(), ^{
NSManagedObjectContext *childContext = [[[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType] autorelease];
childContext.parentContext = self.managedObjectContext;
[childContext performBlock:^{
[self.operationManager POST:URL parameters:nil block:^(AFHTTPRequestOperation *operation, id responseObject) {
//Do something
[childContext performBlock:^{
//Do something
[MyLock unlock]; //Unlock here
}];
}];
}];
});
}
2. NSOperation
I can add each function to NSOperationQueue as a NSOperation, and set concurrent operation number to 1, but the issue is that I can't make sure the code in block has been executed even the NSOperation finishes successfully.
3. #synchronized
I can add #synchronized, but has same issue as NSOperation
My suggestion is using NSOperationQueue and Asynchronous NSOperation.
You can make sure operation execute one by one in two ways
addDependency:
Set maxConcurrentOperationCount to 1
With Asynchronous NSOperation,it is you to decide when this Operation is done.
For example
-(void)my_function {
dispatch_async(dispatch_get_main_queue(), ^{
NSManagedObjectContext *childContext = [[[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType] autorelease];
childContext.parentContext = self.managedObjectContext;
[childContext performBlock:^{
[self.operationManager POST:URL parameters:nil block:^(AFHTTPRequestOperation *operation, id responseObject) {
//Do something
[childContext performBlock:^{
//Do something
//Only mark this NSOperation is done when all task of this function is finished.
}];
}];
}];
});
}
How to make an Asynchronous NSOperation,This is what I usually do,you can also refer it in the document.
Keep two property
#interface AsyncOperation (){
BOOL finished;
BOOL executing;
}
Use these two property to manage Operation state
-(BOOL)isFinished{
return finished;
}
-(BOOL)isExecuting{
return executing;
}
3.Mark Operation start
Note:start function is called on the thread the Operation created
-(void)start{
if ([self isCancelled])
{
// Must move the operation to the finished state if it is canceled.
[self willChangeValueForKey:#"isFinished"];
finished = NO;
[self didChangeValueForKey:#"isFinished"];
return;
}
// If the operation is not canceled, begin executing the task.
[self willChangeValueForKey:#"isExecuting"];
[NSThread detachNewThreadSelector:#selector(main) toTarget:self withObject:nil];
executing = YES;
[self didChangeValueForKey:#"isExecuting"];
}
Mark Operation is done
I use this function
-(void)setOperationFinished{
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
executing = NO;
finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
Related
I am using XCTestExpectation in a lot of tests and sometimes (very randomly) some expectations are not fulfilled (although I am sure they should be).
While investigating this problem I have noticed that some expectations are fulfilled in a main thread and some are fulfilled in a background thread. And so far these problems are with the ones fulfilled in a background thread.
Is it safe to fulfill expectations from a background thread? I could not find any explicit information about that.
Below is an example of how I use XCTestExpectation:
__block XCTestExpectation *expectation = [self expectationWithDescription:#"test"];
[self doSomethingAsyncInBackgroundWithSuccess:^{
[expectation fullfill];
}];
[self waitForExpectationsWithTimeout:10.0 handler:^(NSError *error) {
expectation = nil;
if (error) {
NSLog(#"Timeout Error: %#", error);
}
}];
It's not documented anywhere that XCTestExpectation is thread-safe. due to there being no official documentation on the matter you can only guess by creating test examples:
- (void)testExpectationMainThread;
{
__block XCTestExpectation *expectation = [self expectationWithDescription:#"test"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
- (void)testExpectationStartMainThreadFulfilBackgroundThread;
{
__block XCTestExpectation *expectation = [self expectationWithDescription:#"test"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, kNilOptions), ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
- (void)testExpectationBackgroundThread;
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, kNilOptions);
__block XCTestExpectation *expectation;
dispatch_sync(queue, ^{
expectation = [self expectationWithDescription:#"test"];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
[expectation fulfill];
});
[self waitForExpectationsWithTimeout:2 handler:^(NSError * _Nullable error) {
NSLog(#"%#", error);
}];
}
Here it does not crash or cause a problem however due to the lack of official documentation it is probably safer to stick to the same queue to fulfil.
you should really be stubbing the method doSomethingAsyncInBackgroundWithSuccess and provide the app with local "dummy" data.
Your unit tests should not rely on network as it is something which is variable.
You should be executing the completion block of doSomethingAsyncInBackgroundWithSuccess on the main thread (or at least provide a way to call back consistently on the same thread), you can easily do this with GCD.
- (void)doSomethingAsyncInBackgroundWithSuccess:(void (^)(void))completion;
{
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
or use NSOperationQueue mainQueue
- (void)doSomethingAsyncInBackgroundWithSuccess:(void (^)(void))completion;
{
[NSOperationQueue.mainQueue addOperationWithBlock:^{
completion();
}];
}
I need to complete following tasks in order and update UI after all the task are completed.
Since data is depended on each other I have created NSOperationQueue *myQueue but one of the tasks taking so long to complete meanwhile last task is not waiting for previous task to finish and just updating the UI
Task order should be
1.Show Loading hub
2.Parse html get ids
3.Download Json with the parsed ids
4.populate database with json
4.1 This part also has a lot of background work populating database takes about 5-6 seconds.
5.Hide loading hub
So 5 is executed before 4 and 4.1 finishes....
basically I want this Serial but this is happening I guess Concurrent
I have also tried 'dispatch_sync' no luck
-(void)viewDidAppear:(BOOL)animated
{
//show hud
[self performSelectorOnMainThread:#selector(showLoadingHud) withObject:self waitUntilDone:YES];
[self startProcess];
}
-(void)startProcess
{
NSOperationQueue *myQueue = [NSOperationQueue mainQueue];
[myQueue setMaxConcurrentOperationCount:1];
__block NSMutableArray *imdbIDs=[[NSMutableArray alloc] init];
NSMutableArray *urlList=[[NSMutableArray alloc] initWithArray:[ImdbURLs getImdburlArray]];
NSBlockOperation *getiImdbViaHtmlParser = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakHtmlParser = getiImdbViaHtmlParser;
[weakHtmlParser addExecutionBlock:^{
//first
for (NSString *imdbUrl in urlList) {
...
}];
NSBlockOperation *getJsonFromIMDB = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakGetJsonFromIMDB = getJsonFromIMDB ;
[weakGetJsonFromIMDB addExecutionBlock:^{
//second
for (NSString *imdbStrings in imdbIDs) {
//this also calls database methods and populates database with safe threads
[self AFReqTest:imdbStrings];//I guess this causing the problem
}
}];
NSBlockOperation *checkResult = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakCheckResult = checkResult ;
[weakCheckResult addExecutionBlock:^{
//dismiss hud
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self dismissLoadingHud];//this is getting called before getJsonFromIMDB block finishes
}];
}];
[checkResult addDependency:getJsonFromIMDB];
[getJsonFromIMDB addDependency:getiImdbViaHtmlParser];
[myQueue addOperation:getJsonFromIMDB];
[myQueue addOperation:getiImdbViaHtmlParser];
[myQueue addOperation:checkResult];
}
-(void)AFReqTest:(NSString *)idString
{
...//buch of code
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//here I call another background method with result
[self insertIntoDatabase:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
}
-(void)insertIntoDatabase:(NSDictionary *)responseObject
{
ParseImdbResponse *parse=[[ParseImdbResponse alloc] initWithResponseDic:responseObject];
PopulateDatabase *inserInstance=[[PopulateDatabase alloc] initWithResponseDic:parse];
dispatch_queue_t insertqueue= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_sync(insertqueue, ^{
[inserInstance inserIntoMoviesTable];
});
dispatch_sync(insertqueue, ^{
[inserInstance inserIntoGenresTable];
});
dispatch_sync(insertqueue, ^{
[inserInstance inserIntoActorsTable];
});
dispatch_sync(insertqueue, ^{
[inserInstance inserIntoDirectorsTable];
});
dispatch_sync(insertqueue, ^{
[inserInstance inserIntoMovieGenresTable];
});
dispatch_sync(insertqueue, ^{
[inserInstance inserIntoMovieCastTable];
});
}
Sorry for the long code I am not sure problem is my code structure or am I missing something?
I'm trying to fetch JSON data from 5 different URLs. The network requests can be performed in parallel, though the responses have to be processed in a certain order. In addition, I also want to have a single point of error handling logic.
The code I'm having right now is like the following. The problem is, only the subscription of signalFive and signalSix has been invoked. The subscribeNext block for all the other signals has never been invoked. I suspect the problem is because the subscription happens after the sendNext occurs.
Is there a better/standard way to perform this kind of request?
- (RACSubject *)signalForFetchingFromRemotePath:(NSString *)remotePath
{
RACSubject *signal = [RACSubject subject];
[self.requestOperationManager GET:remotePath parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
for (id obj in responseObject) {
[signal sendNext:obj];
}
[signal sendCompleted];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[signal sendError:error];
}];
return signal;
}
FMDatabase *db = [SomeDatabase defaultDatabase];
[db beginTransaction];
RACSubject *singalOne = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_ONE_PATH]];
RACSubject *singalTwo = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_TWO_PATH]];
RACSubject *singalThree = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_THREE_PATH]];
RACSubject *singalFour = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_FOUR_PATH]];
RACSubject *singalFive = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_FIVE_PATH]];
RACSubject *singalSix = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_SIX_PATH]];
RACSignal *combined = [RACSignal merge:#[singalOne, singalTwo, singalThree, singalFour, singalFive, singalSix]];
[combined subscribeError:^(NSError *error){
[db rollback];
}];
[singalFive subscribeNext:^(NSDictionary *dict) {
[ClassE save:dict];
} completed:^{
[singalSix subscribeNext:^(NSDictionary *dict) {
[ClassF save:dict];
} completed:^{
[singalOne subscribeNext:^(NSDictionary *dict){
[ClassA save:dict];
} completed:^{
[singalTwo subscribeNext:^(NSDictionary *dict){
[ClassB save:dict];
} completed:^{
[singalThree subscribeNext:^(NSDictionary *dict) {
[ClassC save:dict];
} completed:^{
[singalFour subscribeNext:^(NSDictionary *dict){
[ClassD save:dict];
} completed:^{
NSLog(#"Completed");
[db commit];
}];
}];
}];
}];
}];
}];
If you need to enforce a specific order, use +concat: instead of +merge:.
On its own, concatenation means that the requests will not be performed in parallel. If you want to recover that behavior, you can use -replay on each signal (to start it immediately) before passing it to +concat:.
As an aside, nested subscriptions are almost always an anti-pattern. There's usually a built-in operator to do what you want instead.
I usually use combineLatest:
NSArray *signals = #[singalOne, singalTwo, singalThree, singalFour, singalFive, singalSix];
[[RACSignal combineLatest:signals] subscribeNext:^(RACTuple *values) {
// All your values are here
} error:^(NSError *error) {
// error
}];
I have a singleton class that saves JSON objects to a CoreData database in the background using GCD (Grand Central Dispatch) queues. This works perfectly the majority of the time, but on iPad 2 and iPad Mini devices I am experiencing some issues with the process freezing.
My setup is pretty straight forward. I have a background dispatch queue (backgroundQueue) that is set to run serially, and I have a seperate instance of NSManagedObjectContext for the background queue. When I want to save something to the database I call the method that begins the save process, and In that method I use dispatch_async to invoke my save logic on the background thread.
Once all processing logic has ran I save the background MOC, and use NSManagedObjectContextDidSaveNotification to merge the changes from the background MOC to the main thread MOC. The background thread waits for this to complete, and once it's done it'll run the next block in the queue, if any. Below is my code.
dispatch_queue_t backgroundQueue;
- (id)init
{
if (self = [super init])
{
// init the background queue
backgroundQueue = dispatch_queue_create(VS_CORE_DATA_MANAGER_BACKGROUND_QUEUE_NAME, DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
[VS_Log logDebug:#"**** Queue Save JSON Objects for Class: %#", managedObjectClass];
// process in the background queue
dispatch_async(backgroundQueue, ^(void)
{
[VS_Log logDebug:#"**** Dispatch Save JSON Objects for Class: %#", managedObjectClass];
// create a new process object and add it to the dictionary
currentRequest = [[VS_CoreDataRequest alloc] init];
// set the thead name
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
// if there is not already a background context, then create one
if (!_backgroundQueueManagedObjectContext)
{
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the background queue
_backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
[_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundQueueManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundQueueManagedObjectContext.undoManager = nil;
// listen for the merge changes from context did save notification on the background queue
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
}
}
// save the JSON dictionary
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
// save the objects so we can access them later to be re-fetched and returned on the main thread
if (objects.count > 0)
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
if (completion)
currentRequest.completionBlock = completion;
[VS_Log logDebug:#"**** Save MOC for Class: %#", managedObjectClass];
// save all changes object context
[self saveManagedObjectContext];
});
}
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
[VS_Log logDebug:#"**** Merge Changes From Background"];
// save the current request to a local var since we're about to be processing it on the main thread
__block VS_CoreDataRequest *request = (VS_CoreDataRequest *)[currentRequest copy];
__block NSNotification *bNotification = (NSNotification *)[notification copy];
// clear out the request
currentRequest = nil;
dispatch_sync(dispatch_get_main_queue(), ^(void)
{
[VS_Log logDebug:#"**** Start Merge Changes On Main Thread"];
// merge changes to the primary context, and wait for the action to complete on the main thread
[_managedObjectContext mergeChangesFromContextDidSaveNotification:bNotification];
NSMutableArray *objects = [[NSMutableArray alloc] init];
// iterate through the updated objects and find them in the main thread MOC
for (NSManagedObject *object in request.objects)
{
NSError *error;
NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
if (error)
[self logError:error];
if (obj)
[objects addObject:obj];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
[VS_Log logDebug:#"**** Complete Merge Changes On Main Thread"];
// clear the request
request = nil;
});
[VS_Log logDebug:#"**** Complete Merge Changes From Background"];
}
When the issue occurs everything seems to run fine, and it gets into the mergeChangesFromBackground: method, but the dispatch_async() is not invoked.
As I mentioned above the code runs perfectly in most cases. Only time it has issues running is when I am testing on either an iPad 2 or iPad Mini and saving large objects.
Does anyone out there have any ideas why this is happening?
Thanks
** EDIT **
Since writing this I've learned of nested NSManagedObjectContexts. I have modified my code to use it and it seems to be working well. However, I still have the same issues. Thoughts? Also, any comments on nested MOCs?
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
// process in the background queue
dispatch_async(backgroundQueue, ^(void)
{
// create a new process object and add it to the dictionary
__block VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
// set the thead name
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
// if there is not already a background context, then create one
if (!_backgroundQueueManagedObjectContext)
{
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the background queue
_backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundQueueManagedObjectContext setParentContext:[self managedObjectContext]];
[_backgroundQueueManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundQueueManagedObjectContext.undoManager = nil;
}
}
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
// if no objects were processed, then return with an error
if (!objects || objects.count == 0)
{
currentRequest = nil;
// dispatch the completion block
dispatch_async(dispatch_get_main_queue(), ^(void)
{
NSError *error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self logMessage:error.debugDescription];
completion(nil, error);
});
}
// save the objects so we can access them later to be re-fetched and returned on the main thread
if (objects.count > 0)
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
if (completion)
currentRequest.completionBlock = completion;
// save all changes object context
NSError *error = nil;
[_backgroundQueueManagedObjectContext save:&error];
if (error)
[VS_Log logError:error.localizedDescription];
dispatch_sync(dispatch_get_main_queue(), ^(void)
{
NSMutableArray *objects = [[NSMutableArray alloc] init];
// iterate through the updated objects and find them in the main thread MOC
for (NSManagedObject *object in currentRequest.objects)
{
NSError *error;
NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
if (error)
[self logError:error];
if (obj)
[objects addObject:obj];
}
// call the completion block
if (currentRequest.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = currentRequest.completionBlock;
saveCompletionBlock(objects, nil);
}
});
// clear out the request
currentRequest = nil;
});
}
** EDIT **
Hi Everyone,
I'd first like to thank everyone for this input on this, as that is what lead me to finding the solution for my problem. Long story short I did away completely with using the GCD and the custom background queue, and instead am now using nested contexts and the "performBlock" method to perform all saving on the background thread. This is working great and I elevated my hang up issue.
However, I now have a new bug. Whenever I run my app for the first time, whenever I try to save an object that has child object relations, I get the following exception.
-_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!
Below is my new code.
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
[_backgroundManagedObjectContext performBlock:^(void)
{
// create a new process object and add it to the dictionary
VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
currentRequest.managedObjectClass = managedObjectClass;
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];
if (objects.count == 0)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
}
else
{
// save the objects so we can access them later to be re-fetched and returned on the main thread
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
currentRequest.completionBlock = completion;
[_backgroundManagedObjectContext lock];
#try
{
[_backgroundManagedObjectContext processPendingChanges];
[_backgroundManagedObjectContext save:nil];
}
#catch (NSException *exception)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
}
[_backgroundManagedObjectContext unlock];
// complete the process on the main thread
if (currentRequest.error)
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
else
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];
// clear out the request
currentRequest = nil;
}
}];
}
- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
if (request.error)
[self logError:request.error];
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(nil, request.error);
}
}
- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
// get objects from main thread
NSArray *objects = nil;
if (request.objects.count > 0)
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:#"%#", request.managedObjectClass]];
fetchReq.predicate = [NSPredicate predicateWithFormat:#"self IN %#", request.objects];
objects = [self executeFetchRequest:fetchReq];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the MOC for the backgroumd thread
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundManagedObjectContext.undoManager = nil;
// create the MOC for the main thread
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_backgroundManagedObjectContext];
[_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
}
return _managedObjectContext;
}
Do any of you have any idea why this crash is happening?
You might want to consider this kind of context architecture (untested code):
The mainManagedObjectContext will be initialised in the same manner but with NSMainQueueConcurrencyType and no observer (by the way, remember to remove your observer)
- (void) mergeChangesFromBackground:(NSNotification*)notification
{
__block __weak NSManagedObjectContext* context = self.managedObjectContext;
[context performBlockAndWait:^{
[context mergeChangesFromContextDidSaveNotification:notification];
}];
}
- (NSManagedObjectContext*) bgContext
{
if (_bgContext) {
return _bgContext;
}
NSPersistentStoreCoordinator* coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_bgContext setPersistentStoreCoordinator:coordinator];
[_bgContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[_bgContext setUndoManager:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChangesFromBackground:)
name:NSManagedObjectContextDidSaveNotification
object:_bgContext];
return _bgContext;
}
- (void) doSomethingOnBGContextWithoutBlocking:(void(^)(NSManagedObjectContext*))contextBlock
{
__block __weak NSManagedObjectContext* context = [self bgContext];
[context performBlock:^{
NSThread *currentThread = [NSThread currentThread];
NSString* prevName = [currentThread name];
[currentThread setName:#"BGContextThread"];
contextBlock(context);
[context reset];
[currentThread setName:prevName];
}];
}
I'd first like to thank everyone for this input on this, as that is what lead me to finding the solution for my problem. Long story short I did away completely with using the GCD and the custom background queue, and instead am now using nested contexts and the "performBlock" method to perform all saving on the background thread. This is working great and I elevated my hang up issue.
However, I now have a new bug. Whenever I run my app for the first time, whenever I try to save an object that has child object relations, I get the following exception.
-_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!
Below is my new code.
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
[_backgroundManagedObjectContext performBlock:^(void)
{
// create a new process object and add it to the dictionary
VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
currentRequest.managedObjectClass = managedObjectClass;
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];
if (objects.count == 0)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
}
else
{
// save the objects so we can access them later to be re-fetched and returned on the main thread
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
currentRequest.completionBlock = completion;
[_backgroundManagedObjectContext lock];
#try
{
[_backgroundManagedObjectContext processPendingChanges];
[_backgroundManagedObjectContext save:nil];
}
#catch (NSException *exception)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
}
[_backgroundManagedObjectContext unlock];
// complete the process on the main thread
if (currentRequest.error)
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
else
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];
// clear out the request
currentRequest = nil;
}
}];
}
- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
if (request.error)
[self logError:request.error];
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(nil, request.error);
}
}
- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
// get objects from main thread
NSArray *objects = nil;
if (request.objects.count > 0)
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:#"%#", request.managedObjectClass]];
fetchReq.predicate = [NSPredicate predicateWithFormat:#"self IN %#", request.objects];
objects = [self executeFetchRequest:fetchReq];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the MOC for the backgroumd thread
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundManagedObjectContext.undoManager = nil;
// create the MOC for the main thread
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_backgroundManagedObjectContext];
[_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
}
return _managedObjectContext;
}
Do any of you have any idea why this crash is happening?
** EDIT **
Ladies and gents I have found the solution. Apparently, even when using nested contexts you still have to merge the changes via the NSManagedObjectContextDidSaveNotification. Once I added that into my code it all started to work perfectly. Below is my working code. Thanks a million guys!
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the MOC for the backgroumd thread
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundManagedObjectContext.undoManager = nil;
// create the MOC for the main thread
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_backgroundManagedObjectContext];
[_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChangesFromContextDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:_backgroundManagedObjectContext];
}
return _managedObjectContext;
}
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
// perform save on background thread
[_backgroundManagedObjectContext performBlock:^(void)
{
// create a new process object and add it to the dictionary
VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
currentRequest.managedObjectClass = managedObjectClass;
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];
if (objects.count == 0)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
}
else
{
// save the objects so we can access them later to be re-fetched and returned on the main thread
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
currentRequest.completionBlock = completion;
[_backgroundManagedObjectContext lock];
#try
{
[_backgroundManagedObjectContext processPendingChanges];
[_backgroundManagedObjectContext save:nil];
}
#catch (NSException *exception)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
}
[_backgroundManagedObjectContext unlock];
// complete the process on the main thread
if (currentRequest.error)
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
else
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];
// clear out the request
currentRequest = nil;
}
}];
}
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
[_managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
if (request.error)
[self logError:request.error];
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(nil, request.error);
}
}
- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
// get objects from main thread
NSArray *objects = nil;
if (request.objects.count > 0)
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:#"%#", request.managedObjectClass]];
fetchReq.predicate = [NSPredicate predicateWithFormat:#"self IN %#", request.objects];
objects = [self executeFetchRequest:fetchReq];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
}
I am facing some weird results with addOperationWithBlock.
My function looks something like this:
-(void) myFunction{
NSLog(#"VISITED");
..
for (NSDictionary *Obj in myObjects)
{
[operationQueue addOperationWithBlock:^(void){
MyObject* tmp = [self tediousFunction:Obj];
// threadTempObjects is member NSMutableArray
[self.threadTempObjects addObject:tmp];
NSLog(#"ADDED");
}];
}
[operationQueue addOperationWithBlock:^(void){
[self.myArray addObjectsFromArray:self.threadTempObjects];
for(myObject *myObj in self.myArray)
{
// MAIN_QUEUE
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
[self updateUI:myObj];
}];
}
}];
[operationQueue addOperationWithBlock:^(void){
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
[self filterResults];
}];
}];
}
My dictionary contains 4 values, and therefore the ADDED shows in the log 4 times.
BUT,
when I check inside the filterResults, I see that there are only 2 objects inside myArray. Meaning that the 4 times the operationQueue was called did not end before the filterResults operation was called (although it was added later!)
I thought that the operationQueue is serial and that I can count on it that when I add an operation it would be added right after the last operation.
So it is weird that only 2 operations are in the array in the aftermath.
What am I missing? Thanks
From what you shared as your initialisation code we can learn that operationQueue is not serial, meaning it will execute operations, and allocate thread up until the system set maximal thread count (same as with GCD).
This mean that operations added to operationQueue are running in parallel.
To run them serially set the maxConcurrentOperationCount to 1.
Try something like:
__block __weak id weakSelf = self;
[operationQueue setMaxConcurrentOperationCount:1];
for (NSDictionary *Obj in myObjects)
{
[operationQueue addOperationWithBlock:^{
MyObject* tmp = [weakSelf tediousFunction:Obj];
// threadTempObjects is member NSMutableArray
[weakSelf.threadTempObjects addObject:tmp];
NSLog(#"ADDED");
}];
}
[operationQueue addOperationWithBlock:^{
[weakSelf.myArray addObjectsFromArray:weakSelf.threadTempObjects];
for(myObject *myObj in weakSelf.myArray)
{
// MAIN_QUEUE
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
[weakSelf updateUI:myObj];
}];
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
[weakSelf filterResults];
}];
}
}];
But, this is equal (or even less efficient) to simply:
__block __weak id weakSelf = self;
[operationQueue addOperationWithBlock:^{
for (NSDictionary *Obj in myObjects) {
MyObject* tmp = [weakSelf tediousFunction:Obj];
// threadTempObjects is member NSMutableArray
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
[weakSelf updateUI:tmp];
}];
[weakSelf.myArray addObject:tmp];
NSLog(#"ADDED");
}
[[NSOperationQueue mainQueue] addOperationWithBlock:^(void) {
[weakSelf filterResults];
}];
}];