使用UILabel时自定义UICollectionViewCell和内存泄漏

时间:2022-12-09 08:58:41

I have a custom UICollectionViewCell. When scrolling left and right (it is a horizontal UICollectionView, memory usage increases by 0.2MB each time.

我有一个自定义的UICollectionViewCell。向左和向右滚动时(它是水平UICollectionView,每次内存使用量增加0.2MB)。

I believe I am correctly implementing prepareForReuse() in the cell object; within it I remove all subviews of the cell.

我相信我正在单元对象中正确实现prepareForReuse();在其中我删除了单元格的所有子视图。

With didSet of an object on my collection view cell, I call setupViews() within my cell. I add a UIImageView with constraints and add it as a subview. This is fine.

使用我的集合视图单元格中的对象的didSet,我在我的单元格中调用setupViews()。我添加了一个带约束的UIImageView并将其添加为子视图。这可以。

However, when I use a UILabel(), this is when there seems to be a memory leak. When I look in Instruments, I can see: VM: UILabel (CALayer) is repeatedly created every time I scroll between two cells! This does not happen with the UIImageView.

但是,当我使用UILabel()时,就会发生内存泄漏。当我查看乐器时,我可以看到:VM:每次在两个单元格之间滚动时会重复创建UILabel(CALayer)! UIImageView不会发生这种情况。

Just in case it's relevant, here's my prepareForReuse method in my cell:

万一它是相关的,这是我的细胞中的prepareForReuse方法:

override func prepareForReuse() {
    super.prepareForReuse()

    self.moreButtonDelegate = nil

    for subview in subviews {
        subview.removeConstraints(subview.constraints)
        subview.removeFromSuperview()
    }

    self.removeFromSuperview() // BURN EVERYTHING
}

Here's my code:

这是我的代码:

private func setupViews() -> Void {
    let imageView = myImageView // A lazy class property returns this

    innerView.addSubview(imageView) // innerView is just another UIView within this cell

    // Now I add constraints for imageView
}

So with the above, there's no memory leak. Looks like ARC cleans everything up correctly as, even with an image, memory usage does not increase exponentially.

所以有了上面的内容,就没有内存泄漏。看起来ARC正确清理所有内容,因为即使使用图像,内存使用量也不会呈指数级增长。

However, when I add this below imageView...

但是,当我在imageView下面添加这个...

let address = UILabel()
address.translatesAutoresizingMaskIntoConstraints = false
address.text = "TEST"
address.font = UIFont.systemFont(ofSize: 22)
address.adjustsFontSizeToFitWidth = true

// Then I add constraints

I get a new VM: UILabel (CALayer) row appearing on every scroll between cells, and as a result memory usage jumps. Take a look:

我得到一个新的VM:UILabel(CALayer)行出现在单元格之间的每个滚动上,因此内存使用量会跳跃。看一看:

使用UILabel时自定义UICollectionViewCell和内存泄漏

What am I doing wrong? I'm using Xcode 9, iOS 11.2 simulator.

我究竟做错了什么?我正在使用Xcode 9,iOS 11.2模拟器。

1 个解决方案

#1


2  

I'm not sure this will resolve your particular issue, but I believe you misunderstood prepareForReuse and I think there is a good chance that this might be what's wrong with your code. So lets take a look at your implementation:

我不确定这会解决你的特定问题,但我相信你误解了prepareForReuse,我认为你的代码很可能会出现问题。那么让我们来看看你的实现:

override func prepareForReuse() {
    super.prepareForReuse()

    self.moreButtonDelegate = nil

    for subview in subviews {
        subview.removeConstraints(subview.constraints)
        subview.removeFromSuperview()
    }

    self.removeFromSuperview() // BURN EVERYTHING
}

I believe you are looking at the prepareForReuse totally wrong. The main point of the reuse is to reduce the overhead caused by creating the view of the cell (objects instantiation, creating the view hierarchy, layout, etc.). You don't want to burn everything! Instead, you want to keep as much of the contentView as possible. Ideally, you will change just the contents of the views (i.e.: text in UILabel, image in UIImageView, etc.), or maybe some properties (backgroundColor, etc.).

我相信你看看prepareForReuse完全错了。重用的主要目的是减少由创建单元视图(对象实例化,创建视图层次结构,布局等)引起的开销。你不想烧掉一切!相反,您希望尽可能多地保留contentView。理想情况下,您只需更改视图的内容(即:UILabel中的文本,UIImageView中的图像等),或者某些属性(backgroundColor等)。

You can use prepareForReuse to cancel some heavyweight operations that you started to present the cell, but that might not have ended when the cell was removed from view and is supposed to be reused somewhere else. E.g., when you download content from web, user might scroll fast, and the cell goes away from the screen before the web image was downloaded and presented. Now if the cell gets reused, the old downloaded image will be most probably shown - so in prepareForReuse you can cancel this operation.

您可以使用prepareForReuse取消一些您开始呈现单元格的重量级操作,但是当单元格从视图中删除并且应该在其他地方重用时,这可能不会结束。例如,当您从Web下载内容时,用户可能会快速滚动,并且在下载和显示Web图像之前,单元格会离开屏幕。现在,如果单元格被重用,则很可能会显示旧的下载图像 - 因此在prepareForReuse中,您可以取消此操作。

Conclusion - I believe in your case none of the operations you are doing in prepareForReuse really help - vice versa, because of that the collection view will have to again recreate the whole UI of the cell again from scratch (that means all the overhead of object instantiation, etc.). My first advice for you is to drop the whole prepareForReuse implementation.

结论 - 我相信你的情况下你在prepareForReuse中所做的任何操作都没有帮助 - 反之亦然,因为集合视图将不得不再次从头开始重新创建单元格的整个UI(这意味着对象的所有开销)实例化等)。我的第一个建议是删除整个prepareForReuse实现。

Second, once you drop your prepareForReuse implementation, refactor the cell so that it will create the UI only once, ideally in its initializer:

其次,一旦你删除了prepareForReuse实现,重构单元格,使其只创建一次UI,理想情况是在其初始化程序中:

class UI: UITableViewCell {
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        setupViews()
    }
}

Then, in cellForItemAt just configure its contents, that means set texts for labels, images for image views, etc.

然后,在cellForItemAt中只配置其内容,这意味着为标签设置文本,为图像视图设置图像等。

In the end, keep in mind what the documentation says about it (my own emphasis):

最后,请记住文档中有关它的内容(我自己的重点):

Performs any clean up necessary to prepare the view for use again.

执行必要的清理以准备视图以便再次使用。

Do only what is really necessary to do, not a thing more.

只做真正需要做的事情,而不是更多的事情。

Over the last year I have implemented many tableView and collectionView datasources, but I really needed to use prepareForReuse just twice (for the example with image download I mentioned above).

在过去的一年里,我已经实现了许多tableView和collectionView数据源,但我真的需要使用prepareForReuse两次(对于我上面提到的图像下载的例子)。

EDIT

编辑

Example of what I meant:

我的意思是:

struct Model {
    var name: String = ""
}

class CustomCell: UITableViewCell {
    // create it once
    private let nameLabel = UILabel()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        // setup view once
        setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupViews() {
        // add it to view
        self.contentView.addSubview(nameLabel)
        // setup configuration
        nameLabel.textColor = UIColor.red

        // lay it out
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            nameLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 8),
            nameLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8),
            nameLabel.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 8),
            nameLabel.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -8),
            ])
    }

    // this is what you call in cellForRowAt
    func configure(for model: Model) {
        nameLabel.text = model.name
        // someImageView.image = model.image
        // etc.
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        // if it is super important, reset the content, cancel operations, etc., but there is no reason to recreate the UI

        // so e.g. this might be ok (although in this case completely unnecessary):
        nameLabel.text = nil

        // but you definitely don't want to do this (that's done once at the cell initialization):
        // nameLabel = UILabel()
        // setupViews()
    }
}
class CustomTableViewController: UITableViewController {

    var models: [Model] = [Model(name: "Milan")]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomCell.self, forCellReuseIdentifier: "customCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return models.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomCell
        // you just want to set the contents, not to recreate the UI components
        cell.configure(for: models[indexPath.row])
        return cell
    }
}

Moreover, always work with cell's contentView, not directly with the cell. Notice that I used this:

此外,始终使用单元格的contentView,而不是直接使用单元格。请注意我使用了这个:

self.contentView.addSubview(nameLabel)

instead of this:

而不是这个:

self.addSubview(nameLabel)

Official docs:

官方文件:

The content view of a UITableViewCell object is the default superview for content displayed by the cell. If you want to customize cells by simply adding additional views, you should add them to the content view so they will be positioned appropriately as the cell transitions into and out of editing mode.

UITableViewCell对象的内容视图是单元格显示的内容的默认超级视图。如果要通过简单地添加其他视图来自定义单元格,则应将它们添加到内容视图中,以便在单元格进入和退出编辑模式时将它们正确定位。

#1


2  

I'm not sure this will resolve your particular issue, but I believe you misunderstood prepareForReuse and I think there is a good chance that this might be what's wrong with your code. So lets take a look at your implementation:

我不确定这会解决你的特定问题,但我相信你误解了prepareForReuse,我认为你的代码很可能会出现问题。那么让我们来看看你的实现:

override func prepareForReuse() {
    super.prepareForReuse()

    self.moreButtonDelegate = nil

    for subview in subviews {
        subview.removeConstraints(subview.constraints)
        subview.removeFromSuperview()
    }

    self.removeFromSuperview() // BURN EVERYTHING
}

I believe you are looking at the prepareForReuse totally wrong. The main point of the reuse is to reduce the overhead caused by creating the view of the cell (objects instantiation, creating the view hierarchy, layout, etc.). You don't want to burn everything! Instead, you want to keep as much of the contentView as possible. Ideally, you will change just the contents of the views (i.e.: text in UILabel, image in UIImageView, etc.), or maybe some properties (backgroundColor, etc.).

我相信你看看prepareForReuse完全错了。重用的主要目的是减少由创建单元视图(对象实例化,创建视图层次结构,布局等)引起的开销。你不想烧掉一切!相反,您希望尽可能多地保留contentView。理想情况下,您只需更改视图的内容(即:UILabel中的文本,UIImageView中的图像等),或者某些属性(backgroundColor等)。

You can use prepareForReuse to cancel some heavyweight operations that you started to present the cell, but that might not have ended when the cell was removed from view and is supposed to be reused somewhere else. E.g., when you download content from web, user might scroll fast, and the cell goes away from the screen before the web image was downloaded and presented. Now if the cell gets reused, the old downloaded image will be most probably shown - so in prepareForReuse you can cancel this operation.

您可以使用prepareForReuse取消一些您开始呈现单元格的重量级操作,但是当单元格从视图中删除并且应该在其他地方重用时,这可能不会结束。例如,当您从Web下载内容时,用户可能会快速滚动,并且在下载和显示Web图像之前,单元格会离开屏幕。现在,如果单元格被重用,则很可能会显示旧的下载图像 - 因此在prepareForReuse中,您可以取消此操作。

Conclusion - I believe in your case none of the operations you are doing in prepareForReuse really help - vice versa, because of that the collection view will have to again recreate the whole UI of the cell again from scratch (that means all the overhead of object instantiation, etc.). My first advice for you is to drop the whole prepareForReuse implementation.

结论 - 我相信你的情况下你在prepareForReuse中所做的任何操作都没有帮助 - 反之亦然,因为集合视图将不得不再次从头开始重新创建单元格的整个UI(这意味着对象的所有开销)实例化等)。我的第一个建议是删除整个prepareForReuse实现。

Second, once you drop your prepareForReuse implementation, refactor the cell so that it will create the UI only once, ideally in its initializer:

其次,一旦你删除了prepareForReuse实现,重构单元格,使其只创建一次UI,理想情况是在其初始化程序中:

class UI: UITableViewCell {
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        setupViews()
    }
}

Then, in cellForItemAt just configure its contents, that means set texts for labels, images for image views, etc.

然后,在cellForItemAt中只配置其内容,这意味着为标签设置文本,为图像视图设置图像等。

In the end, keep in mind what the documentation says about it (my own emphasis):

最后,请记住文档中有关它的内容(我自己的重点):

Performs any clean up necessary to prepare the view for use again.

执行必要的清理以准备视图以便再次使用。

Do only what is really necessary to do, not a thing more.

只做真正需要做的事情,而不是更多的事情。

Over the last year I have implemented many tableView and collectionView datasources, but I really needed to use prepareForReuse just twice (for the example with image download I mentioned above).

在过去的一年里,我已经实现了许多tableView和collectionView数据源,但我真的需要使用prepareForReuse两次(对于我上面提到的图像下载的例子)。

EDIT

编辑

Example of what I meant:

我的意思是:

struct Model {
    var name: String = ""
}

class CustomCell: UITableViewCell {
    // create it once
    private let nameLabel = UILabel()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        // setup view once
        setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setupViews() {
        // add it to view
        self.contentView.addSubview(nameLabel)
        // setup configuration
        nameLabel.textColor = UIColor.red

        // lay it out
        nameLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            nameLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 8),
            nameLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8),
            nameLabel.leftAnchor.constraint(equalTo: self.contentView.leftAnchor, constant: 8),
            nameLabel.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -8),
            ])
    }

    // this is what you call in cellForRowAt
    func configure(for model: Model) {
        nameLabel.text = model.name
        // someImageView.image = model.image
        // etc.
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        // if it is super important, reset the content, cancel operations, etc., but there is no reason to recreate the UI

        // so e.g. this might be ok (although in this case completely unnecessary):
        nameLabel.text = nil

        // but you definitely don't want to do this (that's done once at the cell initialization):
        // nameLabel = UILabel()
        // setupViews()
    }
}
class CustomTableViewController: UITableViewController {

    var models: [Model] = [Model(name: "Milan")]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomCell.self, forCellReuseIdentifier: "customCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return models.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "customCell", for: indexPath) as! CustomCell
        // you just want to set the contents, not to recreate the UI components
        cell.configure(for: models[indexPath.row])
        return cell
    }
}

Moreover, always work with cell's contentView, not directly with the cell. Notice that I used this:

此外,始终使用单元格的contentView,而不是直接使用单元格。请注意我使用了这个:

self.contentView.addSubview(nameLabel)

instead of this:

而不是这个:

self.addSubview(nameLabel)

Official docs:

官方文件:

The content view of a UITableViewCell object is the default superview for content displayed by the cell. If you want to customize cells by simply adding additional views, you should add them to the content view so they will be positioned appropriately as the cell transitions into and out of editing mode.

UITableViewCell对象的内容视图是单元格显示的内容的默认超级视图。如果要通过简单地添加其他视图来自定义单元格,则应将它们添加到内容视图中,以便在单元格进入和退出编辑模式时将它们正确定位。