要单独处理数据
在列表或者滑动视图中,很大概率 对于大量图片或者文字信息的聚合进行显示。cell 的重用机制,仅仅是系统帮忙处理的UI 的频繁new 的问题。避免了内存的爆增加,从而导致UI上的卡顿,手机发热,耗电 等一系列问题。但是 这个优化仅仅 只能帮到 UI 申请内存的这个环节。 待解决的问题
- 数据读取频繁问题
- 数据不显示 还在进行中消耗内存的问题
- 网络数据,和本地数据 不同的加载
前一篇 SwiftUI to UIKit 之 UICollectionView (UI 篇) 对于数据逻辑 和加载时机进行简单表述,这一批详细来一波
经过反复的demo测试 最后选了一个方案
swift
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 数据加载
// 数据队列出队显示结果
}
swift
func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
// 数据队列入队
}
swift
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// 取消数据正在处理的没来得及显示的操作
// 数据队列出队取消
}
细说这个方案
如果是本地图片
对于本地图片基础的操作是 这样
less
public func getThumbnail(for asset: PHAsset, size: CGSize, completion: @escaping (UIImage?) -> Void) {
let options = PHImageRequestOptions()
options.resizeMode = .none
options.deliveryMode = .highQualityFormat
PHCachingImageManager.default().requestImage(
for: asset,
targetSize: size,
contentMode: .aspectFill,
options: options) { (image, _) in
completion(image)
}
}
通过 PHAsset 这个Photo库中的Class 来提取手机相簿里的照片数据,但是,如果刚刚拍摄完成的图片,会有一定概率的读取缓慢的情况。 这又来一个问题,如何空着读取的操作,控制的方法有很多,开始我想的是用OC 里GCD ,但是GCD语法和封装不方便其他地方使用,这个时候 换了个角度用NSOperation,这样 就多了个将操作 包装
swift
import UIKit
import PhotosUI
internal class DataLoadOperation: Operation {
// MARK: - Public properties
var image: UIImage?
var loadingCompleteHandler: ((UIImage?) -> Void)?
// MARK: - Properties
private var _asset: PHAsset
private var _size: CGSize
override var isAsynchronous: Bool {
return true
}
// MARK: - Methods
init(_ asset: PHAsset, size: CGSize) {
_asset = asset
_size = size
}
override func main() {
if isCancelled { return }
let manager = PhotoLibManager()
manager.getThumbnail(for: _asset, size: _size) { image in
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
if self.isCancelled { return }
self.image = image ?? UIImage()
self.loadingCompleteHandler?(self.image)
}
}
}
// MARK: - Static methods
public static func getImage(for asset: PHAsset, size: CGSize) -> DataLoadOperation? {
return DataLoadOperation(asset, size: size)
}
}
利用Operation 的 开始 暂停 取消等一些列开放API
swift
open class Operation : NSObject, @unchecked Sendable {
open func start()
open func main()
open var isCancelled: Bool { get }
open func cancel()
open var isExecuting: Bool { get }
open var isFinished: Bool { get }
open var isConcurrent: Bool { get }
@available(iOS 7.0, *)
open var isAsynchronous: Bool { get }
open var isReady: Bool { get }
open func addDependency(_ op: Operation)
open func removeDependency(_ op: Operation)
............
............
下一步 就是 调用的 add 和remove 的Operation 的时机,cell 的willDisplay 和 didEndDisplaying 的时候。之前我是在prefetchItemsAt 和 cancelPrefetchingForItemsAt 这样的PreFetchDelelgate 代理方法里面,效果没有这样的好。主要还是 PreFetch Delegate 返回的数据量在列数过多的情况下,返回的indexPath 过多,cpu 飙升 有崩溃风险。
swift
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let updateCellClosure: (UIImage?) -> Void = { [unowned self] image in
cell.setThumbnail(image, data: p_item)
self.loadingOperations.removeValue(forKey: indexPath)
}
if let dataLoader = loadingOperations[indexPath] {
if let image = dataLoader.image {
cell.setThumbnail(image, data: p_item)
loadingOperations.removeValue(forKey: indexPath)
} else {
dataLoader.loadingCompleteHandler = updateCellClosure
}
} else {
let size = CGSize(width: UIScreen.main.bounds.size.width / CGFloat(self.parent.changeUI.numColum), height: UIScreen.main.bounds.size.width / CGFloat(self.parent.changeUI.numColum))
guard let asset = p_item.phAsset else { return }
if let dataLoader = DataLoadOperation.getImage(for: asset, size: size) {
dataLoader.loadingCompleteHandler = updateCellClosure
loadingQueue.addOperation(dataLoader)
loadingOperations[indexPath] = dataLoader
}
}
}
在DisDisPlay 的时候
swift
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// Cancel pending data load operations when the data is no longer required.
if let dataLoader = loadingOperations[indexPath] {
dataLoader.cancel()
loadingOperations.removeValue(forKey: indexPath)
}
cancelFetchPhoto(ofIndex: indexPath)
}
func cancelFetchPhoto(ofIndex indexPath: IndexPath) {
if let dataLoader = loadingOperations[indexPath] {
dataLoader.cancel()
loadingOperations.removeValue(forKey: indexPath)
} else {
if let url = kfRequestURLs[indexPath] {
ImagePrefetcher(urls: [url]).stop()
}
}
}
这里的else 是网络的图片 通过KF 来进行管理,这就是另外的逻辑的。但是调用的时机 是一样的。
网络图片的处理
KingFisher 对于图片的预处理 有自己的逻辑和处理API ImagePrefetcher 就是其中之一,如果开发者对于这个感觉不行,可以参考本地图片处理 来自己写一个,这个可以加入自己对于项目的理解来添加自己的感觉不错性能的算法。 特别是index 的查找问题,数据缓存 存储问题 等,都可以加入自己感觉不错的开源算法,也是提高自己的一个途径,特别是对于 iOS 开发者 处于算法荒漠的情况的一个摆脱
less
/// will display
guard let URL = p_item.thumbnail else { return }
kfRequestURLs[indexPath] = URL
let didModifier:AnyModifier = SKPhotoTool.makeHeadOfImage(p_item)
cell.img.kf.setImage(with: URL, placeholder:UIImage(named: "default_icon"), options: [.downloadPriority(0.5),.requestModifier(didModifier)])
/// cancel
ImagePrefetcher(urls: [url]).stop()
这样对于快速滑动的 视图 的了里面图片 有 了很好的处理内存问题,再在切换,退出 进入 等入口,进行 内存的清理。 通过测试 3w张图片 没有出现卡顿和崩溃的问题,当然手机是iPhone12 ,如果是一台iPhone6 页保证不了了。
小结 :
这个文章写了两篇,可能不会有太多的人关注因为这里仅仅一个问题
UICollection 快速滑动问题,和SwiftUI 嵌入UIKit的问题
但是这个路线其实不是原生开发的选择,是苹果逼着开发者必须走的路线。
因为 LazyVGrid 和List 目前还不能保证 企业 产品的 需求,开发Demo 可能无感,感觉SwiftUI 语法和结构更直观。
但是一旦数据和业务复杂起来,LazyVGrid 的下拉和上拉就要写半天,滑动选中更是要 调试算法了,最后导致一个项目各种地方的下拉和上拉的处理逻辑和方式不一样。 有时候不知道这个是退步还是进步,5分钟 的事情 变成了LazyGrid 的挑战。