UITableView is extremely useful class to list content which Apple provide to iOS developer. UITableView use “reusable” pattern to handle content in each cell. So it can contain 100+ row with good performance.
There are many common scenarios when you deal with UITableView.
One of this is load asynchronous content in cell.
Like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
-(UITableViewCell *) tableView:(UITableView *) tableView cellForRowAtIndexPath:(NSIndexPath *) indexPath { MyCustomeCell *cell = (MyCustomeCell *)[tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; // weak cell __weak MyCustomeCell *weakCell = cell; // Async dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // Get Image NSURL *url = // URL link NSData *data = [NSData dataWithContentsOfURL:url]; UIImage *image = [UIImage alloc] initWithData:data]; // Update UI dispatch_async(dispatch_get_main_queue(), ^{ weakCell.imageView = image; }); }); } |
But, in this code have some issues memory.
Image you have 100 row, each of row you must load async big UIImage. So before cell is being displayed. TableView will call “Data Source” which provide cell. And in cellForRowAtIndexPath: will fire async method to load content.
But if this cell is OUT of visible area. The async operation you called is still doing work. So when this operation update UI in main thread, this UIImage is unnecessary resource. Sometime, it cause some “buggy”.
UITableViewCells are often reused instances. This means that cells being loaded into the view may sometimes contain data that was loaded originally into a completely different cell.
So, to handle this common scenarios. I use NSOperation and NSOperationQueue Apple was introduced NSOperationQueue and many relative class in iOS 4. This was build in top of Grand Dispatch Central. But improve more enhancement. NSOperation can be cancel and resume easily.
In brieft, we will implement step-by-step :
Here is sample :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCustomeCell *cell = (MyCustomeCell *)[tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; // Get URL NSURL *url = _arrURL[indexPath.row]; //Create a block operation for loading the image into the profile image view NSBlockOperation *loadImageOperation = [[NSBlockOperation alloc] init]; //Define weak operation so that operation can be referenced from within the block without creating a retain cycle __weak NSBlockOperation *weakOperation = loadImageOperation; [loadImageOperation addExecutionBlock:^(void){ //Some asynchronous work. Once the image is ready, it will load into view on the main queue UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:url]]; // Update UI in main thread when completion [[NSOperationQueue mainQueue] addOperationWithBlock:^(void) { //Check for cancelation before proceeding. We use cellForRowAtIndexPath to make sure we get nil for a non-visible cell if (! weakOperation.isCancelled) { MyCustomeCell *theCell = (MyCustomeCell *)[tableView cellForRowAtIndexPath:indexPath]; theCell.imageView.image = image; // Remove operation [_myDictionary removeObjectForKey:indexPath]; } }]; }]; //Save a reference to the operation in an NSMutableDictionary so that it can be cancelled later on if (url) { [_myDictionary setObject: loadImageOperation forKey:indexPath]; } //Add the operation to the designated background queue if (loadImageOperation) { [_operationQueue loadImageOperation]; } //This would be a good place to assign a placeholder image cell.imageView.image = placeholdImage; return cell; } |
and we take advantage in didEndDisplayngCell method.
We will cancel operation which called in cellForRowAtIndexPath: before.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { //Fetch operation that doesn't need executing anymore NSBlockOperation *ongoingDownloadOperation = [_myDictionary objectForKey:indexPath]; // Ensure ongoingDownloadOperation is not nil if (ongoingDownloadOperation) { //Cancel operation and remove from dictionary [ongoingDownloadOperation cancel]; [_myDictionary removeObjectForKey:indexPath]; } } |
So, this is the best way to handle this scenarios. You can cancel all of operation if you push to new UIViewController.
1 2 3 4 5 6 7 8 |
-(void) viewWillDisappeard:(BOOL) animated { [super viewWillDisappeard:animated]; // Cancel all [_myOperationQueue cancelAllOpeations]; } |
Finally, you can improve more performance by setting setMaxConcurrentOperationCount.
1 |
[_myOpeationQueue setMaxConcurrentOperationCount:3]; |
Feel free for giving me your comment or your opinion.
Thanks for reading ;]
Can you convert this code to swift please! Do i need to declare the block operation as week in swift as well
Amazingly helpful and informative post. You have helped me achieve 60fps scrolling!
I think there may possibly be an edge-case bug which could happen if the containing UIViewController ever gets deallocated. I’ve provided my solution after the explanation.
If the background thread is the last to store a strong reference to the view controller, then when it releases it, -dealloc will get called on a background thread, which is problematic for UIKit classes.
Inside the second nested block added to [NSOperation mainQueue], there is a reference to the direct instance variable ‘_myDictionary’. This would cause the outer block, which is on a background thread, to store a strong reference to self (the owner of the instance variable). This could then cause ‘the deallocation problem’ as mentioned previously.
By storing the value of the instance variable and using that within the block, a strong reference will be made to the variable instead of self, avoiding retaining self. Example:
NSMutableDictionary *blockMyDictionary = _myDictionary; // outside block
…
[blockMyDictionary removeObjectForKey:indexPath]; // inside block
Let me know if you agree or not, and thanks again for the great post. Here are some links I have based my thoughts on.
1. ‘The Deallocation Problem’ section
https://developer.apple.com/library/content/technotes/tn2109/_index.html#//apple_ref/doc/uid/DTS40010274-CH1-SUBSECTION11
2. ‘Object and Block Variables’ section on instance variable access by reference/value
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Blocks/Articles/bxVariables.html
It’s not working when large amount of data and fast scroll UITableview
If you check the documentation here (https://developer.apple.com/documentation/foundation/nsdata/1547245-datawithcontentsofurl), it says very clearly:
“Don’t use this synchronous method to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.”
Just FYI.
I have noticed you don’t monetize your blog, don’t waste your traffic, you can earn additional cash
every month because you’ve got high quality content. If you want to know
how to make extra $$$, search for: Boorfe’s tips best adsense alternative
I see you don’t monetize your site, don’t waste your
traffic, you can earn extra bucks every month because you’ve
got high quality content. If you want to know how
to make extra $$$, search for: Ercannou’s essential adsense
alternative
I have checked your website and i’ve found some duplicate content, that’s
why you don’t rank high in google’s search results, but there is a tool that
can help you to create 100% unique content, search
for: SSundee advices unlimited content for your blog