一、图片显示

图片的显示分为三步:加载、解码、渲染。解码和渲染是由 UIKit 进行,通常我们操作的只有加载。

以 UIImageView 为例。当其显示在屏幕上时,需要 UIImage 作为数据源。UIImage 持有的数据是未解码的压缩数据,能节省较多的内存和加快存储。

当 UIImage 被赋值给 UIImage 时(例如 imageView.image = image;),图像数据会被解码,变成 RGB 的颜色数据。

解码是一个计算量较大且需要 CPU 来执行的任务;并且解码出来的图片体积与图片的宽高有关系,而与图片原来的体积无关。其体积大小可简单描述为:宽 * 高 * 每个像素点的大小 = width * height * 4bytes。

图像解码操作会造成什么问题?

以常见的 UITableView 和 UICollectionView 为例,假如在使用一个多图片显示的功能:在上下滑动显示图片的过程中,我们会在 cellForRow 的方法加载 UIImage 图片、赋值给 UIImageView,相当于在主线程同时进行 IO 操作、解码操作等,会造成内存迅速增长和 CPU 负载瞬间提升。

并且内存的迅速增加会触发系统的内存回收机制,尝试回收其他后台进程的内存,增加 CPU 的工作量。如果系统无法提供足够的内存,则会先结束其他后台进程,最终无法满足的话会结束当前进程。

1.1 优化一:降采样

在滑动显示的过程中,图片显示的宽高远比真实图片要小,我们可以采用加载缩略图的方式减少图片的占用内存。如下图所示:

我们加载 JPEG的图片,然后进行相关设置,解码后根据设置生成 CGImage 缩略图,最后包装成 UIImage,最终传递给UIImageView 渲染。

思考:这里的解码步骤为何不是上文提到的 imageView.image = image 时机?

- (UIImage *)downsampleImageAt:(NSURL *)imageURL to:(CGSize)pointSize scale:(CGFloat)scale
{
CFDictionaryRef imageSourceOptions = CFDictionaryCreate ( CFAllocatorGetDefault(),
(void *)@[ (NSString *)kCGImageSourceShouldCache ],
(void *)@[ @(YES) ],
1,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks); CGImageSourceRef imageSource = CGImageSourceCreateWithURL((__bridge CFURLRef)imageURL, imageSourceOptions); NSInteger maxDimensionInPixels = MAX(pointSize.width, pointSize.height) * scale; CFDictionaryRef downsampleOptions = (__bridge CFDictionaryRef)@{ (NSString *)kCGImageSourceCreateThumbnailFromImageAlways : @(YES),
(NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
(NSString *)kCGImageSourceCreateThumbnailWithTransform : @(YES),
(NSString *)kCGImageSourceThumbnailMaxPixelSize : @(maxDimensionInPixels) }; CGImageRef downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions); return [UIImage imageWithCGImage:downsampledImage];
}

正常的 UIImage 加载是从 App 本地读取,或者从网络下载图片,此时不涉及图片内容相关的操作,并不需要解码;当图片被赋值给 UIImageView 时,CALayer 读取图片内容进行渲染,所以需要对图片进行解码;而上文的缩略图生成过程中,已经对图片进行解码操作,此时的 UIImage 只是一个 CGImage 的封装,所以当 UIImage 赋值给 UIImageView 时,CALayer 可以直接使用 CGImage 所持有的图像数据。

1.2 优化二:异步处理

从用户的体验来分析,滑动的操作往往是间断性触发,在滑动的瞬间有较大的工作量,而且由于都是在主线程进行操作无法进行任务分配,CPU 2 处于闲置。由此引申出两种优化手段:Prefetching(预处理)和 Background decoding / downsampling(子线程解码和降采样)。综合起来,可以在 Prefetching 时把降采样放到子线程进行处理,因为降采样过程就包括解码操作。

Prefetching 回调中,把降采样的操作放到同步队列 serialQueue 中,处理完毕之后抛给主线程进行 update 操作。

需要特别注意,此处不能是并发队列,否则会造成线程爆炸,原因见总结部分。

{
// 创建串行队列
_serialQueue = dispatch_queue_create("DecodeQueue", DISPATCH_QUEUE_SERIAL);
} /**
* @brief 获取单元格的图片
*/
- (void)collectionView:(UICollectionView *)collectionView prefetchItemsAt:(NSArray<NSIndexPath *> *)indexPaths
{
for (NSIndexPath * indexPath in indexPaths) {
dispatch_async(_serialQueue, ^{
UIImage * downsampledImage = [self downsample]; dispatch_async(dispatch_get_main_queue(), ^{
[self updateAt:indexPath with:downsampledImage];
});
});
}
} /**
* @brief 降采样。自行实现。
*/
- (UIImage *)downsample
{
return nil;
} /**
* @brief 更新单元格的图片
*/
- (void)updateAt:(NSIndexPath *)indexPath with:(UIImage *)image
{ }

1.3 优化三:使用 Image Asset Catalogs

苹果推荐的图片资源管理工具,压缩效率更高,在 iOS 12 的机器上有 10~20% 的空间节约,并且苹果会持续对其进行优化。

WWDC Session

二、总结

应用上述的优化策略,已经能对图片加载有比较好的优化。

WWDC 后续还有对 CustomDrawing 和 CALayer 的 BackingStore 的介绍,与图片关系不大,不在此赘述。

三、WWDC学习

原作者的经验:落影loyinglin

先主观假设两个前提:

  1. 大部分苹果工程师对 iOS 系统内部实现都比我们要清楚;
  2. 能到 WWDC 分享的工程师在苹果内部也是优秀的工程师;那么 WWDC 所讲的内容我们可以认为是正确的事实。

所以可以基于自己已掌握的基础知识,还有对 iOS 系统的了解来分析 WWDC 上面所提到的现象,看我们的 iOS 知识体系是否存在缺陷;另外,WWDC 介绍的很多知识点同样免验证的加入自己的知识体系。

以上文提到的线程爆炸为例,看看这种方式的好处。

原文如下:

Thread Explosion(线程爆炸)

More images to decode than available CPUs(解码图像数量大于 CPU 数量)

GCD continues creating threads as new work is enqueued(GCD 创建新线程处理新的任务)

Each thread gets less time to actually decode images(每个线程获得很少的时间解码图像)

从这个案例我们学习到如何避免图像解码的线程爆炸,我们分析苹果工程师的逻辑,然后扩散思维:

原因:解码任务过多 => 过程:GCD 开启更多线程=> 结果:每个线程获得更少的时间

延伸出来的问题:

GCD 是如何处理并发队列?为何会启动多个线程处理?多少的线程数量合适?线程的 cpu 时间分配和切换代价?...

举一反三。但是这样的思考稍显混乱,仍有优化的空间。把脑海关于 GCD 的认知提炼出来:

  1. GCD 是用来处理一系列任务的同步和异步执行,队列有串行和并发两种,与线程的关系只有主线程和非主线程的区别;
  2. 串行队列是执行完当前的任务,才会执行下一个 block 任务;并行队列是多个 block 任务并行执行,GCD 会根据任务的执行情况分配线程,原则是尽快完成所有任务。

接下来的表现是操作系统相关的知识:

  1. iOS 系统中进程和线程的关联,每个启动的 App 都是一个进程,其中有多个线程;
  2. cpu 的时间是分为多个时间片,每个线程轮询执行;
  3. 线程切换执行有代价,但比进程切换小得多;
  4. 每个 cpu 核心在同一时刻只能执行一个线程。

至此我们可以结合操作系统和 GCD 的知识,猜测底层 GCD 的实现思路和线程爆炸情况下的表现:

主线程把多个任务 block 放到并发队列,GCD 先启动一个线程处理解码任务,线程执行过程中遇到耗时操作时(IO 等待、大量 CPU 计算),短时间内无法完成,为了不阻塞后续任务的执行,GCD 启动新的线程处理新的任务。

结合此案例,我们能回答相关问题:

  1. 现在有一个很复杂的计算任务,例如统计一个 5000*5000 图片中像素点的 RGB 颜色通道,如果用分为 25 个任务放到 GCD 并发队列,把大图切分成 25 个 1000*1000 小图分别统计,是否会速度的提升?
  2. GCD 的串行队列和并发队列的应用场景有何不同?

四、文章

iOS性能优化--图片加载和处理

WWDC2018-Image and Graphics Best Practices

iOS 图片加载和处理的更多相关文章

  1. iOS图片加载到内存中占用内存情况

    我的测试结果: 图片占用内存   图片尺寸           .png文件大小 1MB              512*512          316KB 4MB              10 ...

  2. iOS 图片加载速度优化

    FastImageCache 是 Path 团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动起来更顺畅,来看看它是怎么做的. 一.优化点 iOS 从磁盘加载一张图片,使用 UI ...

  3. iOS图片加载框架-SDWebImage解读

    在iOS的图片加载框架中,SDWebImage可谓是占据大半壁江山.它支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件.在项目中使用SDWebImage ...

  4. iOS图片加载新框架 - FlyImage

    FlyImage 整合了SDWebImage,FastImageCache,AFNetworking的优点,是一个新的性能高效.接口简单的图片加载框架. 特点 高效 可将多张小图解码后存储到同一张大图 ...

  5. iOS 图片加载框架- SDWebImage 解读

    在iOS的图片加载框架中,SDWebImage可谓是占据大半壁江山.它支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件.在项目中使用SDWebImage ...

  6. iOS图片加载-SDWebImage

    一.SDWebImage内部实现过程 1, 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后  SDWeb ...

  7. iOS图片加载速度极限优化—FastImageCache解析

    FastImageCache是Path团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动 优化点 iOS从磁盘加载一张图片,使用UIImageVIew显示在屏幕上,需要经过以下步 ...

  8. iOS 图片加载速度极限优化—FastImageCache解析

    FastImageCache是Path团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动起来更顺畅,来看看它是怎么做的.优化点iOS从磁盘加载一张图片,使用UIImageVIew ...

  9. iOS 图片加载导致内存警告

    虽然UITableView和UICollectionView都有cell复用机制,但是如果利用SDWebImage加载的图片本身过大且cell复用池中的个数比较多(cell的Size越小,复用池中的c ...

随机推荐

  1. 大厂面试官问你META-INF/spring.factories要怎么实现自动扫描、自动装配?

    大厂面试官问你META-INF/spring.factories要怎么实现自动扫描.自动装配?   很多程序员想面试进互联网大厂,但是也有很多人不知道进入大厂需要具备哪些条件,以及面试官会问哪些问题, ...

  2. 在Shadow DOM使用原生模板

    原生模板的优势 延迟了资源加载 延迟了加载和处理模板所引用的资源的时机,这样,用户就能够在模板中使用任意多的资源,却不阻碍页面的渲染. 延迟了渲染内容 无论模板在什么位置,浏览器不会把模板中的内容直接 ...

  3. 前端每日实战:27# 视频演示如何用纯 CSS 创作一个精彩的彩虹 loading 特效

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/vjvoow 可交互视频教程 此视频 ...

  4. Django中的session的使用

    一.Session 的概念 cookie 是在浏览器端保存键值对数据,而 session 是在服务器端保存键值对数据 session 的使用依赖 cookie:在使用 Session 后,会在 Coo ...

  5. YiGo表单建立

    做一个请假单表单(下图是最后的成品图) 表单的类型 实体表单 1.可存储 2.可编辑 虚拟表单 视图(不可存储数据,只有显示功能) 不可编辑 字典 报表 备注 :一张表单是实体还是虚拟取决于其数据对象 ...

  6. Spring Cloud 理论篇

    show me the code and talk to me,做的出来更要说的明白 github同步收录 我是布尔bl,你的支持是我分享的动力! 微服务 在 jsp 时代,应用前后端耦合,前后端 a ...

  7. CrawlSpiders简介

    转:https://www.cnblogs.com/ellisonzhang/p/11124516.html#4295547 一.CrawlSpiders类简介 通过下面的命令可以快速创建 Crawl ...

  8. C++中的内存分配

    C++提供下面两种方法分配和释放未构造的原始内存 (1)allocator 类,它提供可感知类型的内存分配 (2)标准库中的 operator new 和 operator delete,它们分配和释 ...

  9. C++总结之template

    函数模板 我们可以把函数模板当做一种特殊的函数,里面的参数类型可以是任意类型,这样的话我们就可以减少重复定义,从而让这个函数模板自动适应不同的参数类型,也就是说函数可以适应多种类型的参数,例如doub ...

  10. AndroidStudio提高编译速度的几种方法

    第一种: 减少依赖库的使用,让代码更加精简.对于一些必须依赖的库要尽量使用jar包或者依赖库,这样他每次就会在本地直接加载,而不是每次翻墙检查更新 第二种: 打开Android Studio,选择菜单 ...