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

Ios 使用UILabel时自定义UICollectionViewCell和内存泄漏,ios,swift,memory-leaks,uicollectionview,uicollectionviewcell,Ios,Swift,Memory Leaks,Uicollectionview,Uicollectionviewcell,我有一个自定义的UICollectionViewCell。当向左和向右滚动(这是一个水平UICollectionView)时,内存使用量每次增加0.2MB 我相信我在cell对象中正确地实现了prepareforeuse();在其中我删除了cell的所有子视图 使用集合视图单元格中对象的didSet,我在单元格中调用setupViews()。我添加一个带有约束的UIImageView,并将其添加为子视图。这很好 但是,当我使用UILabel()时,这似乎是内存泄漏。当我查看仪器时,我可以看到:V

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

我相信我在cell对象中正确地实现了
prepareforeuse()
;在其中我删除了cell的所有子视图

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

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

为了以防万一,这里是我的单元格中的
prepareforeuse
方法:

override func prepareForReuse() {
    super.prepareForReuse()

    self.moreButtonDelegate = nil

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

    self.removeFromSuperview() // BURN EVERYTHING
}
这是我的密码:

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
}
因此,通过上述操作,没有内存泄漏。看起来ARC可以正确地清理所有内容,因为即使是图像,内存使用量也不会成倍增加

然而,当我在下面添加这个
imageView

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

// Then I add constraints
我在单元格之间的每个滚动条上都会出现一个新的
VM:UILabel(CALayer)
行,结果是内存使用量激增。请看:


我做错了什么?我使用的是Xcode 9,iOS 11.2模拟器。

我不确定这是否能解决您的特定问题,但我相信您误解了
prepareforeuse
,我认为这很可能就是您的代码的问题所在。因此,让我们看看您的实现:

override func prepareForReuse() {
    super.prepareForReuse()

    self.moreButtonDelegate = nil

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

    self.removeFromSuperview() // BURN EVERYTHING
}
我相信您看到的
prepareforeuse
是完全错误的。重用的主要目的是减少创建单元格视图(对象实例化、创建视图层次结构、布局等)所造成的开销。您不想烧掉所有内容!相反,您希望保留尽可能多的contentView。理想情况下,您只需更改视图的内容(即:
UILabel
中的
text
UIImageView
中的
image
,等等),或者可能更改某些属性(
backgroundColor
),等等)

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

结论-我相信在您的案例中,您在
prepareforeuse
中执行的任何操作都没有真正的帮助-反之亦然,因为集合视图必须从头开始重新创建单元格的整个UI(这意味着所有对象实例化的开销,等等)。我给你的第一个建议是,在实施之前,放弃整个
prepareforuse

第二,一旦放弃
prepareforeuse
实现,重构单元格,使其只创建一次UI,最好是在其
初始值设定项中创建一次:

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

        setupViews()
    }
}
然后,在
cellForItemAt
中,只需配置其内容,即为标签设置文本,为图像视图设置图像,等等

最后,请记住作者对此的看法(我自己的重点):

执行任何必要的清理,以准备再次使用视图

只做真正需要做的事,不要做更多的事

在过去的一年中,我实现了许多
tableView
collectionView
数据源,但我确实需要使用
prepareforeuse
仅两次(例如上面提到的图像下载示例)

编辑

举例说明我的意思:

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()
    }
}
与此相反:

self.addSubview(nameLabel)
:

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


非常感谢您对米兰的
prepareforeuse
Milan的解释-您用非常简洁的回答为我澄清了很多,这真的很有帮助,干杯!将其作为可选项添加,然后将其设置为零并不能解决此问题。@Jimbo您确定要在这里写此评论吗?我不理解上下文。添加什么可选?抱歉,我不清楚,您建议更改标签而不是重新创建标签。这需要我将标签存储为类成员,因此是可选的。我这样做了,在准备作为测试重用时,我将其设置为nil,但每次仍会重新创建,因此内存泄漏仍然存在:/Milan,您的answer非常有用!!我知道这篇文章可能不会有太多的观点,但你真的很有帮助,我会把这些知识用在未来。干杯,哥们!
self.contentView.addSubview(nameLabel)
self.addSubview(nameLabel)