Swift-图像的性能优化

前言

随着移动端的发展,现在越来越注重性能优化了。这篇文章将谈一谈对于图片的性能优化。面试中又会经常有这样的问题:如何实现一个图像的圆角,不要用cornerRadius?


模拟器常用性能测试工具

Color Blended Layers(混合图层->检测图像的混合模式)

  • 此功能基于渲染程度对屏幕中的混合区域进行绿->红的高亮(也就是多个半透明层的叠加,其中绿色代表比较好,红色则代表比较糟糕)
  • 由于重绘的原因,混合对GPU(Graphics Processing Unit->专门用来画图的)性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一

GPU:如果有透明的图片叠加,做两个图像透明度之间叠加的运算,运算之后生成一个结果,显示到屏幕上,如果透明的图片叠加的很多,运算量就会很大 png格式的图片是透明的,如果边上有无色的地方,那么可以把底下的背景透过来 一般指定颜色的时候不建议使用透明色,透明色执行效率低

Color Copied Images(图像复制->几乎用不到)

  • 有时候寄宿图片(layer.content)的生成是由Core Animation被强制生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针
  • 这个选项把这些图片渲染成蓝色
  • 复制图片对内存和CPU使用来说都是一项非常昂贵的操作,所以应该尽可能的避免

Color Misaligned Images(拉伸图像->检测图片有没有被拉伸)

  • 会高亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图片(也就是非整型坐标)
  • 通常都会导致图片的不正常缩放,比如把一张大图当缩略图显示,或者不正确的模糊图像

如果图片做拉伸的动作,是消耗CPU的。如果图片显示在一个Cell上面,滚出屏幕再滚动回来的时候,图片仍然需要重新被设置,在进入屏幕之前还需要一次拉伸操作,这些拉伸的操作是会消耗CPU的计算的。这样的设置多了以后就会严重影响性能。一个图片是否被进行了拉伸操作,我们用模拟器就可以判断出来。


为什么我们说这种方法设置图像效果不好

Color Misaligned Images(拉伸图像->检测图片有没有被拉伸)

创建一个自定义尺寸的ImageView,并设置图像

let image = UIImage(named: "avatar_default")

let imageView01 = UIImageView(frame: CGRect(x: 100, y: 100, width: 160, height: 160))
imageView01.image = image
view.addSubview(imageView01)

图片在模拟器上的显示

利用模拟器的DebugColor Misaligned Images功能查看图片状态。如下图所示,图片显示黄色,证明图片被拉伸了。

就知道你可能会不相信,继续看!将ImageView的尺寸设置成和图片一样大小,再利用模拟器Color Misaligned Images功能再次查看图片状态。结果如图所示

事实证明,如果图像尺寸和ImageView尺寸不一致,图像就一定会被拉伸,只要被拉伸,CPU就会工作,如果是在cell上,每次cell离开屏幕再回到屏幕的时候,都会对图片进行拉伸处理。就会频繁的消耗CPU从而导致影响APP的性能。

Color Offscreen-Rendered(离屏渲染->有待完善)

  • 这里会把那些需要离屏渲染的图层高亮成黄色
  • 这些图层很可能需要用shadownPath或者shouldRasterize(栅格化)来优化

好处:图像提前生成 坏处:CPUGPU会频繁的切换,会导致CPU的消耗会高一点,但是性能会提升

小结:

  • 以上性能优化中,有效的检测Color Blended LayersColor Misaligned Images在开发中能够提升图像的性能
  • Color Copied Images几乎遇不到
  • Color Offscreen-Rendered主要用于cell的性能优化

解决图片拉伸问题

利用核心绘图功能实现,根据尺寸获取路径,重新绘制一个目标尺寸的图片

override func viewDidLoad() {
    super.viewDidLoad()
    
    let image = UIImage(named: "avatar_default")
    
    let imageView01 = UIImageView(frame: CGRect(x: 100, y: 100, width: 160, height: 160))
    imageView01.image = image
    view.addSubview(imageView01)
    
    let rect = CGRect(x: 100, y: 300, width: 160, height: 160)
    let imageView02 = UIImageView(frame: rect)
    
    // 自定义创建图像的方法
    imageView02.image = avatarImage(image: image!, size: rect.size)
    view.addSubview(imageView02)
    
}

自定义创建图像的方法

/// 将给定的图像进行拉伸,并且返回新的图像
///
/// - Parameters:
///   - image: 原图
///   - size: 目标尺寸
/// - Returns: 返回一个新的'目标尺寸'的图像
func avatarImage(image: UIImage, size: CGSize) -> UIImage? {
    
    let rect = CGRect(origin: CGPoint(), size: size)
    
    // 1.图像的上下文-内存中开辟一个地址,跟屏幕无关
    /**
     * 1.绘图的尺寸
     * 2.不透明:false(透明) / true(不透明)
     * 3.scale:屏幕分辨率,默认情况下生成的图像使用'1.0'的分辨率,图像质量不好
     *         可以指定'0',会选择当前设备的屏幕分辨率
     */
    UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
    
    // 2.绘图'drawInRect'就是在指定区域内拉伸屏幕
    image.draw(in: rect)
    
    // 3.取得结果
    let result = UIGraphicsGetImageFromCurrentImageContext()
    
    // 4.关闭上下文
    UIGraphicsEndImageContext()
    
    // 5.返回结果
    return result
}

效果如下

如果到这里你以为就完事了,那你真是太年轻了

再解决混合模式(Color Blended Layers)问题

继续刚才的话题,仅仅解决了拉伸问题后,在Color Blended Layers(混合模式)下还是有问题,如图

将绘图选项的透明状态设置为不透明(true)

到这里,如果类似新闻APP图片都只是显示方形的,就可以搞定了。那如果是头像怎么办呢?头像绝大多数都是圆角头像,而且现在越来越多的考虑到性能方面的问题。很多人都不用cornerRadius,认为用cornerRadius不是一个好的解决办法。

设置图像圆角,不用cornerRadius

获取上下文(UIGraphicsBeginImageContextWithOptions)绘图(drawInRect)之间实例化一个圆形的路径,并进行路径裁切

// 1> 实例化一个圆形的路径
let path = UIBezierPath(ovalIn: rect)
// 2> 进行路径裁切 - 后续的绘图,都会出现在圆形路径内部,外部的全部干掉
path.addClip()

效果如下

UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)这里选择了true(不透明),四个角即使被裁切掉(没有在获取到的路径里面)但是由于是不透明的模式,所以看不到下面的颜色,默认看到了黑色的背景。

UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)透明模式改为false(透明)

再看下混合模式,四个叫和头像都是红色,并且颜色深浅程度不一样,越红效率越不好。证明有图层叠加的运算,因此,不能采用透明的模式。

解决办法:给背景设置一个颜色,使其不显示默认的黑色。 这样就可以解决四个角显示黑色的问题,并且在混合模式状态下不会再有红色显示,性能可以非常的好。

开发过程中,用颜色比用图片性能会高一点。 不到万不得已,View的背景色尽量不要设置成透明颜色。

给图像添加边框,绘制内切的圆形

UIColor.darkGray.setStroke()
path.lineWidth = 5      // 默认是'1'
path.stroke()

判断一个应用程序的好坏,看图像处理的是否到位,如果表格里面图像都拉伸,并且设置cornerRadius,那么表格的卡顿可能将会变得非常明显。

下面是方法的最终代码:

/// 将给定的图像进行拉伸,并且返回新的图像
///
/// - Parameters:
///   - image: 原图
///   - size: 目标尺寸
/// - Returns: 返回一个新的'目标尺寸'的图像
func avatarImage(image: UIImage, size: CGSize, backColor:UIColor?) -> UIImage? {
    
    let rect = CGRect(origin: CGPoint(), size: size)
    
    // 1.图像的上下文-内存中开辟一个地址,跟屏幕无关
    /**
     * 1.绘图的尺寸
     * 2.不透明:false(透明) / true(不透明)
     * 3.scale:屏幕分辨率,默认情况下生成的图像使用'1.0'的分辨率,图像质量不好
     *         可以指定'0',会选择当前设备的屏幕分辨率
     */
    UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
    
    // 背景填充(在裁切之前做填充)
    backColor?.setFill()
    UIRectFill(rect)
    
    // 1> 实例化一个圆形的路径
    let path = UIBezierPath(ovalIn: rect)
    // 2> 进行路径裁切 - 后续的绘图,都会出现在圆形路径内部,外部的全部干掉
    path.addClip()
    
    // 2.绘图'drawInRect'就是在指定区域内拉伸屏幕
    image.draw(in: rect)
    
    // 3.绘制内切的圆形
    UIColor.darkGray.setStroke()
    path.lineWidth = 5      // 默认是'1'
    path.stroke()
    
    // 4.取得结果
    let result = UIGraphicsGetImageFromCurrentImageContext()
    
    // 5.关闭上下文
    UIGraphicsEndImageContext()
    
    // 6.返回结果
    return result
}

封装

为了方便自己以后用,因此,将其封装起来。如果有更好的改进办法欢迎给我提出。

建立了一个空白文件HQImage,在UIImageextension里面自定义了两个方法创建头像图像(hq_avatarImage)创建矩形图像(hq_rectImage)

// MARK: - 创建图像的自定义方法
extension UIImage {
    
    /// 创建圆角图像
    ///
    /// - Parameters:
    ///   - size: 尺寸
    ///   - backColor: 背景色(默认`white`)
    ///   - lineColor: 线的颜色(默认`lightGray`)
    /// - Returns: 裁切后的图像
    func hq_avatarImage(size: CGSize?, backColor: UIColor = UIColor.white, lineColor: UIColor = UIColor.lightGray) -> UIImage? {
        
        var size = size
        
        if size == nil {
            size = self.size
        }
        
        let rect = CGRect(origin: CGPoint(), size: size!)
        
        // 1.图像的上下文-内存中开辟一个地址,跟屏幕无关
        /**
         * 1.绘图的尺寸
         * 2.不透明:false(透明) / true(不透明)
         * 3.scale:屏幕分辨率,默认情况下生成的图像使用'1.0'的分辨率,图像质量不好
         *         可以指定'0',会选择当前设备的屏幕分辨率
         */
        UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
        
        // 背景填充(在裁切之前做填充)
        backColor.setFill()
        UIRectFill(rect)
        
        // 1> 实例化一个圆形的路径
        let path = UIBezierPath(ovalIn: rect)
        // 2> 进行路径裁切 - 后续的绘图,都会出现在圆形路径内部,外部的全部干掉
        path.addClip()
        
        // 2.绘图'drawInRect'就是在指定区域内拉伸屏幕
        draw(in: rect)
        
        // 3.绘制内切的圆形
        UIColor.darkGray.setStroke()
        path.lineWidth = 1      // 默认是'1'
        path.stroke()
        
        // 4.取得结果
        let result = UIGraphicsGetImageFromCurrentImageContext()
        
        // 5.关闭上下文
        UIGraphicsEndImageContext()
        
        // 6.返回结果
        return result
    }
    
    /// 创建矩形图像
    ///
    /// - Parameters:
    ///   - size: 尺寸
    ///   - backColor: 背景色(默认`white`)
    ///   - lineColor: 线的颜色(默认`lightGray`)
    /// - Returns: 裁切后的图像
    func hq_rectImage(size: CGSize?, backColor: UIColor = UIColor.white, lineColor: UIColor = UIColor.lightGray) -> UIImage? {
        
        var size = size
        
        if size == nil {
            size = self.size
        }
        
        let rect = CGRect(origin: CGPoint(), size: size!)
        
        // 1.图像的上下文-内存中开辟一个地址,跟屏幕无关
        /**
         * 1.绘图的尺寸
         * 2.不透明:false(透明) / true(不透明)
         * 3.scale:屏幕分辨率,默认情况下生成的图像使用'1.0'的分辨率,图像质量不好
         *         可以指定'0',会选择当前设备的屏幕分辨率
         */
        UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
        
        // 2.绘图'drawInRect'就是在指定区域内拉伸屏幕
        draw(in: rect)
        
        // 3.取得结果
        let result = UIGraphicsGetImageFromCurrentImageContext()
        
        // 4.关闭上下文
        UIGraphicsEndImageContext()
        
        // 5.返回结果
        return result
    }
}

性能测试

没有对比就无从谈起性能优化,以下是我根据两种方法,循环创建100ImageViewCPU内存消耗(个人感觉1张图片不一定能说明问题,所以搞了100个)

系统方法创建图像

for _ in 0..<100 {
    
    let imageView01 = UIImageView(frame: CGRect(x: 100, y: 100, width: 160, height: 160))
    imageView01.image = image
    view.addSubview(imageView01)
}

自定义方法创建图像

for _ in 0..<100 {
    
    let rect02 = CGRect(x: 100, y: 300, width: 160, height: 160)
    let imageView02 = UIImageView(frame: rect02)
    imageView02.image = avatarImage(image: image!, size: rect02.size, backColor: view.backgroundColor)
    view.addSubview(imageView02)
}

由此可见,新方法对CPU消耗明显减少,内存较以前稍微上涨,CPU消耗减少,则性能有所提升。(因为每次消耗不是一个定数,我这里也是测了很多次取的大概的平均值。)


2017年08月30日补充

感谢linbx08给我提出的问题,是一个关于矩形图像调用我的方法hq_rectImage图像右侧显示黑线的问题。

解决办法是在开启图形上下文后,对其做背景填充。

// 背景填充(在裁切之前做填充)
backColor.setFill()
UIRectFill(rect)

但黑线的原因暂时尚未查明。我之前的思路是按照做圆形头像的代码继续做的。直接UIBezierPath(rect: rect)实例化了一个矩形的路径,然后在路径内绘图。但是突然想到不用裁切,不用设置圆形头像的边框,突然感觉这样就有点多此一举了,因此将多余的代码就都删除了。没想到删多了,出问题了,不过好在有人及时给我提出了问题。并帮助我改正、再次感谢!


2017年09月04日补充

又发现一个问题

就是如果按照最之前写的代码,在设置矩形图片时,如果不在开启图形上下文后,对背景做填充,那么当你的图像不是一个矩形的时候(是任意的不规则形状),那么,背景被填充的是黑色,在你的图形以外的范围内会被看见。如下图

看下我写的代码

class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: CGSize(width: 50, height: 50))
        }
    }

解决办法同之前的方式,开启图形上下文后,填充背景色就OK了。

DEMO传送门 : ImagePerformanceOptimization

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏生信宝典

CIRCOS圈图绘制 - 染色体信息展示和调整

CIRCOS圈图绘制 - 最简单绘图和解释介绍了CIRCOS的安装、基本的配置文件的解释、如何最简单的获得一个CIRCOS图。最主要的部分还是配置文件的位置信息...

62250
来自专栏林德熙的博客

WPF 在image控件用鼠标拖拽出矩形

今天有小伙伴问我一个问题,在image控件用鼠标拖拽出矩形,本文告诉大家如何使用鼠标画出矩形

17410
来自专栏程序员宝库

如何用 vue 制作一个探探滑动组件

前言 嗨,说起探探想必各位程序汪都不陌生(毕竟妹子很多),能在上面丝滑的翻牌子,探探的的堆叠滑动组件起到了关键的作用,下面就来看看如何用vue写一个探探的堆叠组...

858130
来自专栏十月梦想

html常用标签标记

本博客所有文章如无特别注明均为原创。作者:十月梦想 ,复制或转载请以超链接形式注明转自 十月梦想博客 。 原文地址《html常用标签标记》

23130
来自专栏练小习的专栏

SVG图形绘制入门第一弹

之前很长一段时间,我是不重视SVG的,认为他就是在AI里画画,然后导出来做个矢量图标。直到我在上家公司遇到图表的绘制,因为不会写不得已而拿插件实现,而插件绘制的...

34070
来自专栏iOS开发攻城狮的集散地

UIScrollView视觉差动画

那么拖拽中,逐渐移动复位rightView上的RightImage的X坐标:rightView.contentX = 需要移动距离长度 - 移动百分比 * 需...

338140
来自专栏大数据挖掘DT机器学习

信息图制作教程案例

当大家看到很多好看的信息图的时候最喜欢问的两个问题是:用什么软件做的?怎么做的? 在工具选择上,使用Adobe Illustrator,制作过程大家...

42270
来自专栏HTML5学堂

一步步教你弹性框架-下篇

HTML5学堂:本文继续为大家讲解弹性框架,在前两篇文章当中,我们从最基本的来回运动,讲解到缓冲运动、有摩擦力的运动。基本实现了弹性动画效果。今天我们主要来进行...

38040
来自专栏coding for love

CSS入门13-单位详解

(注1:如果有问题欢迎留言探讨,一起学习!转载请注明出处,喜欢可以点个赞哦!) (注2:更多内容请查看我的目录。)

8820
来自专栏偏前端工程师的驿站

语义化HTML:i、b、em和strong标签

一、前言                             在HTML4.1中i和b作为表象标签分别表示斜体和粗体样式,而强调样式与内容分离的XHTML中...

25390

扫码关注云+社区

领取腾讯云代金券