UITableView lazy loading optimization - iphone

I have UITableView. in tableView:cellForRow:atIndexPath: method (when data populates to cells) I've implemented some kind of lazy loading. If there's no object for key(key==row number) in rowData NSDictionary program launches requestDataForRow: method in background. so the data in cells gets populated a bit after the cell becomes visible.
Here's the code:
static int requestCounter=0;
-(void)requestDataForRow:(NSNumber *)rowIndex
{
requestCounter++;
//NSLog(#"requestDataForRow: %i", [rowIndex intValue]);
PipeListHeavyCellData *cellData=[Database pipeListHeavyCellDataWithJobID:self.jobID andDatabaseIndex:rowIndex];
[rowData setObject:cellData forKey:[NSString stringWithFormat:#"%i", [rowIndex intValue]]];
requestCounter--;
NSLog(#"cellData.number: %#", cellData.number);
if (requestCounter==0)
{
//NSLog(#"reloading pipe table view...");
[self.pipeTableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
};
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *MyIdentifier = #"pipeCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
[[NSBundle mainBundle] loadNibNamed:#"PipesForJobCell" owner:self options:nil];
cell = pipeCell;
self.pipeCell = nil;
PipeListHeavyCellData *cellData=[[PipeListHeavyCellData alloc] init];
if ([rowData objectForKey:[NSString stringWithFormat:#"%i", indexPath.row]]==nil)
{
//NSLog(#" nil data for row: %i", indexPath.row);
[self performSelectorInBackground:#selector(requestDataForRow:) withObject:[NSNumber numberWithInt:indexPath.row]];
}
else
{
//NSLog(#" has data for row: %i", indexPath.row);
PipeListHeavyCellData *heavyData=[[PipeListHeavyCellData alloc] init];
heavyData=(PipeListHeavyCellData *)[rowData objectForKey:[NSString stringWithFormat:#"%i", indexPath.row]];
cellData._id=[heavyData._id copy];
cellData.number=[heavyData.number copy];
cellData.status=[heavyData.status copy];
};
This code works, everything is OK, BUT my table has 2000 rows and If users scrolls from cell with index 10 to cell with index 2000 very quickly. He must wait for a long time until all pulling data requests will complete (for rows 11, 12, 13, ..., 2000) cause that rows became visible while user was scrolling table view so the method requestDataForRow was called for them.
How can I optimize those things?

I had to do something similar. You'll need to create a a queue that processes the most recently added items first.
For example, the user opens the table and 10 requests are queued up. You dequeue the first object and start fetching the data for the first row. However, the user then scrolls down to rows 31-40. You'll then have to insert those rows before the first 10 in your queue because they are now higher priority. The key is that you don't immediately start 10 requests at once, but process them in order. That way, when the user scrolls, you only "waste" one request - the last request that was made.
An easy way to actually implement this is to use [tableView indexPathsForVisibleRows].

Related

Slow Scrolling UITableView?

My UITableView, after the messages (content) is loaded into the cells, experiences a very noticeable lag in scrolling and sometimes freezes up for a few seconds. This is weird because all the messages are loaded once the user scrolls. Any ideas on how to make this fast scrolling no problem?
Thank you!
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *simpleTableIdentifier = #"MailCell";
MailCell *cell = (MailCell *)[tableView dequeueReusableCellWithIdentifier:simpleTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MailCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
// Anything that should be the same on EACH cell should be here.
UIView *myBackView = [[UIView alloc] initWithFrame:cell.frame];
myBackView.backgroundColor = [UIColor colorWithRed:40.0/255.0 green:148.0/255.0 blue:196.0/255.0 alpha:1];
cell.selectedBackgroundView = myBackView;
cell.messageText.textAlignment = NSTextAlignmentLeft;
cell.messageText.lineBreakMode = NSLineBreakByTruncatingTail;
}
NSUInteger row = [indexPath row];
// Extract Data
// Use the message object instead of the multiple arrays.
CTCoreMessage *message = [[self allMessages] objectAtIndex:row];
// Sender
CTCoreAddress *sender = [message sender];
NSString *senderName = [sender name];
// Subject
NSString *subject = [message subject];
if ([subject length] == 0)
{
subject = #"(No Subject)";
}
// Body
BOOL isPlain = YES;
NSString *body = [message bodyPreferringPlainText:&isPlain];
body = [[body componentsSeparatedByCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]]
componentsJoinedByString:#" "];
body = [body stringByReplacingOccurrencesOfString:#" " withString:#" "];
// Populate Cell
[[cell nameText] setText:senderName];
[[cell subjectField] setText:subject];
[[cell messageText] setText:body];
if ([message isUnread])
{
cell.nameText.textColor = [UIColor colorWithRed:15.0/255.0 green:140.0/255.0 blue:198.0/255.0 alpha:1];
}
else
{
cell.nameText.textColor = [UIColor blackColor];
}
return cell;
}
xCode comes with a profiler called Instruments. It's CPU time profiler is perfect for figuring out which code is slowing things down. Run your app with the profiler and spend a few seconds just scrolling around. It will give you statistics.
Keep in mind, the code inside if (cell == nil) will run about 10 times (UITableView caches just enough cells to fill itself). But the code outside the if is expensive - it runs every time a cell becomes visible.
I would guess the most expensive operations in the code you posted are:
Giving iOS too many subviews to draw on a cell
Do your own drawing instead.
Replacing runs of whitespace in the entire body text with single spaces
The code you posted allocates new strings for each word, plus an array to hold them. Then it allocates two more copies (one with words rejoined and one with runs of spaces compacted). It processes the entire body text string, even if the majority will never be visible to the user in a tiny preview of the body!
Cache the resulting string so that this operation is performed only once per cell.
Also, you can create a new mutable string, reserve space in it, and copy characters from the original in a loop (except runs of whitespace). Instead of processing the entire body text, you could stop at 100 characters or so (enough to fill a table cell). Faster and saves memory.
Slow UITableView scrolling is a very very common question. See:
How to solve slow scrolling in UITableView
iPhone UITableView stutters with custom cells. How can I get it to scroll smoothly?
Nothing seems wrong with your code. I'd recommend using a table optimization framework such as the free Sensible TableView.

Image in tableviewcell slow down scrolling

My tableview consists of images loading from server, this slow down my tableview scrolling. Below is my code . any idea, please help me.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomTblViewCellFacetoface *cell = (CustomTblViewCellFacetoface *) [tableView dequeueReusableCellWithIdentifier:#"cellA"];
if (cell == nil)
{
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomTblViewCellFacetofaceNib" owner:Nil options:nil];
for (id currentObject in topLevelObjects)
{
if ([currentObject isKindOfClass:[UITableViewCell class]])
{
cell = (CustomTblViewCellFacetoface *) currentObject;
break;
}
}
}
// configure cell
IStructFacetofaceRequests *objappointmentdetails = [M_ArrFacetofaceRequests objectAtIndex:indexPath.row];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.backgroundColor = [UIColor clearColor];
cell.m_CtrllblName.text = objappointmentdetails.m_strUsername;
cell.m_CtrllblVenue.text = [NSString stringWithFormat:#"Venue : %#",objappointmentdetails.m_strVenue];
cell.m_CtrllblDate.text = [NSString stringWithFormat:#"%# - %#",objappointmentdetails.m_strStartDate,objappointmentdetails.m_strEndDate];
[cell.m_CtrllblName setTextColor:[UIColor colorWithRed:(113/255.f) green:(113/255.f) blue:(113/255.f) alpha:1.0f]];
[cell.m_CtrllblVenue setTextColor:[UIColor colorWithRed:(113/255.f) green:(113/255.f) blue:(113/255.f) alpha:1.0f]];
[cell.m_CtrllblDate setTextColor:[UIColor colorWithRed:(113/255.f) green:(113/255.f) blue:(113/255.f) alpha:1.0f]];
cell.m_CtrllblName.font=[UIFont fontWithName:#"Arial-BoldMT" size:16];
NSData * imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString: objappointmentdetails.m_strImageurl]];
cell.m_CtrlImgViewUser.image=[UIImage imageWithData: imageData];
[cell.m_CtrlBtnView addTarget:self action:#selector(MoveToNextView:) forControlEvents:UIControlEventTouchUpInside];
cell.m_CtrlBtnView.tag=indexPath.row;
return cell;
}
this is the code i used in cellfor row at index path.
You can solve that by using Asynchronous loading..
please go through this tutorial..
http://www.markj.net/iphone-asynchronous-table-image/
You can use UITableView lazy loading .
here an sample from apple
and u can see this project at github
Yes, UITableView is designed around the assumption of lazy loading. When you make a UITableView, you do not load any of the entries at all. Instead you wait for the system framework to call your cellForRowAtIndexPath method. That method gets called once for every cell that needs to be loaded, and that is just for the cells that are visible at the time. As the user scrolls the table view, new cells come into view, and your cellforRowAtIndexPath method gets called again for each new cell coming into view.
Now that is the general principle. But if your cells are being populated by data from a server, then there are some additional considerations. You could structure your cellForRowAtIndexPath to be as lazy as possible and call the server for each and every cell that comes into view. But the network delays would make the user experience really awful. So it is to your advantage to buffer up a number of cells worth of data ahead of time in a data structure other than the cells in a UITableView. That way, when cellForRowAtIndexPath gets called, you will be able to quickly supply the contents of the cell by constructing it from the buffered data. Exactly how much data to buffer depends on how large each data element is, and what else your application is doing in the way of memory allocation. Offhand, I see nothing wrong with buffering 500 to 1000 cells worth of data in your app. But don't mix up your buffering of the data with the UITableView's queuing and reusing of cells. There is no reason to maintain a large number of cells ready to go - just the data that goes into those cells.

iPhone : friend list, how did Facebook accomplished that?

I wonder if anyone can speculate or better yet provide a piece of code as for the implementation of the lengthy friends list in the Facebook iPhone app.
when you open the app and go strait to the friends list, you get the list almost in an instant, at least for me with ~500 friends.
when I try it in my own app it takes lots of precious seconds to populate the table view with the same data, so how does Facebook accomplished such a quick response time ?
upon looking at the tableview in the facebook app you notice there is no scroll bar usually found in such tableview, could that be one sign of the neat trick facebook is utilizing to achieve this rapid rows insert ? could it be they implemented some sort of a virtual tableview with only holds a few dozen rows but rotates them ?
any thoughts ?
the UITableView will let you do this. There are a number of examples on the internet with UITableView and Custom Cell's
Essentially, you load your images in the background, and you reuse the Cells that are in the tableview
EDIT Added example code to demonstrate how this is accomplished.
IMPORTANT NOTE
This code was not tested and may or may not actually function as is.
It was pasted with some editing for length. I did a lot more then this in my app, but in the interest of keeping with the example requested I omitted a lot.
On with the example:
Here is where I get the cell, load it with the items that are readily available. And send it to the background thread to load the rest.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"OfferCell";
static NSString *CellNib = #"OfferItem";
OfferCell* cell = (OfferCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CellNib owner:self options:nil];
cell = (OfferCell*)[nib objectAtIndex:0];
}
NSDictionary* couponPackage = [self.jsonOfferData valueForKey:#"result"];
NSArray *couponList = [couponPackage valueForKey:#"offers"];
if ([couponList count] >= indexPath.row )
{
NSDictionary* couponData = [couponList objectAtIndex:indexPath.row];
Coupon *coupon = [[Coupon alloc] initWithDictionary:couponData];
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:cell,#"cell",coupon,#"coupon", nil];
//Right here you would try to load any cached imaged from disk.
//Then send a Thread to the background to load the image.
[self performSelectorInBackground:#selector(loadTableViewCellData:) withObject:params];
//Load up the rest of the custom info into the custom cell.
[cell.captionLabel setText:coupon.name];
[cell.subTextLabel setText:coupon.subText];
[cell setAccessoryType:UITableViewCellAccessoryDetailDisclosureButton];
[cell setCommand:coupon.command];
[cell setParameter:coupon.commandArgs];
[cell setImageURL:coupon.imageURL];
[cell setImageAltURL:coupon.imageAltURL];
[cell setRegistrationCode:coupon.registrationCode];
[coupon release];
}
return cell;
}
as you can see, i call a background thread before i even load the custom content in the cell.
- (void) loadTableViewCellData:(NSDictionary*) objectData
{
OfferCell *cell = [objectData objectForKey:#"cell"];
Coupon *coupon = [objectData objectForKey:#"coupon"];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[coupon iconURL]]]];
[objectData setValue:image forKey:#"image"];
self performSelectorOnMainThread:#selector(setImageOnMainThread:) withObject:objectData
}
after downloading the image, i send a Main thread request to update the Image that is in the cell object.
- (void) setImageOnMainThread:(NSDictionary*) objectData
{
OfferCell *cell = [objectData objectForKey:#"cell"];
Coupon *coupon = [objectData objectForKey:#"coupon"];
UIImage *image = [objectData objectForKey:#"image"];
cell.icon.image = image;
}
##AGAIN This May not Actually Function. ##
I did not copy all of my code for this. this is a hammer out so you can get the idea.
play with the code and test it. but the fundamentals are.
Dequeue the cell that will fit your needs (Reuse Identifier)
Use the cell if it can be dequeue'd or create a new one with a reuse identifier (my example uses a xib file named OfferItem.xib)
Send a thread to the background that will load the image data from disk or url (a combination of both is recommended)
Send a thread back to the UI when you are ready to load the image into the View (Updating the UI must be done on the main thread)
if you can do that, then your friends list (or in this case offers) will be loaded up as fast as possible. and the Images will pop on the screen as soon as they download.
Also if you use a Caching technique it will be faster for subsequent loads because in the the first method {tableView:cellForRowAtIndexPath:} you would load up the cached image immediately.
Aside from that, this should load your cell's pretty fast.
They obviously load the data from a local resource (plist, ManagedObject, ...)
Have a look at some sample code to draw a TableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *kCellIdentifier = #"MyCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:kCellIdentifier] autorelease];
}
return cell;
}
The dequeueReusableCellWithIdentifier: thing is one reason why TableViews in iOS can draw quickly. It works somehow like this:
1)You provide an identifier for a cell you're creating.
2)Cells that are visible at first get alloced (with identifier)
3)When a Cell is moved off the screen it gets put on a pile MyCellIdentifier
4)Whenever the system needs to draw a cell of identifier:MyCellIdentifier it first looks whether there are any cells currently unused on the MyCellIdentifier pile. If that's the case it picks one off the pile and thus doesn't have to alloc a new one. That way expensive allocing can be kept at a minimum.
I hope this answers your question :)

UITableView's memory leaking issue

Hi All
I have a UITableView with the list of items fetched from sqllite. But there's a memory leak when rendering the view. Following is my cellForRowAtIndexPath method.
static NSString *CellIdentifier = #"BarListItemCell";
BarListItemViewCell *cell = (BarListItemViewCell *)[tableView
dequeueReusableCellWithIdentifier: CellIdentifier];
if (cell == nil) {
NSArray * nib = [[NSBundle mainBundle] loadNibNamed:#"BarListItemViewCell" owner:self options:nil];
for (id cellObject in nib) {
if ([cellObject isKindOfClass : [BarListItemViewCell class]]) {
cell = (BarListItemViewCell *) cellObject;
//break;
}
}
NSString * key = [keys objectAtIndex:[indexPath section]];
NSDictionary * unit = [[barListDataSource objectForKey:key] objectAtIndex:[indexPath row]];
NSLog(#"unit count is %d", [unit retainCount]);
cell.name.text = [unit objectForKey:#"name"];
cell.address.text = [unit objectForKey:#"address1"];
cell.features.text = [unit objectForKey:#"features"];
cell.logo.image = [UIImage imageWithData:[unit objectForKey:#"logo"]];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
//cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
return cell;
You can see the line "NSLog(#"unit count is %d", [unit retainCount]);". It's very strange that after viewDidLoad, the console displays 3 lines "unit count is 2" (I have 3 items in the whole screen). But when I drag the screen to let the UITableView show the next item, the console displays "unit count is 1". When the [tableView reloadData] method is called, the console displays also "unit count is 1". So it seems that the UITableView will release the datasource automatically. That's why I keeps memory reference count of unit being 2, otherwise, over releasing will happen. But the cost is that the memory occupied by unit will never be freed!
Do not call -retainCount.
The absolute retain count of an object is meaningless.
You should call release exactly same number of times that you caused the object to be retained. No less (unless you like leaks) and, certainly, no more (unless you like crashes).
See the Memory Management Guidelines for full details.
There is nothing strange about the retain count of unit from what you describe. Your code appears to be printing the retain count of an object that may or may not have been created during the current pass through the event loop and, thus, might have been retain/autoreleased multiple times. Or not. It doesn't matter.
What does matter is what the Allocations Instrument shows. Do you see an accretion of objects over time? If so, what are they and what is retaining them (or not releasing them)?
If you do, it might be the case that Heapshot analysis would prove useful to you.

EXC_BAD_ACCESS on ReloadData

I'm loading data dynamically from an API using a UISearchBar and trying to display it, using something like this:
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
NSLog(#"Search Text: %#",[searchBar text]);
[self startSearchingWithText:[searchBar text]];
}
- (void) startSearchingWithText:(NSString *)text {
...
// Go through the list of services and set up a query for the search term
QueryServiceManager *manager = [[QueryServiceManager alloc] initWithServices: services];
// Set up the query
if (![manager addKeyword: text])
NSLog(#"PROBLEM!");
// Execute the query and store the titles in an array
self.data = [[manager makeQuery] copy]; // Returns a NSArray
NSLog(#"%#",self.data);
// Add the items to the tableView
[self.tableView reloadData];
My UITableViewController code is set up to read from self.data, which is initially a NSArray of #"" elements, as such:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
[self.data count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
// Set up the cell...
cell.textLabel.text = [self.data objectAtIndex:indexPath.row];
return cell;
}
All I get when I do this is:
(gdb) continue
2010-02-26 18:43:32.515 iSave[11690:207] (
"Toshiba Satellite L505-GS5037 TruBrite 15.6-Inch Laptop (Black)",
"Dell Inspiron 11 11.6-Inch Obsidian Black Laptop (Windows 7 Premium)",
"ASUS Eee PC Seashell 1005PE-MU17-BK 10.1-Inch Black Netbook - Up to 11 Hours of Battery Life",
"SwissGear Computer Backpack (Red)",
"Belkin 36-Piece Demagnatized Computer Tool Kit with Case (Black)",
"Compaq EVO N610C",
"Belkin F8E062 55-Piece Computer Tool Kit with Black Case (Demagnetized Tools)",
"ASUS Eee PC Seashell 1005PE-PU17-BK 10.1-Inch Black Netbook - Up to 14 Hours of Battery Life",
"Harman Kardon SoundSticks II 2.1 Plug and Play Multimedia Speaker System",
"ION Audio VCR 2 PC USB VHS Video to Computer Converter"
)
(gdb) continue
Program received signal: “EXC_BAD_ACCESS”.
(gdb)
Any ideas? It looks like the NSArray is being populated properly, then things are failing on/after the reloadData call (breakpoints confirm this, but can't isolate where things are going wrong)
EDIT: I replaced theNSLog() with enumeration
for ( NSString *elem in self.data )
NSLog(elem);
And it still crashes. I managed to coax a log out of it: http://pastebin.com/NDVKLsJC
When I remove the NSLog() entirely, it doesn't crash, but the UITableViewdoesn't update, either.
Have you identified the object which you're trying to access but which no longer exists, the object which is giving EXC_BAD_ACCESS?
If not then you should enable Zombies in your app, then you can issue a command to gdb which will tell you which object is causing the crash.
Something weird: You're saying that you only have 1 row in each section, yet you're using indexPath.row to index into your array? One of those is wrong, and I'm guessing it's the first. Usually if you're displaying an array (of n elements) in a UITableView, you have one section and n rows in that section, which means you'd use [myArray objectAtIndex:indexPath.row] to retrieve the appropriate object.
If you want a separate section for each item in the array, then you say that you have n sections, and 1 row in each section, and then use [myArray objectAtIndex:indexPath.section] to retrieve the appropriate object.
Oh, and you're leaking memory all over the place.
What about:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.data count];
}
instead of always returning 1.
Hit shift-command-b to analyze the project and solve all problems displayed (potential leaks, etc.)