Objective-C中的SDWebImage图片异步加载
一、SDWebImage简介
SDWebImage 是一个广泛应用于 iOS 开发中的图片加载框架,它为 Objective-C 开发者提供了便捷且强大的图片异步加载功能。在 iOS 应用开发中,图片的加载是一个常见且关键的操作,尤其在处理网络图片时,如果采用同步加载的方式,很可能会阻塞主线程,导致应用界面卡顿,严重影响用户体验。SDWebImage 很好地解决了这一问题,通过异步加载图片,确保主线程的流畅运行,让用户在浏览图片时感受到丝滑的体验。
SDWebImage 的主要特点包括:
- 异步加载:核心功能,能够在后台线程加载图片,不影响主线程的 UI 渲染和交互。
- 缓存机制:具备完善的内存和磁盘缓存策略。当图片第一次加载后,会被缓存到内存中,下次请求相同图片时,如果内存中有缓存,则直接从内存中获取,大大提高了加载速度。同时,它也会将图片缓存到磁盘,以便在应用重启后仍然能够快速加载已缓存过的图片。
- 支持多种图片格式:除了常见的 JPEG、PNG 格式外,还支持 GIF 等动态图片格式的加载和显示。
- 高度可定制:开发者可以根据自己的需求定制图片加载的各个环节,如自定义缓存策略、下载器等。
二、SDWebImage的架构与原理
(一)架构组成
- SDWebImageManager:这是 SDWebImage 的核心管理类,负责协调图片的下载、缓存以及加载等操作。它维护了一个操作队列(
NSOperationQueue
)来管理图片下载任务,同时与缓存系统进行交互。当我们调用SDWebImage
的方法请求加载一张图片时,实际上是SDWebImageManager
在幕后进行调度和管理。 - SDImageCache:缓存管理类,负责处理图片的内存缓存和磁盘缓存。内存缓存使用
NSCache
来实现,NSCache
是一种自动释放内存的缓存机制,当系统内存不足时,会自动释放缓存中的对象,避免内存泄漏。磁盘缓存则通过文件系统来存储图片,使用NSFileManager
进行文件的读写操作。 - SDWebImageDownloader:图片下载器,负责从网络或本地资源中下载图片。它基于
NSURLSession
进行网络请求,支持 HTTP/HTTPS 协议。在下载过程中,它会处理请求的发送、接收以及错误处理等操作。 - UIImageView+WebCache:这是
UIImageView
的一个分类,为UIImageView
增加了图片异步加载的功能。通过这个分类,我们可以直接在UIImageView
上调用方法来加载图片,而无需手动管理下载和缓存逻辑。
(二)工作原理
- 加载流程
- 当我们调用
UIImageView
的sd_setImageWithURL:
等方法时,首先会进入UIImageView+WebCache
分类中的方法。 - 该方法会将图片的 URL 传递给
SDWebImageManager
。SDWebImageManager
会先检查内存缓存中是否有该图片。如果内存缓存中有,则直接返回图片并显示在UIImageView
上。 - 如果内存缓存中没有,
SDWebImageManager
会检查磁盘缓存。如果磁盘缓存中有,则从磁盘中读取图片,将其加入内存缓存,并返回图片显示在UIImageView
上。 - 如果内存和磁盘缓存中都没有,
SDWebImageManager
会创建一个下载任务,通过SDWebImageDownloader
从网络上下载图片。下载完成后,将图片存储到内存和磁盘缓存中,并显示在UIImageView
上。
- 当我们调用
- 缓存策略
- 内存缓存:
SDImageCache
使用NSCache
作为内存缓存。当图片下载完成后,会先将图片存入内存缓存。在获取图片时,优先从内存缓存中查找。由于NSCache
的特性,当系统内存不足时,会自动释放缓存中的对象,确保应用的内存使用在合理范围内。 - 磁盘缓存:磁盘缓存使用文件系统存储图片。
SDImageCache
会根据图片的 URL 生成一个唯一的文件名,将图片以文件的形式存储在指定的目录下。在检查磁盘缓存时,会根据 URL 对应的文件名查找文件,如果文件存在,则读取文件内容作为图片。磁盘缓存的优点是即使应用关闭后再次启动,仍然可以快速加载已缓存的图片。
- 内存缓存:
三、在Objective - C项目中集成SDWebImage
(一)使用CocoaPods集成
- 安装CocoaPods:如果你的开发环境还没有安装 CocoaPods,需要先安装。打开终端,执行以下命令:
sudo gem install cocoapods
安装过程中可能需要输入电脑的密码。安装完成后,可以通过 pod --version
命令检查安装是否成功。
2. 创建Podfile:在你的 Xcode 项目目录下,打开终端,执行 pod init
命令,这会在项目目录下生成一个 Podfile
文件。
3. 编辑Podfile:打开生成的 Podfile
文件,添加以下内容:
platform :ios, '9.0'
target 'YourTargetName' do
pod 'SDWebImage', '~> 5.14.1'
end
请将 YourTargetName
替换为你项目的实际 target 名称。~> 5.14.1
表示安装版本号大于等于 5.14.1 且小于 5.15.0 的 SDWebImage
版本。你可以根据实际需求调整版本号。
4. 安装依赖:在终端中,进入项目目录,执行 pod install
命令。CocoaPods 会根据 Podfile
的配置下载并安装 SDWebImage
及其依赖库。安装完成后,会生成一个 .xcworkspace
文件。之后打开项目时,需要使用这个 .xcworkspace
文件,而不是原来的 .xcodeproj
文件。
(二)手动集成
- 下载SDWebImage:从 SDWebImage 的官方 GitHub 仓库(https://github.com/SDWebImage/SDWebImage)下载最新的源代码。
- 添加文件到项目:解压下载的文件,将
SDWebImage
文件夹中的所有文件添加到你的 Xcode 项目中。在 Xcode 中,选择项目导航栏,右键点击项目名称,选择Add Files to "YourProjectName"
,然后选择SDWebImage
文件夹中的所有文件并添加。 - 配置项目设置:
- 添加框架依赖:在项目的
Build Phases
->Link Binary With Libraries
中,添加UIKit.framework
、Foundation.framework
、ImageIO.framework
、CoreGraphics.framework
、MobileCoreServices.framework
等必要的框架。这些框架是SDWebImage
正常运行所依赖的。 - 设置编译选项:在项目的
Build Settings
->Other Linker Flags
中,添加-ObjC
选项。这是为了确保项目能够正确链接SDWebImage
中的静态库。
- 添加框架依赖:在项目的
四、SDWebImage的基本使用
(一)UIImageView加载图片
在 UIImageView
上加载图片是 SDWebImage
最常见的用法。通过 UIImageView+WebCache
分类,我们可以很方便地实现这一功能。
#import <SDWebImage/UIImageView+WebCache.h>
// 假设我们有一个UIImageView
@property (nonatomic, strong) UIImageView *imageView;
// 在视图控制器的某个方法中加载图片
- (void)loadImage {
NSURL *imageURL = [NSURL URLWithString:@"https://example.com/image.jpg"];
[self.imageView sd_setImageWithURL:imageURL];
}
在上述代码中,我们创建了一个 NSURL
对象表示图片的地址,然后通过 sd_setImageWithURL:
方法在 UIImageView
上异步加载图片。SDWebImage
会自动处理图片的下载、缓存以及显示。
(二)设置占位图
在图片下载完成之前,我们可以设置一个占位图来显示,给用户一个视觉提示。
#import <SDWebImage/UIImageView+WebCache.h>
// 假设我们有一个UIImageView
@property (nonatomic, strong) UIImageView *imageView;
// 在视图控制器的某个方法中加载图片并设置占位图
- (void)loadImageWithPlaceholder {
NSURL *imageURL = [NSURL URLWithString:@"https://example.com/image.jpg"];
UIImage *placeholderImage = [UIImage imageNamed:@"placeholder"];
[self.imageView sd_setImageWithURL:imageURL placeholderImage:placeholderImage];
}
这里我们通过 sd_setImageWithURL:placeholderImage:
方法设置了一个占位图。当图片开始下载时,会先显示占位图,直到图片下载完成并显示。
(三)加载完成回调
有时候我们需要在图片加载完成后执行一些额外的操作,比如更新 UI、记录日志等。SDWebImage
提供了加载完成回调的功能。
#import <SDWebImage/UIImageView+WebCache.h>
// 假设我们有一个UIImageView
@property (nonatomic, strong) UIImageView *imageView;
// 在视图控制器的某个方法中加载图片并设置回调
- (void)loadImageWithCompletion {
NSURL *imageURL = [NSURL URLWithString:@"https://example.com/image.jpg"];
[self.imageView sd_setImageWithURL:imageURL completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (image) {
NSLog(@"图片加载成功,缓存类型:%zd", cacheType);
} else {
NSLog(@"图片加载失败,错误:%@", error);
}
}];
}
在上述代码的回调块中,我们可以根据 image
是否为空判断图片是否加载成功,同时可以获取图片的缓存类型 cacheType
。如果加载失败,error
会包含详细的错误信息。
五、SDWebImage的高级用法
(一)自定义缓存策略
虽然 SDWebImage
自带了一套默认的缓存策略,但在某些情况下,我们可能需要自定义缓存逻辑。比如,我们可能希望设置不同的缓存有效期,或者根据图片的类型选择不同的缓存方式。
- 自定义内存缓存:我们可以继承
SDImageCache
类,并重写其内存缓存相关的方法。
#import "SDImageCache.h"
@interface CustomImageCache : SDImageCache
@end
@implementation CustomImageCache
// 重写保存图片到内存缓存的方法
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toMemory:(BOOL)toMemory {
// 这里可以添加自定义的内存缓存逻辑,例如设置缓存有效期
NSTimeInterval expirationTime = 60 * 60; // 1小时
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:expirationTime];
NSMutableDictionary *imageInfo = [NSMutableDictionary dictionary];
[imageInfo setObject:image forKey:@"image"];
[imageInfo setObject:expirationDate forKey:@"expirationDate"];
[self.memCache setObject:imageInfo forKey:key];
}
// 重写从内存缓存获取图片的方法
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
NSMutableDictionary *imageInfo = [self.memCache objectForKey:key];
if (imageInfo) {
NSDate *expirationDate = [imageInfo objectForKey:@"expirationDate"];
if ([expirationDate compare:[NSDate date]] == NSOrderedDescending) {
return [imageInfo objectForKey:@"image"];
} else {
// 缓存已过期,从内存缓存中移除
[self.memCache removeObjectForKey:key];
}
}
return nil;
}
@end
- 自定义磁盘缓存:同样可以通过继承
SDImageCache
类来重写磁盘缓存相关的方法。
#import "SDImageCache.h"
@interface CustomDiskImageCache : SDImageCache
@end
@implementation CustomDiskImageCache
// 重写保存图片到磁盘缓存的方法
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (toDisk) {
// 获取磁盘缓存路径
NSString *cachePath = [self defaultCachePathForKey:key];
NSData *imageData = UIImagePNGRepresentation(image);
// 这里可以添加自定义的磁盘缓存逻辑,例如对文件名进行加密
NSString *encryptedKey = [self encryptKey:key];
NSString *encryptedCachePath = [self defaultCachePathForKey:encryptedKey];
[imageData writeToFile:encryptedCachePath atomically:YES];
}
}
// 重写从磁盘缓存获取图片的方法
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
// 获取磁盘缓存路径
NSString *cachePath = [self defaultCachePathForKey:key];
// 如果文件名加密了,需要先解密
NSString *encryptedKey = [self encryptKey:key];
NSString *encryptedCachePath = [self defaultCachePathForKey:encryptedKey];
NSData *imageData = [NSData dataWithContentsOfFile:encryptedCachePath];
if (imageData) {
return [UIImage imageWithData:imageData];
}
return nil;
}
// 简单的加密方法示例
- (NSString *)encryptKey:(NSString *)key {
// 这里可以实现更复杂的加密逻辑,例如使用AES加密
return [key stringByAppendingString:@"encryptedSuffix"];
}
@end
- 使用自定义缓存:在项目中使用自定义缓存时,需要在
SDWebImageManager
中设置自定义的SDImageCache
实例。
#import "CustomImageCache.h"
#import "CustomDiskImageCache.h"
#import <SDWebImage/SDWebImageManager.h>
// 在应用启动时设置自定义缓存
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
CustomImageCache *customMemCache = [[CustomImageCache alloc] init];
CustomDiskImageCache *customDiskCache = [[CustomDiskImageCache alloc] init];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
manager.imageCache = customDiskCache;
return YES;
}
(二)自定义下载器
在某些特殊场景下,我们可能需要自定义图片的下载逻辑,比如使用自定义的网络请求库,或者在下载过程中添加额外的请求头。
- 创建自定义下载器:继承
SDWebImageDownloader
类,并重写相关方法。
#import <SDWebImage/SDWebImageDownloader.h>
@interface CustomDownloader : SDWebImageDownloader
@end
@implementation CustomDownloader
// 重写开始下载的方法
- (NSURLSessionDataTask *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 添加自定义请求头
[request addValue:@"CustomValue" forHTTPHeaderField:@"CustomHeader"];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
completedBlock(nil, error, NO);
} else {
UIImage *image = [UIImage imageWithData:data];
completedBlock(image, nil, YES);
}
}];
[task resume];
return task;
}
@end
- 使用自定义下载器:在
SDWebImageManager
中设置自定义的下载器实例。
#import "CustomDownloader.h"
#import <SDWebImage/SDWebImageManager.h>
// 在应用启动时设置自定义下载器
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
CustomDownloader *customDownloader = [[CustomDownloader alloc] init];
SDWebImageManager *manager = [SDWebImageManager sharedManager];
manager.imageDownloader = customDownloader;
return YES;
}
(三)加载动态图片(GIF)
SDWebImage
对 GIF 等动态图片的加载和显示提供了很好的支持。
#import <SDWebImage/UIImageView+WebCache.h>
// 假设我们有一个UIImageView
@property (nonatomic, strong) UIImageView *imageView;
// 在视图控制器的某个方法中加载GIF图片
- (void)loadGIFImage {
NSURL *gifURL = [NSURL URLWithString:@"https://example.com/animation.gif"];
[self.imageView sd_setImageWithURL:gifURL];
}
通过上述代码,SDWebImage
会自动识别图片为 GIF 格式,并在 UIImageView
上显示动态效果。如果需要对 GIF 图片的显示进行更多控制,比如设置播放次数、播放速度等,可以使用 FLAnimatedImage
类(SDWebImage
对 FLAnimatedImage
有集成支持)。
#import <SDWebImage/SDWebImage.h>
#import <FLAnimatedImage/FLAnimatedImage.h>
// 假设我们有一个UIImageView
@property (nonatomic, strong) UIImageView *imageView;
// 在视图控制器的某个方法中加载GIF图片并设置播放次数
- (void)loadGIFImageWithControls {
NSURL *gifURL = [NSURL URLWithString:@"https://example.com/animation.gif"];
[SDWebImageDownloader.sharedDownloader downloadImageWithURL:gifURL options:0 progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
if (image && finished) {
FLAnimatedImage *animatedImage = [FLAnimatedImage animatedImageWithGIFData:data];
animatedImage.loopCount = 3; // 设置播放次数为3次
FLAnimatedImageView *animatedImageView = (FLAnimatedImageView *)self.imageView;
animatedImageView.animatedImage = animatedImage;
}
}];
}
在上述代码中,我们通过 SDWebImageDownloader
下载 GIF 图片的数据,然后使用 FLAnimatedImage
将数据转换为动态图片,并设置了播放次数。最后将 FLAnimatedImage
设置到 FLAnimatedImageView
上显示(UIImageView
可以转换为 FLAnimatedImageView
来支持动态图片显示)。
六、SDWebImage的性能优化与注意事项
(一)性能优化
- 合理设置缓存大小:虽然
SDWebImage
有默认的缓存策略,但我们可以根据应用的实际需求调整缓存大小。对于内存缓存,可以通过SDImageCache
的maxMemoryCost
和maxCacheItems
属性来设置最大内存占用和最大缓存图片数量。对于磁盘缓存,可以通过maxDiskSize
和maxDiskAge
属性来设置最大磁盘占用和缓存有效期。
SDImageCache *cache = [SDImageCache sharedImageCache];
cache.maxMemoryCost = 1024 * 1024 * 64; // 设置最大内存缓存为64MB
cache.maxCacheItems = 100; // 设置最大缓存图片数量为100张
cache.maxDiskSize = 1024 * 1024 * 200; // 设置最大磁盘缓存为200MB
cache.maxDiskAge = 60 * 60 * 24 * 7; // 设置缓存有效期为7天
- 避免重复下载:在列表视图等场景中,可能会频繁出现图片的加载。如果不加以处理,可能会导致同一张图片被多次下载。
SDWebImage
本身已经有一定的缓存机制来避免这种情况,但我们可以进一步优化。比如,在列表滚动时,暂停当前不可见 cell 上的图片下载任务,等 cell 滚动到可见区域时再继续加载。
@interface MyTableViewController : UITableViewController <UIScrollViewDelegate>
@property (nonatomic, strong) NSMutableDictionary<NSIndexPath *, NSURLSessionDataTask *> *downloadTasks;
@end
@implementation MyTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.downloadTasks = [NSMutableDictionary dictionary];
self.tableView.delegate = self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
NSURL *imageURL = [NSURL URLWithString:@"https://example.com/image\(indexPath.row).jpg"];
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
NSURLSessionDataTask *task = [downloader downloadImageWithURL:imageURL options:0 progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) {
if (image && finished) {
UIImageView *imageView = (UIImageView *)[cell viewWithTag:100];
imageView.image = image;
}
}];
self.downloadTasks[indexPath] = task;
return cell;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSArray *visibleIndexPaths = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *indexPath in self.downloadTasks.allKeys) {
if (![visibleIndexPaths containsObject:indexPath]) {
NSURLSessionDataTask *task = self.downloadTasks[indexPath];
[task cancel];
}
}
}
@end
在上述代码中,我们在 tableView:cellForRowAtIndexPath:
方法中记录每个 cell 的图片下载任务,在 scrollViewDidScroll:
方法中检查当前 cell 是否可见,如果不可见则取消下载任务,避免不必要的网络请求。
(二)注意事项
- 内存管理:虽然
SDWebImage
有自动的内存缓存管理,但在处理大量图片或复杂图片(如高清图片)时,仍需注意内存使用情况。确保在图片不再使用时,能够及时从内存缓存中移除,避免内存泄漏。可以通过SDImageCache
的removeImageForKey:fromDisk:completion:
等方法手动移除缓存中的图片。 - 网络请求:在使用
SDWebImage
进行网络图片加载时,要注意网络请求的错误处理。下载过程中可能会遇到网络连接失败、服务器响应错误等情况,需要在加载完成回调中妥善处理这些错误,给用户提供友好的提示。 - 版本兼容性:随着
SDWebImage
的不断更新,不同版本可能会有一些 API 变化或新特性。在升级SDWebImage
版本时,要仔细阅读官方文档,确保项目中的代码能够与新版本兼容,避免出现编译错误或运行时异常。
通过以上对 SDWebImage
在 Objective - C 中的详细介绍,包括其架构原理、集成方法、基本和高级用法、性能优化以及注意事项,相信开发者能够在 iOS 项目中更好地运用 SDWebImage
实现高效、流畅的图片异步加载功能,提升应用的用户体验。