I have a UITableView that sometimes has rapid insertions of new rows. The insertion of the new rows is handled by a notification observer listening for the update notification fired whenever the underlying data changes. I use a #synchronized block around all the data model changes and the actual notification post itself... hoping that each incremental data change (and row insertion) will be handled separately. However, there are times when this still fails. The exception will tell me that it expects 10 rows (based on the count from the data model), it previously had 8 rows, but the update notification only told it to insert a single row (as this is the first of two rapidly fired notifications).
I'm trying to understand how other people tend to handle these types of situations. How do other developers mitigate the problems of having multi-threaded race conditions between two table view update operations? Should I have a more secure lock that controls the update notifications (and why isn't #synchronized doing what it's supposed to)?
Any advice is greatly appreciated! Thanks.
Some pseudo-code:
My model class has a method like this, which gets called by other threads to append new rows to the table view:
- (void)addRow:(NSString *)data
{
#synchronized(self.arrayOfData)
{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
}
}
My controller class has a method like this to accept the kDataUpdatedNotification notification and actually perform the row insertion:
- (void)onDataUpdatedNotification:(NSNotification *)notification
{
NSDictionary *changes = notification.userInfo;
[self.tableView insertRowsAtIndexPaths:changes[#"insert"] withRowAnimation:UITableViewRowAnimationBottom];
}
You're going to have this problem if you change your data model asynchronously with the main queue because your table view delegate methods are looking at the current state of the data model, which may be ahead of the inserts you've reported to the table view.
UPDATE
One solution is to queue your updates on a private queue and have that queue update your data model on the main queue synchronously (I have not tested this code):
#interface MyModelClass ()
#property (strong, nonatomic) dispatch_queue_t myDispatchQueue;
#end
#implementation MyModelClass
- (dispatch_queue_t)myDispatchQueue
{
if (_myDispatchQueue == nil) {
_myDispatchQueue = dispatch_queue_create("myDispatchQueue", NULL);
}
return _myDispatchQueue;
}
- (void)addRow:(NSString *)data
{
dispatch_async(self.myDispatchQueue, ^{
dispatch_sync(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
});
}
The reason you need the intermediate dispatch queue is as follows. In the original solution (below), you get a series of blocks on the main queue that look something like this:
Add row N
Add row N+1
Block posted by table view for row N animation
Block posted by table view for row N+1 animation
In step (3), the animation block is out-of-sync with the table view because (2) happened first, which results in an exception (assertion failure, I think). So, by posting the add row blocks to the main queue synchronously from a private dispatch queue, you get something like the following:
Add row N
Block posted by table view for row N animation
Add row N+1
Block posted by table view for row N+1 animation
without holding up your worker queues.
ORIGINAL Solution still has issues with overlapping animations.
I think you'll be fine if you update your data model on the main queue:
- (void)addRow:(NSString *)data
{
dispatch_async(dispatch_get_main_queue(), ^{
NSInteger nextIndex = self.arrayData.count;
[self.arrayData addObject:data];
[NSNotificationCenter.defaultCenter postNotificationName:kDataUpdatedNotification object:self userInfo:#{#"insert": #[[NSIndexPath indexPathForRow:nextIndex inSection:0]]}];
});
}
Related
I am going to build lazy loading functionality which has dynamic size records in database, so i am doing this in following manner
Initially i fetch say 20 records ,
Then i process one,two, or any number of records based on user's clicks (I am not holding user to wait for finish some service call or pending task, so user can do anything with the list like scroll or click etc.)
Now to maintain consistency i am fetching few records first and then process users record, here i am doing so because the records which i processed are removed from the actual list of records, so here the records at Database and app side are not fixed list.
I am doing right now with following ways :
-(void)viewDidLoad {
// Here i fetch first 20 records and then show in UITableView
// After user see the list he can click on any button to process that records
// And i am trying to queue that service call and maintain consistency in records
}
-(void)processOneRecord {
dispatch_queue_t queue = dispatch_queue_create("com.developer.serialqueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
// To get further records service call
[self getMoreRecordsCompletionBlock:^{
[self ProcessOneRecordServiceCall];
}];
});
}
Here i am creating a method processOneRecord which calls every time user clicks on button on cell
In processOneRecord method i create completion block for first service call, so second service call definitely run after first service all response. But now when user click on button on another cell i want to wait that first service call until second service call response return to application.
Thanks for the response guys.
I have achieved desired functionality with help of Rob's answer, I have created one function in which i write one NSBlockOperation which later i added to NSOperaiotnQueue. And every time when this function calls i cheated whether previously added operation is finished, if yes then add another operation else it will wait previous operation until its completion.
In below code snippet below variable are global to my current UIViewController and also they are initialised in my UIViewController's ViewDidLoad and other respective methods.
Bool isRecordProcessed, isLoadMoreCallFinihsed;
NSOperationQueue *operationQueue;
-(void)callTwoAPIServiceCalls {
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
// To get further records first
[self getMoreRecordsCompletionBlock:^{
[self ProcessOneRecordServiceCall];
}];
}];
if (operationQueue.operations.count == 0 && isRecordProcessed && isLoadMoreCallFinihsed) {
[operationQueue addOperation:operation];
}
else {
[self performSelector:#selector(callTwoServiceCalls:) withObject:nil afterDelay:0.1];
}
}
I am using Realm for my app, i want to be able to query results on a background thread and receive them on the main thread.
What is the best way to achieve this? and what is the best practice to use realm (having different method for main thread and background? and in main using a static instance of realm across the app? maybe another good way?)
I've read and saw this options are available:
- parsing the realm object to my own object and return them (kind of a copy of the results).
- returning a the key of the object and querying again from main thread.
Thanks for any help anyone can give me, i really think realm has great potential but there is a lack of good tutorials and best practices.
First, since Realm is fast enough in most cases, you do not need to run a query in the background.
So the basic strategy is; update in background, fetch in the main thread.
The most general way is to take advantage of the feature of the live update.
RLMResults and Results have live update. You can hold RLMResults/Results instances by query. Then you'll make any changes in background thread, the changes are notified and reflected automatically when committed.
// Hold RLMResults for the data source
self.array = [[DemoObject allObjects] sortedResultsUsingProperty:#"date" ascending:YES];
// Reload table view when changed by other threads
__weak typeof(self) weakSelf = self;
self.notification = [RLMRealm.defaultRealm addNotificationBlock:^(NSString *note, RLMRealm *realm) {
[weakSelf.tableView reloadData];
}];
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// RLMResults is updated automatically
return self.array.count;
}
// Update in background
- (void)backgroundAdd
{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// Import many items in a background thread
dispatch_async(queue, ^{
// Get new realm and table since we are in a new thread
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
for (NSInteger index = 0; index < 5; index++) {
// Add row via dictionary. Order is ignored.
[DemoObject createInRealm:realm withValue:#{#"title": [self randomString],
#"date": [self randomDate]}];
}
[realm commitWriteTransaction];
});
}
For more details, you can see the table view example in Realm's repo.
If a few cases that Realm doesn't fast enough when fetch on the main thread, you can fetch in background thread. Then aggregate an array of primary keys. Then pass the array and re-fetch on the main thread using the primary keys.
FYI: We are working to add support for running queries asynchronously https://github.com/realm/realm-cocoa/pull/2842
If this feature will be released, you don't need to aggregate primary keys and re-fetch.
I have this very strange error happening when I'm changing view controllers in my iOs app.
First some background info.
I am retrieving an image from the web in a background NSOperation. This operation is started from another (background) operation that is instantiated in an collection view cell. The way this works is that the cell creates an object , then sets itself as an observer for that object, then creates an NSOperation with that object as a parameter. This first level operation will start the second operation that will get the image from the web and another NSOperation that will try to get the data from a file (if possible) and report it via delegation to the first operation. That first level operation will change a property on the observed object thus triggering the KVO. The collection/tableView cell will update from the - observeValueChange method.
Here is the problem:
Sometime the cell disappears (reused or deallocated) and when the background thread tries to set the value on the observed object it triggers an EXC_BREAKPOINT exception ([collectionViewCell message retain sent to deallocated instance]).
In order to prevent this I tried implementing -prepareForReuse and -dealloc on the cells. But the error keeps happening.
The flow seem like this:
-User loads VC that has collectionViewWithCells
-cell creates object and NSOperation 1
NSoperation 1 creates NSOperation2 (this is of two types get from web or get from file)
NSOpeartion 2 get image from internet or from a local file
NSoperation 2 sends data to NSOperation1
User has left this screen
NSOperation 1 tries to set data on observed object
-- CRASH
Here is the code inside the cell:
#interface CustomCollectionViewCell ()
#property (strong, nonatomic) NSOperationQueue *imagesOperationQueue;
#property (strong, nonatomic) ImageObject *imgObj;
#end
#implementation CustomCollectionViewCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)prepareForReuse{
[self clearDelegatesAndObservers];
[super prepareForReuse];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
- (void) getImage {
self.imgObj = [ImageObject newRefrenceWithId:obj_ref];
[self.imgObj addObserver:self forKeyPath:#"data" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
TaskImageReqCache *imgReq = [[TaskImageReqCache alloc] initWithUrl:imgUrl andImageObject:self.imgObj];
[self.imagesOperationQueue addOperation:imgReq];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.imgObj) {
UIImage *img = [UIImage imageWithData:self.imgObj.data];
self.thumbnailImage.image = img;
}
}
- (void)dealloc
{
[self clearDelegatesAndObservers];
}
- (void)clearDelegatesAndObservers
{
[self.imagesOperationQueue cancelAllOperations];
self.thumbnailImage.image = nil;
[self.imgObj removeObserver:self forKeyPath:#"data"];
[self.pageListAdapter removeDelegateAtIndex:self.myIndexInCollection];
self.imgObj = nil;
}
In the first Level NSOperation this is where the exception breakpoint shows the crash happening:
- (void)didLoadDataFromFile:(NSData *)data
{
if (self.isCancelled) {
[self.opQueue cancelAllOperations];
[self completeOperation];
return;
}
if (!fileDownloadedFromWeb) {
self.observedObject.data = data; // CRASH
}
dataFromDisk = data;
fileReadDone = YES;
if (debugLog) {
NSLog(#"image loaded from local cache (%#)",self.sUrl);
}
}
Any suggestion on how to prevent this crash?
Thanks.
Edited to add:
what I am trying to achieve is: When a tableView cell is displayed a nsoperation is activated to get an image from the net. If the user scrolls quickly and the operation has not finished I need to end it and deallocate any data, and when the cell is reused start a new operation to get the appropriate image from the internet...
Based on comments below, we know that:
- (void)didLoadDataFromFile:(NSData *)data
is called on a different thread to dealloc, so there is a race condition. You need to access self.observedObject on the same thread as the thread it is deallocated on. I'm presuming "observedObject" is a weak reference?
dispatch_sync(dispatch_get_main_queue(), ^{
if (!fileDownloadedFromWeb) {
// Get a strong reference. This will retain observedObject - we must do this
// on the same thread as observedObject:dealloc is called, to prevent retaining
// an object during (or after) dealloc.
ObservedObject *strongRef = self.observedObject;
// This will do nothing if strongRef is nil.
strongRef.data = data;
}
});
A more structured approach would be to have the cell fetch all its images from a singleton cache (it looks as though at the moment there is no caching). The cell would obviously need to register itself as an observer for a particular URL in the cache, and the cache would notify the cell when the URL had downloaded. The cache should post that notification on the main thread.
The cache itself would manage all downloads, and there would be no background deallocation problem because it would be a singleton.
If you don't want to cache, that's fine. Use the same architecture, but call the cache an image fetcher instead. You can always add caching later if you want to.
EDIT - if your objects may be reused, rather than deallocated, as is the case for UITableViewCells, then the cell needs to be careful to ignore notifications about images that relate to a previous fetch. Either of these models will work
a) The cell retains a reference to the NSOperation until the NSOperation calls it back, or until prepareForReuse is called. Any callback from an unrecognised NSOperation must be a previous fetch (that we tried to cancel), and should be ignored. I don't really recommend this model, having the cell know about the operation AND vice versa seems silly.
b) The NSOperation sends a notification when it completes (on the main thread), and in the user info specifies the url/path that was requested. The UITableViewCell remembers what url/path it was trying to fetch, and ignores notifications that relate to other images. It unobserved that path in dealloc/prepareForReuse.
This was getting to long to be a comment so I'll make it an answer.
The reason why it's crashing has to do with the fact that UICollectionViewCells get recycled and deallocated. ARC is has put a [cvcell retain] in the wrong place. So, there are a few options:
One way to fix this is to just not create a NSOperation from a UICollectionViewCell.
Force the users to stay on the UICollectionViewController / UICollectionView so that it stays in memory.
Keep a property / pointer to the UICollectionViewController / UICollectionView so that it stays in memory even when the user has left it. (Make sure you retain it as strong or retain).
NOTE: All of these solutions do the same thing, force ARC to put the retain call somewhere else or to remove it entirely.
Cells get reused and reassigned frequently without your control so you should avoid assigning pending requests or operations to them.
Instead handle operations in your collection view data source (the view controller), and keep track of the operations not per cell but per indexPath's in a dictionary.
Even better keep this as a good experience and use something trusted and tested such as SDwebImage.
In my app, I have a UITableViewController.
Its tableView is divided in 3 sections.
I download datas for each of those sections from my server. To do this, I have 3 functions (for example f1 f2 and f3). Each updates a corresponding NSArray, used as data source for my table.
Now what I want is to reload datas using this functions and refresh my tableView once this 3 functions are done, but without disturbing the user.
I'm not used with asynchronous request, blocks, threads etc... and I'm looking for tips.
Actually, here is what I do :
-(void)viewDidLoad
{
//some settings
[NSTimer scheduledTimerWithTimeInterval:15.0 target:self selector:#selector(reloadDatas) userInfo:nil repeats:YES];
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
[self reloadDatas];
});
}
-(void)reloadDatas
{
dispatch_queue_t concurrentQueue = dispatch_get_main_queue();
dispatch_async(concurrentQueue, ^{
[self f1];
[self f2];
[self f3];
[myDisplayedTable reloadData];
});
}
-(void)f1
{
//load datas with a url request and update array1
}
-(void)f2
{
//load datas with a url request and update array2
}
-(void)f3
{
//load datas with a url request and update array3
}
But here, my tableView is "frozen" until it is refreshed.
I don't care about the order of execution of f1 f2 and f3, but I need to wait for this 3 functions to be done before refresh my tableView.
Thanks for your help.
EDIT
Thanks for all your answers.
Here is the working solution :
As mros suggets, I removed the dispatch queue from the viewDidLoad, and replace in reloadDatas:
dispatch_queue_t concurrentQueue = dispatch_get_main_queue();
with
dispatch_queue_t mainThreadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
And finally, I reload my table in a main thread
dispatch_async(dispatch_get_main_queue(), ^{ [myDisplayedTable reloadData]; });
So your "background thread" is actually your main thread. You have to use dispatch_get_global_queue and specify a priority to actually get a different thread. Also, the dispatch async in viewDidLoad is useless as all view controller lifecycle methods are called in the main thread. I would recommend doing something as follows in your f1, f2 and f3 methods:
Start by launching an asynchronous url request, then in the completion block, update arrayX and reload a particular section of your tableview. This way all three requests can happen simultaneously and the table just updates the necessary data when each one finishes. Alternatively, if you only want to reload once, just replace the concurrentQueue variable you have with a background thread and then perform [tableView reloadData] on the main thread.
The previous answers are absolutely right. However your implementation of reloadDatas & viewDidLoad is a bit problematic.
Just to clarify:
You want to complete the time consuming data loading stuff in a background thread, then update the UI/Cells when your data is ready on the main thread.
Like so:
-(void)viewDidLoad
{
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.my.backgroundQueue", NULL);
dispatch_async(concurrentQueue, ^{
[self reloadDatas];
});
}
-(void)reloadDatas
{
// Expensive operations i.e pull data from server and add it to NSArray or NSDictionary
[self f1];
[self f2];
[self f3];
// Operation done - now let's update our table cells on the main thread
dispatch_queue_t mainThreadQueue = dispatch_get_main_queue();
dispatch_async(mainThreadQueue, ^{
[myDisplayedTable reloadData]; // Update table UI
});
}
One other thing. Pulling data from a server and updating table cells is pretty common.
No need for queues or timers here.
Here's an alternative structure.
Say you're pulling mp3's from your server :
Your model class is : Music.h/m
Your Model manager is : MusicManager.h/m (Singleton) - it will contain an array of music objects - that singleton is basically your datasource;
and finally your UItableViewController : MusicTableVC.h/m
In MusicManager.h/m : You have an NSMutableArray which will be loaded with Music.h objects that you've pull from the server. You can do that as soon as you app loads without even waiting for the TableViewController.
Inside MusicManager you have a few helper methods to add or remove items from the mutableArray and provide the count and of course your networking methods.
Finally : Post a notification in your network code. Your UITableViewController should listen/observe that notification and "reload" accordingly.
[[NSNotificationCenter defaultCenter] postNotificationName:#"NewMusicAdded" object:nil];
You query data from your server, parse the data into Music objects add them to your NSMutable array and post a notification to let the table update itself.
Pretty standard recipe.
In reloadDatas method you should change this line:
dispatch_queue_t concurrentQueue = dispatch_get_main_queue();
To:
dispatch_queue_t concurrentQueue = dispatch_queue_create("some queue", NULL);
But when you call [myDisplayedTable reloadData], you need to call this operation in the main queue.
dispatch_async(dispatch_get_main_queue(), ^{ [myDisplayedTable reloadData]; });
I need some help in using objects from Core Data with GCD; I seem to get NSManagedObjects that are aren't faulted into the main thread, even when I access their properties. Would appreciate some help.
This is what I'm doing: on launch, I need to load a list of Persons from the Core Data DB, do some custom processing in the background, then reload the table to show the names. I am following the guidelines for Core Data multi-threading by only passing in the objectIDs into the GCD queues. But when I reload the tableview on the main thread, I never see the name (or other properties) displayed for the contacts, and on closer inspection, the NSManagedObjects turn out to be faults on the main thread, even though I access various properties in cellForRowAtIndexPath. The name property is visible in the background thread when I NSLog it; and it's also showing correctly on the main thread in NSLogs in cellForRowAtIndexPath. But they don't show in the tableView no matter what I do. I tried accessing the name property using the dot notation, as well as valueForKey, but neither worked.
Here's my code …. it's called from the FRC initializer:
- (NSFetchedResultsController *)fetchedResultsController
{
if (__fetchedResultsController != nil)
{
return __fetchedResultsController;
}
__fetchedResultsController = [self newFetchedResultsControllerWithSearch:nil]; // creates a new FRC
[self filterAllContactsIntoDictionary: __fetchedResultsController];
return [[__fetchedResultsController retain] autorelease];
}
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
NSArray *fetchedIDs = [[frc fetchedObjects] valueForKey:#"objectID"];
NSArray *fetched = [frc fetchedObjects];
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *backgroundContext = [[[NSManagedObjectContext alloc] init] autorelease];
[backgroundContext setPersistentStoreCoordinator:[[self.fetchedResultsController managedObjectContext] persistentStoreCoordinator]];
NSMutableArray *backgroundObjects = [[NSMutableArray alloc] initWithCapacity: fetchedIDs.count];
// load the NSManagedObjects in this background context
for (NSManagedObjectID *personID in fetchedIDs)
{
Person *personInContext = (Person *) [backgroundContext objectWithID: personID];
[backgroundObjects addObject:personInContext];
}
[self internal_filterFetchedContacts: backgroundObjects]; // loads contacts into custom buckets
// done loading contacts into character buckets ... reload tableview on main thread before moving on
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
What am I doing wrong here? Is there any other way to explicitly make the Person objects fire their faults on the main thread? Or am I doing something wrong with GCD queues and Core Data that I'm not aware of?
Thanks.
Why not take the easy route, since you are not saving anything new ?
Instead of creating an extra context for the background thread and working with IDs, use the main managedObjectContext in the background thread after locking it.
for example:
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *context = ... // get the main context.
[context lock]; // lock the context.
// do something with the context as if it were on the main thread.
[context unlock]; // unlock the context.
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
This works for me when I call a method with performSelectorInBackground, so I guess it should work for GCD dispatch too.
Well, mergeChangesFromContextDidSaveNotification: is your friend. You'll need to tell the MOC on the main thread that there have been changes elsewhere. This will do the trick.
Here's Apple's documentation. To quote from there:
This method refreshes any objects which have been updated in the other context, faults in any newly-inserted objects, and invokes deleteObject:: on those which have been deleted.
EDIT: original answer removed, OP is not fetching in the background
I looked closer at your code and it doesn't look like you are doing anything that will change data and/or affect the context on the main thread.
You have a fetchedResultsController on the main thread. Presumably, this is working and your table is populating with data. Is this true?
When filterAllContentsIntoDictionary is invoked, you pass an array of the fetchedResultsController's current objectIDs to a background thread and do some processing on them (presumably filtering them based on some criteria) but you are not changing data and saving backgroundContext.
internalFilterFetchedContents is a black box. Without knowing what you intend for it to do, hard to say why it's not working.
When this is done, you reload the table on the main thread.
You haven't made any changes to the store, the context, or the fetchedResultsController so of course, the table shows the same data it did before. The missing details to help further are:
Is your tableView showing correct data from the fetchedResultsController to begin with? If not, most likely your only problem is in handling the tableView delegate and dataSource methods and the rest of this isn't really relevant.
What do you intend to happen in filterAllContentsIntoDictionary and internalFilterFetchedContents?
If your intent is to filter the data as displayed by the fetchedResultsController not sure you need to do anything in the background. If you modify the fetchRequest and do performFetch again your table will reload based on the new results.
I you need more help, please answer my questions, add more relevant code to your post and let me know if I'm missing anything wrt the problem and what you're trying to accomplish.
good luck!