2018 WWDC 苹果官方给出了关于iOS图像处理的最佳实践,本文主要是就官方文档进行分析总结以及较为全面的拓展延伸。面试
官方文档:Image and Graphics Best Practices缓存
代码很easy呀,两行搞定性能优化
UIImage *image = [UIImage imageNamed:@"xxxxx"];
imageView.image = image;
复制代码
可是这中间的图片加载真实过程以下bash
按照经典的MVC架构,UIImage扮演model角色,负责承载图片数据,UIImageView充当View的角色,负责渲染和展现图片。系统提供接口很是的简单,这中间隐藏了解码的过程。微信
Buffer是一段连续的内存区域,下面咱们看下图片处理相关的Buffer网络
Data Buffer存储了图片的元数据,咱们常见的图片格式,jpeg,png等都是压缩图片格式。Data Buffer的内存大小就是源图片在磁盘中的大小。架构
Image Buffer存储的就是图片解码后的像素数据,也就是咱们常说的位图。 Buffer中每个元素描述的一个像素的颜色信息,buffer的size和图片的size成正相关关系。app
Frame Buffer 存储了app每帧的实际输出框架
和OpenGL中FrameBuffer相似,苹果不容许咱们直接渲染操做屏幕显示,而是把渲染数据放入帧缓存中,由系统按照60hz-120hz的频率扫描显示。iphone
当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,可看法码后的内存占用是至关大的。
内存的占用会致使咱们app的CPU占用高,直接致使耗电大,APP响应慢
在视图比较小,图片比较大的场景下,直接展现原图片会形成没必要要的内存和CPU消耗,这里就可使用ImageIO的接口,DownSampling,也就是生成缩略图
具体代码以下,指定显示区域大小
这里有两个注意事项
这样的缩略图方式能够省去大量的内存和CPU消耗,官方Case给出的先后内存对比
解码过程是很是占用CPU资源的,放在主线程必定会形成阻塞,因此这个操做应该放在异步线程。代码以下
Prefetching:预加载,也就是提早为以后的cell预加载数据(基本上主流的app都有这么作滴,iOS10以后,系统引入的tableView(_:prefetchRowsAt:) 能够更加方便的实现预加载。)
小tips: 这里使用串行队列能够很好地避免Thread Explosion,线程切换的代价是很是昂贵的,因此在咱们app中应该使用GCD串行队列建立一个解码线程。
咱们如今须要实现下面的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绘制方案的问题
因此,正确的实现姿式是将这个大的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
}
}
复制代码
直接使用UIImageView,这里有个技巧,若是是纯色图片,想要展现不一样颜色的同一张图片,可使用UIImageView的tintColor属性平铺颜色,来达到复用图片的目的。
代码以下:
UIImage.withRenderingMode(_:)
UIImageView.tintColor
复制代码
文本使用UILabel能够减小百分之75的Backing Store开销,系统针对UILabel作了优化,而且自动更新Backing Store的size,针对emoji和富文本内容。
最终Live按钮的正确实现方案以下图
对于图片的实时处理推荐使用CoreImage框架。 例如将一张图片的灰度值进行调整这样的操做,有滴小伙伴可能使用CoreGraphics获取图像的每一个像素点数据,而后改变灰度值,最终生成目标图标,这种作法将大量gpu擅长的工做放在了cpu上处理,合理的作法是: 使用CoreImage的滤镜filter或者metal,OpenGL的shader,让图像处理的工做交给GPU去作。
对于须要离屏渲染的场景推荐使用UIGraphicsImageRenderer替代UIGraphicsBeginImageContext,性能更好,而且支持广色域。
用提问的方式来拓展一下,针对每一个问题进行深刻的思考
问题一:图像展现有这么多细节在里面,但是为何在日常开发中为何没有感受到,能够从哪些地方对本身的工程进行优化。
答:咱们日常大部分会使用UIImage imageNamed这样的API加载了本地图片,而网络图片则使用了SDWebImage或者YYWebImage等框架来加载。因此没有去细究。
进而引伸出
问题二: 使用imageNamed,系统什么时候去解码,有没有缓存,缓存的大小是多少,有没有性能问题,和imageWithContentsOfFile有什么区别
答: 一一来解答这个问题
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,须要在首屏展现的图片。而不适用于滑动的下载好的大量网络图片的本地加载。会形成主线程阻塞。
其实这里SDWebImage或者YYWebImage等框架已经给出了正确的姿式,细节能够挑其中一个阅读源码便可。
分享下优秀的源码解析
下载图片主要简化流程以下
加载图片的主要简化流程以下
分析:
以前咱们分析过1080p的图片解码后的内存大小,大约是7.9mb,若是是4k,8k图,这个内存占用将会很是的大,若是使用SDWebImage或者YYWebImage的默认解码缓存技术方案去加载多张这样的大图,带来的结果会是内存爆掉。闪退。
能够设置SDWebImage或者YYWebImage的Option选项不解码下载好的图片
那么大图该怎么处理呢,这里有两个场景
解决方法: 使用苹果推荐的缩略图DownSampling方案便可
解决方法: 使用苹果的CATiledLayer去加载。原理是分片渲染,滑动时经过指定目标位置,经过映射原图指定位置的部分图片数据解码渲染。这里再也不累述,有兴趣的小伙伴能够自行了解下官方API。
了解图像加载的细节和全过程很是有必要,有助于咱们在日常开发中选择合适的方案,作出合理的性能优化。