collectionView 单元格中的图像由于内存问题导致终止
Image in collectionView cell causing Terminated due to memory issue
我查看了这个问题的几个答案,但 none 有所帮助。
vc1 是常规 vc,我从 firebase 抓取了 20 张图像,在 vc2 中,我使用 ARKit 根据用户选择显示这些图像。我在 vc2 中有一个 collectionView,它可以对来自 firebase 的另外 20 个图像进行分页。问题是当接下来的 20 张图片正在加载并且我开始滚动时,应用程序崩溃并显示 Message from debugger: Terminated due to memory issue
。当滚动这些新图像时,我查看了内存图,发现它突然增加到 1 gig,所以这就是崩溃的原因。 ARKit 和我四处浮动的节点也会导致内存颠簸,但它们并不是导致崩溃的原因,如下所述。
1- 在单元格内,我使用 SDWebImage
在 imageView 中显示图像。一旦我注释掉 SDWebImage
一切正常,滚动很流畅,不再崩溃,但当然我看不到图像。我切换到 URLSession.shared.dataTask
,同样的内存问题再次出现。
2- 图像最初是用 iPhone 全屏相机拍摄的,并用 jpegData(compressionQuality: 0.3).
保存。单元格大小为 40x40。在 SDWebImage
完成块中,我尝试 但内存崩溃仍然存在。
3- 我使用 Instruments
> Leaks
来查找内存泄漏并且出现了一些 Foundation 泄漏但是当我关闭 vc2 Deinit
总是运行。在 vc2 中没有任何长 运行 计时器或循环,我在所有闭包中使用 [weak self]
。
4- 正如我在第二点中所述,问题肯定是 imageView/image,因为一旦我将其从流程中删除,一切正常。如果我不显示任何图像,一切正常(将出现 40 个没有图像的红色图像视图)。
我该如何解决这个问题?
分页以拉取更多图片
for child in snapshot.children.allObjects as! [DataSnapshot] {
guard let dict = child.value as? [String:Any] else { continue }
let post = Post(dict: dict)
datasource.append(post)
let lastIndex = datasource.count - 1
let indexPath = IndexPath(item: lastIndex, section: 0)
UIView.performWithoutAnimation {
collectionView.insertItems(at: [indexPath])
}
}
cellForItem:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellId, for: indexPath) as! PostCell
cell.resetAll()
cell.post = dataSource[indexPath.item]
return cell
}
PostCell
private lazy var imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFill
iv.layer.cornerRadius = 15
iv.layer.masksToBounds = true
iv.backgroundColor = .red
iv.isHidden = true
return iv
}()
private lazy var spinner: UIActivityIndicatorView = {
let actIndi = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.medium) // ...
}()
var post: Post? {
didSet {
guard let urlSr = post?.urlStr, let url = URL(string: urlSr) else { return }
spinner.startAnimating()
// if I comment this out everything works fine
imageView.sd_setImage(with: url, placeholderImage: placeHolderImage, options: [], completed: {
[weak self] (image, error, cache, url) in
// in here I tried resizing the image but no diff
DispatchQueue.main.async { [weak self] in
self?.showAll()
}
})
setAnchors()
}
}
func resetAll() {
spinner.stopAnimating()
imageView.image = nil
imageView.removeFromSuperView()
imageView.isHidden = true
}
func showAll() {
spinner.stopAnimating()
imageView.isHidden = false
}
func setAnchors() {
contentView.addSubview(imageView)
imageView.addSubview(cellSpinner)
// imageView is pinned to all sides
}
@matt 的评论是 100% 正确的,问题是图像对于 40x40 大小的 collectionView 来说太大了,我需要调整图像的大小。由于某种原因,我的问题中的 resize link 不起作用,所以我将其用于 resize image instead. I also used URLSession and this
我分10步做的,都注释掉了
// 0. set this anywhere outside any class
let imageCache = NSCache<AnyObject, AnyObject>()
邮政信箱:
private lazy var imageView: UIImageView = { ... }()
private lazy var spinner: UIActivityIndicatorView = { ... }()
// 1. override prepareForReuse, cancel the task and set it to nil, and set the imageView.image to nil so that the wrong images don't appear while scrolling
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
task = nil
imageView.image = nil
}
// 2. add a var to start/stop a urlSession when the cell isn't on screen
private var task: URLSessionDataTask?
var post: Post? {
didSet {
guard let urlStr = post?.urlStr else { return }
spinner.startAnimating()
// 3. crate the new image from this function
createImageFrom(urlStr)
setAnchors()
}
}
func createImageFrom(_ urlStr: String) {
if let cachedImage = imageCache.object(forKey: urlStr as AnyObject) as? UIImage {
// 4. if the image is in cache call this function to show the image
showAllAndSetImageView(with: cachedImage)
} else {
guard let url = URL(string: urlStr) else { return }
// 5. if the image is not in cache start a urlSession and initialize it with the task variable from step 2
task = URLSession.shared.dataTask(with: url, completionHandler: {
[weak self](data, response, error) in
if let error = error { return }
if let response = response as? HTTPURLResponse {
print("response.statusCode: \(response.statusCode)")
guard 200 ..< 300 ~= response.statusCode else { return }
}
guard let data = data, let image = UIImage(data: data) else { return }
// 6. add the image to cache
imageCache.setObject(image, forKey: photoUrlStr as AnyObject)
DispatchQueue.main.async { [weak self] in
// 7. call this function to show the image
self?.showAllAndSetImageView(with: image)
}
})
task?.resume()
}
}
func showAllAndSetImageView(with image: UIImage) {
// 8. resize the image
let resizedImage = resizeImageToCenter(image: image, size: 40) // can also use self.frame.height
imageView.image = resizeImage
showAll()
}
// 9. func to resize the image
func resizeImageToCenter(image: UIImage, size: CGFloat) -> UIImage {
let size = CGSize(width: size, height: size)
// Define rect for thumbnail
let scale = max(size.width/image.size.width, size.height/image.size.height)
let width = image.size.width * scale
let height = image.size.height * scale
let x = (size.width - width) / CGFloat(2)
let y = (size.height - height) / CGFloat(2)
let thumbnailRect = CGRect.init(x: x, y: y, width: width, height: height)
// Generate thumbnail from image
UIGraphicsBeginImageContextWithOptions(size, false, 0)
image.draw(in: thumbnailRect)
let thumbnail = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return thumbnail!
}
func resetAll() { ... }
func showAll() { ... }
func setAnchors() { ... }
我查看了这个问题的几个答案,但 none 有所帮助。
vc1 是常规 vc,我从 firebase 抓取了 20 张图像,在 vc2 中,我使用 ARKit 根据用户选择显示这些图像。我在 vc2 中有一个 collectionView,它可以对来自 firebase 的另外 20 个图像进行分页。问题是当接下来的 20 张图片正在加载并且我开始滚动时,应用程序崩溃并显示 Message from debugger: Terminated due to memory issue
。当滚动这些新图像时,我查看了内存图,发现它突然增加到 1 gig,所以这就是崩溃的原因。 ARKit 和我四处浮动的节点也会导致内存颠簸,但它们并不是导致崩溃的原因,如下所述。
1- 在单元格内,我使用 SDWebImage
在 imageView 中显示图像。一旦我注释掉 SDWebImage
一切正常,滚动很流畅,不再崩溃,但当然我看不到图像。我切换到 URLSession.shared.dataTask
,同样的内存问题再次出现。
2- 图像最初是用 iPhone 全屏相机拍摄的,并用 jpegData(compressionQuality: 0.3).
保存。单元格大小为 40x40。在 SDWebImage
完成块中,我尝试
3- 我使用 Instruments
> Leaks
来查找内存泄漏并且出现了一些 Foundation 泄漏但是当我关闭 vc2 Deinit
总是运行。在 vc2 中没有任何长 运行 计时器或循环,我在所有闭包中使用 [weak self]
。
4- 正如我在第二点中所述,问题肯定是 imageView/image,因为一旦我将其从流程中删除,一切正常。如果我不显示任何图像,一切正常(将出现 40 个没有图像的红色图像视图)。
我该如何解决这个问题?
分页以拉取更多图片
for child in snapshot.children.allObjects as! [DataSnapshot] {
guard let dict = child.value as? [String:Any] else { continue }
let post = Post(dict: dict)
datasource.append(post)
let lastIndex = datasource.count - 1
let indexPath = IndexPath(item: lastIndex, section: 0)
UIView.performWithoutAnimation {
collectionView.insertItems(at: [indexPath])
}
}
cellForItem:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellId, for: indexPath) as! PostCell
cell.resetAll()
cell.post = dataSource[indexPath.item]
return cell
}
PostCell
private lazy var imageView: UIImageView = {
let iv = UIImageView()
iv.translatesAutoresizingMaskIntoConstraints = false
iv.contentMode = .scaleAspectFill
iv.layer.cornerRadius = 15
iv.layer.masksToBounds = true
iv.backgroundColor = .red
iv.isHidden = true
return iv
}()
private lazy var spinner: UIActivityIndicatorView = {
let actIndi = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.medium) // ...
}()
var post: Post? {
didSet {
guard let urlSr = post?.urlStr, let url = URL(string: urlSr) else { return }
spinner.startAnimating()
// if I comment this out everything works fine
imageView.sd_setImage(with: url, placeholderImage: placeHolderImage, options: [], completed: {
[weak self] (image, error, cache, url) in
// in here I tried resizing the image but no diff
DispatchQueue.main.async { [weak self] in
self?.showAll()
}
})
setAnchors()
}
}
func resetAll() {
spinner.stopAnimating()
imageView.image = nil
imageView.removeFromSuperView()
imageView.isHidden = true
}
func showAll() {
spinner.stopAnimating()
imageView.isHidden = false
}
func setAnchors() {
contentView.addSubview(imageView)
imageView.addSubview(cellSpinner)
// imageView is pinned to all sides
}
@matt 的评论是 100% 正确的,问题是图像对于 40x40 大小的 collectionView 来说太大了,我需要调整图像的大小。由于某种原因,我的问题中的 resize link 不起作用,所以我将其用于 resize image instead. I also used URLSession and this
我分10步做的,都注释掉了
// 0. set this anywhere outside any class
let imageCache = NSCache<AnyObject, AnyObject>()
邮政信箱:
private lazy var imageView: UIImageView = { ... }()
private lazy var spinner: UIActivityIndicatorView = { ... }()
// 1. override prepareForReuse, cancel the task and set it to nil, and set the imageView.image to nil so that the wrong images don't appear while scrolling
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
task = nil
imageView.image = nil
}
// 2. add a var to start/stop a urlSession when the cell isn't on screen
private var task: URLSessionDataTask?
var post: Post? {
didSet {
guard let urlStr = post?.urlStr else { return }
spinner.startAnimating()
// 3. crate the new image from this function
createImageFrom(urlStr)
setAnchors()
}
}
func createImageFrom(_ urlStr: String) {
if let cachedImage = imageCache.object(forKey: urlStr as AnyObject) as? UIImage {
// 4. if the image is in cache call this function to show the image
showAllAndSetImageView(with: cachedImage)
} else {
guard let url = URL(string: urlStr) else { return }
// 5. if the image is not in cache start a urlSession and initialize it with the task variable from step 2
task = URLSession.shared.dataTask(with: url, completionHandler: {
[weak self](data, response, error) in
if let error = error { return }
if let response = response as? HTTPURLResponse {
print("response.statusCode: \(response.statusCode)")
guard 200 ..< 300 ~= response.statusCode else { return }
}
guard let data = data, let image = UIImage(data: data) else { return }
// 6. add the image to cache
imageCache.setObject(image, forKey: photoUrlStr as AnyObject)
DispatchQueue.main.async { [weak self] in
// 7. call this function to show the image
self?.showAllAndSetImageView(with: image)
}
})
task?.resume()
}
}
func showAllAndSetImageView(with image: UIImage) {
// 8. resize the image
let resizedImage = resizeImageToCenter(image: image, size: 40) // can also use self.frame.height
imageView.image = resizeImage
showAll()
}
// 9. func to resize the image
func resizeImageToCenter(image: UIImage, size: CGFloat) -> UIImage {
let size = CGSize(width: size, height: size)
// Define rect for thumbnail
let scale = max(size.width/image.size.width, size.height/image.size.height)
let width = image.size.width * scale
let height = image.size.height * scale
let x = (size.width - width) / CGFloat(2)
let y = (size.height - height) / CGFloat(2)
let thumbnailRect = CGRect.init(x: x, y: y, width: width, height: height)
// Generate thumbnail from image
UIGraphicsBeginImageContextWithOptions(size, false, 0)
image.draw(in: thumbnailRect)
let thumbnail = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return thumbnail!
}
func resetAll() { ... }
func showAll() { ... }
func setAnchors() { ... }