Twitter Image Pipeline (a.k.a. TIP)
背景
《Twitter Image Pipeline》(简称 TIP)是一个用于在应用程序中 获取 和 存储 图像的优化框架。其核心概念是,所有获取或存储图像的请求都通过一个 图像管道 进行处理,该管道会对内存缓存和磁盘缓存进行检查,然后在网络上检索图像之前以及在网络上保持缓存始终保持最新状态和精简。
目标和需求
由于 Twitter iOS 用例的各种需求日益增多,Twitter Image Pipeline 得以产生。TIP 之前图像加载的系统脆弱且效率低下,一些边缘情况严重。从头开始设计一个新的框架来整体解决图像加载需求是最优路线,导致了 TIP。
- 渐进式图像加载支持(渐进式 JPEG)
- PJPEG 可以仅使用所需完整图像字节的极小部分即可渲染渐进式扫描
- 用户可以看到图像可见速度提高了35%到65%(有时甚至更好)
- PJPEG图像平均比它们的非渐进JPEG图像小10%
- PJPEG在iOS设备上可以硬件解码,就像非渐进JPEG图像一样
- 支持可续传下载
- 如果在图像部分加载时加载终止(通过失败或取消),则下一次加载该图像应从停止的地方恢复,从而节省需要传输的字节数
- 与渐进JPEG结合使用具有复合效益,因为恢复部分加载的图像可以在渐进扫描立即显示到屏幕上的同时加载剩余的字节以提高质量
- 支持编程/手动将图像存储到缓存中
- 通过能够将图像存储到底层缓存中,在图像上传时,可以不需要检索就直接将图像存储在正确的位置,这使得时间线的图像检索立即发生,并避免了网络请求。
- 当检索到较小的变体时,支持提供较大的变体
- 通过在缓存中维持最大的变体,我们可以简单地缩放图像(在后台),并出售该图像,而不是访问网络
- 当检索到较大的变体时,支持出售较小的变体
- 当在缓存中检索到较小的图像变体时,在从网络加载较大的变体时,您可以可选地消耗较小的变体
- 这通过在加载高质量的变体时以低质量提供图像来改善用户体验,而不是只显示空的/空白UI或占位符
- 异步架构
- 使用请求封装要加载的内容,使用操作执行异步工作,并具有委托以提供回调,我们可以提供强大且可扩展的图像加载模式
- 可取消检索
- 当图像检索不再相关(如从尚未加载完成的图像导航离开)时,我们应该允许取消检索
- 基于HTTP/1.1的检索下载,如果是取消的,将产生负面效果,即关闭TCP连接,这比重新建立连接时的带宽节约和避免与前一个网络请求的冲突更加昂费
- 基于HTTP/2(或SPDY)的检索下载将没有负面效果,因为该协议支持中流取消,而不会影响整体网络性能
- 快速访问缓存图像
- 通过同步加载已缩放和缓存的图像,可以避免图像立即可用时的中断,从而保持UI流畅
- 检索图像的背景渲染/缩放/解码
- 检索的图像需要解码,通常需要缩放,甚至渲染,这样在后台线程上执行可以消除从主线程尝试执行同样昂贵的工作时的帧率下降
- 隔离缓存/管道
- 通过使缓存支持隔离,Twitter应用可以利用这种隔离将缓存分开以供每个用户帐户使用。在帐户删除时,可以清除该帐户的缓存,而不会影响其他帐户的缓存。
- 图像检索活化支持
- 某些图像检索将需要检索对要加载的网络请求进行签名,支持活化步骤将允许使用“拉”模式,而不是需要提前应用任何此类请求构造的“推”模式。
- 支持自定义网络执行图像下载。
- Twitter对所有网络通信必须通过其网络层传输有严格的要求,因此
抽象了网络层,以便任何网络层都可以通过抽象接口插入以便进行下载。 - 默认情况下使用基于NSURLSession的下载插件,但消费者可以插入他们所需的任何网络层。
- Twitter对所有网络通信必须通过其网络层传输有严格的要求,因此
- 支持所需的任何图像编解码器
- 默认情况下,所有ImageIO支持的图像类型都受支持
- 插件架构支持为TIP中编码和解码图像自定义编解码器
- 用例包括WebP支持、JPEG解码等自定义解码支持,例如具有共享量化表和/或标题的JPEG解码,甚至将某些视觉变换(如模糊)作为渲染的一部分应用
架构
缓存
针对每个图像处理管道有三个独立的缓存:内存中的渲染缓存,图像数据内存缓存和磁盘缓存。缓存的条目根据由获取请求的创建者提供的图像标识符或自动从图像获取的URL生成。
- 磁盘缓存将维护针对图像标识符的最新部分图像和最大完成图像
- 内存中的图像数据缓存将维护基于图像标识符的最大匹配图像数据(未解码)
- 内存中的渲染缓存将维护与(基于图像标识符)匹配的最新的三个已调整大小和渲染/解码UIImages
从网络检索图像时,图像将同时以原始字节形式加载到内存中并写入磁盘缓存。部分图像也会持久化,不会替换缓存中的完成图像。
一旦从任何缓存或网络检索到图像,检索到的图像将以各种形式通过缓存。
缓存将在全局范围内可配置,具有最大大小。这个最大值将在同一类型的所有图像处理管道缓存中强制执行,并通过组合生存时间(TTL)到期和最少最近使用(LRU)清理来维护。(这解决了Twitter iOS应用长期存在的未绑定缓存问题,可能会导致数GB的磁盘空间被消耗)。
执行
获取操作背后的架构相当直接且已经被简化成了一个流水线(因此,“图像流水线”)。
当发起请求时,获取操作将执行以下步骤:
- 同步咨询内存中缓存的已渲染图像,以查找适合目标尺寸和内容模式的图像。
- 如果未命中,则异步咨询包含最大匹配图像(根据标识符)的内存中图像数据缓存的《图像数据内存缓存》。
- 如果未命中,则异步咨询包含最大匹配图像数据的磁盘缓存《磁盘缓存》。作为一个优化,TIP还将更进一步,并咨询所有其他已注册的《流水线磁盘缓存》——这样做可以从磁盘提取,从而节省网络负载成本。跨流水线检索的图像将存储到获取流水线的缓存中,以保持图像流水线的隔离。注:
- 如果未命中,则异步咨询任何提供的《附加缓存》(基于URL)。这样,在过渡到《TIP》时,可以提取旧缓存,而无需强制重新加载所有资源。
- 如果未命中,则异步从《网络》检索图像,恢复任何可能存在于磁盘缓存中的部分加载的数据。
预览支持
除了这个简单的流程之外,获取操作还将提供第一个匹配的(基于图像标识符)完整图像作为预览图像,当URL不匹配时在内存缓存或磁盘缓存中。此时,获取代理可以选择仅使用预览图像或继续使用《网络》加载最终图像。这在获取图像URL小于缓存中图像大小时特别有用,无需触网:)
渐进式支持
《图像流水线》提供的价值之一是,如果图像是PJPEG格式,则在从网络加载图像时,能够流式传输图像的渐进扫描。这种渐进式渲染在iOS 8+上得到原生支持,《TIP》的最低操作系统版本现在是iOS 10+。渐进式支持是可选的,并且可以配置扫描的加载方式。
终止图像下载
如前所述,通过将图像的局部加载持久化到磁盘缓存中,我们可以支持可续传下载。这不需要任何接口,这只是图像处理管道工作方式的一部分。
针对目标尺寸渲染
从2.20版本开始,图像处理管道将从数据加载图像,到fetch请求中指定的目标尺寸,从而避免了仅为了缩小到正确的尺寸,将整个图像加载到大型位图中的开销。如果目标尺寸大于图像数据,它将加载该图像位图,并将其扩展到由fetch请求指定的目标尺寸。如果请求未提供目标尺寸(或尺寸表示不进行调整),它将输出全尺寸图像,如预期的那样。
Twitter图像处理管道功能
- 获取
- 进度报告
- 可定制渐进式加载策略
- 预览加载并提供避免继续加载的选项
- 占位符支持(用于被清除的非规范图像)
- 自动缩放至目标视图的大小
- 自定义缓存设置
- 自定义加载源集(缓存和网络)
- 基于NSOperation的
- 可取消的
- 支持优先级
- 支持依赖关系链
- 委托模式(用于稳健支持)
- 块回调模式(用于简单用例)
- 存储
- 支持手动存储(UIImage、NSData或磁盘上的文件)
- 支持手动清除
- 支持依赖关系链(类似于NSOperation)
- 缓存
- 针对渲染图像的同步/快速缓存
- 针对图像数据的异步内存缓存
- 针对图像数据的异步磁盘缓存
- 自动LRU清除
- 自动TTL清除
- 隔离缓存(通过多个
TIPImagePipeline
实例实现) - 支持从额外的非TIP缓存加载数据(有助于迁移)
- 公开方法以直接复制磁盘缓存图像
- 下载
- 合并图像下载
- 内置图像下载续传支持
- 图像响应的“Accept-Ranges”必须是“bytes”且必须具有“Last-Modified”标头
- 使用“Range”和“I-Range”标头指定续传
- 可插入的网络(使用您自己的网络层)
- 查看如何将Twitter网络层作为可插入下载器集成到以下代码片段中
- 自定义填充(对于需要身份验证的获取很有用)
- 深入了解
- 全局管道可见性
- 单个管道可见性
- 全局问题可见性(非致命问题,用于监控)
- 断言可以启用/禁用
- 可插入日志记录
- 可检查(可以检查每个管道的条目)
- 稳健的错误
- 抓取操作完成指标详细
- 稳健的图像支持
- 可插拔编解码器(可以添加WebP或其他图像编解码器)
- 可以序列化对Context上下文的访问
- UIImage便利方法
- 动画图像支持(默认支持GIF)
- UIKit集成
- 使用
TIPImageViewFetchHelper
解耦逻辑与视图的专用辅助对象 - 获取辅助器提供了有用的获取行为封装
- 调试覆盖功能,可以看到图像视图的调试详情
UIImageView
类别,方便与TIPImageViewFetchHelper
配对
- 使用
- 可配置
- 缓存大小(字节和图像计数)
- 最大缓存条目大小
- 断开下载的最大时间
- 最大并发下载
Twitter图像管道组件
TIPGlobalConfiguration
- TIP的全局配置
- 配置/修改此配置以调整TIP的行为以满足您的需求
TIPImagePipeline
- 从和存储到多个图像的管道
- 可以存在多个管道,提供用例隔离
- 通过提供一个包含委托(
TIPImageFetchDelegate
)或完成块(TIPImagePipelineFetchCompletionBlock
)的请求(TIPImageFetchRequest
)来构造一个获取操作。然后可以将该操作提供给该管道以开始获取。这种两步骤的方法对于支持同步和异步加载是必要的,同时不会给开发者带来过多的负担。
TIPImageFetchRequest
- 封装检索图像所需信息的协议
TIPImageFetchDelegate
- 处理动态决策和事件回调的委托
TIPImageFetchOperation
- 执行请求并提供操作句柄的
NSOperation
- 操作维护了获取进度的状态,在执行中
- 操作提供了几项功能
- 可取消
- 支持依赖项
- 优先级(可以随时更改)
- 区分操作的唯一引用
- 执行请求并提供操作句柄的
TIPImageStoreRequest
- 封装编程存储图像所需信息的协议
TIPImageContainer
- 封装获取图像相关信息的对象
TIPImageFetchDelegate
将使用TIPImageContainer
实例进行回调,而TIPImageFetchOperation
将在执行过程中维护TIPImageFetchOperation
属性。
TIPImageViewFetchHelper
- 可以封装大多数图像加载和在
UIImageView
中显示用例的强大类 - 99%的图像加载和显示用例都可以通过使用此类、配置它以及提供委托和/或数据源来解决
- 将逻辑放在此类中,可以避免在MVC实践中将controller代码与view代码耦合
- 可以封装大多数图像加载和在
UIView(TIPImageFetchable)
和UIImageView(TIPImageFetchable)
- 在
UIImageView
和UIView
上的便利类别,用于关联TIPImageViewFetchHelper
- 在
用法
使用TIP的最简单方法是使用TIPImageViewHelper
的副本。
对于具体的编码示例,请参阅 TIP Sample App 和 TIP Swift Sample App(分别为Objective-C和Swift的示例)。
以下是一个使用TIP与具有图像视图数组的UIViewController
进行交互的简单示例。
/* category on TIPImagePipeline */
+ (TIPImagePipeline *)my_imagePipeline
{
static TIPImagePipeline *sPipeline;
static dispatch_once_t sOnceToken;
dispatch_once(&sOnceToken, ^{
sPipeline = [[TIPImagePipeline alloc] initWithIdentifier:@"com.my.app.image.pipeline"];
// support looking in legacy cache before hitting the network
sPipeline.additionalCaches = @[ [MyLegacyCache sharedInstance] ];
});
return sPipeline;
}
// ...
/* in a UIViewController */
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (nil == self.view.window) {
// not visible
return;
}
[_imageFetchOperations makeAllObjectsPerformSelector:@selector(cancelAndDiscardDelegate)];
[_imageFetchOperations removeAllObjects];
TIPImagePipeline *pipeline = [TIPImagePipeline my_imagePipeline];
for (NSInteger imageIndex = 0; imageIndex < self.imageViewCount; imageIndex++) {
UIImageView *imageView = _imageView[imageIndex];
imageView.image = nil;
id<TIPImageFetchRequest> request = [self _my_imageFetchRequestForIndex:imageIndex];
TIPImageFetchOperation *op = [pipeline operationWithRequest:request context:@(imageIndex) delegate:self];
// fetch can complete sync or async, so we need to hold the reference BEFORE
// triggering the fetch (in case it completes sync and will clear the ref)
[_imageFetchOperations addObject:op];
[[TIPImagePipeline my_imagePipeline] fetchImageWithOperation:op];
}
}
- (id<TIPImageFetchRequest>)_my_imageFetchRequestForIndex:(NSInteger)index
{
NSAssert(index < self.imageViewCount);
UIImageView *imageView = _imageViews[index];
MyImageModel *model = _imageModels[index];
MyImageFetchRequest *request = [[MyImageFetchRequest alloc] init];
request.imageURL = model.thumbnailImageURL;
request.imageIdentifier = model.imageURL.absoluteString; // shared identifier between image and thumbnail
request.targetDimensions = TIPDimensionsFromView(imageViews);
request.targetContentMode = imageView.contentMode;
return request;
}
/* delegate methods */
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op
didLoadPreviewImage:(id<TIPImageFetchResult>)previewResult
completion:(TIPImageFetchDidLoadPreviewCallback)completion
{
TIPImageContainer *imageContainer = previewResult.imageContainer;
NSInteger idx = [op.context integerValue];
UIImageView *imageView = _imageViews[idx];
imageView.image = imageContainer.image;
if ((imageContainer.dimension.width * imageContainer.dimensions.height) >= (originalDimensions.width * originalDimensions.height)) {
// scaled down, preview is plenty
completion(TIPImageFetchPreviewLoadedBehaviorStopLoading);
} else {
completion(TIPImageFetchPreviewLoadedBehaviorContinueLoading);
}
}
- (BOOL)tip_imageFetchOperation:(TIPImageFetchOperation *)op
shouldLoadProgressivelyWithIdentifier:(NSString *)identifier
URL:(NSURL *)URL
imageType:(NSString *)imageType
originalDimensions:(CGSize)originalDimensions
{
// only load progressively if we didn't load a "preview"
return (nil == op.previewImageContainer);
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op
didUpdateProgressiveImage:(id<TIPImageFetchResult>)progressiveResult
progress:(float)progress
{
NSInteger idx = [op.context integerValue];
UIImageView *imageView = _imageViews[idx];
imageView.image = progressiveResult.imageContainer.image;
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op
didLoadFinalImage:(id<TIPImageFetchResult>)finalResult
{
NSInteger idx = [op.context integerValue];
UIImageView *imageView = _imageViews[idx];
imageView.image = finalResult.imageContainer.image;
[_imageFetchOperations removeObject:op];
}
- (void)tip_imageFetchOperation:(TIPImageFetchOperation *)op
didFailToLoadFinalImage:(NSError *)error
{
NSInteger idx = [op.context integerValue];
UIImageView *imageView = _imageViews[idx];
if (!imageView.image) {
imageView.image = MyAppImageLoadFailedPlaceholderImage();
}
NSLog(@"-[%@ %@]: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), error);
[_imageFetchOperations removeObject:op];
}
检查图像管道
Twitter Image Pipeline 内置了对通过便捷类检查缓存的支援。 TIPGlobalConfiguration
包含一个inspect:
方法,可以检查所有已注册的TIPImagePipeline
实例(即使它们尚未显式加载),并提供详细结果以供缓存及其中图像之用。您还可以调用特定TIPImagePipeline
实例上的inspect:
方法,以获得针对该特定管道的详细信息。在主线程上调用检查回调之前,将在后台线程上异步执行检查。这可以提供非常有用的调试信息。作为示例,Twitter为内部构建内置了UI和工具,这些工具使用了TIP的检查支持。
许可协议
版权所有 2015-2020 Twitter, Inc。
遵循Apache License,版本2.0:[链接](https://www.apache.org/licenses/LICENSE2.0)
安全问题?
请通过Twitter的漏洞赏金计划([链接](https://hackerone.com/twitter))而不是GitHub来报告敏感的安全问题。