前言
关于页面的性能优化,我们能做些什么?个人想了一下,能想到的就是以下几点:
- 优化布局计算
- 异步进行耗时操作
- 解决图片问题(对于
APP
来说,图片是造成内存问题的最大瓶颈点) - 避免离屏渲染
优化布局计算
关于auto layout
的布局,最直接的优化是使用手动布局计算frame
。
因为自动布局的原理是:通过创建一个与view
绑定的对象engine
,使用engine
记录下来相关的约束信息,在布局计算的时候,带入相关参数计算出来frame
.
如果能去掉这一步,肯定是能节省性能的。
虽然iOS 12
系统之后,苹果对auto layout
进行了优化,优化后的效率和手动布局差不太多。但是我们的用户还是会有很多在12系统以下的。
因此,还是可以考虑优化的。
不过我目前还没有遇到过使用auto layout
造成页面性能出现问题的案例。
异步耗时操作
图片解码操作
图片为什么需要解码?
首先我们项目里的本地图和加载过来的网图,都是经过压缩的二进制数据(常用的jpg
,png
)。
在显示到界面上的时候,需要将这些二进制数据绘制到对应的“画布”上。
这个绘制的过程就是解码。
异步图片解码
系统会默认在UIImage
加载到UIImageView
或者CALayer
的content
上的时候,在主线程进行解码。
这个解码操作是耗时的,如果不处理可能会造成卡顿问题,因此需要放到子线程里去异步执行。
我们使用的三方框架都有此类处理。
比如SDWebImage
是创建了一个串行队列,异步执行解码操作。
通过SDWebImageAvoidDecodeImage
参数可以控制是否进行解码操作。
不过这里有个疑惑,为什么不使用并发队列?使用串行队列就意味着图片解码操作要顺序执行,这样效率岂不是有点低?
缓存高度
对于不同cell
的滑动列表,可以利用缓存来避免多次计算,达到提效的目的。
可以做的简单点利用数据模型来持有这些信息。
也可以利用三方的框架: UITableView FDTemplateLayoutCell
解决图片问题
图片占有内存问题
从上图可以看到,图片占用内存的大小计算方式是: width
* height
* 每个像素占用的内存大小
(一般是4字节)
一张图,如果分辨率比较大,就容易造成很大的内存问题
当页面上有多个图片的时候,这个内存就会暴增。
避免无谓的解码操作
SDWebImage
在加载图片的时候默认会进行解码操作。
网上随便找了三张图,使用SDWebImageManager
去加载。在开启和关闭解码操作后,在开启解码操作的时候,内存占用了10M
。 关闭解码操作的时候,内存占用了5M
。
使用第三方库的时候,如果只是预加载图片,可以考虑设置不解码。
DownSampling(向下采样)
DownSampling
就是在Decode
的时候指定尺寸,只Decode
部分数据,减少内存的使用。
比如我一个控件大小是100 * 100
,但是原图可能是300 * 300
的。使用DownSampling
后,只需要解码少量数据就可以达到所需。
这个SDWebImage
也已经支持,大家只需在加载图片的时候,利用context
参数设置图片的大小和控件的大小相同即可。
@{SDWebImageContextImageThumbnailPixelSize:@(size)}
复制代码
经过我的实验,效果还是很明显的。
网上找了6
张大图。
在不进行DownSampling
的情况下,加载6
张图片都消耗了 25M
但是在使用DownSampling
的时候,指定尺寸为100*100
,内存直接降低到了 5M
目前运用到项目中的首页Feeds流上,效果很显著。在只加载不到三屏的情况下(一屏大概4-6张图),都节省了40M
左右。
注意使用的时候传入的尺寸要考虑到高清屏的系数。
因此,我们在搭建界面的时候,尽量加载和控件一样大的图,否则可能你看到的只是一个小图,其实占用了很大的内存,同时还需要CPU
帮你去做一些压缩,剪裁的工作。
建议使用Image Assets
去管理
- 查找快
如果是放到bundle里,是需要遍历bundle的文件夹。但是
Image Assets
的查找做了优化。 - 更加智能的缓存策略
- 可以减少拆分后的包体积
- 支持图片拉伸等特性。
减少Backing Store
的使用
什么是backing store?
CALayer
的contents
所指向的区域。
如果使用了draw
函数,CALayer
会创建一个与view
相同尺寸的Backing store
,在上面进行draw
的操作,然后提交到frame buffer
中用于渲染。
这一步会造成内存的消耗。
因为系统的UILabel
对单色的string
做了优化处理,可节省75%的Backing Store
,并能自动更新Backing Store
的size
以适配富文本
或emoji
。
对于一些复杂的view
样式,可以通过多个subView
组合到一起来实现,尽量减少draw
的操作,就可以减少这部分内存的消耗。
图片复用
对于纯色图片,尽量复用一个图片,用tint Color
来进行不同的渲染。达到复用图片的目的。
对于使用频繁的图片,可以使用[UIImage imageWithNamed:@""]
方式创建,利用系统级别的缓存来提高效率,减少内存。
对于使用不频繁的图片,建议使用直接读文件的方式加载,用完就会自动释放,减少内存。
离屏渲染
什么是离屏渲染?
正常情况下,系统会按照60FPS
或者120FPS
的频率来执行渲染流程。
在每个屏幕渲染周期内,系统会从帧的缓冲区里拿到已经渲染好的数据,渲染到屏幕上。
而由于图层或者其他因素,导致在屏幕内无法直接渲染,需要在屏幕外开辟一个空间用来合成帧数据。
这就是所谓的离屏渲染。
离屏渲染的坏处
离屏渲染之所以不好,原因是:
1.开辟了一块额外的空间,内存增加了
2.切换环境造成的牺牲很大
很容易发生在渲染周期内,数据无法渲染好,因此造成卡顿问题。
造成离屏渲染的方式
关于离屏渲染,实际开发中基本上都是:
- 圆角 剪裁的组合
- 设置layer的mask
- 设置阴影
- 光栅化
- 抗锯齿
解决离屏渲染
对于设置阴影造成的离屏渲染,解决方式就是使用贝塞尔曲线绘制好path
,这样就能解决问题。
这里想更多的介绍一下圆角方面的优化。
对于UIImageView
的圆角方案
最开始的时候,我的想法和网上的方案一样,就是:
利用子线程将图片进行切角处理,同时缓存下来这张图片,然后异步到主线程使用图片。这样就可以解决了圆角的离屏渲染问题。
但是在实际操作过程中,觉得这一步还是有问题的。
问题一:内存占用增加
在滑动列表里,我们不可能一直不停地对图片进行切圆角操作。
不然就需要一直消耗CPU
进行切圆角操作,还要频繁切换线程。
因此,我们就需要使用空间换时间了,将切好的圆角图片也缓存下来。
这个时候问题来了:一张图,被网络框架加载并存储了一份,现在又存下来了一张圆角图片
那基本上意味着内存占用double了一下。
问题二:实现功能很容易,但是想写好太难
虽然网上给出了很多的demo,但是在我看来写的都不好。
- 首先,很粗暴的放到了一个全局并发队列里进行绘制,没有考虑到线程的消耗和安全问题。
- 其次是没有一个很完善的防重用逻辑。
一般我们都是使用的滑动视图,里面
cell
上的控件都是会重用的。如果不像SDWebImage
一样先将之前的获取图片操作移除,如何确保重用的时候不出问题? - 使用起来麻烦
如果自己实现了一套获取图片的逻辑,会发现代码量增加很多,远远不如使用
SDWebImage
分类来的方便。(当然我们自己也可以使用一个分类来完成这个任务)
上面说的这些问题,其实都可以通过扩展SDWebImage
来支持。
但是需要花费时间和精力来搞定,未来有机会的话,可以尝试一下。
我认为的圆角最优解
对现在有的机器(iphone 11)进行了简单的验证,使用异步绘制圆角图片的方案解决离屏渲染后,通过instrument
的分析,发现CPU
和GPU
都没有一个很明显的收益变化。
如果非得优化,让设计师切一个遮罩盖在上面是我认为的最好的解决方案。
毕竟前面也提到了,苹果认为组合subViews
的方式比自己绘制的方式好很多。
参考链接:
github.com/seedante/iO…
www.jianshu.com/p/a6bfaf1e0…