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.
Related
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.
This is the code that i am using to display my tableview custom cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"CustomCell";
CustomeCellHome *cell = (CustomeCellHome *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomeCellHome" owner:self options:nil];
for (id currentObject in topLevelObjects){
if ([currentObject isKindOfClass:[UITableViewCell class]]){
cell = (CustomeCellHome *) currentObject;
break;
}
}
}
NSString *strImgUrl = [[[[myarray valueForKey:#"mykey1"]objectAtIndex:indexPath.row] valueForKey:#"url"] objectAtIndex:0];
[[AsyncImageLoader sharedLoader] cancelLoadingImagesForTarget:cell.imgView];
cell.imgView.imageURL = [NSURL URLWithString:strImgUrl];
cell.lbl1.text = [[myarray valueForKey:#"mykey2"]objectAtIndex:indexPath.row];
cell.lbl2.text = [[myarray valueForKey:#"mykey3"]objectAtIndex:indexPath.row];
cell.lbl3.text = [[myarray valueForKey:#"mykey3"]objectAtIndex:indexPath.row];
cell.lbl4.text = [[myarray valueForKey:#"mykey4"]objectAtIndex:indexPath.row];
cell.lbl5.text = [[myarray valueForKey:#"mykey5"]objectAtIndex:indexPath.row];
cell.lbl6.text = [[myarray valueForKey:#"mykey6"]objectAtIndex:indexPath.row];
}
CustomeCellHome is sub class of UITableViewCell and i am using Nib to display it. CustomeCellHome have all the IBOutlet and connected to Nib. All IBOutlet is property and synthesize and i have released that in dealloc method.
I am displaying 50 records in tableview at a time. If user want to see more record he can see that by clicking on next button and also view the previous record by clicking on previous button.
When user presses next button i am calling web service and getting next 50 record from the server. I am keeping my previous 50 records in the array and adding this new records to that array. But in "myarray" i am keeping only 50 at a time to display in tableview (Means i am displaying 50 records only at a time).
When i reach to view the 850 to 900 or near to that record my table view is getting stretched and it will stopped and displaying after some time.
Also after viewing 1450 to 1500 or near to that record i am receiving memory warning and my app getting crashed. I am not getting this. Why is it so ?
I have checked with memory tool i didn't get any memory leak. Am i using wrong to display the custom cell or my web service call causes more memory ?
Please any one can guide me for this. Thanks in advance.
It sounds like you are consuming too much memory. You don't need to leak memory to have your app shot by the watchdog.
I would profile the memory consumption of your app as you load more and more data. You'll probably want to respond to a memoryWarning message and delete all of your objects except for the current set. You could (de)serialize the objects to disc to preserve user's bandwidth.
You don't show the lifespan of your objects, but assuming they are fetched/stored in your UI(Table)ViewController you'll want to implement code in - (void)didReceiveMemoryWarning or - (void)viewDidUnload (iOS5 vs. iOS6) to clear out the older data.
SideNote: is myarray actually a dictionary? It's good practice to give your variables descriptive names to make your code easier to understand (for others, and yourself in 2 months when you need to fix a bug).
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].
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 :)
this code works fine until I start scrolling:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
Route *r = [data routeForDay:day index:indexPath.row];
cell.textLabel.text = r.name;
cell.imageView.image = r.image;
return cell;
}
It works perfectly for every row until I scroll down, when it crashes with objc_msgsend on cell.imageView.image = r.image; I've confirmed that nothing is nil, I've even checked the retainCount for everything involved. I'm completely at a loss, any ideas? Thanks.
Edit:
I solved the problem, but do not understand how the change in code makes the bug go away, so I'd appreciate a hint if anyone knows.
This is how the image was initially created, in the Route init method. image became deallocated when the table was scrolled, in my tableview controller.
NSString *imagePath = [[NSBundle mainBundle] pathForResource:[dict valueForKey:#"image"] ofType:#"png"];
image = [UIImage imageWithContentsOfFile:imagePath];
When I changed the second line to
image = [[UIImage alloc] initWithContentsOfFile:imagePath];
it worked fine.
I'm just a little confused and unhappy.
Your bug is most definitely a retain/release problem. Try to turn on zombies. If you don’t know how to do that, see below and read this tech note.
I assume one of the objects pointed to by data, r, or r.image is not retained properly.
One more thing: don’t look at retainCount before understanding memory management (and especially autorelease pools) in depth. Otherwise you’ll only be confused by the returned value.
How to enable zombies:
Choose Project > Edit Active Executable to open the executable Info window.
Click Arguments.
Click the add (+) button in the “Variables to be set in the environment” section.
Enter NSZombieEnabled in the Name column and YES in the Value column.
Make sure that the checkmark for the NSZombieEnabled entry is selected.
Edit:
Congratulations on finding the bug. The only pice of the puzzle you’re still missing is understanding Cocoa’s memory management. I recommend that you read the official documentation, as it is concise and easy to read.
In short: imageWithContentsOfFile: returns an autoreleased object, while initWithContentsOfFile: returns a retained object. But again: read the docs, or you’ll continue having memory errors.