Reputation: 25907
I am doing some heavy calculations when I am creating cells. I am trying to figure out the best way to keep the UITableView fluid, but at the same type do the calculations on the background (keep the UI thread without too much processing).
For testing purposes only, I am using this as my heavy calculation method:
+(NSString*)bigCalculation
{
int finalValue=0;
int j=0;
int i=0;
for (i=0; i<1000; i++) {
for (j=0; j<10000000; j++) {
j++;
}
finalValue+=j/100*i;
}
return [NSString stringWithFormat:@"%d",finalValue];
}
Inside the cellForRowAtIndexPath, I am only doing the following:
- (UITableViewCell *)tableView:(UITableView *)aTableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier=@"identifier";
UITableViewCell *cell=nil;
cell=[aTableView dequeueReusableCellWithIdentifier:identifier];
if(!cell)
{
cell=[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier];
}
NSString *text=[dataSource objectForKey:[[dataSource allKeys] objectAtIndex:indexPath.row]];
dispatch_queue_t a_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
;
dispatch_async(a_queue, ^{
NSString *subtitle=[CalculationEngine bigCalculation];
dispatch_async(dispatch_get_main_queue(), ^{
[[cell detailTextLabel] setText:subtitle];
dispatch_release(a_queue);
});
});
[[cell textLabel] setText:text];
return cell;
}
At the moment I have the UITableView fluid, while in the background everything is working ok. So my questions are:
1) Is this the best way to achieve what I want, could KVO be the answer as well?
2) Before doing this:
dispatch_queue_t a_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
I was doing:
dispatch_queue_create("com.mydomain.app.newimagesinbackground", DISPATCH_QUEUE_SERIAL)
And the performance was very poor. Could you explain me why?
Upvotes: 1
Views: 870
Reputation: 2831
your implementation is fundamentally flawed. You take a reference to cell into the block which executes in the background. When you scroll and the cells are taken off screen they are put into a reuse pool. As new cells come onto the screen they are pulled from this pool. When your block completes the cell may no longer be in the same row. To illustrate this I put this statement at the start of the work block:
NSLog(@"my Row is: %d myCell is: %x", indexPath.row, (unsigned int)cell);
which results in:
my Row is: 0 myCell is: 16fd20
my Row is: 13 myCell is: 16fd20
my Row is: 24 myCell is: 16fd20
my Row is: 35 myCell is: 16fd20
my Row is: 44 myCell is: 16fd20
my Row is: 56 myCell is: 16fd20
my Row is: 66 myCell is: 16fd20
So here you can see 7 blocks, all calculating different rows but all pointing back to the same cell.
If the user has scrolled when the blocks complete, the wrong cell will be updated.
You also want to use dispatch_sync(dispatch_get_main_queue(),0)
when you update the cell to ensure it is processed immediately.
KVO is one way of solving this issue leading to..
KVO would be a good way to do this. As Rob Napier mentioned, you should have a separate model object for each item in your list. In order to manage the updating of the cells in the tableView you want to subclass UITableViewCell. The cell then subscribes to the model object for notifications and and can update itself when they come in. If the cell model object is changed you simply have to resign notifications for the old model object and subscribe to the new one.
This should guarantee that you never display inaccurate information in cells as is possible with your current code.
Something to watch out for:
KVO sends notifications on the same thread that set the new value. This means that you need to ensure that you either set the new value on the main thread or dispatch a block on the main thread in your observeValueForKeyPath:
method.
The reason that getting a queue this way:
dispatch_queue_create("com.mydomain.app.newimagesinbackground", DISPATCH_QUEUE_SERIAL);
was so slow is that you are generating one serial queue per block scheduled. These queues will all be executed at the same time. Since you only have a small number of cores, the queues have smaller and smaller slices of time to execute themselves in. The main queue (which runs the UI) is just another queue, the amount of time it has to execute is also smaller and smaller.
In my test I found I had one thread per queue. As many threads as cells.
Using the global concurrent queue:
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
allows the system to manage the number of blocks to execute at one time. The end result of this is that instead of hundreds of queues and hundreds of threads, you have one queue and, on my iPhone 4s, two threads. One thread per core. This makes the scheduling much simpler and assigns more accurate priorities to threads. The end result is that the main thread has enough time to execute.
Upvotes: 6
Reputation: 299275
This isn't a good approach. You're breaking MVC here by pushing the calculation work into the view controller. You should keep your data in model objects and manage the calculations there. The table view should then just display the current value from the model, using reloadRowsAtIndexPaths:withRowAnimation:
when the data changes. KVO is one reasonable way to determine when the data changes.
If you want to be lazy in calculations, then you can make calls to the model like recalculateIfNeeded
to let the model know that someone (the table view in this case) would like a fresh value. You shouldn't recalculate if the input data hasn't actually changed, however. The model is where to keep track of whether the data has changed.
Upvotes: 3