MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中的SDWebImage图片异步加载

2023-10-256.9k 阅读

一、SDWebImage简介

SDWebImage 是一个广泛应用于 iOS 开发中的图片加载框架,它为 Objective-C 开发者提供了便捷且强大的图片异步加载功能。在 iOS 应用开发中,图片的加载是一个常见且关键的操作,尤其在处理网络图片时,如果采用同步加载的方式,很可能会阻塞主线程,导致应用界面卡顿,严重影响用户体验。SDWebImage 很好地解决了这一问题,通过异步加载图片,确保主线程的流畅运行,让用户在浏览图片时感受到丝滑的体验。

SDWebImage 的主要特点包括:

  1. 异步加载:核心功能,能够在后台线程加载图片,不影响主线程的 UI 渲染和交互。
  2. 缓存机制:具备完善的内存和磁盘缓存策略。当图片第一次加载后,会被缓存到内存中,下次请求相同图片时,如果内存中有缓存,则直接从内存中获取,大大提高了加载速度。同时,它也会将图片缓存到磁盘,以便在应用重启后仍然能够快速加载已缓存过的图片。
  3. 支持多种图片格式:除了常见的 JPEG、PNG 格式外,还支持 GIF 等动态图片格式的加载和显示。
  4. 高度可定制:开发者可以根据自己的需求定制图片加载的各个环节,如自定义缓存策略、下载器等。

二、SDWebImage的架构与原理

(一)架构组成

  1. SDWebImageManager:这是 SDWebImage 的核心管理类,负责协调图片的下载、缓存以及加载等操作。它维护了一个操作队列(NSOperationQueue)来管理图片下载任务,同时与缓存系统进行交互。当我们调用 SDWebImage 的方法请求加载一张图片时,实际上是 SDWebImageManager 在幕后进行调度和管理。
  2. SDImageCache:缓存管理类,负责处理图片的内存缓存和磁盘缓存。内存缓存使用 NSCache 来实现,NSCache 是一种自动释放内存的缓存机制,当系统内存不足时,会自动释放缓存中的对象,避免内存泄漏。磁盘缓存则通过文件系统来存储图片,使用 NSFileManager 进行文件的读写操作。
  3. SDWebImageDownloader:图片下载器,负责从网络或本地资源中下载图片。它基于 NSURLSession 进行网络请求,支持 HTTP/HTTPS 协议。在下载过程中,它会处理请求的发送、接收以及错误处理等操作。
  4. UIImageView+WebCache:这是 UIImageView 的一个分类,为 UIImageView 增加了图片异步加载的功能。通过这个分类,我们可以直接在 UIImageView 上调用方法来加载图片,而无需手动管理下载和缓存逻辑。

(二)工作原理

  1. 加载流程
    • 当我们调用 UIImageViewsd_setImageWithURL: 等方法时,首先会进入 UIImageView+WebCache 分类中的方法。
    • 该方法会将图片的 URL 传递给 SDWebImageManagerSDWebImageManager 会先检查内存缓存中是否有该图片。如果内存缓存中有,则直接返回图片并显示在 UIImageView 上。
    • 如果内存缓存中没有,SDWebImageManager 会检查磁盘缓存。如果磁盘缓存中有,则从磁盘中读取图片,将其加入内存缓存,并返回图片显示在 UIImageView 上。
    • 如果内存和磁盘缓存中都没有,SDWebImageManager 会创建一个下载任务,通过 SDWebImageDownloader 从网络上下载图片。下载完成后,将图片存储到内存和磁盘缓存中,并显示在 UIImageView 上。
  2. 缓存策略
    • 内存缓存SDImageCache 使用 NSCache 作为内存缓存。当图片下载完成后,会先将图片存入内存缓存。在获取图片时,优先从内存缓存中查找。由于 NSCache 的特性,当系统内存不足时,会自动释放缓存中的对象,确保应用的内存使用在合理范围内。
    • 磁盘缓存:磁盘缓存使用文件系统存储图片。SDImageCache 会根据图片的 URL 生成一个唯一的文件名,将图片以文件的形式存储在指定的目录下。在检查磁盘缓存时,会根据 URL 对应的文件名查找文件,如果文件存在,则读取文件内容作为图片。磁盘缓存的优点是即使应用关闭后再次启动,仍然可以快速加载已缓存的图片。

三、在Objective - C项目中集成SDWebImage

(一)使用CocoaPods集成

  1. 安装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 文件。

(二)手动集成

  1. 下载SDWebImage:从 SDWebImage 的官方 GitHub 仓库(https://github.com/SDWebImage/SDWebImage)下载最新的源代码。
  2. 添加文件到项目:解压下载的文件,将 SDWebImage 文件夹中的所有文件添加到你的 Xcode 项目中。在 Xcode 中,选择项目导航栏,右键点击项目名称,选择 Add Files to "YourProjectName",然后选择 SDWebImage 文件夹中的所有文件并添加。
  3. 配置项目设置
    • 添加框架依赖:在项目的 Build Phases -> Link Binary With Libraries 中,添加 UIKit.frameworkFoundation.frameworkImageIO.frameworkCoreGraphics.frameworkMobileCoreServices.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 自带了一套默认的缓存策略,但在某些情况下,我们可能需要自定义缓存逻辑。比如,我们可能希望设置不同的缓存有效期,或者根据图片的类型选择不同的缓存方式。

  1. 自定义内存缓存:我们可以继承 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
  1. 自定义磁盘缓存:同样可以通过继承 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
  1. 使用自定义缓存:在项目中使用自定义缓存时,需要在 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;
}

(二)自定义下载器

在某些特殊场景下,我们可能需要自定义图片的下载逻辑,比如使用自定义的网络请求库,或者在下载过程中添加额外的请求头。

  1. 创建自定义下载器:继承 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
  1. 使用自定义下载器:在 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 类(SDWebImageFLAnimatedImage 有集成支持)。

#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的性能优化与注意事项

(一)性能优化

  1. 合理设置缓存大小:虽然 SDWebImage 有默认的缓存策略,但我们可以根据应用的实际需求调整缓存大小。对于内存缓存,可以通过 SDImageCachemaxMemoryCostmaxCacheItems 属性来设置最大内存占用和最大缓存图片数量。对于磁盘缓存,可以通过 maxDiskSizemaxDiskAge 属性来设置最大磁盘占用和缓存有效期。
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天
  1. 避免重复下载:在列表视图等场景中,可能会频繁出现图片的加载。如果不加以处理,可能会导致同一张图片被多次下载。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 是否可见,如果不可见则取消下载任务,避免不必要的网络请求。

(二)注意事项

  1. 内存管理:虽然 SDWebImage 有自动的内存缓存管理,但在处理大量图片或复杂图片(如高清图片)时,仍需注意内存使用情况。确保在图片不再使用时,能够及时从内存缓存中移除,避免内存泄漏。可以通过 SDImageCacheremoveImageForKey:fromDisk:completion: 等方法手动移除缓存中的图片。
  2. 网络请求:在使用 SDWebImage 进行网络图片加载时,要注意网络请求的错误处理。下载过程中可能会遇到网络连接失败、服务器响应错误等情况,需要在加载完成回调中妥善处理这些错误,给用户提供友好的提示。
  3. 版本兼容性:随着 SDWebImage 的不断更新,不同版本可能会有一些 API 变化或新特性。在升级 SDWebImage 版本时,要仔细阅读官方文档,确保项目中的代码能够与新版本兼容,避免出现编译错误或运行时异常。

通过以上对 SDWebImage 在 Objective - C 中的详细介绍,包括其架构原理、集成方法、基本和高级用法、性能优化以及注意事项,相信开发者能够在 iOS 项目中更好地运用 SDWebImage 实现高效、流畅的图片异步加载功能,提升应用的用户体验。