[性能优化]UITableView性能优化的一点感悟及计算UILabel高度的新方法

建了一个面试题解答的项目,大家可以看一看,希望大家帮忙给一个star,谢谢了! 

项目地址:https://github.com/NotFound9/interviewGuide 


在使用过程中发现,我们App的首页在快速滑动时会出现掉帧,以及在上拉加载更多时会抖动,因为首页模块是以前的同事写的,很多代码已不适应当前的需求,所以产生了优化的想法,优化主要分为以下几个方面:

1.缓存cell高度(发现了一种计算Label高度的新方法)

2.优化cellForRow方法

3.图片加载优化

4.禁止tableView预估高度

5.删除无用数据处理逻辑

缓存cell高度

在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 + 62

   }

   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进行赋值,类似于这样:

思路一 异步计算Label高度

   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高度,代码如下:

思路二 计算Label高度的新方法 通过遍历字符串来计算高度

-(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方法相比,这个方法也能够提高计算速度,只是收益不那么明显

优化cellForRow方法

因为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预估高度

因为tableView会根据estimatedRowHeight*行数来计算contentSize,并且在滑动时进行修正,所以会发生抖动,所以可以通过以下代码,禁用预估高度,因为iOS11以后预估高度的值不为0,所以需要显式赋值为0   

       tableView.estimatedRowHeight = 0

       tableView.estimatedSectionHeaderHeight = 0

       tableView.estimatedSectionFooterHeight = 0

删除无用数据处理逻辑

主要注释了代码中没有用的数据处理逻辑

总结

以上其实只是针对我们项目一些比较基本的优化的地方,当然还有很多地方可以进行优化,例如将cell中view的布局进行缓存,减少不必要的计算,还有将一些Label通过异步渲染的方式绘制在cell中,减少view的层级,将一部分渲染的工作放在子线程中,但是这样会对我们的项目改动过大,所以暂时没有采用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容