iOS图像最佳实践总结

栏目: IOS · 发布时间: 6年前

内容简介:18年WWDC苹果官方给出了关于iOS图像处理的最佳实践,本文主要是就官方文档进行分析总结,以及对实际工程中常用的图像处理的case进行较为全面的拓展延伸。官方文档:开发童鞋看到这个表示很easy呀,两行代码搞定

18年WWDC苹果官方给出了关于iOS图像处理的最佳实践,本文主要是就官方文档进行分析总结,以及对实际工程中常用的图像处理的case进行较为全面的拓展延伸。

官方文档: Image and Graphics Best Practices

2. 基础预备知识

本地图片显示到屏幕中,经历了哪些过程

开发童鞋看到这个表示很easy呀,两行代码搞定

UIImage *image = [UIImage imageNamed:@"xxxxx"];
    imageView.image = image;
复制代码

事实上这两行代码过程包含了如下图的步骤

iOS图像最佳实践总结
  1. 从磁盘读取原始压缩的图片数据(png/jpeg格式等等)缓存到内存
  2. CPU解压成未压缩的图片数据,imageBuffer
  3. 渲染图片(会生成frameBuffer,帧缓存,最终显示到手机屏幕)

按照经典的MVC架构,UIImage充当的model角色,负责承载图片数据,UIImageView充当View的角色,负责渲染和展示图片。系统接口隐藏了解码的过程,但是在性能优化这部分,研究解码十分的重要。

Buffers

Buffer是一段连续的内存区域,下面我们看下图片处理相关的Buffer

Data Buffer

iOS图像最佳实践总结

Data Buffer存储了图片的元数据,我们常见的图片格式,jpeg,png等都是压缩图片格式。Data Buffer的内存大小就是源图片在磁盘中的大小。

Image Buffer

iOS图像最佳实践总结

Image Buffer存储的就是图片解码后的像素数据,也就是我们常说的位图。 Buffer中每一个元素描述的一个像素的颜色信息,buffer的size和图片的size成正相关关系。

Frame Buffer

iOS图像最佳实践总结

Frame Buffer 存储了app每帧的实际输出

和OpenGL中FrameBuffer类似,苹果不允许我们直接渲染操作屏幕显示,而是把渲染数据放入帧缓存中,由系统按照60hz-120hz的频率扫描显示。

当app视图层级发生变化时,UIKit 会结合 UIWindow 和 Subviews,渲染出一个 frame buffer,然后按60hz的频率扫描(ipad最高可以达到120hz)显示到屏幕上。

解码操作

UIImage负责解压Data Buffer内容并申请buffer(Image Buffer)存储解压后的图片信息。UIImageView负责将Image Buffer 拷贝至 framebuffer,用于显示屏幕展示。

解压过程会大量占用cpu,所以UIImage会持有解压后的图片数据,以便给需要渲染的地方复用数据。

渲染流程

iOS图像最佳实践总结

综上我们可以看到渲染的全过程。这里需要注意的是,解码后的ImageBuffer大小理论上只和图片尺寸相关。

ImageBuffer中数据其实也就是位图数据,按照每个像素RGBA四个字节大小,一张1080p的图片解码后的位图大小是1920 * 1080 * 4 / 1024 / 1024,约7.9mb,而原图如果是jpg的压缩比1比20,大约350kb,可见解码后的内存占用是相当大的。

3. 官方最佳实践

内存的占用会导致我们app的CPU占用高,直接导致耗电大,APP响应慢

iOS图像最佳实践总结

DownSampling(降低采样)

在视图比较小,图片比较大的场景下,直接展示原图片会造成不不必要的内存和CPU消耗,这里就可以使用ImageIO的接口DownSampling,也就是生成缩略图

iOS图像最佳实践总结

具体代码如下,指定显示区域大小

iOS图像最佳实践总结

这里有两个注意事项

  • 设置kCGImageSourceShouldCache为false,避免缓存解码后的数据,64位设置上默认是开启缓存的,(很好理解,因为下次使用该图片的时候,可能场景不同,需要生成的缩略图大小是不同的,显然不能做缓存处理)
  • 设置kCGImageSourceShouldCacheImmediately为true,避免在需要渲染的时候才做解码,默认选项是false

这样的缩略图方式可以省去大量的内存和CPU消耗,官方Case给出的前后内存对比

iOS图像最佳实践总结

Prefetching && Background decoding

显然解码过程是非常占用CPU资源的,如果放在主线程一定会造成阻塞,所以这个操作应该放在异步线程。代码如下

iOS图像最佳实践总结

Prefetching:预加载,也就是提前为之后的cell预加载数据,基本上主流的app都是这么做滴,iOS10之后,系统引入的tableView(_:prefetchRowsAt:) 可以更加方便的实现预加载。

这里使用串行队列可以很好地避免Thread Explosion,线程切换的代价是非常昂贵的,所以在我们app中使用GCD串行队列创建一个解码线程,是非常有必要滴。

官方实现UI实例

我们现在需要实现下面的live按钮

iOS图像最佳实践总结

先看一种不合理的实现方式

iOS图像最佳实践总结

我们先来分析这种方案的问题所在,

iOS图像最佳实践总结

UIView是通过CALayer创建FrameBuffer最后显示的。重写了drawRect方法,Calayer会创建一个Backing Store,然后在Backing Store上执行draw函数,最后将内容传递给frameBuffer最终显示。

Backing Store的默认大小和View的大小成正比,以iphone6为例,750 * 1134 * 4 字节 ≈ 3.4 Mb。

iOS 12,对 backing store 有做优化,它的大小会根据图片的色彩空间,动态改变。 在此之前,如果你使用 sRGB 格式,但是实际绘制的内容,只使用了单通道,那么大小会比实际要的大,造成不必要开销。iOS 12 会自动优化这部分。

总结下这种使用drawRect绘制方案的问题

    1. Backing Store的创建造成了不必要的内存开销
    1. UIImage先绘制到Backing Store,再渲染到frameBuffer,中间多了一层内存拷贝
    1. 背景颜色不需要绘制到Backing Store,直接使用BackGroundColor绘制到FrameBuffer

所以,正确的实现姿势是将这个大的view拆分成小的subview逐个实现。

背景颜色实现

这里有一个圆角的处理

UIView的maskView 及CALayer.maskLayer都会将图层渲染到临时的image buffer中,也就是我们常说的离屏渲染,本质上CALayer.cornerRadius不会造成离屏渲染,真正造成离屏渲染的是设置MaskToBounds这样的属性。所以背景图直接使用UIView设置BackGroudColor即可。

这里拓展下圆角的处理,先看一种不正确的做法

override func drawRect(rect: CGRect) {
    let maskPath = UIBezierPath(roundedRect: rect,
                                byRoundingCorners: .AllCorners,
                                cornerRadii: CGSize(width: 5, height: 5))
    let maskLayer = CAShapeLayer()
    maskLayer.frame = self.bounds
    maskLayer.path = maskPath.CGPath
    self.layer.mask = maskLayer
}

复制代码

首先同理,重写drawRect造成不必要的backing store内存开销,并且这种做法的本质是创建遮罩mask,再进行图层混合,同样会造成离屏渲染。

正确的姿势, 对于UIView直接使用CornerRadius,CoreAnimation可以为我们在不额外创建内存开销的情况下绘制出圆角。

对于UIImageView可以使用CoreGraphics自己裁剪出带圆角的Image,实例代码如下

extension UIImage {
    func drawRectWithRoundedCorner(radius radius: CGFloat, _ sizetoFit: CGSize) -> UIImage {
        let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: sizetoFit)
        
        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
        CGContextAddPath(UIGraphicsGetCurrentContext(),
                         UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.AllCorners,
                                      cornerRadii: CGSize(width: radius, height: radius)).CGPath)
        CGContextClip(UIGraphicsGetCurrentContext())
        
        self.drawInRect(rect)
        CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
        let output = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        
        return output
    }
}

复制代码

Live图片实现

直接使用UIImageView,这里有个技巧,如果是纯色图片,想要使用不同颜色的同一张图片,可以使用UIImageView的tintColor属性平铺颜色,来达到复用图片的目的,不占用额外的内存

iOS图像最佳实践总结

代码如下:

UIImage.withRenderingMode(_:)
UIImageView.tintColor

复制代码

文本实现

文本使用UILabel可以减少百分之75的Backing Store开销,系统针对UILabel做了优化,并且自动更新Backing Store的size,针对emoji和富文本内容。

最终实现

最终Live按钮的正确实现方案如下图

iOS图像最佳实践总结

推荐使用Image Assets

  • 基于名称和特效优化了查找效率,更快的查找图片
  • 运行时,对内存的管理也有优化
  • App Slicing,app安装包瘦身。iOS 9 后会从 Image Assets 中保留设备支持的图片 (2x 或者 3x)
  • iOS 11 后的 Preserve Vector Data。支持矢量图的功能,放大也不会失真

Advanced Image Effects

对于图片的实时处理推荐使用CoreImage框架。 例如将一张图片的灰度值进行调整这样的操作,有滴小伙伴可能使用CoreGraphics获取图像的每个像素点数据,然后改变灰度值,最终生成目标图标,这种做法将大量gpu擅长的工作放在了cpu上处理,事实上使用CoreImage一个滤镜filter或者metal,OpenGL的shader就可以处理了。让图像处理的工作交给GPU去做。

Drawing Off-Screen

对于需要离屏渲染的场景推荐使用UIGraphicsImageRenderer替代UIGraphicsBeginImageContext,性能更好,并且支持广色域。

4. 拓展与思考

用提问的方式来拓展一下,针对每个问题进行深入的思考

问题一:原来图像展示有这么多细节在里面,可是我在平常开发中为什么没有感觉到,应该需要从哪些地方对自己的工程进行优化。

答:原因是我们平常大部分会使用UIImage imageNamed这样的API加载了本地图片,而网络图片则使用了SDWebImage或者YYWebImage等框架来加载。所以没有去细究。

进而引申出

问题二: 使用imageNamed,系统何时去解码,有没有缓存,缓存的大小是多少,有没有性能问题,和imageWithContentsOfFile有什么区别

答: 一一来解答这个问题

  1. 首先先说imageNamed和imageWithContentsOfFile有什么区别,相比大部分小伙伴都很清楚,因为这也是面试老生常谈的东西。imageNamed加载本地图片会缓存图片,也就是加载一千张相同的本地图片,内存中也只会有一份,而imageWithContentsOfFile不会缓存,也就是重复加载相同图片,在内存中会有多份图片数据。
  2. imageNamed的内存缓存大小其实是根据机型不同滴,一旦加载如内存缓存后,只有收到内存警告的时候才会释放缓存,有兴趣的小伙伴可以调试一下。
  3. 关于UIImage对象何时去解码,其实刚刚我们在降低采样的时候已经提到了,kCGImageSourceShouldCacheImmediately属性系统默认是false,我们可以看ImageIO/CGImageSource.h文件中kCGImageSourceShouldCache的注释
pecifies whether image decoding and caching should happen at image creation time. The value of this key must be a CFBooleanRef. The default value is kCFBooleanFalse (image decoding will happen at rendering time).

也就是说UIImage只有在屏幕上渲染时再解码的。而关于UIImageView的操作一定是在主线程,所以如果在tableview滑动中频繁的创建UIImage,会造成主线程阻塞。

总结: imageNamed默认带缓存,缓存通过NSCache实现。适合会频繁复用的图片的加载,而imageWithContentsOfFile不会缓存,适合不常用较大图片的加载,由于系统默认主线程解码UIImage,所以imageNamed适用于加载较小的例如APP各个tab的icon,需要在首屏展示的图片。而不适用于滑动的下载好的网络图片的本地加载。会造成主线程阻塞。

5. 正确的网络图片加载方式

其实这里SDWebImage或者YYWebImage等框架已经给出了正确的姿势,细节可以挑其中一个阅读源码即可。

分享下优秀的源码解析

YImage 设计思路,实现细节剖析

YYWebImage 源码剖析:线程处理与缓存策略

下载图片主要简化流程如下

  1. 从网络下载图片源数据,默认放入内存和磁盘缓存中
  2. 异步解码,解码后的数据放入内存缓存中
  3. 回调主线程渲染图片
  4. 内部维护磁盘和内存的cache,支持设置定时过期清理,内存cache的上限等

加载图片的主要简化流程如下

  1. 从内存中查找图片数据,如果有并且已经解码,直接返回数据,如果没有解码,异步解码缓存内存后返回
  2. 内存中未查找到图片数据,从磁盘查找,磁盘查找到后,加载图片源数据到内存,异步解码缓存内存后返回,如果没有去网络下载图片。走上面的流程。

分析: 这样滴流程解决了UIImage imageNamed这种加载一定在主线程解码图片的问题,异步加载,避免了主线程阻塞。同时通过缓存内存方式,避开了频繁的磁盘IO,通过缓存解码后的图片数据,避开了频繁解码的CPU消耗。

6. 超大大图片的处理

之前我们分析过1080p的图片解码后的内存大小,大约是7.9mb,如果是4k,8k图,这个大小将会非常的大,所以如果使用SDWebImage或者YYWebImage去使用默认解码缓存的方式去加载多张这样的大图,带来的结果一定是内存爆掉。闪退。

可以设置SDWebImage或者YYWebImage的Option选项不解码下载好的图片

那么大图该怎么处理呢,显然大图是不适合解码缓存的。这里有两个场景

  1. 一张超大图加载在一个小的view上

解决方法: 使用苹果推荐的缩略图DownSampling方案即可

  1. 加载超大图拖动显示

解决方法: 使用苹果的CATiledLayer去加载。原理是分片渲染,滑动时通过指定目标位置,通过映射原图指定位置的部分图片数据解码渲染。这里不再累述,有兴趣的小伙伴可以自行了解下官方API。

7. 总结

了解图像加载的细节和全过程全过程非常有必要,有助于我们在平常开发中选择合适的方案,做出合理的性能优化。


以上所述就是小编给大家介绍的《iOS图像最佳实践总结》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Learning Python, 5th Edition

Learning Python, 5th Edition

Mark Lutz / O'Reilly Media / 2013-7-6 / USD 64.99

If you want to write efficient, high-quality code that's easily integrated with other languages and tools, this hands-on book will help you be productive with Python quickly. Learning Python, Fifth Ed......一起来看看 《Learning Python, 5th Edition》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具