1. 前言

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

官方文档:Image and Graphics Best Practices

2. 基础预备知识

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

代码很easy呀,两行搞定

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

但是这中间的图片加载真实过程如下

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

按照经典的MVC架构,UIImage扮演model角色,负责承载图片数据,UIImageView充当View的角色,负责渲染和展示图片。系统提供接口非常的简单,这中间隐藏了解码的过程。

Buffers

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

Data Buffer

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

Image Buffer

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

Frame Buffer

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会持有解压后的图片数据,以便给需要渲染的地方复用数据。

渲染流程

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

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

3. 官方最佳实践

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

DownSampling(降低采样)

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

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

这里有两个注意事项

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

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

Prefetching && Background decoding

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

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

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

官方实现UI实例

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

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

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

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.boundsmaskLayer.path = maskPath.CGPathself.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属性平铺颜色,来达到复用图片的目的。

代码如下:


UIImage.withRenderingMode(_:)
UIImageView.tintColor复制代码

文本实现

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

最终实现

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

推荐使用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. 总结

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

转载于:https://juejin.im/post/5c84bd676fb9a049e702ecd8

iOS图像最佳实践总结相关推荐

  1. iOS 7最佳实践:一个天气App案例

    转自:sjpsega's Blog iOS7最佳实践:一个天气App案例(一) iOS7最佳实践:一个天气App案例(二) 注:本文译自:raywenderlich ios-7-best-practi ...

  2. iOS 7 最佳实践;一个天气应用: Part 1/2

    为什么80%的码农都做不了架构师?>>>    每个开发者都有他们自己开发一个应用的好方法?一些开发者使用Auto-Layout,一些开发者使用图标算法,甚至一些开发者喜欢用Vim编 ...

  3. ios图像和图形最佳实践(三)

    没有前面进度的同学还是从(二)开始,否则会感觉比较突兀 ios图像和图形最佳实践(二) - 对于我们的app所附带的图片 苹果强烈建议我们使用图像素材来存储 这其中有很多原因 图像素材针对基于名称和基 ...

  4. iOS应用开发最佳实践

    <iOS应用开发最佳实践> 基本信息 作者: 王浩 出版社:电子工业出版社 ISBN:9787121207679 上架时间:2013-7-22 出版日期:2013 年8月 开本:16开 页 ...

  5. 最佳实践(2):iOS开发篇

    本文由 伯乐在线 - 戴仓薯 翻译,dopcn 校稿.未经许可,禁止转载! 英文出处:futurice.欢迎加入翻译组. 这份文档就像软件项目一样,如果我们不维护它就会逐渐腐坏.欢迎大家跟我们一起来维 ...

  6. iOS系统中导航栏的转场解决方案与最佳实践

    背景 目前,开源社区和业界内已经存在一些 iOS 导航栏转场的解决方案,但对于历史包袱沉重的美团 App 而言,这些解决方案并不完美.有的方案不能满足复杂的页面跳转场景,有的方案迁移成本较大,为此我们 ...

  7. 《iOS网络编程与云端应用最佳实践》微博转发送书了!

    <iOS网络编程与云端应用最佳实践>微博转发送书了! 新浪微博地址:http://weibo.com/2078101705/zy15U4a5D,转发本条微博即有机会获得<iOS网络编 ...

  8. 转发网络《iOS网络编程与云端应用最佳实践》微博转发送书了

    在写这篇文章之前,xxx已经写过了几篇关于改转发网络主题的文章,想要了解的朋友可以去翻一下之前的文章 <iOS网络编程与云端应用最佳实践>微博转发送书了! 新浪微博地址:http://we ...

  9. iOS应用开发最佳实践:编写高质量的Objective-C代码

    点标记语法 属性和幂等方法(多次调用和一次调用返回的结果相同)使用点标记语法访问,其他的情况使用方括号标记语法. 良好的风格: view.backgroundColor = [UIColor oran ...

最新文章

  1. 员工提出离职,称害怕猝死,HR却说:先猝死了再说!
  2. Android 启动模式简介
  3. 3.4.1 流量控制与可靠传输机制
  4. 数据库系统概论:第六章 关系数据库理论
  5. 推荐:Dapper扩展-Dapper.SimpleCRUD
  6. 集群高并发情况下如何保证分布式唯一全局ID生成
  7. python假如输入错误重新输入_认识python之输入(4)
  8. seo模拟点击软件_百度快排软件原理分析
  9. 发现一个 WPF/E Asp.net Server Control
  10. 计算机睡眠功能命令,windows睡眠命令怎么使用
  11. 中国物流产业发展形势与竞争格局展望报告2022版
  12. css背景透明度影响字体--实现背景透明字体不透明
  13. Android实现图片(拍照+相册)上传功能
  14. layer子父传值(1)
  15. 学计算机需要什么基础
  16. 字节跳动工作总结:工作一年的真心话
  17. 证明N={1,2,...,n,...}有最大元 黄小宁
  18. 电脑锁屏挂机锁绿色版简单实用工具
  19. 各大搜索引擎蜘蛛名称
  20. 服务器虚拟化书籍,服务器虚拟化解决方案书模板-精选版.doc

热门文章

  1. kafka如何扩容分区Partition、并重新分区
  2. vuex的使用说明(个人专用)
  3. 我的世界末日之后无限法则服务器,Last Day Rules官方版
  4. Java虚拟机是如何识别目标方法的?
  5. 对于安装破解版BurpSuite无法用java打开注册机的问题
  6. D90四种对焦点模式
  7. java 字符串转换成map_java中string类型转换成map
  8. 电竞英雄联盟数据API接口 - 【近期赛事列表】API调用示例代码
  9. Centos7下安装Sentry22.1.0,接入LDAP
  10. EBox3300开发简明流程