这篇文章不会深究界面渲染原理和底层绘制机制,只讨论 iOS 中 UIImageView 的一些使用上的性能优化。Xcode 为我们提供了很多牛逼的性能检测工具,今天我们会使用模拟器自带的 UI 性能检测功能,来优化我们的 UIImageView 性能。
首先,我们得知道什么情况下会引发 UI 性能问题,才能针对性的去处理这些问题。
当我们给 UIImageView 设置带有 alpha 通道的图片(有透明度的 png 格式图片)的时候,当我们拉伸或者缩放图片去适应 UIImageView 显示的时候,并且有时候如果我们做用户头像的时候会用图层的 cornerRadius 属性设置圆角,让图片看起来更加美观,这些情况都会造成性能问题。我们使用模拟器来一一测试这些问题。
在模拟器 Debug 选项下面,有下图所示的几个选项:
Color Blended Layers (透明图片、混合模式)
这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮,性能越差的区域就越红(也就是多个半透明图层的叠加)。由于重绘的原因,混合对 GPU 性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。
Color Misaligned Images (尺寸拉伸、缩放)
会使用黄色高亮那些被压缩或者拉伸以及没有正确对齐到像素边界的图片(也就是非整型坐标),这些中的大多数通常会导致图片的不正常缩放,如果把一张大图当缩略图显示,或者不正确地模糊图像。
Color Copied Images (图片拷贝)
有时候寄宿图片(layer.content)的生成是由 Core Animation 被强制生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针,这个选项把这些图片渲染成蓝色。复制图片对内存和 CPU 使用来说都是一项非常昂贵的操作,所以应该尽可能的避免。
Color Offscreen-Rendered Yellow (离屏渲染)
这里会把那些需要离屏渲染的图层高亮成黄色,这些图层很可能需要用 shadowPath 或者 shouldRasterize 来优化。离屏渲染主要是针对 UITableViewCell 的优化,让还没有显示出来的 Cell 先绘制好。
混合模式
我们用两种方式加载同一张带 alpha 通道的透明图片,注意绘制图片的图片上下文也需要是不透明的。
override func viewDidLoad() { super.viewDidLoad() let image = UIImage(named: "avatar") let width: CGFloat = 150 let size = CGSize(width: width, height: width) let x: CGFloat = (UIScreen.main.bounds.size.width - width) * 0.5 // 不经过任何处理的图片 let imageView1 = UIImageView(frame: CGRect(x: x, y: 50, width: size.width, height: size.height)) imageView1.image = image view.addSubview(imageView1) // 重绘后的普通图片 let imageView2 = UIImageView(frame: CGRect(x: x, y: 250, width: width, height: width)) imageView2.image = redrawImage(image: image, size: size) view.addSubview(imageView2) } /// 重新绘制图片 /// /// - Parameters: /// - image: 原图 /// - size: 绘制尺寸 /// - Returns: 新图 func redrawImage(image: UIImage?, size: CGSize) -> UIImage? { // 传进来nil我就返回nil guard let image = image else { return nil } // 绘制区域 let rect = CGRect(origin: CGPoint(), size: size) // 开启图形上下文 size:绘图的尺寸 opaque:不透明 scale:屏幕分辨率系数,0会选择当前设备的屏幕分辨率系数 UIGraphicsBeginImageContextWithOptions(rect.size, true, 0) // 绘制 在指定区域拉伸并绘制 image.draw(in: rect) // 从图形上下文获取图片 let result = UIGraphicsGetImageFromCurrentImageContext() // 关闭上下文 UIGraphicsEndImageContext() return result }
效果如下所示:
我们可以看到我们绘制的图片四周会有黑色的边框,那是因为边框区域原来是透明的颜色,现在绘制成不透明的了,暂时忽略这个问题,后面会解决。
然后我们使用模拟器 Debug -> Color Blended Layers 来查看混合模式优化结果。
我们再滑上去看看刚才那句话 “这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮,性能越差的区域就越红” ,我们可以从图中直观的看到直接加载的透明图片很红,而我们自己绘制的图片则正常。
如果我们把 UIGraphicsBeginImageContextWithOptions(rect.size, true, 0) 方法的 opaque 参数设置为 false ,则是开启一个透明的图形上下文,我们可以试试最终效果。
上图也就验证了透明图片会引发性能问题。
图片拉伸
继续验证图片拉伸影响性能问题,我们先把上面开启图形上下文时的 opaque 参数改为 false ,也就是开启一个不透明的图形上下文,其他代码不变。我们这次去掉 Color Blended Layers ,勾选 Color Misaligned Images ,这个选项会使用黄色高亮那些被压缩或者拉伸以及没有正确对齐到像素边界的图片。
可以看到没有经过任何处理的图片变成黄色了,这是因为图片被拉伸了。而我们自己绘制的图片,由于是一张尺寸由我们自己设定的新图片,并没有发生图片拉伸情况,所以一切正常。
圆角图片
设置圆角图片,如果你直接使用视图 layer 的 cornerRadius 属性,那就会导致图层混合从而加大 GPU 绘制图层的开销,我们也可以使用绘图来解决这个问题。
override func viewDidLoad() { super.viewDidLoad() let image = UIImage(named: "avatar") let width: CGFloat = 150 let size = CGSize(width: width, height: width) let x: CGFloat = (UIScreen.main.bounds.size.width - width) * 0.5 // 不经过任何处理的图片 let imageView1 = UIImageView(frame: CGRect(x: x, y: 50, width: size.width, height: size.height)) imageView1.image = image imageView1.layer.cornerRadius = width * 0.5 imageView1.layer.masksToBounds = true view.addSubview(imageView1) // 重绘后的普通图片 let imageView2 = UIImageView(frame: CGRect(x: x, y: 250, width: width, height: width)) imageView2.image = redrawOvalImage(image: image, size: size, bgColor: view.backgroundColor) view.addSubview(imageView2) } /// 重新绘制圆形图片 /// /// - Parameters: /// - image: 原图 /// - size: 绘制尺寸 /// - bgColor: 裁剪区域外的背景颜色 /// - Returns: 新图 func redrawOvalImage(image: UIImage?, size: CGSize, bgColor: UIColor?) -> UIImage? { // 传进来nil我就返回nil guard let image = image else { return nil } // 绘制区域 let rect = CGRect(origin: CGPoint(), size: size) // 开启图形上下文 size:绘图的尺寸 opaque:不透明 scale:屏幕分辨率系数,0会选择当前设备的屏幕分辨率系数 UIGraphicsBeginImageContextWithOptions(rect.size, true, 0) // 背景颜色填充 bgColor?.setFill() UIRectFill(rect) // 圆形路径 let path = UIBezierPath(ovalIn: rect) // 进行路径裁切,后续的绘图都会出现在这个圆形路径内部 path.addClip() // 绘制图像 在指定区域拉伸并绘制 image.draw(in: rect) // 从图形上下文获取图片 let result = UIGraphicsGetImageFromCurrentImageContext() // 关闭上下文 UIGraphicsEndImageContext() return result }
这里我们使用 UIBezierPath 来绘制一个圆形路径,并裁切上,我们将图片绘制到裁切后的圆形内。并传入填充的背景颜色,让我们的圆形图片更好的融合到 UIImageView 所在的父控件上,而不会出现黑边。
如果我们直接开启一个透明的图形上下文,则无需填充背景颜色,也能实现上面的显示效果。但是这样也会造成混合性能问题,大家可以自行反复测试。
单独勾选 Color Blended Layers ,如下图所示:
至此,图片拉伸缩放、图片透明度、图片圆角引起的性能问题就这样解决了。并且,我们可以把重绘方法封装起来,以便以后直接使用。
[button]demo下载[/button]