onevcat / Kingfisher

A lightweight, pure-Swift library for downloading and caching images from the web.
MIT License
23.42k stars 2.66k forks source link

【QUESTION】Kingfisher In Memory Graph #2266

Open make1a opened 5 months ago

make1a commented 5 months ago

image image image image

想咨询一下, 我正在进行 app 的内存诊断工作,通过instrument Mark Generation 内存断点排查增长的内存会有大量的内存堆积在SessionDataTask(如下图)。 搭配 Memory Graph 诊断的时候 dirty memory 有两块大额内存集中在IOSurface、Image IO, 是使用了DownsamplingImageProcessor ,内部的 ImageIO scale 图片产生的。 我无法分辨,这些 dirty memory是否是应该存在的。 当我app 达到内存预警水位的时候,如何释放一部分内存。因为我注意到SessionDataTask 这块内存似乎一直在增长,clearMemoryCache无法清理这块内存。

image

image 我描述一下业务场景, 这些内存残留是在进入一个直播间后, 退出房间在控制器都销毁的情况下还遗留的

make1a commented 5 months ago

这是我的使用代码

extension UIImageView {

    @nonobjc
    /// 设置网络图片的统一入口方法
    /// - Parameters:
    ///   - source: 可以传入 String 或者 URL 类型, 必须
    ///   - placeholder: 占位图,  在列表上使用的时候, 推荐放一张图片作为占位使用
    ///   - size: 图片解码到内存之前,按照大小去解码图片,解决大图 OOM 的问题。在已知图片大小的情况下,强烈建议使用该参数
    public func kfSetImage(
        url source: Any?,
        placeholder: UIImage? = nil,
        size: CGSize = .zero,
        options: KingfisherOptionsInfo? = nil,
        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) {

            guard let source = source else {
                self.image = placeholder
                return
            }

            if #available(iOS 14, *) {} else if completionHandler == nil {
                setImageIos13(url: source, placeholder: placeholder)
                return
            }

            var defaultOptions: KingfisherOptionsInfo = [
                .scaleFactor(UIScreen.main.scale),
                .cacheOriginalImage
            ]

            if let options = options {
                defaultOptions += options
            }

            guard let url = self.url(from: source) else {
                self.image = placeholder
                return
            }

            setImage(with: url, placeholder: placeholder, size: size, options: &defaultOptions, completionHandler: completionHandler)
        }

    private func setImage(
        with url: URL,
        placeholder: UIImage?,
        size: CGSize,
        options: inout KingfisherOptionsInfo,
        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) {

            if #available(iOS 14, *) {

                /// autolayout 取不到 size
                /// 在已经获取到容器大小的情况下,强制开启ImageIO图片下取样
                var size = size
                if size == .zero {
                    size = self.bounds.size
                }

                // 获取合适大小的图片展示
                if !url.absoluteString.contains("gif"), size != CGSize.zero {
                    options += [.processor(DownsamplingImageProcessor(size: size))]
                }
            } else if url.absoluteString.contains("format,webp") {
                options += [.processor(WebPProcessor.default), .cacheSerializer(WebPSerializer.default)]
            }

            kf.setImage(with: url, placeholder: placeholder, options: options, completionHandler: completionHandler)
        }
}
make1a commented 5 months ago

我在 stack overflow 上同样在跟进此问题: https://stackoverflow.com/questions/78676826/how-to-optimize-or-release-excessive-dirty-memory-caused-by-imageio-in-ios

onevcat commented 5 months ago

建议可以监测一下在下面的情况下这部分内存能不能被释放:

  1. 建议检查和确保相关的图片不再有强引用 (比如 image view 已经销毁)
  2. 调用 clearMemoryCache
  3. 触发一次 memory warning
make1a commented 5 months ago
  1. 可以确认是一定释放了的,反复做了很多测试。
  2. clearMemoryCache 得到了以下的结果,释放了大块内存:. image

image

  1. KF 内部有监听 memory warning 通知,会执行 clearMemoryCache, 但是似乎在内存危险水位无法触发或者无法及时触发, 我尝试设置了以下优化措施, 任然无法彻底解决OOM 的问题, 我有几个预想的方案:
  2. totalCostLimit设置的更小, 比如 crashAmountMemory * 0.15
  3. 定时去清理 clearMemoryCache
  4. 或者在一些功能内敛的模块,如直播,在退出时候固定清理这块图片内存。
    public static func crashAmountMemory() -> Double {
        let name = UIDevice.name

        switch name {
        case .unknown:
            return Double(ProcessInfo.processInfo.physicalMemory) * 0.55

        case .iPad2: return Double(275) * 1024 * 1024
        case .iPad3: return Double(645) * 1024 * 1024
        case .iPad4: return Double(585) * 1024 * 1024
        case .iPadMini: return Double(297) * 1024 * 1024
        case .iPadAir: return Double(697) * 1024 * 1024
        case .iPadAir2: return Double(1383) * 1024 * 1024
        case .iPhone4: return Double(325) * 1024 * 1024
        case .iPhone4s: return Double(286) * 1024 * 1024
        case .iPhone5: return Double(645) * 1024 * 1024
        case .iPhone5s, .iPhone5c: return Double(646) * 1024 * 1024
        case .iPhone6: return Double(645) * 1024 * 1024
        case .iPhone6Plus: return Double(645) * 1024 * 1024
        case .iPhone6s: return Double(1396) * 1024 * 1024
        case .iPhone6sPlus: return Double(1392) * 1024 * 1024
        case .iPhoneSE: return Double(1395) * 1024 * 1024
        case .iPhone7: return Double(1395) * 1024 * 1024
        case .iPhone7Plus: return Double(2040) * 1024 * 1024
        case .iPhone8, .iPhone8Plus, .iPhoneXsMax: return Double(1364) * 1024 * 1024
        case .iPhoneX, .iPhoneXs, .iPhoneXr: return Double(1392) * 1024 * 1024
        case .iPhone11, .iPhone11Pro, .iPhone11ProMax: return Double(2068) * 1024 * 1024
        default: return Double(ProcessInfo.processInfo.physicalMemory) * 0.55
        }
    }

            ImageCache.default.memoryStorage.config.totalCostLimit = Int(DeviceChecker.crashAmountMemory() * 0.2)
onevcat commented 4 months ago

如果能够通过 clearMemoryCache 释放的话,说明这部分内存的申请和持有都没有问题。除了 memory warning 触发(这个只能选择信任OS会在合适的时候给出)的 NSCache 的清理外,Kingfisher 自身也监听了切后台的通知,会在app切换到后台时清理掉这部分内存缓存。

设置更小的内存 cache size 以及手动去更多调用 clearMemoryCache 也都是可以采用的方法。