在使用过程当中发现,咱们App的首页在快速滑动时会出现掉帧,以及在上拉加载更多时会抖动,由于首页模块是之前的同事写的,不少代码已不适应当前的需求,因此产生了优化的想法,优化主要分为如下几个方面:
git
在Feed流中,UITableViewCell的高度一般是变化的,须要根据返回的数据中的cell类型以及label的文字长度来计算高度,而在UITableView中func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell面试
是一个高频调用的方法,为了减小CPU的计算,尽量减小掉帧,因此须要将高度进行缓存,在咱们的项目中,首页的数据是这样一个操做流程后台返回的JSON->FeedListModel->FeedsModel->各类cell的ViewModel(例如小图片的cell对应的model-SmallImageCellViewModel,大图片的cell对应的-BigImageCellViewModel)FeedListModel主要是包含了一些页码信息和FeedsModel数组FeedsModel储存着后台返回的cell所需的信息BigImageCellViewModel是cell对FeedsModel进行处理后获得cell所需的信息数组
优化之前,咱们的高度是经过BigImageCellViewModel中计算属性height去获取的缓存
var height: CGFloat {微信
guard let title = title else {异步
return ((UIScreen.mainWidth - 30) * 9)/16.0 + 62async
}ide
let constraintRect = CGSize(width: UIScreen.mainWidth - 30, height: 38.5)布局
let attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 16)]测试
let rect = title.boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: attributes,
context: nil)
if let type = itemType, type == ItemType.sohuVideo {
return ((UIScreen.mainWidth - 30) * 9)/16.0 + rect.height + 62
}
return ((UIScreen.mainWidth - 30) * 9)/16.0 + rect.height + 62
}
这样的话每次取值时,会须要经过计算而后返回height属性,因此一开始我也是把计算属性改为存储属性了,可是仍是很耗时(后来才发现是由于高度是存在BigImageCellViewModel中的,而每次数据更新后,因为业务须要会对当前的列表数据从新遍历处理,生成新的BigImageCellViewModel,新的BigImageCellViewModel的高度天然是每次须要计算),在使用instruments分析时发现,在加载数据时,1s内有20%的时候是用于计算每一个cell的高度,由于计算cell高度时须要根据model.title肯定cell中的标题Label显示几行,从而肯定Label的高度,进而算出cell的高度,而计算Label高度通常都是使用这个方法,
@available(iOS 7.0, *)
open func boundingRect(with size: CGSize, options: NSStringDrawingOptions = [], attributes: [NSAttributedString.Key : Any]? = nil, context: NSStringDrawingContext?) -> CGRect
由于即使是同一个字符串,字体大小同样,字体不一样时,高度会不必定同样,这个方法会根据字符串和对应字体进行绘制计算后获得的高度,并且这个操做是在主线程进行的,因此会致使掉帧,而后我就网上查阅资料怎么优化这个方法,网上这方面的资料比较少由于这个方法的耗时自己是可接受范围之内的,只是咱们的height没有真正缓存上致使这个方法测试时特别耗时,这种思路是思路一在子线程中调用这个方法,而后对height进行赋值,相似于这样:
var height:CGFloat = 70
let queue = DispatchQueue.global()
queue.async {
let labelRect = title.boundingRect(with: constraintRect,
options: .usesLineFragmentOrigin,
attributes: attributes,
context: nil)
height = labelRect.height + 50
}
就是经过先给height赋一个几率最大的值,而后经过异步计算后,获得一个准确值,再给height赋值上,可是在实际测试中发现,大部分cell取的是咱们预设的高度默认值,这样在下次reloadData时,cell的高度会取算出来的值,而后会致使tableView的contentSize变化,视图抖动而后我就本身思考,其实咱们的标题并不复杂,大部分是中文,其余是数字,标点符号,字母,而后我就测试了一下在UIFont.boldSystemFont(ofSize: 16)下,中文,数字,标点符号,字母的大小,而后测试发现中文 15pt 数字是8pt左右,主要的一些标点符号16pt 小写字母大概8pt,大写自贸银11pt,就想能不能经过对标题字符串进行遍历,判断字符的类型来计算标题的总宽度,以后再将总宽度除以标题的最大宽度获得行数,而后计算得出cell高度,代码以下:
-(CGFloat)calculateTotalWidthInBold16 {
CGFloat totalWidth = 0;
for (int i = 0; i < self.length; i++) {
unichar character = [self characterAtIndex:i];
//中 占15pt 数字 占7 英文 a 8.2 A 10.1 B 10.6 , ? 16.6pt
if ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember:character]) {//数字
totalWidth += 8;
} else if ([[NSCharacterSet lowercaseLetterCharacterSet] characterIsMember:character]) {//小写字母
totalWidth += 10;
} else if ([[NSCharacterSet uppercaseLetterCharacterSet] characterIsMember:character]) {//大写字母
totalWidth += 12;
} else if ([[NSCharacterSet punctuationCharacterSet] characterIsMember:character]) {//标点符号
totalWidth += 17;
} else if (character >= 0x4E00 && character <= 0x9FA5) {
totalWidth += 15;
} else {
totalWidth += 15;
}
}
return totalWidth + 5;
}
在不缓存高度的状况下,这个方法可以很快得计算出高度,让tableview达到平均55帧以上的帧率,可是缺点是须要对使用的字体下进行测试,在UIFont.boldSystemFont(ofSize: 16)字体下,中文是固定的15pt,可是数字,小写字母,大写字母的长度不是固定的,因此若是须要作到很是准确,须要对每一个数字,字母在这个字体下的长度进行测试。
在缓存高度的状况下,与boundingRect方法相比,这个方法也可以提升计算速度,只是收益不那么明显
由于tableView的cellForRow方法也是一个调用频率特别高的方法,因此应该避免在cellForRow对cell进行约束修改,frame变化等操做,
open func cellForRow(at indexPath: IndexPath) -> UITableViewCell? // returns nil if cell is not visible or index path is out of range
主要是把这部分代码注释掉了,这部分操做主要是为了隐藏最后一个cell的分割线,可是咱们是预加载的,其实不多能看到最后一个cell的底部,因此其实没有必要
default: //feed流
let cellViewModel = viewModel.viewModels.value[indexPath.row]
let cell = configFeedCell(tableView: tableView, cellViewModel: cellViewModel, indexPath: indexPath)
// cell.saHorizontalSpace = (15, 15)
// if viewModel.isInfrontOfFeedSpacAble(indexPath: indexPath) {
// cell.saSeparaptorLineStyle = .bottom
// } else if cellViewModel as? FeedSpacAble != nil {
// cell.saSeparaptorLineStyle = .bottom
// } else {
// cell.saSeparaptorLineStyle = .none
// }
return cell
主要使用charles进行抓包,看项目有没有加载比较大的图片,咱们项目首页的三张图片的资讯使用的是大图,一张图片长达4M,因此我改为小图了
由于tableView会根据estimatedRowHeight*行数来计算contentSize,而且在滑动时进行修正,因此会发生抖动,因此能够经过如下代码,禁用预估高度,由于iOS11之后预估高度的值不为0,因此须要显式赋值为0
tableView.estimatedRowHeight = 0
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0
主要注释了代码中没有用的数据处理逻辑
以上其实只是针对咱们项目一些比较基本的优化的地方,固然还有不少地方能够进行优化,例如将cell中view的布局进行缓存,减小没必要要的计算,还有将一些Label经过异步渲染的方式绘制在cell中,减小view的层级,将一部分渲染的工做放在子线程中,可是这样会对咱们的项目改动过大,因此暂时没有采用
PS: 最近加了一些iOS开发相关的QQ群和微信群,可是感受都比较水,里面对于技术的讨论比较少,因此本身建了一个iOS开发进阶讨论群,欢迎对技术有热情的同窗扫码加入,加入之后你能够获得:
1.技术方案的讨论,会有在大厂工做的高级开发工程师尽量抽出时间给你们解答问题
2.每周按期会写一些文章,而且转发到群里,你们一块儿讨论,也鼓励加入的同窗积极得写技术文章,提高本身的技术
3.若是有想进大厂的同窗,里面的高级开发工程师也能够给你们内推,而且针对性得给出一些面试建议
群已经满100人了,想要加群的小伙伴们能够扫码加这个微信,备注:“加群+昵称”,拉你进群,谢谢了 !