使用 UILabel 时自定义 UICollectionViewCell 和内存泄漏
Custom UICollectionViewCell and memory leak when using UILabel
我有一个习惯UICollectionViewCell
。左右滚动时(水平滚动UICollectionView
,每次增加0.2MB内存占用
我相信我在单元格对象中正确地实现了 prepareForReuse()
;在其中我删除了单元格的所有子视图。
我的集合视图单元格上有一个对象 didSet
,我在我的单元格中调用 setupViews()
。我添加了一个带有约束的 UIImageView
并将其添加为子视图。这很好。
但是,当我使用 UILabel()
时,似乎出现了内存泄漏。当我查看 Instruments 时,我可以看到:VM: 每次在两个单元格之间滚动时,都会重复创建 UILabel (CALayer)! UIImageView
.
不会发生这种情况
为了以防万一,这是我在单元格中的 prepareForReuse
方法:
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 模拟器。
我不确定这是否能解决您的特定问题,但我相信您误解了 prepareForReuse
,而且我认为这很有可能是您的代码有问题。那么让我们看看您的实现:
override func prepareForReuse() {
super.prepareForReuse()
self.moreButtonDelegate = nil
for subview in subviews {
subview.removeConstraints(subview.constraints)
subview.removeFromSuperview()
}
self.removeFromSuperview() // BURN EVERYTHING
}
我相信您对 prepareForReuse
的看法完全错误。重用的要点是减少创建cell的视图(对象实例化、创建视图层级、布局等)带来的开销。你不想烧掉一切!相反,您希望尽可能多地保留 contentView。理想情况下,您将只更改视图的内容(即:UILabel
中的 text
、UIImageView
中的 image
等),或者可能更改某些属性(backgroundColor
, 等等).
您可以使用 prepareForReuse
来取消您开始显示单元格的一些重量级操作,但是当单元格从视图中移除并且应该在其他地方重复使用时,这些操作可能还没有结束。例如,当您从 Web 下载内容时,用户可能会快速滚动,并且在下载和显示 Web 图像之前单元格会离开屏幕。现在,如果重新使用该单元格,很可能会显示旧的下载图像 - 因此在 prepareForReuse
中您可以取消此操作。
结论 - 我相信你的情况 none 你在 prepareForReuse
中所做的操作确实有帮助 - 反之亦然,因为集合视图将不得不再次重新创建整个 UI 再次从头开始(这意味着对象实例化等的所有开销)。我给你的第一个建议是放弃整个 prepareForReuse
实现。
其次,一旦你放弃你的 prepareForReuse
实现,重构单元格以便它只创建一次 UI,最好是在它的 initializer
:
class UI: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
}
然后,在 cellForItemAt
中配置其内容,即设置标签的文本、图像视图的图像等。
最后,请记住documentation所说的(我自己强调的):
Performs any clean up necessary to prepare the view for use again.
只做真正需要做的事,一事不做。
在过去的一年里,我实现了许多 tableView
和 collectionView
数据源,但我真的只需要使用 prepareForReuse
两次(对于我上面提到的图像下载示例) .
编辑
我的意思的例子:
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
}
}
此外,始终使用单元格 contentView
,而不是直接使用单元格。请注意,我使用了这个:
self.contentView.addSubview(nameLabel)
而不是这个:
self.addSubview(nameLabel)
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.
我有一个习惯UICollectionViewCell
。左右滚动时(水平滚动UICollectionView
,每次增加0.2MB内存占用
我相信我在单元格对象中正确地实现了 prepareForReuse()
;在其中我删除了单元格的所有子视图。
我的集合视图单元格上有一个对象 didSet
,我在我的单元格中调用 setupViews()
。我添加了一个带有约束的 UIImageView
并将其添加为子视图。这很好。
但是,当我使用 UILabel()
时,似乎出现了内存泄漏。当我查看 Instruments 时,我可以看到:VM: 每次在两个单元格之间滚动时,都会重复创建 UILabel (CALayer)! UIImageView
.
为了以防万一,这是我在单元格中的 prepareForReuse
方法:
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 模拟器。
我不确定这是否能解决您的特定问题,但我相信您误解了 prepareForReuse
,而且我认为这很有可能是您的代码有问题。那么让我们看看您的实现:
override func prepareForReuse() {
super.prepareForReuse()
self.moreButtonDelegate = nil
for subview in subviews {
subview.removeConstraints(subview.constraints)
subview.removeFromSuperview()
}
self.removeFromSuperview() // BURN EVERYTHING
}
我相信您对 prepareForReuse
的看法完全错误。重用的要点是减少创建cell的视图(对象实例化、创建视图层级、布局等)带来的开销。你不想烧掉一切!相反,您希望尽可能多地保留 contentView。理想情况下,您将只更改视图的内容(即:UILabel
中的 text
、UIImageView
中的 image
等),或者可能更改某些属性(backgroundColor
, 等等).
您可以使用 prepareForReuse
来取消您开始显示单元格的一些重量级操作,但是当单元格从视图中移除并且应该在其他地方重复使用时,这些操作可能还没有结束。例如,当您从 Web 下载内容时,用户可能会快速滚动,并且在下载和显示 Web 图像之前单元格会离开屏幕。现在,如果重新使用该单元格,很可能会显示旧的下载图像 - 因此在 prepareForReuse
中您可以取消此操作。
结论 - 我相信你的情况 none 你在 prepareForReuse
中所做的操作确实有帮助 - 反之亦然,因为集合视图将不得不再次重新创建整个 UI 再次从头开始(这意味着对象实例化等的所有开销)。我给你的第一个建议是放弃整个 prepareForReuse
实现。
其次,一旦你放弃你的 prepareForReuse
实现,重构单元格以便它只创建一次 UI,最好是在它的 initializer
:
class UI: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupViews()
}
}
然后,在 cellForItemAt
中配置其内容,即设置标签的文本、图像视图的图像等。
最后,请记住documentation所说的(我自己强调的):
Performs any clean up necessary to prepare the view for use again.
只做真正需要做的事,一事不做。
在过去的一年里,我实现了许多 tableView
和 collectionView
数据源,但我真的只需要使用 prepareForReuse
两次(对于我上面提到的图像下载示例) .
编辑
我的意思的例子:
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 } }
此外,始终使用单元格 contentView
,而不是直接使用单元格。请注意,我使用了这个:
self.contentView.addSubview(nameLabel)
而不是这个:
self.addSubview(nameLabel)
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.