群组
Flutter,iOS,Android,Python等,闲聊工作&技术&问题;
个人主页:https://waitwalker.cn
telegram群链接:https://t.me/joinchat/Ej0o0A1ntlq5ZFIMzzO5Pw
背景
作者基于之前自己所写的Swift项目–仿照推特客户端用纯Swift写的一个项目,目前已经公开放在GitHub上(https://github.com/waitwalker/MyTwitter); 接口用Python写的几个(https://github.com/waitwalker/MyTwitterAPI),目前所实现的功能是登录,注册,发推,首页列表等功能,其他页面都是一些假数据.这里不主要分析项目了,有时间在详细说一下.本文的主要重点是说说自己对性能调优–tableView控件优化的一点理解,有些问题理解的不是很透彻,希望大家能多给些意见,建议,谢谢.下图是整个项目的总览(文章中多是gif动态,尺寸较大,使用流量看得慎重):
有一句话说:过早的优化是万恶之源, 过早并不是开发过程的早期,而是在还没弄清楚业务需求的情形下去做所谓的优化,有时候会适得其反——费时、费力、不讨好。正确的方法是,先有质量地实现你的需求,写够测试用例,然后做profile去找到性能的瓶颈,考虑究竟哪些地方应该优化,应该如何优化,哪些不应该优化.
作者对一些显示的垂直信号,水平信号原理不是了解,大家如果想深入了解的可以查一下相关资料,相关大神也有总结,或许从硬件底层优化可能效果会更好.造成tableView卡顿的原因,从硬件上来说无非就两个,一个是CPU原因,一个是GPU原因.如果CPU核数较多,并发处理问题的能力也就越强,处理大量计算也不在话下;如果GPU显存够大,渲染能力足够强,处理复杂图形界面也就得心应手.但是,硬件的配置是有限度的,我们的目标是在有限度的硬件上,让其发挥最大限度的作用.这个也就是造成tableView卡顿的程序原因(软件原因)–卡住了主线程,本文将主要讨论是从程序角度讨论怎么优化tableView问题.
基础
最基本的就是减少cell的自定义类型,重用cell,每次只绘制屏幕显示cell的数量,其它cell从缓存中取.这些基础大家应该比我了解,这里不再陈述了.
1. 减轻CPU负荷
我们知道CPU的主要负责快速调度任务,大量计算工作,所以在tableView快速滚动的过程中让CPU的计算量降低是优化应该考虑的方向.下面总结了三个方面来尽可能的降低CPU计算:
1.1提前计算好cell的高度,缓存在相应的数据源模型中
大家都已经知道tableView的代理回调方法中,先调用的是返回cell高度的方法,然后在返回实例化cell的方法.我们可以在返回cell高度时,提前计算好cell的高度,缓存到数据源模型中.例如:MTTHomeModel对应的是首页cell的数据模型,我们可以看到下面两个变量,是来存储cell的高度和内容高度的,:
var cellHeight:CGFloat?
var contentHeight:CGFloat?
在获取数据后台数据的时候,把cell高度计算出来,缓存起来:
homeModel.contentHeight = self.calculateTextHeight(text: homeModel.contentTextString!) + 150
if(homeModel.retwitterType?.count)! > Int(0)
{
homeModel.cellHeight = 255 + homeModel.contentHeight! - 150
} else{
homeModel.cellHeight = 230 + homeModel.contentHeight! - 150 + 15
}
在返回cell高度的方法中,直接读取缓存的高度,而不需要在重新计算了.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
if self.homeDataArray != nil
{
let homeModel = homeDataArray![indexPath.row]
return homeModel.cellHeight!
} else
{
return 300
}
}
1.2 尽可能的降低storyboard,xib等使用度
之前看到一些大家分享的相关资料,通过Interface知道xib或者storyboard本身就是一个xml文件,添加删除控件必然中间多了一个encode/decode过程,增加了cpu的计算量.并且 还要避免臃肿的 XIB 文件,因为XIB文件在主线程中进行加载布局.当用到一些自定义View或者XIB文件时,XIB的加载会把所有内容加载进来,如果XIB里面的一些控件并不会用到,这就可能造成一些资源的消耗浪费.网上有说:Storyboard 没这个问题,只会按需加载,这个作者还没有去考证.
1.3 滑动过程中尽量减少重新布局
自动布局就是给控件添加约束,约束最终还是转换成frame.所以,在满足业务需求情况下,如果图层层次较为复杂,要尽量减少自动布局约束,转为手动计算布局,大量的约束重叠也会增加cpu的计算量
作者在获取到数据源时,每次都重新布局控件,这个也是一个重要开销,也是接下来需要优化的方向.
private funclayoutSubview(homeModel:MTTHomeModel) -> Void
{
topLineView?.snp.makeConstraints({ (make) in
make.left.right.top.equalTo(0)
make.height.equalTo(0.3)
})
}
2. 不要阻塞主线程
UIKit的工作基本上都是在主线程上进行,界面绘制,用户输入响应等等.当所有的代码逻辑都放在主线程时,某些耗时任务可能会卡住主线程造成程序无法响应,流畅度降低等问题;在主线程中绘制大量界面图层,网络I/O,磁盘I/O等都可以造成界面卡顿现象.
下面我们通过Xcode自带的调试工具Instruments来看看项目界面的流畅度,及其一些建议,Instruments给我提供了各种各样的调试查看工具,下面简单介绍一下:
1)Blank: 创建一个空的模板,可以从Library库中添加其他模板.
2)Activity Monitor: 监控进程级别的CPU,内存,磁盘,网络使用情况,可以得到你的应用程序在手机运行时总共占用的内存大小.
3)Allocations: 跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史,可以检测每一个堆对象的分配内存情况.
4)Cocoa Layout : 观察NSLayoutConstraint对象的改变,帮助我们判断什么时间什么地点的constraint是否合理.观察约束变化,找出布局代码的问题所在.
5)Core Animation: 这个模块显示程序显卡性能以及CPU使用情况,查看界面流畅度.
6)CoreData: 这个模块跟踪Core Data文件系统活动.
7)Counters : 收集使用时间或基于事件的抽样方法的性能监控计数器(PMC)事件.
8)Energy Log: 耗电量监控.
9)File Activity: 检测文件创建,移动,变化,删除等.
10)Leak: 一般的措施内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录.
11)Metal System Trace: Metal API是apple 2014年在ios平台上推出的高效底层的3D图形API,它通过减少驱动层的API调用CPU的消耗提高渲染效率.
12)Network: 用链接工具分析你的程序如何使用TCP/IP和UDP/IP链接.
13)SceneKit: 3D性能状况分析.
14)System Trace: 系统跟踪,通过显示当前被调度线程提供综合的系统表现,显示从用户到系统的转换代码通过两个系统调用或内存操作.
15)System Usage: 这个模板记录关于文件读写,sockets,I/O系统活动,输入输出.
16)Time Profiler(时间探查): 执行对系统的CPU上运行的进程低负载时间为基础采样.
17)Zombies: 测量一般的内存使用,专注于检测过度释放的野指针对象,也提供对象分配统计,以及主动分配的内存地址历史.
本文主要使用的是Instruments中的第5个工具:Core Animation(图形性能),这个模块显示程序显卡性能以及CPU使用情况,查看界面流畅度.
首先我们必须要把源码安装到测试设备上,1)连接Xcode运行程序;2)然后选择快捷键(Command + Control + i)调出Instruments,选择Core Animation.打开后我们可以看到Debug Options里面有多个调试选项,下面我们挨个尽量来分析看一下:
###2.1 Color Blended Layers
这个选项选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮显示,越红表示性能越差,会对帧率等指标造成较大的影响.红色通常是由于多个半透明图层叠加引起.
作者项目可能项目比较简单,图层也不是很复杂,所以通过Color Blended Layers查看,深红色并不是很明显,在快速滑动的过程中,帧率依然能够保持在55+以上,并且图层中也没有大量的深红色区域出现.
###2.2 Color Hits Green and Misses Red
当UIView.layer.shouldRasterize = YES 时,耗时的图片绘制会被缓存,并当做一个简单的扁平图片来呈现.这时候,如果页面的其他区块(比如 UITableViewCell 的复用)使用缓存直接命中,就显示绿色,反之,如果不命中,这时就显示红色.红色越多,性能越差.因为栅格化生成缓存的过程是有开销的,如果缓存能被大量命中和有效使用,则总体上会降低开销,反之则意味着要频繁生成新的缓存,这会让性能问题雪上加霜.
这里笔者还要提一下图片的加载方式,我们知道图片的一般加载方式有两种:imageNamed 和imageWithContentsOfFile;它们的不同在于前者会对图片进行缓存,而后者只是简单的从文件加载文件.如果你加载的是大图,并且只会用到一次,比如欢迎引导图,那么就没必要缓存这个图片,可以使用[UIImage imageWithContentsOfFile:],用完就释放了.如果会多次使用到一张图时,用[UIImage imageNamed:] 就会高效很多,因为这种加载图片方式有一个缓存机制.YYImage实现原理应该就是后面这种思路,自己手动添加缓存.
###2.3 Color Copied Images
对于 GPU 不支持的色彩格式的图片只能由 CPU 来处理,把这样的图片标为蓝色.蓝色越多,性能越差.因为,我们不希望在滚动视图的时候,由 CPU 来处理图片,这样可能会对主线程造成阻塞.
###2.4 Color Immediately
通常 Core Animation Instruments 以每毫秒 10 次的频率更新图层调试颜色。对某些效果来说,这显然太慢了.这个选项就可以用来设置每帧都更新(可能会影响到渲染性能,而且会导致帧率测量不准,所以不要一直都设置它).
###2.5 Color Misaligned Images
这个选项检查了图片是否被缩放,以及像素是否对齐.被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色.黄色,紫色越多,性能越差.
这里UI在切图的时候尽量切得尺寸和你控件的尺寸保持一致,尽量让图片保持原始尺寸.笔者这里所用图片几乎全部拉伸,由于图片都是从本地加载的,没有经过处理.
###2.6 Color Offscreen-Rendered Yellow
这个选项会把那些离屏渲染的图层显示为黄色.黄色越多,性能越差.这些显示为黄色的图层很可能需要用 shadowPath 或者 shouldRasterize 来优化.
离屏渲染,即 Off-Screen Rendering.与之相对的是 On-Screen Rendering,即在当前屏幕渲染,意思是渲染操作是用于在当前屏幕显示的缓冲区进行.那么离屏渲染则是指图层在被显示之前是在当前屏幕缓冲区以外开辟的一个缓冲区进行渲染操作.
离屏渲染需要多次切>换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上又需要将上下文环境从离屏切换到当前屏幕,而上下文环境的切换是一项高开销的动作.
一般对控件属性操作会触发离屏渲染:
1)阴影(UIView.layer.shadowOffset/shadowRadius/…)
2)圆角(当 UIView.layer.cornerRadius 和UIView.layer.maskToBounds 一起使用时)
3)图层蒙板
在实际开发中应尽量避免触发离屏渲染.
###2.7 Color OpenGL Fast Path Blue
这个选项会把任何直接使用OpenGL 绘制的图层显示为蓝色.蓝色越多,性能越好.如果仅仅使用 UIKit 或者 Core Animation 的 API,那么不会有任何效果.如果使用 GLKView 或者 CAEAGLLayer,那如果不显示蓝色块的话就意味着你正在强制 CPU 渲染额外的纹理,而不是绘制到屏幕.
总结
任何优化都是以业务需求为前提,在满足基本需求的情况下,逐步提高代码的质量,提升程序性能,不仅是自我能力的表现,也能从中获得一些收获及成就感.以上优化方向思路也是在前人总结的基础上,作者在自己的项目中的简单应用,里面还有许许多多需要改进提升的地方,也希望大家能给一些深层次上的建议意见.
参考资料
https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/