I'm developing an iOS app with a view containing a TableView.
Some method receives data from the web, opens a new thread to calculate information and inserts a row into the table at run time with the method: insertRowsAtIndexPaths.
Now if a lot of data is coming at once, the table may update itself after a few insertions and not after each one, and thats provokes an exception saying that the number of rows in section isn't right (that's because it thinks it should have an increment of one row but the threads already inserted the array of data some more cells).
Even if I make a lock on the insertion to the datasource array and the insertRowsAtIndexPaths method, it's still do the same.
NSLock *mylock = [[NSLock alloc] init];
[mylock lock];
[array addObject:object];
[tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationLeft];
[mylock unlock];
help please,
Thank you!
you have to run this method on the main thread. All User Interface interaction has to be done on the main thread.
Let's say your method looks like this:
- (void)addSomeObject:(id)object {
[array addObject:object];
[tableView insertRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationLeft];
}
and you are calling it like this:
[self addSomeObject:anObject];
then you would change this call to something like this:
[self performSelectorOnMainThread:#selector(addSomeObject:) withObject:anObject waitUntilDone:NO];
Related
You can have any number of threads working on the same Realms in parallel, and since they all have their own snapshots, they will never cause each other to see inconsistent state
Consider if the write thread change some value and complete faster than the read thread, however, since the read thread has its own snapshots, it won't change because what have done on the write thread, but if the read thread's operation is base on the latest value, so it's that means the read thread can not do the right operation? thx for helping.
my test demo to describe my confusion
By "inconsistent", the documentation mean "partially modified". They cannot be partially modified, because you see specific version that was created by transactional changes, at all times.
However, updating is not immediate across threads.
since the read thread has its own snapshots, it won't change because what have done on the write thread, but if the read thread's operation is base on the latest value, so it's that means the read thread can not do the right operation?
True, that is exactly why the read thread should use collection notifications.
// from https://realm.io/docs/objc/latest/#collection-notifications
- (void)viewDidLoad {
[super viewDidLoad];
// Observe RLMResults Notifications
__weak typeof(self) weakSelf = self;
self.notificationToken = [[Person objectsWhere:#"age > 5"] addNotificationBlock:^(RLMResults<Person *> *results, RLMCollectionChange *changes, NSError *error) {
if (error) {
NSLog(#"Failed to open Realm on background worker: %#", error);
return;
}
UITableView *tableView = weakSelf.tableView;
// Initial run of the query will pass nil for the change information
if (!changes) {
[tableView reloadData];
return;
}
// Query results have changed, so apply them to the UITableView
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[changes deletionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:[changes insertionsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView reloadRowsAtIndexPaths:[changes modificationsInSection:0]
withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
}];
}
- (void)dealloc {
[self.notificationToken stop];
}
In addition to that, transactions always "create the latest version" (and there can be only 1 transaction at a time), so a query inside a write transaction cannot be out-of-date.
dispatch_async(writeQueue, ^{
[[RLMRealm defaultRealm]] transactionWithBlock:^{
Cinema* b = [Cinema allObjects].firstObject; // query cannot be out of date
...
Obviously this does NOT mean that you should execute transaction on the UI thread just to bump its version. Use the notification token for that.
Yes, this is correct. If you want to make sure that you always get consistent data form Realm, do not access it concurrently from separate threads, since the write transactions done on one thread will not be reflected on the other thread. This is the behaviour that can be seen in your example as well.
There is also no need to access realm asynchronously unless you have a huge number of objects to read from it/write to it. If you want to work with such a big number of objects that your queries/write transactions would take several seconds and hence doing them synchronously would block UI updates for too long, make sure that you only access Realm from a single thread and also make sure that your asynchronous read/write operations are not running in parallel.
I am struggling with a UITableView that I previously had working and somehow I have broken it!
It is part of a unit in Paul Hegarty's course
The symptom is that the view loads but it is empty. I am clearly misunderstanding something fairly basic.
So far as I understand the two key methods are 1 the section rows count which in my case is returning zero, which I know is wrong!
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// #warning Incomplete method implementation.
// Return the number of rows in the section.
NSLog(#"TopPlaces %#",self.topPlaces);
//return 100;
return [self.topPlaces count];
}
Because of the above the following method is never called because there are no rows.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
And the second is within ViewDidLoad where I can log my data to the console and everything appears fine. i.e. my data is generated within ViewDidLoad
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t dowloadQueue = dispatch_queue_create("flick downloader", NULL);
dispatch_async(dowloadQueue, ^{
NSArray *topPlaces = [FlickrFetcher topPlaces];
//NSLog(#"Array is %#",topPlaces); // array is OK here
dispatch_async(dispatch_get_main_queue(), ^{
NSSortDescriptor *woeDescriptor = [[NSSortDescriptor alloc] initWithKey:#"_content" ascending:YES];
NSArray *woeDescriptors = #[woeDescriptor];
NSArray *sortedReturns = [topPlaces sortedArrayUsingDescriptors:woeDescriptors];
self.topPlaces = sortedReturns;
//all the data is present here, count is 100 and array will log to console
NSLog(#"count here is %u",[self.topPlaces count]);
});
});
// Uncomment the following line to preserve selection between presentations.
self.clearsSelectionOnViewWillAppear = NO;
}
The problem is that you make an asynchronous call to fetch the data (which means that your array should be full of data at some point in the future), but you do not reload your tableview afterwards. Calling reloadData will do the trick:
...
self.topPlaces = sortedReturns;
//all the data is present here, count is 100 and array will log to console
NSLog(#"count here is %u",[self.topPlaces count]);
[self.tableView reloadData]; // Assuming that 'tableView' is your outlet
This will instruct your tableview to query once again its datasource and will eventually load all the data in your (now non-empty) topPlaces array.
Further Explanation:
I saw in the comment of #nerak99 that he is not completely sure why the problem fixed with reloadData.
Well, let's use an example:
Imagine that you've got a restaurant.
You open the place at 06:00 in the morning and you see that you've got nothing to cook. So you ask from one of your guys to go to the market for supplies (that's your asynchronous call).
At the same time you instruct a waitress to write today's menu, so she writes... well, nothing (that's your tableview asking for number of rows).
Now at 07:00 the guy who went to the market returns with 10 items. What is the next logical step in order to update your menu? To actually inform the waitress (that's your reloadData) about the items you've returned with.
I hope that this makes sense :)
What is self.topPlaces? Try to NSLog the array and see if there's any contents. If not, make sure that it's being set.
I'll be able to write a more specific answer if you provide more info.
I'm trying to incorporate favicons into a UITableView. The table basically fetches websites, and I want to display the favicon onto the right. I put a placeholder icon at the right initially and let a function in the background run. This function takes the URL of the website, parses it and attempts to find the favicon. If it can't find it, it keeps the same placeholder image; otherwise, it replaces it with the site's favicon. I initially tried using [tableView reloadData] which worked well in the simulator, but it did really odd and unreliable things (like for instance, it would create some cells, but then leave a giant, blank cell). Anyway, I stumbled upon reloadRowsAtIndexPaths, and it seems like the function I need to use. However, the results are still pretty unreliable. I have my fetching function running in the background as such:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...//other code here for showing website labels
//obtain the favicon.ico
if(!this_article.iconLoaded){
this_article.iconLoaded = TRUE;
NSDictionary *args = [NSDictionary dictionaryWithObjectsAndKeys:this_article, #"article", indexPath, #"indexPath", nil];
[self performSelectorInBackground:#selector(fetchFaviconWrapper:) withObject:args];
}
cell.favicon.image = this_article.icon;
return cell;
}
in FetchFaviconWrapper:
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[articleTable beginUpdates];
[articleTable reloadRowsAtIndexPaths:paths withRowAnimation:UITableViewRowAnimationFade];
[articleTable endUpdates];
[pool release];
}
Basically, fetchFavicon takes a website, takes the host URL, appends "/favicon.ico", constructs it into an NSData object, and finds the image (if it exists). However, this has also been pretty unreliable. What would be the best way to replace the placeholder image while running a thread in the background? I could do everything on the main thread, but that just makes the table load slowly. There seems to be something that I'm overlooking, or something that I just forgot to add...just can't figure it out.
It is not entirely clear if you are accessing and modifying your UI from the separate thread.
If so, this is the cause of your unreliability. UIKit can be accessed only from the main thread. If you are interested you will find many questions on S.O. and many discussions on the web.
There is a workaround, if you want to keep your second thread. Indeed, you can send messages to your UI object using:
-performSelectorOnMainThread:withObject:waitUntilDone:
instead of sending them directly from the secondary thread.
If this workaround does not solve the issue for you, then I would suggest redesigning your app so that the secondary thread only access your model, without accessing the UI. All the operations that modify the UI should be executed on the main thread. If you need to call reloadData on your table when the model is ready, you can do it using performSelectorOnMainThread:withObject:waitUntilDone.
I'd suggest a few things.
Appending ./favicon.ico isn't always accurate. Look into various methods of adding favicons to sites to support them.
As far as replacing the default placeholder, I suggest using the NSNotificationCenter to inform the main thread when to make the changes.
I definitely reiterate the no UI from background thread.
change your initial code:
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[articleTable beginUpdates];
[articleTable reloadRowsAtIndexPaths:paths
withRowAnimation:UITableViewRowAnimationFade];
[articleTable endUpdates];
[pool release];
}
to this, and see if you're still having the problem
- (void)fetchFaviconWrapper:(NSDictionary *)args {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[self fetchFavicon:[args objectForKey:#"article"]];
NSArray *paths = [NSArray arrayWithObject:[args objectForKey:#"indexPath"]];
[self performSelectorOnMainThread:#selector(reloadData)
withObject:nil
waitUntilDone:YES];
[pool release];
}
The waitUntilDone is irrelevant in this case, and it's preferred form to say YES.
Another problem to suspect when you have funky cell behavior is if you are correctly handling cell reuse. You didn't show us that code, so we can't tell. If you are keeping a reference to a cell somewhere and setting the image data into that - you'll hose yourself when the table reuses the cell out from under you. (I don't think that's the problem in this case.)
I have a UITableView that, when items are selected, loads a viewController, which inside it performs some operations in the background using performSelectorInBackground.
Everything works fine if you slowly tap items in the tableView (essentially allowing the operations preforming in background to finish). But when you select the items quickly, the app quickly returns some memory warnings until it crashes, usually after about 7 or 8 "taps" or selections.
Any idea why this would be? When I move my code from the background thread to the main thread, everything works fine as well. You just can't make the tableView selections as quickly because it's waiting for the operations to finish.
Code snippets:
//this is called from - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
-(void) showLeaseView:(NSMutableDictionary *)selLease
{
LeaseDetailController *leaseController = [[LeaseDetailController alloc] initWithNibName:#"LeaseDetail" bundle:nil];
leaseController.lease = selLease;
//[leaseController loadData];
[detailNavController pushViewController:leaseController animated:NO];
[leaseController release];
}
//this is in LeaseDetailController
- (void)viewDidLoad {
[self performSelectorInBackground:#selector(getOptions) withObject:nil];
[super viewDidLoad];
}
-(void) getOptions
{
NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
NSArray *arrayOnDisk = [[NSArray alloc] initWithContentsOfFile:[appdel.settingsDir stringByAppendingPathComponent:#"optionData"]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(LEASE_ID contains[cd] %#)", [lease leaseId]];
self.options = [NSMutableArray arrayWithArray:[arrayOnDisk filteredArrayUsingPredicate:predicate]];
[arrayOnDisk release];
[apool release];
}
Every time you perform the getOptions selector in the background, what's really happening is a new thread is being created on your behalf, and the work is being done there. When the user taps your table cells a bunch of times in a row, a new thread is created each time to handle the work. If the work done by getOptions takes some time to complete, you will have multiple threads calling getOptions at the same time. That is to say, the system doesn't cancel previous requests to perform getOptions in the background.
If you assume that it takes N bytes of memory to perform the work done by getOptions, then if the user taps on five table cells in a row and getOptions doesn't finish right away, then you'll find that your app is using 5 * N bytes at that point. In contrast, when you change your app to call getOptions on the main thread, it has to wait for each call to getOptions to complete before it can call getOptions again. Thus when you do your work on the main thread you don't run into the situation where you're using 5 * N bytes of memory to do the work of five instances of getOptions simultaneously.
That's why you run out of memory when you do this work in the background and the user taps multiple table cells: you're doing multiple instances of the work, and each instance requires its own amount of memory, and when they all get added up, it's more than the system can spare.
It looks like you're just calling getOptions once when the user selects a table cell and navigates into a new view controller. Since the user will only be looking at one of these view controllers at a time, you don't really need to have multiple instances of getOptions going on simultaneously in the background. Instead, you want to cancel the previously-running instance before starting the new one. You can do this using an NSOperationQueue, like so:
- (NSOperationQueue *)operationQueue
{
static NSOperationQueue * queue = nil;
if (!queue) {
// Set up a singleton operation queue that only runs one operation at a time.
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
}
return queue;
}
//this is called from - (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
-(void) showLeaseView:(NSMutableDictionary *)selLease
{
LeaseDetailController *leaseController = [[LeaseDetailController alloc] initWithNibName:#"LeaseDetail" bundle:nil];
leaseController.lease = selLease;
// Cancel any pending operations. They'll be discarded from the queue if they haven't begun yet.
// The currently-running operation will have to finish before the next one can start.
NSOperationQueue * queue = [self operationQueue];
[queue cancelAllOperations];
// Note that you'll need to add a property called operationQueue of type NSOperationQueue * to your LeaseDetailController class.
leaseController.operationQueue = queue;
//[leaseController loadData];
[detailNavController pushViewController:leaseController animated:NO];
[leaseController release];
}
//this is in LeaseDetailController
- (void)viewDidLoad {
// Now we use the operation queue given to us in -showLeaseView:, above, to run our operation in the background.
// Using the block version of the API for simplicity.
[queue addOperationWithBlock:^{
[self getOptions];
}];
[super viewDidLoad];
}
-(void) getOptions
{
NSAutoreleasePool *apool = [[NSAutoreleasePool alloc] init];
NSArray *arrayOnDisk = [[NSArray alloc] initWithContentsOfFile:[appdel.settingsDir stringByAppendingPathComponent:#"optionData"]];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(LEASE_ID contains[cd] %#)", [lease leaseId]];
NSMutableArray * resultsArray = [NSMutableArray arrayWithArray:[arrayOnDisk filteredArrayUsingPredicate:predicate]];
// Now that the work is done, pass the results back to ourselves, but do so on the main queue, which is equivalent to the main thread.
// This ensures that any UI work we may do in the setter for the options property is done on the right thread.
dispatch_async(dispatch_queue_get_main(), ^{
self.options = resultsArray;
});
[arrayOnDisk release];
[apool release];
}
So I've got an NSFetchedResultsController that I activate on ViewDidLoad with the managedobjectcontext that has been passed on from the appdelegate on load.
I put a predicate on some field let's call it "sectionNumber" and say it needs to equal 1 in my predicate.
NSFetchResultsController works fine until I add a new object to the MOContext...
I use MyMO *newObj = [NSEntityDescription insertnewentity]...
start filling the different fields
[newobj setName:#"me"];
[newobj setAge:12];
etc...
Once I put [newobj setSectionNumber:1] - it finds it at that very instant and causes the app to crash with different weird errors that all lead to EXC_BAD_ACCESS.
All of this happens on the MAIN THREAD.
Any ideas why? How could one get around that?
UPDATE:
It only happens when I use my saveMOC method which is called at the end of an NSXMLParser specific thread I spawned off. The saveMOC is called on a successful parse with the [self performSelectorOnMainThread].... If i just added the extra managedobject via ViewDidLoad (just to check weather this is related somehow to to threading) the problem does NOT occur.
So it's obviously something with the new thread even tho the selector should have been run on the main thread.
UPDATE #2:
This is my spawned thread for the XML Parser:
-(void)getAndParseXML {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
DLog(#"Online storage");
NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:theUrl];
XMLTranslator *translator = [[XMLTranslator alloc] init];
[parser setDelegate:translator];
if ([parser parse]) {
//success call MOC change routine on main thread
DLog(#"success parsing");
[self performSelectorOnMainThread:#selector(saveMOC:) withObject:translator waitUntilDone:NO];
} else {
DLog(#"error: %#",[parser parserError]);
}
[parser setDelegate:nil];
[parser release];
DLog(#"XML parsing completed");
[pool release];
}
Then this is my saveMOC:
-(void)saveMOC:(XMLTranslator*)translator {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:#"yyyy-MM-dd HH:mm:ss"];
for (NSDictionary *dict in [translator retrievedData]) {
APost *newPost = [NSEntityDescription insertNewObjectForEntityForName:#"APost"
inManagedObjectContext:managedObjectContext];
//parse time into NSDate
[newPost setTime:[formatter dateFromString:[dict objectForKey:#"time"]]];
//author, category, description
[newPost setAuthor:[dict objectForKey:#"author"]];
[newPost setCategory:[dict objectForKey:#"category"]];
[newPost setContent:[dict objectForKey:#"description"]];
//create a post id so that the validation will be alright
[newPost setPostid:[NSNumber numberWithInteger:[[dict objectForKey:#"postid"] integerValue]]];
[newPost setSectionDesignator:sectionDesignator];
}
This saveMoc method continues and has a [managedobjectcontext save:&error] and more... but it's not relevan to our case as my method crashes I've discovered thru commenting one line after another at the point where I set the sectionDesignator since it equals to the current predicate in my NSFetchedResultsController.
The problem is most likely in the NSFetchedResultsController delegate methods or the lack thereof.
When you add a new object to any context and then save the context, that changes the persistent store which triggers the FRC on any thread to begin an update of the tableview. All the index paths change, especially if you set a value for an attribute used as a sectionNameKeyPath. If the table ask for a cell during the update, it will cause a crash because the table can ask for a cell at a index path rendered invalid by the insertion of the new managed object.
You need to make sure you implement the FRC's delegate methods and that you send the table a beginUpdate message to freeze it while the FRC changes all its index paths.
I am sorry to admit that the problem this whole time was releasing an array that held the sort descriptors in the fetch request that was used within the FRC.
Looking at alot of examples I released that array tho unlike the examples I created my array with [NSArray arrayWithObject:.............];
So there was an overrelease each time the fetch request was accessed more than once.
Feel free to close this. Thank you everybody for your help. I discovered this when peter wrote to look at the whole stack and not just one frame.
I have further analyzed the problem and have realized it occurs inside the loop.
I have further understood that it only happens when I have more than one object, meaning that one FRC takes over after an object insertion into MOC and tries to come back to the for loop, it tries to access an object or a reference that's not there. I haven't found what object causes it and how to retain it properly.
Any suggestions?
Consider the following:
for (int i=0; i<2; i++) {
NSLog(#"%i",i);
APost *thePost = [NSEntityDescription insertNewObjectForEntityForName:#"HWBPost" inManagedObjectContext:managedObjectContext];
[thePost setCategory:#"CAAA"];
[thePost setContent:#"SSSSSS"];
[thePost setSectionDesignator:sectionDesignator];
}
If I change the for loop to i<1 meaning it only runs once, the app does NOT crash. As soon as it is more than one object insertion the app crashes.