1. SDwebImage 内部实现原理步骤
1.1 实现步骤
入口 setImageWithUrl:placeHolderImage:options: 会把 placeHolderImage 显示, 然后 SDWebImageManager 根据 URL 开始处理图片.
进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo: 交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:
先从内存图片缓存查找是否有图片, 如果内存中已经有图片缓存, SDImageCacheDelegate 回调 imageCache:didFineImage:forKey:userInfo: 到 SDWebImageManager.
SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView + WebCache 等前端展示图片.
如果内存缓存中没有, 生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存
根据 URLKey 在硬盘缓存目录下尝试读取图片文件. 这一步是在 NSOperation 进行的操作, 所以回主线程进行结果回调 notifyDelegate.
如果上一操作从硬盘读取到了图片, 将图片添加到内存缓存中 (如果空闲内存过小 会先清空内存缓存).SDImageCacheDelegate 回调 imageCache:didFinishImage:forKey:userInfo: 进而回调展示图片.
如果从硬盘缓存目录读取不到图片, 说明所有缓存都不存在该图片, 需要下载图片, 回调 imageCache:didNotFindImageForKey:userInfo.
共享或重新生成一个下载器 SDWebImageDownLoader 开始下载图片
图片下载由 NSURLConnection 来做, 实现相关 delegate 来判断图片下载中, 下载完成和下载失败
connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果
connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理
图片解码处理在一个 NSOperationQueue 完成, 不会拖慢主线程 UI. 如果有需要对下载的图片进行二次处理, 最好也在这里完成, 效率会好很多.
在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成 imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader
imageDownLoader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成
通知所有的 downloadDelegates 下载完成, 回调给需要的地方展示图片
将图片保存到 SDImageCache 中内存缓存和硬盘缓存同时保存, 写文件到硬盘也在以单独 NSInvocationOperation 完成, 避免拖慢主线程
SDImageCache 在初始化的时候会注册一些消息通知, 在内存警告或退到后台的时候清理内存图片缓存, 应用结束的时候清理过期图片
SDWI 也提供 UIButton + WebCache 和 MKAnnptation + WebCache 方便使用
SDWebImagePrefetcher 可以预先下载图片, 方便后续使用
再用一张图说明:
1.2 API 中参数枚举类型
例如, 设置图片的两个例子
- [self.image2 sd_setImageWithURL:imagePath2 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
- NSLog(@"这里可以在图片加载完成之后做些事情");
- }];
- // 使用默认图片, 而且用 block 在完成后做一些事情
- [self.image1 sd_setImageWithURL:imagePath1 placeholderImage:[UIImage imageNamed:@"default"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
- NSLog(@"图片加载完成后做的事情");
- }];
还有获取下载进度的例子
- // 使用默认图片, 而且用 block 在完成后做一些事情
- [self.image1 sd_setImageWithURL:imagePath1 placeholderImage:[UIImage imageNamed:@"default"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
- NSLog(@"图片加载完成后做的事情");
- }];
上面的例子, 都有个 SDImageCacheTyp 的参数, 你记得这个参数有哪些?
- SDImageCacheType
- // 定义 Cache 类型
- typedef NS_ENUM(NSInteger, SDImageCacheType) {
- // 不使用 cache 获得图片, 依然会从 web 下载图片
- SDImageCacheTypeNone,
- // 图片从 disk 获得
- SDImageCacheTypeDisk,
- // 图片从 Memory 中获得
- SDImageCacheTypeMemory
- };
2. 最大缓存和时间设置
SDImageCache 类的源码
- // 这个变量默认值为 YES, 显示比较高质量的图片, 但是会浪费比较多的内存, 可以通过设置 NO 来缓解内存
- @property (assign, nonatomic) BOOL shouldDecompressImages;
- // 总共的内存允许图片的消耗值
- @property (assign, nonatomic) NSUInteger maxMemoryCost;
- // 图片存活于内存的时间初始化的时候默认为一周
- @property (assign, nonatomic) NSInteger maxCacheAge;
- // 每次存储图片大小的限制
- @property (assign, nonatomic) NSUInteger maxCacheSize;
设置 maxCacheSize 的例子
- SDWebImageManager *manager = [SDWebImageManager sharedManager];
- [manager.imageCache setMaxMemoryCost:1000000];// 设置总缓存大小, 默认为 0 没有限制
- [manager.imageCache setMaxCacheSize:640000];// 设置单个图片限制大小
- [manager.imageDownloader setMaxConcurrentDownloads:1];// 设置同时下载线程数, 这是下载器的内容, 下面将会介绍
- [manager downloadImageWithURL:[NSURL URLWithString:@"http://p9.qhimg.com/t01eb74a44c2eb43193.jpg"]
- options:SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize) {
- NSLog(@"%lu", receivedSize);
- } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
- self.imageView1.image = image;
- }];
- [manager downloadImageWithURL:[NSURL URLWithString:@"http://img.article.pchome.net/00/28/33/87/pic_lib/wm/kuanpin12.jpg"]
- options:SDWebImageProgressiveDownload progress:^(NSInteger receivedSize, NSInteger expectedSize) {
- NSLog(@"%lu", receivedSize);
- } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
- self.imageView2.image = image;
- }];
- NSUInteger size = [manager.imageCache getSize];
- NSUInteger count = [manager.imageCache getDiskCount];
- NSLog(@"size = %lu", size); // 644621(两张测试图片)
- NSLog(@"count = %lu", count); // 2
- [manager.imageCache clearDisk];
- size = [manager.imageCache getSize];
- count = [manager.imageCache getDiskCount];
- NSLog(@"sizeClean = %lu", size); // 0
- NSLog(@"countClean = %lu", count); // 0 这里使用的是 clear
3. 区分: 三种种缓存
先查看内存图片缓存, 内存图片缓存没有, 后生成操作, 查看磁盘图片缓存
磁盘图片缓存有, 就加载到内存缓存, 没有就下载图片
在建立下载操作之前, 判断下载操作是否存在
默认情况下, 下载的图片数据会同时缓存到内存和磁盘中
关于缓存位置
内存缓存是通过 NSCache 的子类 AutoPurgeCache 来实现的;
磁盘缓存是通过 NSFileManager 来实现文件的存储 (默认路径为 / Library/Caches/default/com.hackemist.SDWebImageCache.default), 是异步实现的.
关于图片下载操作
SDWebImage 的大部分工作是由缓存对象 SDImageCache 和异步下载器管理对象 SDWebImageManager 来完成的.
SDWebImage 的图片下载是由 SDWebImageDownloader 这个类来实现的, 它是一个异步下载管理器, 下载过程中增加了对图片加载做了优化的处理. 而真正实现图片下载的是自定义的一个 Operation 操作, 将该操作加入到下载管理器的操作队列 downloadQueue 中, Operation 操作依赖系统提供的 NSURLConnection 类实现图片的下载.
4. 高清和低清图片与网络环境的问题
网络判断的问题 -- 利用 AFNetworking 的 API 首先, 启用监控
- // AppDelegate.m 文件中
- - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
- {
- // 监控网络状态
- [[AFNetworkReachabilityManager sharedManager] startMonitoring];
- }
然后, 在需要的地方获取监控管理
- // 以下代码在需要监听网络状态的方法中使用
- AFNetworkReachabilityManager *mgr = [AFNetworkReachabilityManager sharedManager];
- if (mgr.isReachableViaWiFi) { // 在使用 Wifi, 下载原图
- } else { // 其他, 下载小图
- }
并不能简单的这样: WIFI 就下载高清图, 蜂窝网络就下载缩略图. 要考虑和利用缓存的因素.
一个典型例子
- - setItem:(CustomItem *)item
- {
- _item = item;
- // 占位图片
- UIImage *placeholder = [UIImage imageNamed:@"placeholderImage"];
- // 从内存 \ 沙盒缓存中获得原图,
- UIImage *originalImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:item.originalImage];
- if (originalImage) { // 如果内存 \ 沙盒缓存有原图, 那么就直接显示原图 (不管现在是什么网络状态)
- self.imageView.image = originalImage;
- } else { // 内存 \ 沙盒缓存没有原图
- AFNetworkReachabilityManager *mgr = [AFNetworkReachabilityManager sharedManager];
- if (mgr.isReachableViaWiFi) { // 在使用 Wifi, 下载原图
- [self.imageView sd_setImageWithURL:[NSURL URLWithString:item.originalImage] placeholderImage:placeholder];
- } else if (mgr.isReachableViaWWAN) { // 在使用手机自带网络
- // 用户的配置项假设利用 NSUserDefaults 存储到了沙盒中
- // [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"alwaysDownloadOriginalImage"];
- // [[NSUserDefaults standardUserDefaults] synchronize];
- #warning 从沙盒中读取用户的配置项: 在 3G\4G 环境是否仍然下载原图
- BOOL alwaysDownloadOriginalImage = [[NSUserDefaults standardUserDefaults] boolForKey:@"alwaysDownloadOriginalImage"];
- if (alwaysDownloadOriginalImage) { // 下载原图
- [self.imageView sd_setImageWithURL:[NSURL URLWithString:item.originalImage] placeholderImage:placeholder];
- } else { // 下载小图
- [self.imageView sd_setImageWithURL:[NSURL URLWithString:item.thumbnailImage] placeholderImage:placeholder];
- }
- } else { // 没有网络
- UIImage *thumbnailImage = [[SDImageCache sharedImageCache] imageFromDiskCacheForKey:item.thumbnailImage];
- if (thumbnailImage) { // 内存 \ 沙盒缓存中有小图
- self.imageView.image = thumbnailImage;
- } else { // 处理离线状态, 而且有没有缓存时的情况
- self.imageView.image = placeholder;
- }
- }
- }
- }
5. 可能的问题
后台没有处理的高清大图导致 APP 内存过大而奔溃
思路, 改写 sd_imageWithData 方法的源代码, 可参考 https://blog.csdn.net/benyoulai5/article/details/50462586
来源: https://juejin.im/post/5b2d2343e51d4558e360174e