在iOS中高效的加载图片
<p>在iOS开发中,图片(UIImage)是我们在开发中,占用手机<code>内存</code>比较大的对象,如果在运行过程中,内存占用过大,对<code>电池寿命</code>会造成影响,如果超过了<code>内存占用的最大值</code>,会造成App的<code>crash</code>。这篇文章从<code>图片的加载</code>原理和<code>SDWebImage</code>的源码实现的角度来介绍图片加载。</p><h3 id="图片的渲染流程">图片的渲染流程</h3>
<p>在iOS中使用 <code>UIImage</code>和<code>UIImageView</code>来记载图片,他俩遵守经典的<code>MVC</code>架构,<code>UIImage</code>相当于<code>Model</code>,<code>UIImageView</code>相当于<code>View</code>:</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-0eb49b4e242621d9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p><code>UIImage</code>负责<code>加载图片</code>,<code>UIImageView</code>负责<code>渲染图片</code>。</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-70830b6e894218ed.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>图片的渲染流程分为<code>3个阶段</code>:<code>加载(Load),解码(Decoder)和渲染(Render)</code></p>
<blockquote>
<p><strong>作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:834688868,不管你是大牛还是小白都欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!</strong></p>
</blockquote>
<p>如果你正在面试,或者正准备跳槽不妨动动小手,添加一下咱们的交流群:834688868来获取一份详细的大厂面试资料为你的跳槽加薪多一份保障</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-c58ca54b92e3333e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>在每个阶段都会有相对应的<code>缓冲区</code>:<code>数据缓冲区(DataBuffer),图像缓冲区(imageBuffer)和帧缓冲区(framebuffer)</code>。</p>
<p>我们以加载一个图片的尺寸为:<code>2048 px * 1536 px</code>,在磁盘上的大小为:<code>590kb</code>的图片为例,来分析前两个阶段的缓冲区。</p>
<h4 id="databuffer">DataBuffer</h4>
<p><code>DataBuffer</code>只是一种包含<code>一系列字节</code>的缓冲区。通常以某些<code>元数据</code>开头,<code>元数据</code>描述了存储在数据缓冲区中的图像大小,包含图形数据本身,<code>图像数据以某种形式编码</code> 如 JPEG压缩或PNG,这意味着,<code>该字节并不直接描述图像中像素的任何内容</code>。此时的 <code>DataBuffer</code>大小为 <code>590kb</code>。</p>
<h5 id="sd源码分析">SD源码分析</h5>
<p>在<code>SDWebImage</code>中,图片加载完成后,在 <code>sd_imageFormatForImageData</code>的方法中,是通过<code>DataBuffer</code>的<code>第一个字节</code>来判断图片的格式的。</p>
<pre><code> uint8_t c;
;
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
retur SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
......
}
复制代码
</code></pre>
<h4 id="imagebuffer">ImageBuffer</h4>
<p>在<code>图片加载</code>完后,需要将<code>Data Buffer</code>的<code>JPEG,PNG或其他编码的数据</code>,转换为<code>每个像素</code>的<code>图像信息</code>,这个过程,称为<code>Decoder(解码)</code>,将<code>像素信息</code>存放在<code>ImageBuffer</code>。</p>
<h5 id="占用内存大小">占用内存大小</h5>
<p>图片占用的内存大小与<code>图像的尺寸有关</code>,与它的<code>文件大小无关</code>,在iOS<code>SRGB</code>显示格式中<code>(4byte空间显示一个像素)</code>,如果解析所有的像素,需要 <code>2048 px * 1536 px * 4 byte/px = 10MB</code>的空间,此时的 <code>ImageBuffer</code>的大小为<code>10MB</code>。</p>
<p>在<code>ImageBuffer</code>解析完后,提交给<code>frameBuffer</code>进行渲染显示。</p>
<p>总的来说,图片加载过程和消耗的内存如下图所示:</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-69ef939fcdb2620e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<h5 id="xcode测试">Xcode测试</h5>
<p>在<code>Xcode</code>工程中,当push新页面的时候,只加载一个图片。</p>
<p>加载前内存值:</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-c7a64f7fb797d24f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>加载后内存值:</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-44411651e622940e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>大多数情况下,我们并不需要如此高精度的显示图片,占用了这么多的内存,能否减少加载图片时占用的内存值呢?</p>
<h3 id="如何减少图像占用内存">如何减少图像占用内存</h3>
<h4 id="向下采样">向下采样</h4>
<p>在苹果官方文档中,建议我们使用<code>向下采样(Downsampleing)</code>的技术,来加载图片,减少<code>ImageBuffer</code>的大小。</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-6f9e037d59cd2d0c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>方法如下:</p>
<pre><code>func downsample(imageAt imageURL: URL, to pointSize:CGSize, scale:CGFloat) ->UIImage {
let imageSourcesOptions = as CFDictionary
let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourcesOptions)!
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize:maxDimensionInPixels
] as CFDictionary
let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!
return UIImage(cgImage: downsampledImage)
}
复制代码
</code></pre>
<p>我们来测试一下:</p>
<pre><code> let imageStr = Bundle.main.path(forResource: "view_site.jpeg", ofType: nil)
let imageURL = URL(string: "file://" + (imageStr ?? ""))
guard let imgURL = imageURL else {
return
}
imageView.image = downsample(imageAt:imgURL , to: CGSize(width: 200, height: 200), scale: UIScreen.main.scale)
复制代码
</code></pre>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-c341e66339a0289e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<p>加载之前时是<code>13M</code>,加载之后是 <code>17M</code>,效果是很明显的,节省了大约 <code>5M</code>的内存空间。</p>
<p><code>在对图片进行压缩时,我们应首选向下采样技术</code>。</p>
<h5 id="sd源码分析解码过程">SD源码分析解码过程</h5>
<p>在<code>SDWebIamge</code>中,一共有3种类型的解码器:<code>SDImageIOCoder, SDImageGIFCoder, SDImageAPNGCoder</code>,根据<code>DataBuffer</code>的编码类型,使用相对应的编码器。</p>
<p>在 <code>-(UIImage *)decodedImageWithData:(NSData *)data</code>方法中,配置解码参数,开始进行解码操作。</p>
<p>在 <code>+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary )options</code> 中,完成图像解码</p>
<p><img src="https://upload-images.jianshu.io/upload_images/23620676-acbf4975a6b2ac25.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"></p>
<h4 id="选择正确的图片渲染格式">选择正确的图片渲染格式</h4>
<h5 id="渲染格式">渲染格式</h5>
<p>在 iOS中,渲染图片格式有4种</p>
<ul>
<li><code>Alpha 8 Format</code>:<code>1字节</code>显示<code>1像素</code>,擅长显示单色调的图片。</li>
<li><code>Luminance and alpha 8 format</code>: 亮度和 alpha 8 格式,<code>2字节</code>显示<code>1像素</code>,擅长显示有透明度的单色调图片。</li>
<li><code>SRGB Format</code>: 4个字节显示<code>1像素</code>。</li>
<li><code>Wide Format</code>: 广色域格式,8个字节显示<code>1像素</code>。适用于高精度图片,</li>
</ul>
<h5 id="如何正确的选择渲染格式">如何正确的选择渲染格式</h5>
<p>正确的思路是:<code>不选择渲染格式,让渲染格式选择你</code>。</p>
<p>使用 <code>UIGraphicsImageRender</code>来替换<code>UIGraphicsBeginImageContextWithOptions</code>,前者在<code>iOS12</code>以后,会自动选择渲染格式,后者默认都会选择<code>SRGB Format</code>。</p>
<pre><code>func render() -> UIImage{
let bounds = CGRect(x: 0, y: 0, width: 300, height: 100)
let render = UIGraphicsImageRenderer(size: bounds.size)
let image = render.image { context in
UIColor.blue.setFill()
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: UIRectCorner.allCorners, cornerRadii: CGSize(width: 20, height: 20))
path.addClip()
UIRectFill(bounds)
}
return image
}
复制代码
</code></pre>
<p>此时,系统为自动选择<code>Alpha 8 Format</code>格式,内容空间占用,将会减少<code>75%</code>。</p>
<h4 id="减少后备存储器的使用">减少后备存储器的使用</h4>
<h5 id="减少或者不使用-drawrect-方法">减少或者不使用 draw(rect:) 方法</h5>
<p>在需要绘制带有子视图的View时,不使用 <code>draw(rect:)</code>方法,使用<code>系统的View属性</code>或者<code>添加子视图</code>的方式,将绘制工作交给系统来处理。</p>
<p>背景色直接通过<code>UIView.backgroundColor</code>设置,而非使用<code>draw(rect:)</code></p>
<h3 id="如何在列表中加载图片">如何在列表中加载图片</h3>
<p>我们在开发中,一般会对图片进行<code>子线程异步加载</code>,在后台进行 <code>解码和下采样</code>。在列表中,有时会加载很多图片,此时应该注意<code>线程爆炸</code>问题。</p>
<h4 id="线程爆炸">线程爆炸</h4>
<p>当我们要求<code>系统去做比CPU能够做的工作更多的工作时</code>就会发生这种情况,比如我们要显示<code>8张图片</code>,但我们只有<code>两个CPU</code>,就不能一次完成所有这些工作,无法在不存在的CPU上进行并行处理,<code>为了避免向一个全局队列中异步的分配任务时发生死锁</code>,<code>GCD</code>将创建新线程来捕捉我们要求它所做的工作,然后CPU将花费大量时间,在这些<code>线程</code>之间进行<code>切换</code>,尝试在所有工作上取得我们要求操作系统为我们做的<code>渐进式进展</code>,在这些线程之间<code>不停切换</code>,实际上是相当大的开销,现在<code>不是简单地将工作分派到全局异步队列之一</code>,而是<code>创建一个串行队列</code>,在预取的方法中,异步的将工作分派到该队列,它的确意味着单个图像的加载,可能要比以前晚才能开始取得进展,但CPU将花费更少的时间,在它可以做的小任务之间来回切换。</p>
<p>在<code>SDWebImage</code>中,解码的队列 <code>_coderQueue.maxConcurrentOperationCount = 1</code>就是一个串行队列。这样就很好的解决了<code>多图片异步解码</code>时,<code>线程爆炸</code>问题。</p>
<p>作者:Bel李玉<br>
链接:https://juejin.cn/post/7019623908500324389</p><br><br>
来源:https://www.cnblogs.com/iOSer1122/p/15422432.html
頁:
[1]