memory retention/leak on iOS - ios

I have a UItableViewCell defined from a XIB file. This cell contains a UIScrollView, which is filled on the fly with UIView Objects - viewXY -. I use arc, everywhere in the code. The UI looks like:
cell 0: [ view00 view01 view02 ...]
cell 1: [ view10 view11 view12 ...]
etc..., where [ ] is a cell content, which is horizontally scrollable.
Problem: I profiled the app, and as I scroll down vertically, the memory footprint grows rapidly, and as far as I could see, never goes back to normal, nor reaches a plateau. More importantly, if I scroll down a few cells and then back up, memory increases too....
I checked for leaks with the instrument tool, nothing.
I narrowed down the problem around the part of the code that creates the viewXY . If I use:
myDisplay* viewXY;//myDisplay is a UIView subclass
-->viewXY = [[NSBundle mainBundle] loadNibNamed:AW_UI_VE owner:self options:nil][0];
[scrollView addSubview:viewXY];
Memory grows out of control as I flip through the cells. The NIB is a simple UIView+UIImageView+2 Labels.... If I replace say
UIImageView *viewXY = [UIImageView alloc] initWithImage:[blablabla]]
[scrollView addSubview:viewXY];
Everything is fine. The Live Bytes in instruments reaches rapidly a plateau.
As part of the tests, I load the NIB file without exercising any of the methods defined in its custom class, so it comes out with the IBOutlets as defined in the IB (default label text, image etc...).
This stuff is driving me crazy.
Suggestions to understand what I'm doing wrong are most welcome.

In your ViewController's viewDidLoad you should have something like this:
[yourTable registerNib:[UINib nibWithNibName:#"yourUITableViewCellNibName" bundle:nil]
forCellReuseIdentifier:#"reuseIdentifier"];
and in your table datasource method(cellForRowAtIndexPath) you should have the following:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCustomCell* cell = [tableView dequeueReusableCellWithIdentifier:#"reuseIdentifier"];
//Set your cell's values.
return cell;
}

I’m not sure if this is related to the memory usage, but the docs recommend UINib for this use case:
Your application should use UINib objects whenever it needs to repeatedly instantiate the same nib data.
Another possibility is that there is a retain cycle in myDisplay. Does it have any strong properties that should be weak?

[scrollView addSubview:viewXY];
There is your problem. You're adding something to the scroll view, and never remove it.
From the sound of things, you aren't using the UITableView properly. You should have a view controller that impliments methods from UITableViewDelegate and UITableViewDataSource protocols, and use those to fill the table with data. You never add anything to the table directly, you only provide it with cells when it requests them.

Related

Using dequeueReusableCellWithIdentifier does not appear faster (or lower memory)?

When I want to create a table with custom cells, this is how I will write the standard codes:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"CustomCell";
CustomCell *cell = (CustomCell*)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
cell = [topLevelObjects objectAtIndex:0];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
else
{
NSLog(#"reuse!");
}
cell.property1 = ....
cell.property2 = ....
return cell;
}
This is roughly how I will create the custom cells:
create a CustomCell.h (inheriting UITableViewCell) and CustomCell.m and a CustomCell.xib
in the CustomCell.xib, change the 'class' property of the top level UIView to 'CustomClass'
I have been doing this for sometime but today I finally decided to do a test to see if the table is really reusing the cells correctly.
Nope. The table is NOT reusing any cell!. The NSLog(#"reuse!") in the code snippet above is never triggered.
I decided to do a performance comparisons, with a tableview with 1 million cells, using these two methods:
Method #1 used the method described above. Take note that UIView is the top level view in my CustomCell.xib and no reuse identifier is configured on xib
cell not reused (NSLog reused! not printed)
peak memory about 3.4MB, peak CPU 60%
scrolling is smooth
Method #2 uses a UITableViewCell in the top level of the xib, and I put all my controls on the content view of this UITableViewCell. In the XIB, I configured the reuse identifier of this cell to be 'CustomCell'.
cell reused (NSLog reused printed multiple times)
peak memory about 6.1MB, peak CPU about 88%
scrolling is smooth
Two questions:
Why am i not seeing much lower CPU and memory usage on method #2, isn't method #1 wrong and method #2 right because method #1 is not reusing any cell at all? Or looking from another point of view, why method #1 is still scrolling so well even though it is stupidly loading nib again and again?
When creating a custom xib for a custom cell, does it make any difference whether the top level object is a UIViewnor UITableViewCell? (Looks like no difference?)
It seems that reuseIdentifier is not set properly in your XIB file.
You can set it programmatically also by adding
[yourTable registerNib:[UINib nibWithNibName:#"CustomCell" bundle:nil] forCellReuseIdentifier:#"CustomCell"];
somewhere before using table view. In viewDidLoad for example.
UPDATE
Question 1. If you do not set cell reuse identifier cells life circle is following:
1.It is created.
2.It is shown.
3.It is moved out of visible area.
4.It is not needed anymore so it is deleted.
If you use reuse identifier cells TYPICAL life circle is:
1.It is popped from reusable queue..
2.It is shown.
3.It is moved out of visible area.
4.It is pushed to reusable queue.
So performance difference is due to what is executed faster: create/release operation or pop/push. Memory usage should be approximately the same for both variants.
you need implement
-(NSString *)reuseIdentifier {
return #"CustomCell";
}
in you cell class CustomCell, because reuseIdentifier is readonly property you can't set it from outside

UITableViewCell being reused while still on-screen?

I have what probably seems like a really weird problem (it does to me!)
I am using a UITableView to display cells which each contain a UIWebView. I realise that this is a bad idea on the face of it, but I can't really do this any other way.
I am caching the heights of each cell when the UIWebView finishes loading, and then calling:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[cellIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
All of the germane code is in a Gist here.
I also have the UIWebViews cached in a dictionary on the data source, so it can be reused when the cell is reloaded.
This seems to sort of work, but I am encountering a lot of issues whereby the cells' contents will randomly disappear. I have added some logging into determine what's going on, and in what order, and it seems like some of the cells are being reused while they're still on-screen.
I see this in my logs while scrolling down:
2014-02-11 13:45:49.091 EApp[45936:70b] Generating cell for 1: Panning
2014-02-11 13:45:49.245 EApp[45936:70b] Generating cell for 2: Calibration
2014-02-11 13:45:50.063 EApp[45936:70b] Generating cell for 3: Aperture Priority
2014-02-11 13:45:50.063 EApp[45936:70b] Reusing cell: Stopping down
"Stopping down" in this case is a cell that is still on-screen. The "generating cell" items are logged inside the data source's cellForRowAtIndexPath and the "reusing" messages inside the cells' prepareForReuse.
Does anyone know what could be happening here? I know this seems complex.
The following line in your prepareForReuse is probably the culprit:
if ([self.contentWebView isDescendantOfView:self.contentWebView]) {
[self.contentWebView removeFromSuperview];
}
As the contentWebView is never a descendant of itself, it will not be removed from the cell, and the contentView will contain two webviews after the cellForRowAtIndexPath:
You probably meant to say:
if ([self.contentWebView isDescendantOfView:self.contentView]) {
[self.contentWebView removeFromSuperview];
}
Or simply:
[self.contentWebView removeFromSuperview];
One of the features/limitations of UITableView is that you don't know if, and can't depend on, a cell is being created or reused. You should always be able to handle both.
GENERALLY, when you call -reloadRowsAtIndexPaths:withRowAnimation:, you will get the cell from that indexPath to reuse. If that indexPath was on screen, it will be a cell that was on screen.
I don't know if it's the problem, but in the code you provided, you don't even initialize your cell...
I'm even surprise it works.
static NSString *CellIdentifier = #"FeedItemCell";
EFeedItemCell *cell = [self.tableViewController.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
you should add to it :
if (!cell) {
cell = [EfeedItemCell alloc] initWithReus....];
}
From your code it seems that you are caching the webViews and then are adding them to cells programmatically. This can create random problems similar to what I had faced in the passed.
You must use EFeedItemCellWebView in your storyboard. Just add a UIWebView and change the class name to your custom class. And then when the data is loaded just simply change its contents in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

setNeedsLayout relayouts the cell only after is has been displayed

I have a table view with cells, which sometimes have an optional UI element, and sometimes it has to be removed.
Depending on the element, label is resized.
When cell is initialised, it is narrower than it will be later on. When I set data into the label, this code is called from cellForRowAtIndexPath:
if (someFlag) {
// This causes layout to be invalidated
[flagIcon removeFromSuperview];
[cell setNeedsLayout];
}
After that, cell is returned to the table view, and it is displayed. However, the text label at that point has adjusted its width, but not height. Height gets adjusted after a second or so, and the jerk is clearly visible when all cells are already displayed.
Important note, this is only during initial creation of the first few cells. Once they are reused, all is fine, as optional view is removed and label is already sized correctly form previous usages.
Why isn't cell re-layouted fully after setNeedsLayout but before it has been displayed? Shouldn't UIKit check invalid layouts before display?
If I do
if (someFlag) {
[flagIcon removeFromSuperview];
[cell layoutIfNeeded];
}
all gets adjusted at once, but it seems like an incorrect way to write code, I feel I am missing something else.
Some more code on how cell is created:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ProfileCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
[cell setData:model.items[indexPath.row] forMyself:YES];
return cell;
}
// And in ProfileCell:
- (void)setData:(Entity *)data forMyself:(BOOL)forMe
{
self.entity = data;
[self.problematicLabel setText:data.attributedBody];
// Set data in other subviews as well
if (forMe) {
// This causes layouts to be invalidated, and problematicLabel should resize
[self.reportButton removeFromSuperview];
[self layoutIfNeeded];
}
}
Also, if it matters, in storyboard cell looks like this, with optional constraint taking over once flag icon is removed:
I agree that calling layoutIfNeeded seems wrong, even though it works in your case. But I doubt that you're missing something. Although I haven't done any research on the manner, in my experience using Auto Layout in table cells that undergo a dynamic layout is a bit buggy. That is, I see herky jerky layouts when removing or adding subviews to cells at runtime.
If you're looking for an alternative strategy (using Auto Layout), you could subclass UITableViewCell and override layoutSubviews. The custom table cell could expose a flag in its public API that could be set in the implementation of tableView:cellForRowAtIndexPath:. The cell's layoutSubviews method would use the flag to determine whether or not it should include the optional UI element. I make no guarantees that this will eliminate the problem however.
A second strategy is to design two separate cell types and swap between the two in tableView:cellForRowAtIndexPath: as necessary.
You've added additional code to the question, so I have another suggestion. In the cell's setData:forMyself: method, try calling setNeedsUpdateConstraints instead of layoutIfNeeded.

UICollectionView's cell disappearing

What's happening
Currently I have an application that uses two UICollectionViews inside a UITableView. This way I create a Pulse News look like application. My problem with this is that sometimes the 6th and 11th row disappears completely, leaving a blank space where it should be the cell. I wouldn't actually mind, if all the cells were like this (and this way I could assume that I wasn't doing things correctly), but the thing is, is just happening with those specific ones.
My theory
The 6th and 11th rows are the ones that appears when I start scrolling, so by default I am able to see 5 cells, and when I do the first horizontal scrolling the 6th comes up (blank space sometimes).
What I have
The only thing I am doing at the moment is this:
[self.collectionView registerNib:[UINib nibWithNibName:CELL_NIB_NAME bundle:nil] forCellWithReuseIdentifier:CELL_IDENTIFIER];
On the viewDidLoad. And on the creation of the cell:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CELL_IDENTIFIER forIndexPath:indexPath];
NSMutableDictionary *dictionary = [self.DataSource objectAtIndex:[indexPath row]];
[cell buildViewWithDictionary:dictionary withReferenceParent:self.referenceViewController];
return cell;
}
So on my understating nothing fancy going on here. I though there was something wrong on the data source (a dummy JSON file), but sometimes it works ok and the cell shows, so I guess from that part is ok.
So my "question": Does anyone knows what's going on? I don't really like to say that it's a bug from iOS, but I can't think of anything else.
Edit 1.0
The amazing part is that this method
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
Is going from indexPath [0,4] to [0,6] without calculating the [0,5]. First time I actually see this happening in iOS.
Edit 2.0
I have switched the way I am creating the cells, and instead of dequeuing I am using the old way:
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:CELL_NIB_NAME owner:self options:nil];
MyCell *cell = (MyCell *)[nib objectAtIndex:0];
Still the same sad result.
So, what did work?
1) Subclass UICollectionViewFlowLayout.
2) Set the flowLayout of my UICollectionView to my new subclass.
3) On the init method of the UICollectionViewFlowLayout subclass, set the orientation you want:
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
In my case it is Horizontal.
4) The important part:
-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
At this moment, I should theorise a bit, but honestly I don't have a clue.
The above answers didn't work for me, but after downloading the images, I replaced the code
[self.myCollectionView reloadData]
with
[self.myCollectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
to refresh the collectionview and it shows all cells, you can try it.
None of the solutions given by anyone helped me in my custom layout that we need to have in our app.
Instead, I had to do this: (And yeah, IT WORKS!!!)
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect{
CGSize size = [self collectionViewContentSize];
rect.size.height = size.height*2;
NSArray *atrributes_Super = [super layoutAttributesForElementsInRect:rect];
return atrributes_Super;
}
After all, UICollectionView is just looking for the attributes for the elements to be displayed in your screen's rect.
Rob's tip about the bug helped me. The bug states that if the section insets and cells widths and spacing add up exactly to the width of the screen then the first column sometimes randomly dissappears and reappears for some cells in some places. Rather than subclass or change the cell widths, I changed the section insets for left and right in my storyboard from 6 to 4 and it I haven't seen the problem again.
As I run the same problem suddenly and spent some time figuring out one of possible reasons of cell disappearing during the scroll, I will add my answer as well.
Prerequisites of the problem:
You have a UICollectionView instance
You have a UICollectionViewFlowLayoutSubclass
The problem
Cells disappear from the Collection View after scrolling to the certain point.
The source of the problem is in wrong subclassing of the UICollectionViewFlowLayout.
As it explicitly said in documentation:
Every layout object should implement the following methods:
- collectionViewContentSize
- layoutAttributesForElements(in:)
- layoutAttributesForItem(at:)
- layoutAttributesForSupplementaryView(ofKind:at:) // (if your layout supports -supplementary views)
-layoutAttributesForDecorationView(ofKind:at:) // (if your layout supports decoration views)
- shouldInvalidateLayout(forBoundsChange:)
By relying on UICollectionViewFlowLayout implementation of methods above we miss the fact, that func layoutAttributesForElements(in rect: CGRect) and collectionViewContentSize will generate wrong contentSize (the size that would be correct if all the cells would have itemSize size and the content size would be corresponding. As soon as scroll offsetY will be greater that contentSize height cell will all disappear.
The solution
The solution is in proper UICollectionViewFlowLayout subclassing. Override all the methods that are required to override and everything will work just fine.
In my case (vertical scroll, with cells disappearing in first view), cells were disappearing due to incorrect estimated size. It seems, UICollectionView uses the estimated size to calculate the items to load in first view. I'd set the estimated size too high which was resulting in wrong calculations for number of items to load in first screen.
The moment I made the estimated height bit low, all the cells appeared correctly.
We ran into disappearing cells recently and found that rather than skipping 'hidden' cells we were accidentally inserting 0x0 sized cells. The resulting behavior was very confusing and did not suggest these invisible cells were the issue. We would see the correctly sized cells and layout, but a few of the valid cells would consistently disappear after scrolling off/on screen. I have no idea why intermingling 0 sized cells would cause this behavior, but removing them fixed the problem. This may not be causing your problem, but this may be helpful to devs searching for similar symptoms.
Just ran into an issue where all UICollectionView cells were disappearing on scroll.
This happened because I had declared
extension UICollectionViewLayout {
static let defaultLayout: UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
return layout
}()
}
... meaning the same layout instance was being used in multiple UICollectionViews. I had meant to make that a computed var. Hope this helps someone who's accidentally using the same layout object in multiple collection views.
What caused the cells to disappear in my case was that the data source was being deallocated prematurely. UICollectionView.dataSource is a weak property, which means that unless you keep a strong reference to it, the object will be deallocated at the end of the scope in which you created. The problem manifested itself with disappearing cells as soon as I tapped on the UICollectionView.
For me this issue seemed to be related with the way i make my collectionview adapt to an open keyboard to prevent content overlaps.
in my observer to respond to KeyboardWillShow i had this:
var userInfo = obj.UserInfo[UIKeyboard.FrameEndUserInfoKey];
if (userInfo is NSValue value)
{
var rect = value.CGRectValue;
var windowOffset = this.Superview.ConvertPointToView(this.Frame.Location, UIApplication.SharedApplication.KeyWindow);
var newHeight = rect.Y - windowOffset.Y;
this._controller.CollectionView.Frame = new CGRect(0, 0, this._controller.CollectionView.Frame.Width, newHeight);
}
After changing it to this:
var userInfo = obj.UserInfo[UIKeyboard.FrameBeginUserInfoKey];
if (userInfo is NSValue value)
{
var rect = value.CGRectValue;
UIEdgeInsets contentInsets = new UIEdgeInsets(0, 0, rect.Height, 0);
this._controller.CollectionView.ContentInset = contentInsets;
this._controller.CollectionView.ScrollIndicatorInsets = contentInsets;
}
The cell disappearance issue completely went away. This is C# from working with xamarin but i hope it helps someone else.
I think this is not a UICollectionView‘s bug, maybe your not return right data in - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect method.
You can see this demo: https://github.com/lqcjdx/YLTagsChooser , all cells can appear when scolling the UICollectionView.

Why is scrolling performance poor for custom table view cells having UISegmentedControl objects?

I have a UITableView with custom cells that were defined in the xib file, and am experiencing poor scrolling performance (choppy) on my device when the cells have a UISegmentedControl on them. NSLog statements reveal that the cells are being allocated and reused as they ought. My code for cellForRowAtIndexPath method is below. Connections are made in the xib as per Apple's documentation. (Scrolls smoothly in simulator btw)
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:#"TableViewCell"
owner:self
options:nil];
cell = self.tvCell;
self.tvCell = nil;
}
cell.layer.shouldRasterize = YES; // build error is here
UILabel *lbl = (UILabel *)[cell viewWithTag:1];
[lbl setText:[NSString stringWithFormat:#"Q%i", indexPath.row+1]];
return cell;
}
Any drawing that a table cell has to do while it's being scrolled is going to cause performance issues; when you have a lot of subviews, there tends to be a lot of drawing going on, and that will—as you've observed—make your scrolling pretty choppy. There are a couple of ways to try to reduce that.
The first step is to make sure that your cells themselves, and as many of their subviews as possible, have their opaque properties set to YES. Opaque views don't have to get blended with the content underneath them, and that saves a lot of time.
You may also want to set your cells' layers to rasterize themselves, like this:
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
This will collapse your view hierarchy into one flat bitmap, which is the kind of thing Core Animation just loves to draw. Note that any animating views—activity indicators, for instance—will force that bitmap to be updated every time they change, i.e. a lot. In that case, you won't want the cell to rasterize everything; you might just use a subview with all of your relatively static views (e.g. labels) beneath another subview with any such dynamic content, and only have the first of those rasterized.
Make sure your identifier is 'MyIdentifier' in the xib. You'll get a good performance hit if it's not. I'm guessing that 'allocated and reused as they ought' means a few allocated on startup and no more allocated after. If that's true then you're probably all set.
Another way to improve performance is to construct your table view with code. It's a good deal faster than using xib's. When I construct table views I usually build them in IB, then copy over the frame values into code and construct in code.
Set aside some time to watch the WWDC 2010 performance video's. A lot of great information, I learn something new each time I watch them.

Resources