前言: 在iOS
开发中,UITableview
的使用场景是非常广泛的,随着业务逐渐复杂,对于cell的高度的处理也是越来越麻烦的,有的时候甚至会非常痛苦。
一、常规解决方案
当前主流的做法是在model
中计算一次然后缓存在model
中,以提升体验和性能,至于计算的时机,可以在model
解析完成后计算,也可以在heightForRowAt indexPath
调用时再计算,都可以,是可以解决问题,不过存在弊端:
-
做这些高度的计算非常的麻烦,麻烦随复杂度提升,会出现很多的
if-else
-
高度计算并不精确
a. 计算
label
高度 不精确
b. 计算attributeString
高度 不精确
c. ... -
为了计算高度,做了许多额外的事情,额外的内存开销(ps:计算过的人都懂的)
-
如果是在
model
解析完成后计算,那首次加载和下一页时,会出现卡顿 -
如果在
heightForRowAt indexPath
调用时计算,cell
首次被渲染会出现卡顿
二、Cell 高度自动计算
于是需要一个小工具,可以自动计算cell
的高度,来规避这些麻烦。
cell高度计算考虑的点:
-
1.计算原理:使用数据设置完
cell
后,强制布局self.layoutIfNeeded()
,然后获取高度 -
2.计算的方式:
a. 最初的思路是,直接拿整个
cell
的话,遍历所有的子视图,循环累加
b. 后来觉得没有必要做一次循环,使用者传入一个用来计算指定的位于底部的视图,用这视图的y值加上height得到的就是cell的高度了,也方便一些复杂cell
中各种隐藏和显示的使用,也略微提升性能 -
3.保证性能:每个
cell
必须只计算一次,换句话来说,需要有缓存的功能 -
4.场景覆盖:
a. 有些
cell
不一样的状态,需要显示不一样的内容,此时的高度很有可能不一样,需要支持
b. 一些带操作事件的按钮,执行完一些操作后,cell
的元素可能出现增减,此时很可能需要重新计算高度,需要支持 -
5.其他:
cell
之间一般都是有间距的,以卡片风格为例,其实真正需要关注的只是卡片的高度,而实际高度是需要加上间距的,需要支持一下这种类型,算出实际的高度之后加上一个偏移值,其他场景可能也需要,默认值为0
三、源码
为UITableview扩展两个属性,用于实现缓存功能:
-
cacheHeightDictionary
:缓存cell
行高的Dictionary
,key
为model
的JSONString
或指定的其他唯一标识,value
为自动计算好的行高 -
cacheCellDictionary
:缓存用来获取或计算行高的cell
,保证性能(理论上只需要一个cell
来计算行高即可,降低消耗)
/*
TableViewCell 使用 SnapKit 布局 自动计算行高并缓存
*/
public extension UITableView {
/// 缓存 cell 行高 的 DIC(key为model的JSONString或指定的其他唯一标识,value为自动计算好的行高)
var cacheHeightDictionary: NSMutableDictionary? {
get {
let dict = objc_getAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightDictKey) as? NSMutableDictionary
if let cache = dict {
return cache
}
let newDict = NSMutableDictionary()
objc_setAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightDictKey, newDict, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newDict
}
}
/// 缓存用来获取或计算行高的cell,保证性能(理论上只需要一个cell来计算行高即可,降低消耗)
var cacheCellDictionary: NSMutableDictionary? {
get {
let dict = objc_getAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightReuseCellsKey) as? NSMutableDictionary
if let cache = dict {
return cache
}
let newDict = NSMutableDictionary()
objc_setAssociatedObject(self, &kUITableViewSnapKitCellCacheHeightReuseCellsKey, newDict, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return newDict
}
}
}
为UITableViewCell
扩展两个属性,用于实现高度的计算:
-
hpc_lastViewInCell
:所指定的距离cell
底部较近的参考视图,必须指定,若不指定则会assert失败 -
hpc_bottomOffsetFromLastViewInCell
:可选设置的属性,表示cell
的高度需要从指定的lastViewInCell
需要偏移多少,默认为0,小于0也为0
/**
TableViewCell 使用 Masonry布局 自动计算 cell 行高 category
-- UI布局必须放在UITableViewCell的初始化方法中:- initWithStyle:reuseIdentifier:
*/
public extension UITableViewCell {
// 可选设置的属性,表示cell的高度需要从指定的lastViewInCell需要偏移多少,默认为0,小于0也为0
@objc var hpc_bottomOffsetFromLastViewInCell: CGFloat {
get {
if let number = objc_getAssociatedObject(self, &kBottomOffsetFromLastViewInCellKey) as? NSNumber {
return CGFloat(number.floatValue)
}
return 0.0
}
set {
objc_setAssociatedObject(self, &kBottomOffsetFromLastViewInCellKey, NSNumber(value: Float(newValue)), .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
/// 所指定的距离cell底部较近的参考视图,必须指定,若不指定则会assert失败
var hpc_lastViewInCell: UIView? {
get {
let lastView = objc_getAssociatedObject(self, &kLastViewInCellKey)
return lastView as? UIView
}
set {
objc_setAssociatedObject(self, &kLastViewInCellKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
计算逻辑:
extension UITableViewCell {
/// 带缓存功能,自动计算行高
///
/// - Parameters:
/// - tableView: 目标tableview
/// - config: 计算行高配置回调
/// - cache: 缓存参数(key,唯一指定key【可以是model的id,或者model的JSONString】,stateKey,行高状态【可选】,shouldUpdate,【可选,默认false,是否要更新指定stateKey中缓存高度,若为true, 不管有没有缓存,都会重新计算)
/// - Returns: 行高
public static func cellHeight(forTableView tableView: UITableView,
config: ((_ cell: UITableViewCell) -> Void)?,
updateCacheIfNeeded cache: (() -> (key: String, stateKey: String?, shouldUpdate: Bool?))?) -> CGFloat {
// 有缓存则从缓存中取
if let cacheBlock = cache {
let keyGroup = cacheBlock()
let key = keyGroup.key
let stateKey = keyGroup.stateKey ?? kSnapKitCellCacheStateDefaultKey
let shouldUpdate = keyGroup.shouldUpdate ?? false
if shouldUpdate == false {
if let cacheDict = tableView.cacheHeightDictionary,
let stateDict = cacheDict[key] as? NSMutableDictionary, // 状态高度缓存
let height = stateDict[stateKey] as? NSNumber {
if height.intValue != 0 {
return CGFloat(height.floatValue)
}
}
}
}
let className = self.description()
var cell = tableView.cacheCellDictionary?.object(forKey: className) as? UITableViewCell
if cell == nil {
if Thread.isMainThread {
cell = self.init(style: .default, reuseIdentifier: nil)
} else {
// 这里在第一次计算时,可能会卡住主线程,需要优化,考虑使用信号量 @山竹
DispatchQueue.main.sync {
cell = self.init(style: .default, reuseIdentifier: nil)
}
}
tableView.cacheCellDictionary?.setObject(cell!, forKey: className as NSCopying)
}
if let block = config { block(cell!) }
// 添加子线程处理
var height: CGFloat = 0
if Thread.isMainThread {
height = cell!.calculateCellHeight(forTableView: tableView, updateCacheIfNeeded: cache)
} else {
DispatchQueue.main.sync {
height = cell!.calculateCellHeight(forTableView: tableView, updateCacheIfNeeded: cache)
}
}
return height
}
/// 获取缓存高度并缓存
///
/// - Parameters:
/// - tableView: 目标tableview
/// - cache: 缓存参数
/// - Returns: 高度
private func calculateCellHeight(forTableView tableView: UITableView,
updateCacheIfNeeded cache: (() -> (key: String, stateKey: String?, shouldUpdate: Bool?))?) -> CGFloat {
assert(self.hpc_lastViewInCell != nil, "hpc_lastViewInCell property can't be nil")
self.layoutIfNeeded()
var height = self.hpc_lastViewInCell!.frame.origin.y + self.hpc_lastViewInCell!.frame.size.height
height += self.hpc_bottomOffsetFromLastViewInCell
if let cacheBlock = cache {
let keyGroup = cacheBlock()
let key = keyGroup.key
let stateKey = keyGroup.stateKey ?? kSnapKitCellCacheStateDefaultKey
if let cacheDict = tableView.cacheHeightDictionary {
// 状态高度缓存
let stateDict = cacheDict[key] as? NSMutableDictionary
if stateDict != nil {
stateDict?[stateKey] = NSNumber(value: Float(height))
} else {
cacheDict[key] = NSMutableDictionary(object: NSNumber(value: Float(height)), forKey: stateKey as NSCopying)
}
}
}
return height
}
}
核心代码只有三处,其他都是一些判断,是否有缓存,有的话从缓存中取,没有的话,计算高度,然后缓存起来:
- 初次调用,会
new
一个cell
出来,用来计算高度,然后缓存起来,用于下次调用
- 初次调用,会
let className = self.description()
var cell = tableView.cacheCellDictionary?.object(forKey: className) as? UITableViewCell
if cell == nil {
if Thread.isMainThread {
cell = self.init(style: .default, reuseIdentifier: nil)
} else {
// 这里在第一次计算时,可能会卡住主线程,需要优化,考虑使用信号量 @山竹
DispatchQueue.main.sync {
cell = self.init(style: .default, reuseIdentifier: nil)
}
}
tableView.cacheCellDictionary?.setObject(cell!, forKey: className as NSCopying)
}
- 将
cell
实例通过回调给调用者用于渲染cell
- 将
回调出去:
if let block = config { block(cell!) }
此处为使用使用的地方,config回调,得到的就是用于计算高度的cell实例:
OrderListCell.cellHeight(forTableView: tableView, config: { (targetCell) in
if let cell = targetCell as? OrderListCell {
cell.configCell(itemModel, self.viewModel.orderType, self.viewModel.orderState, indexPath.section)
}
}, updateCacheIfNeeded: { () -> (key: String, stateKey: String?, shouldUpdate: Bool?) in
return (itemModel.cellHeightCacheKey.0 + "+\(indexPath.section)+\(indexPath.row)", nil, itemModel.cellHeightCacheKey.1)
})
- 计算高度,通过强制布局刷新,用指定用来计算的子视图的
y
加上功能height
,然后加上用户定义的偏移值,就是cell
需要的实际高度
- 计算高度,通过强制布局刷新,用指定用来计算的子视图的
self.layoutIfNeeded()
var height = self.hpc_lastViewInCell!.frame.origin.y + self.hpc_lastViewInCell!.frame.size.height
height += self.hpc_bottomOffsetFromLastViewInCell
四、使用
- 在
cell
的ui
初始化时,指定hpc_lastViewInCell
- 在
// 自动高度计算
hpc_lastViewInCell = countLabel
hpc_bottomOffsetFromLastViewInCell = 6
- 在渲染
cell
的地方,如果有需要,需要修改hpc_lastViewInCell
的值,有些复杂cell中各种组合非常多,底部子视图不确定是哪一个,是需要修改hpc_lastViewInCell
的,有些底部子视图是固定的cell
则不用
- 在渲染
// 以下为自动高度计算代码
var lastView: UIView = countLabel
var offset: CGFloat = 6
if !(model.expectDeliveryTime ?? "").isEmpty {
if model.haveMaterial {
lastView = checkMaterialViewWithDelivery
offset = 8
} else {
lastView = expectDeliveryLabel
}
} else {
lastView = checkMaterialView
offset = 8
}
hpc_lastViewInCell = lastView
hpc_bottomOffsetFromLastViewInCell = offset
- 在
heightForRowAt indexPath
方法中,调用自动高度计算,有些特殊需求,有一个最小高度,小于最小高度则使用最小高度,反之则使用计算的高度,没有这种需求的,直接返回计算的高度就好
- 在
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if viewModel.showItemArray?.count ?? 0 > indexPath.section, viewModel.showItemArray?[indexPath.section].tradeOrderTOList?.count ?? 0 > indexPath.row,
let itemModel = viewModel.showItemArray?[indexPath.section].tradeOrderTOList?[indexPath.row] {
// 自动高度计算
let calculateHeight = OrderListCell.cellHeight(forTableView: tableView, config: { (targetCell) in
if let cell = targetCell as? OrderListCell {
cell.configCell(itemModel, self.viewModel.orderType, self.viewModel.orderState, indexPath.section)
}
}, updateCacheIfNeeded: { () -> (key: String, stateKey: String?, shouldUpdate: Bool?) in
return (itemModel.cellHeightCacheKey.0 + "+\(indexPath.section)+\(indexPath.row)", nil, itemModel.cellHeightCacheKey.1)
})
return calculateHeight > itemModel.cellHeight ? calculateHeight : itemModel.cellHeight
}
return 0
}
五、性能
以下是第一页20条数据计算所花费的时间,第一条数据由于要new
一个cell
实例出来,耗时略长,其他的都是很少的,对性能无任何影响:
calculate cell height use time: 9.206771850585938 ms
calculate cell height use time: 0.8568763732910156 ms
calculate cell height use time: 0.7119178771972656 ms
calculate cell height use time: 0.6990432739257812 ms
calculate cell height use time: 0.6499290466308594 ms
calculate cell height use time: 1.8820762634277344 ms
calculate cell height use time: 0.7300376892089844 ms
calculate cell height use time: 0.9169578552246094 ms
calculate cell height use time: 0.7901191711425781 ms
calculate cell height use time: 0.14495849609375 ms
calculate cell height use time: 0.4162788391113281 ms
calculate cell height use time: 0.4220008850097656 ms
calculate cell height use time: 0.17404556274414062 ms
calculate cell height use time: 0.15878677368164062 ms
calculate cell height use time: 0.6649494171142578 ms
calculate cell height use time: 0.14901161193847656 ms
calculate cell height use time: 0.12803077697753906 ms
calculate cell height use time: 0.12111663818359375 ms
calculate cell height use time: 0.7219314575195312 ms
calculate cell height use time: 1.0538101196289062 ms
calculate cell height use time: 0.0209808349609375 ms
calculate cell height use time: 0.0171661376953125 ms
calculate cell height use time: 0.01811981201171875 ms
网友评论