测试已测试 | ✗ |
语言语言 | Obj-CObjective C |
许可证 | MIT |
发布上次发布 | 2016年2月 |
由Mallory Paine维护。Mallory Paine.
Fast Image Cache是一种高效、持久且最重要是快速的方式来在iOS应用程序中存储和检索图像。任何优秀的iOS应用程序的用户体验都包括快速平滑的滚动,而Fast Image Cache有助于让这一点变得更简单。
对于像Path这样的图形丰富型应用程序来说,图像加载对性能造成了很大的压力。从磁盘加载单个图像的传统方法实在太慢,尤其是当在滚动时。Fast Image Cache正是为了解决这一问题而创建的。
为了理解Fast Image Cache的工作原理,了解许多处理图像的应用程序所遇到的一个典型场景是有益的。
在iOS应用中,尤其是社交网络领域的应用,通常会同时显示大量图像,如用户照片。直观的传统方式是从API请求图像数据,处理原始图像,以创建所需的尺寸和样式,并将这些处理后的图像存储在设备上。
后来,当应用程序需要显示这些图像时,它们会从磁盘加载到内存中,并在图像视图中显示,或者以其他方式渲染到屏幕上。
从压缩的磁盘图像数据到用户实际能看到的渲染Core Animation层的转化过程实际上是代价很高的。随着要显示的图像数量增加,这种成本很容易积累,导致帧率明显下降。可滚动的视图进一步加剧了这种情况,因为内容可以快速变化,需要快速处理时间来保持60FPS流畅。
考虑从磁盘加载图像到屏幕显示时的工作流程
+[UIImage imageWithContentsOfFile:]
使用Image I/O从内存映射数据创建一个CGImageRef
。此时,图像尚未解码。UIImageView
。CATransaction
捕获这些层树修改。这些成本很容易累计并杀死程序的感知性能。尤其是在滚动时,用户体验让人不满意,这与整体的iOS体验不符。
1 60FPS
≈ 每帧0.01666秒
= 每帧16.7毫秒
。这意味着任何主线程工作超过16毫秒都可能导致应用程序丢失动画帧。
2《CALayer》类的contents
属性说明,将该属性赋值会导致该图层使用你的图像,而不是[创建]单独的后备存储。然而,“使用你的图像”的含义仍然模糊不清。使用 Instruments分析应用程序时,通常会揭示对CA::Render::copy_image
的调用,即使Core Animation工具表明没有任何图像被复制。Core Animation需要图像副本的一个原因是字节对齐不当。
3截至iOS 7,苹果不再将其硬件JPEG解码器提供给第三方应用程序使用。因此,这个步骤仅使用较慢的软件解码器。
Fast Image Cache通过各种技术最大限度地减少(或完全避免)上述许多工作
快速图像缓存的原理在于图像表。图像表类似于精灵表,常用于2D游戏。图像表将相同维度的图像合并为单个文件。此文件将一次性打开,并保持打开状态,以便在应用程序存储在内存中时读取和写入。
图像表使用mmap
系统调用将文件数据直接映射到内存。不发生memcpy
。该系统调用仅创建磁盘上的数据与内存区域之间的映射。
当请求图像缓存返回特定图像时,图像表会在常数时间内找到它在维护的文件中所需图像数据的位置。该文件数据区域被映射到内存中,并创建一个新的CGImageRef
,其支持存储是映射的文件数据。
当返回的CGImageRef
(包装成UIImage
)准备好绘制到屏幕上时,iOS的虚拟内存系统在实际上传文件数据。这是使用映射内存的另一个好处;VM系统将自动为我们处理内存管理。此外,映射内存“不计入”应用程序的实际内存使用。
类似地,当图像数据存储在图像表中时,创建了一个内存映射位图上下文。除了原始图像外,此上下文还传递给图像表对应的实体对象。该对象负责将图像绘制到当前上下文中,可选地进一步配置上下文(例如,将上下文剪切为圆角矩形)或进行任何其他绘图(例如,在原始图像上绘制覆盖图像)。mmap
将绘制的图像数据发送到磁盘,因此不在内存中分配图像缓存。
为了避免昂贵的图像解压缩操作,图像表将其文件中存储未压缩的图像数据。如果源图像被压缩,它必须首先解压缩,以便图像表能够与它一起工作。 这是一个一次性成本。 此外,还可以利用图像格式家族来执行此解压缩,使类似图像格式的集合仅执行一次。
然而,这种做法有明显的后果。未压缩的图像数据需要更多的磁盘空间,压缩和未压缩文件大小之间的差异可能很大,尤其是在JPEG等图像格式中。因此,快速图像缓存最适合小图像,尽管没有API限制强制执行这一点。
为了实现高性能的滚动,Core Animation能够在不首先创建副本的情况下使用图像是非常重要的。Core Animation创建图像副本的原因之一是图像的基本CGImageRef
字节对齐不正确。每行字节的正确对齐值必须是8像素 × 每像素字节数
的倍数。对于典型的ARGB图像,对齐的字节每行值是64的倍数。每个图像表都配置为从开始就让每个图像都与Core Animation对齐。因此,当从图像表中检索图像时,它们已经是Core Animation可以直接处理的形式,无需创建副本。
图像表是通过图像格式配置的,它指定(包括其他内容)图像表可以拥有的最大条目数(即单个图像)。这是为了避免图像表文件的大小无限制增长。
图像表每像素分配4字节,因此图像表文件可能占用的最大空间可以这样计算
每像素4字节 × 像素宽 × 像素高 × 最大条目数
使用快速图像缓存的程序应仔细考虑每个图像表应包含多少图像。当将新图像存储在已满的图像表中时,它将替换最少最近访问的图像。
图像表文件存储在用户的缓存目录下的ImageTables
子目录中。iOS可以在任何时候删除缓存文件以释放磁盘空间,因此使用快速图像缓存的应用程序必须能够重新创建任何存储的图像,并且不应依赖于图像表文件永久存在。
注意:作为提醒,存储在用户缓存目录中的数据不会备份到iTunes或iCloud。
快速图像缓存不会持久化创建存储在图像表中的图像数据的实体处理的原始源图像。
例如,如果实体将原始图像调整大小以创建存储在图像表中的缩略图,那么是应用程序的责任去要么持久化原始图像,要么能够重新检索或重新创建它。
可以通过指定图像格式系列来有效地利用单个源图像。有关更多信息,请参见使用图像格式系列。
在iOS 4中,苹果引入了数据保护功能。当用户的设备被锁定或关闭时,磁盘被加密。默认情况下写入磁盘的文件是受到保护的,尽管应用程序可以手动指定它所管理的每个文件的数据保护模式。随着iOS 7中新的后台模式的引入,即使在设备锁定的情况下,应用程序现在也可以在后台短暂运行。因此,如果应用程序尝试访问加密的文件,数据保护可能会引起问题。
快速图像缓存允许每个图像格式指定创建其备份图像表文件时所使用的数据保护模式。请注意,为图像表文件启用数据保护意味着当磁盘加密时,快速图像缓存可能无法从或向这些文件读取或写入图像数据。
快速图像缓存需要iOS 6.0或更高版本,并依赖于以下框架:
注意:从版本1.1开始,快速图像缓存确实使用了自动引用计数(ARC)。
FastImageCacheDemo
Xcode项目需要Xcode 5.0或更高版本,并配置为针对iOS 6.0进行部署。
FastImageCache
根目录,将内部FastImageCache
子目录的源文件复制到您的Xcode项目中。FICImageCache.h
。FICEntity
的类,导入FICEntity.h
。在可以使用图像缓存之前,它需要被配置。这必须在每次启动时进行,因此应用程序代理可能是一个做这件事的好地方。
每个图像格式对应一个图像缓存将使用的图像表。可以使用相同源图像渲染其在图像表中存储的图像的图像格式应该属于同一个图像格式系列。有关如何确定适当的最大计数的信息,请参见图像表大小。
static NSString *XXImageFormatNameUserThumbnailSmall = @"com.mycompany.myapp.XXImageFormatNameUserThumbnailSmall";
static NSString *XXImageFormatNameUserThumbnailMedium = @"com.mycompany.myapp.XXImageFormatNameUserThumbnailMedium";
static NSString *XXImageFormatFamilyUserThumbnails = @"com.mycompany.myapp.XXImageFormatFamilyUserThumbnails";
FICImageFormat *smallUserThumbnailImageFormat = [[FICImageFormat alloc] init];
smallUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailSmall;
smallUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;
smallUserThumbnailImageFormat.style = FICImageFormatStyle16BitBGR;
smallUserThumbnailImageFormat.imageSize = CGSizeMake(50, 50);
smallUserThumbnailImageFormat.maximumCount = 250;
smallUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;
smallUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;
FICImageFormat *mediumUserThumbnailImageFormat = [[FICImageFormat alloc] init];
mediumUserThumbnailImageFormat.name = XXImageFormatNameUserThumbnailMedium;
mediumUserThumbnailImageFormat.family = XXImageFormatFamilyUserThumbnails;
mediumUserThumbnailImageFormat.style = FICImageFormatStyle32BitBGRA;
mediumUserThumbnailImageFormat.imageSize = CGSizeMake(100, 100);
mediumUserThumbnailImageFormat.maximumCount = 250;
mediumUserThumbnailImageFormat.devices = FICImageFormatDevicePhone;
mediumUserThumbnailImageFormat.protectionMode = FICImageFormatProtectionModeNone;
NSArray *imageFormats = @[smallUserThumbnailImageFormat, mediumUserThumbnailImageFormat];
图像格式的样式基本上决定了图像表中存储的图像的位深度。以下样式目前可用:
如果源图像没有透明度(例如,JPEG图像),则可以通过使用无alpha成分的32位颜色来实现更好的Core Animation性能。如果源图像颜色细节较少,或者图像格式中图像的大小相对较小,则可能足以使用16位颜色,这样可以几乎没有质量损失。这结果是在磁盘上存储更小的图像表文件。
一旦定义了一个或多个图像格式,它们需要分配给图像缓存。除分配图像缓存的代理之外,图像缓存本身不能再进行其他配置。
FICImageCache *sharedImageCache = [FICImageCache sharedImageCache];
sharedImageCache.delegate = self;
sharedImageCache.formats = imageFormats;
实体是符合FICEntity
协议的对象。实体唯一标识图像表中的条目,并负责在图像缓存中绘制它们希望存储的图像。有已经定义了模型对象的应用程序(可能是由Core Data管理的)通常是合适的实体候选者。
@interface XXUser : NSObject <FICEntity>
@property (nonatomic, assign, getter = isActive) BOOL active;
@property (nonatomic, copy) NSString *userID;
@property (nonatomic, copy) NSURL *userPhotoURL;
@end
以下是对FICEntity
协议的一个示例实现。
- (NSString *)UUID {
CFUUIDBytes UUIDBytes = FICUUIDBytesFromMD5HashOfString(_userID);
NSString *UUID = FICStringWithUUIDBytes(UUIDBytes);
return UUID;
}
- (NSString *)sourceImageUUID {
CFUUIDBytes sourceImageUUIDBytes = FICUUIDBytesFromMD5HashOfString([_userPhotoURL absoluteString]);
NSString *sourceImageUUID = FICStringWithUUIDBytes(sourceImageUUIDBytes);
return sourceImageUUID;
}
- (NSURL *)sourceImageURLWithFormatName:(NSString *)formatName {
return _sourceImageURL;
}
- (FICEntityImageDrawingBlock)drawingBlockForImage:(UIImage *)image withFormatName:(NSString *)formatName {
FICEntityImageDrawingBlock drawingBlock = ^(CGContextRef context, CGSize contextSize) {
CGRect contextBounds = CGRectZero;
contextBounds.size = contextSize;
CGContextClearRect(context, contextBounds);
// Clip medium thumbnails so they have rounded corners
if ([formatName isEqualToString:XXImageFormatNameUserThumbnailMedium]) {
UIBezierPath clippingPath = [self _clippingPath];
[clippingPath addClip];
}
UIGraphicsPushContext(context);
[image drawInRect:contextBounds];
UIGraphicsPopContext();
};
return drawingBlock;
}
理想情况下,实体的UUID
不应该改变。这就是为什么当应用程序在处理从API获取的资源时,它很好地对应于模型对象的服务器生成的ID。
实体的sourceImageUUID
可以改变。例如,如果用户更新了他们的个人资料照片,该照片的URL也应当改变。虽然UUID
保持不变并标识同一用户,但更改后的个人资料照片URL将表明有一个新的源图像。
注意:通常,最好将用于定义
UUID
和sourceImageUUID
的标识符进行哈希处理。快速图像缓存提供了一些工具来做这个。由于哈希可能很昂贵,建议只计算一次(或者在标识符改变时)并将哈希值存储在实例变量中。
当请求图像缓存为特定实体和格式名称提供图像时,实体负责提供URL。该URL不一定指向实际资源——例如,URL可能由一个自定义URL-scheme构建——,但它必须是一个有效的URL。
图像缓存仅使用这些URL来跟踪哪些图像请求正在运行;对于相同图像的多个请求都会被正确处理,而不会浪费任何努力。选择使用URL作为基于键控图像缓存请求的基础,实际上与许多实际应用设计相辅相成,在这些设计中,服务器提供的模型数据中包含了图像资源的URL而不是图像本身。
注意:快速图像缓存不提供任何进行网络请求的机制。这是图像缓存代理的责任。
最终,一旦源图像可用,将要求实体提供绘图块。将存储最终图像的图像表设置一个文件映射的位图上下文并调用实体的绘图块。这使得每个实体能够决定如何为特定图像格式处理源图像。
快速图像缓存在按需、延迟加载的模式下工作,这是Cocoa中常见的。
XXUser *user = [self _currentUser];
NSString *formatName = XXImageFormatNameUserThumbnailSmall;
FICImageCacheCompletionBlock completionBlock = ^(id <FICEntity> entity, NSString *formatName, UIImage *image) {
_imageView.image = image;
[_imageView.layer addAnimation:[CATransition animation] forKey:kCATransition];
};
BOOL imageExists = [sharedImageCache retrieveImageForEntity:user withFormatName:formatName completionBlock:completionBlock];
if (imageExists == NO) {
_imageView.image = [self _userPlaceholderImage];
}
有几件事情需要注意。
UIImage
。请求的图像包含在完成块中。返回值将指示图像是否已经在图像缓存中。-retrieveImageForEntity:withFormatName:completionBlock:
是一个同步方法。如果请求的图像已经存在于图像缓存中,则立即调用完成块。与此方法对应的异步方法是 -asynchronouslyRetrieveImageForEntity:withFormatName:completionBlock:
。注意:同步和异步的区别仅适用于从图像缓存中检索已存在的图像的过程。在这种情况下,如果同步请求尚未在图像缓存中存在的图像,图像缓存并不会阻止调用线程直到它有图像。检索方法将立即返回
NO
,并且将在稍后调用完成块。请参阅
FICImageCache
类头文件,以详细了解图像检索的执行生命周期,特别是与处理完成块相关的内容。
有两种方法向图像缓存提供源图像。
按需:这是首选方法。图像缓存的代理负责向图像缓存提供源图像。
- (void)imageCache:(FICImageCache *)imageCache wantsSourceImageForEntity:(id<FICEntity>)entity withFormatName:(NSString *)formatName completionBlock:(FICImageRequestCompletionBlock)completionBlock {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Fetch the desired source image by making a network request
NSURL *requestURL = [entity sourceImageURLWithFormatName:formatName];
UIImage *sourceImage = [self _sourceImageForURL:requestURL];
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock(sourceImage);
});
});
}
这是基于URL的图像请求管理的方便之处。首先,图像缓存对由图像缓存的代理正在处理的图像(例如,下载大图片)的图像检索请求只是添加到第一个请求的完成块数组中。其次,如果源图像是从互联网下载的(这是常见情况),此类网络请求的URL是可用的。
注意:必须在主线程上调用完成块。Fast Image Cache 架构保证这个调用不会阻塞主线程,因为源图像的处理在图像缓存自己的串行派发队列中进行。
手动:可以手动将图像数据插入到图像缓存中。
// Just finished downloading new user photo
XXUser *user = [self _currentUser];
NSString *formatName = XXImageFormatNameUserThumbnailSmall;
FICImageCacheCompletionBlock completionBlock = ^(id <FICEntity> entity, NSString *formatName, UIImage *image) {
NSLog(@"Processed and stored image for entity: %@", entity);
};
[sharedImageCache setImage:newUserPhoto forEntity:user withFormatName:formatName completionBlock:completionBlock];
注意:Fast Image Cache 不持久化源图像。有关更多信息,请参阅 源图像持久化。
如果图像请求已经正在进行中,它可以被取消
// We scrolled up far enough that the image we requested in no longer visible; cancel the request
XXUser *user = [self _currentUser];
NSString *formatName = XXImageFormatNameUserThumbnailSmall;
[sharedImageCache cancelImageRetrievalForEntity:user withFormatName:formatName];
当发生这种情况时,Fast Image Cache 清理其内部记账,与此对应的图像请求的任何完成块在此点将不再执行。然而,图像缓存的代理仍然负责确保取消任何挂起的源图像请求(例如,网络请求)。
- (void)imageCache:(FICImageCache *)imageCache cancelImageLoadingForEntity:(id <FICEntity>)entity withFormatName:(NSString *)formatName {
[self _cancelNetworkRequestForSourceImageForEntity:entity withFormatName:formatName];
}
将图像格式分类到一系列中的优势在于,当该系列中的任一图像格式被处理时,图像缓存的代理可以告诉图像缓存处理该系列中的所有图像格式的源图像。默认情况下,所有图像格式都会被给定系列处理,除非你实现了此代理并返回其他结果。
- (BOOL)imageCache:(FICImageCache *)imageCache shouldProcessAllFormatsInFamily:(NSString *)formatFamily forEntity:(id<FICEntity>)entity {
BOOL shouldProcessAllFormats = NO;
if ([formatFamily isEqualToString:XXImageFormatFamilyUserThumbnails]) {
XXUser *user = (XXUser *)entity;
shouldProcessAllFormats = user.active;
}
return shouldProcessAllFormats;
}
一次处理一类中的所有图像格式的优势在于,源图像不需要被反复下载(或加载到内存中,如果存储在磁盘上)。
例如,如果用户更改了他们的个人资料照片,那么在处理第一个图像格式的同时处理每个变体的新源图像可能是有意义的。也就是说,如果图像缓存正在处理名为 XXImageFormatNameUserThumbnailSmall
的图像格式的新的用户个人资料照片,那么也为同一用户处理和保存名为 XXImageFormatNameUserThumbnailMedium
的图像格式的新的图像数据也是有意义的。
Fast Image Cache 的头文件已全部文档化,可以使用 appledoc 生成各种形式的文档,包括 HTML 和 Xcode DocSet。
HTML 文档可以在这里找到:[点击访问](https://s3.amazonaws.com/fast-image-cache/documentation/index.html)。
该存储库中包含一个示例应用程序 Xcode 项目。它展示了传统加载和显示图像的方法与 Fast Image Cache 方法的不同之处。查看运行示例应用程序 Xcode 项目的需求。
注意:示例应用程序必须提供 JPEG 图像,或者必须运行
fetch_demo_images.sh
脚本,该脚本位于FastImageCacheDemo
目录中。
注意:在此示例视频中,第一种展示的方法是传统方法。第二种方法是使用图像表。
以下是从示例应用程序运行中测量的统计数据
方法 | 滚动性能 | 磁盘使用量 | RPRVT1 |
---|---|---|---|
传统 | ~35FPS |
568KB |
2.40MB : 1.06MB + 1.34MB |
Fast Image Cache | ~59FPS |
2.2MB |
1.15MB : 1.06MB + 0.09MB |
要点是 Fast Image Cache 以牺牲磁盘使用量为代价,以实现更快的帧率和更低的总体内存使用。
1 第一个值是一个方法显示一屏 JPEG 缩略图所使用的总 RPRVT。第二个值是基准 RPRVT,其中所有表格视图单元格和图像视图都显示在屏幕上,但没有任何图像视图设置图像。第三个值是每个方法额外使用的 RPRVT。
Mallory Paine — 作者和原始 API 设计
@mallorypaine
Michael Potter — 文档和 API 重构
@LucasTizma
Fast Image Cache在以下许可证下提供:MIT许可证
The MIT License (MIT) Copyright (c) 2013 Path, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.