简介(摘至官网 )
YYWebImage 是一个异步图片加载框架 (YYKit 组件之一).
其设计目的是试图替代 SDWebImage、PINRemoteImage、FLAnimatedImage 等开源框架,它支持这些开源框架的大部分功能,同时增加了大量新特性、并且有不小的性能提升。
它底层用 YYCache 实现了内存和磁盘缓存, 用 YYImage 实现了 WebP/APNG/GIF 动图的解码和播放。
你可以查看这些项目以获得更多信息。
特性
- 异步的图片加载,支持 HTTP 和本地文件。
- 支持 GIF、APNG、WebP 动画(动态缓存,低内存占用)。
- 支持逐行扫描、隔行扫描、渐进式图像加载。
- UIImageView、UIButton、MKAnnotationView、CALayer 的 Category 方法支持。
- 常见图片处理:模糊、圆角、大小调整、裁切、旋转、色调等。
- 高性能的内存和磁盘缓存。
- 高性能的图片设置方式,以避免主线程阻塞。
- 每个类和方法都有完善的文档注释。
基本用法
从 URL 加载图片
// 加载网络图片
imageView.yy_imageURL = [NSURL URLWithString:@"http://github.com/logo.png"];
// 加载本地图片
imageView.yy_imageURL = [NSURL fileURLWithPath:@"/tmp/logo.png"];
加载动图
// 只需要把 `UIImageView` 替换为 `YYAnimatedImageView` 即可。
UIImageView *imageView = [YYAnimatedImageView new];
imageView.yy_imageURL = [NSURL URLWithString:@"http://github.com/ani.webp"];
渐进式图片加载
// 渐进式:边下载边显示
[imageView yy_setImageWithURL:url options:YYWebImageOptionProgressive];
// 渐进式加载,增加模糊效果和渐变动画 (见本页最上方的GIF演示)
[imageView yy_setImageWithURL:url options:YYWebImageOptionProgressiveBlur | YYWebImageOptionSetImageWithFadeAnimation];
加载、处理图片
// 1. 下载图片
// 2. 获得图片下载进度
// 3. 调整图片大小、加圆角
// 4. 显示图片时增加一个淡入动画,以获得更好的用户体验
[imageView yy_setImageWithURL:url
placeholder:nil
options:YYWebImageOptionSetImageWithFadeAnimation
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
progress = (float)receivedSize / expectedSize;
}
transform:^UIImage *(UIImage *image, NSURL *url) {
image = [image yy_imageByResizeToSize:CGSizeMake(100, 100) contentMode:UIViewContentModeCenter];
return [image yy_imageByRoundCornerRadius:10];
}
completion:^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
if (from == YYWebImageFromDiskCache) {
NSLog(@"load from disk cache");
}
}];
图片缓存
YYImageCache *cache = [YYWebImageManager sharedManager].cache;
// 获取缓存大小
cache.memoryCache.totalCost;
cache.memoryCache.totalCount;
cache.diskCache.totalCost;
cache.diskCache.totalCount;
// 清空缓存
[cache.memoryCache removeAllObjects];
[cache.diskCache removeAllObjects];
// 清空磁盘缓存,带进度回调
[cache.diskCache removeAllObjectsWithProgressBlock:^(int removedCount, int totalCount) {
// progress
} endBlock:^(BOOL error) {
// end
}];
YYWebImage 架构结构
源码介绍
图片缓存 YYImageCache
YYImageCache 是 YYWebImage 的一个缓存实现类,底层依赖 YYCache 与 YYImage。采用缓存的时候提供了四种方式进行选择,分别是 YYImageCacheTypeNone(无缓存)、YYImageCacheTypeMemory(内存缓存)、YYImageCacheTypeDisk(采用磁盘缓存)、YYImageCacheTypeAll(采用内存+磁盘缓存)。
取缓存图片
源码
// 获取缓存图片
- (UIImage *)getImageForKey:(NSString *)key withType:(YYImageCacheType)type {
// 容错处理
if (!key) return nil;
// 去内存缓存中查找图片
if (type & YYImageCacheTypeMemory) {
UIImage *image = [_memoryCache objectForKey:key];
if (image) return image;
}
// 去磁盘缓存中查找图片
if (type & YYImageCacheTypeDisk) {
// 查找出来的是一个 二进制数据,需要把二进制数据转化为 图片对象返回
NSData *data = (id)[_diskCache objectForKey:key];
UIImage *image = [self imageFromData:data];
if (image && (type & YYImageCacheTypeMemory)) {
[_memoryCache setObject:image forKey:key withCost:[self imageCost:image]];
}
return image;
}
return nil;
}
- 如果 type 的值为 YYImageCacheTypeMemory 则去内存缓存中查找,查找就返回结果。如果没有查找到就进入下面的磁盘缓存判断。
- 如果 type 的值为 YYImageCacheTypeDisk 则去磁盘缓存中查找,如果查找道理,判断是否同时采用了 内存缓存,如果采用了内存缓存,则把图片缓存一个备份到内存中去。然后返回图片结果
- 如果 type 的值为 YYImageCacheTypeNone 则返回 nil
存图片
源码
// 缓存一张图片
- (void)setImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key withType:(YYImageCacheType)type {
// 如果图片对象和图片的二进制数据同时为空时,则表示存入失败
if (!key || (image == nil && imageData.length == 0)) return;
// 弱化本身,避免循环应用问题
__weak typeof(self) _self = self;
// 假如采用了图片内存缓存,则走入判断条件内部
if (type & YYImageCacheTypeMemory) { // add to memory cache
// 当图片对象不为空,走入
if (image) {
// 为一张动态图,也就是 YYImage 系列的解码的图片
if (image.yy_isDecodedForDisplay) {
// 同步存入,应为图片是已经解码的了
[_memoryCache setObject:image forKey:key withCost:[_self imageCost:image]];
//
} else {
// 把图片解码之后在存入内存缓存中
dispatch_async(YYImageCacheDecodeQueue(), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self.memoryCache setObject:[image yy_imageByDecoded] forKey:key withCost:[self imageCost:image]];
});
}
// 如果是 图片 二进制数据,把 二进制转为 位图数据再存入内存缓存中,这个过程使用的是异步操作
} else if (imageData) {
dispatch_async(YYImageCacheDecodeQueue(), ^{
__strong typeof(_self) self = _self;
if (!self) return;
UIImage *newImage = [self imageFromData:imageData];
[self.memoryCache setObject:newImage forKey:key withCost:[self imageCost:newImage]];
});
}
}
// 如果采用了磁盘缓存,则走入条件内部
if (type & YYImageCacheTypeDisk) { // add to disk cache
// 如果图片二进制数据存在,转换为图片扩展数据
if (imageData) {
// 判断图片是否存在
if (image) {
[YYDiskCache setExtendedData:[NSKeyedArchiver archivedDataWithRootObject:@(image.scale)] toObject:imageData];
}
// 存入磁盘中
[_diskCache setObject:imageData forKey:key];
//
} else if (image) {
// 将图片转换为 二进制数据,然后存入磁盘中
dispatch_async(YYImageCacheIOQueue(), ^{
__strong typeof(_self) self = _self;
if (!self) return;
NSData *data = [image yy_imageDataRepresentation];
[YYDiskCache setExtendedData:[NSKeyedArchiver archivedDataWithRootObject:@(image.scale)] toObject:data];
[self.diskCache setObject:data forKey:key];
});
}
}
}
- 如果图片缓存方式 YYImageCacheTypeMemory ,则存如内存中
- 如果图片缓存方式 YYImageCacheTypeDisk ,则存入磁盘中
- 如果图片缓存方式 YYImageCacheTypeNone ,不做任何操作
删除缓存
源码
// 移除
- (void)removeImageForKey:(NSString *)key withType:(YYImageCacheType)type {
// 从内存中移除缓存
if (type & YYImageCacheTypeMemory) [_memoryCache removeObjectForKey:key];
// 从磁盘中移除缓存
if (type & YYImageCacheTypeDisk) [_diskCache removeObjectForKey:key];
}
图片下载 YYWebImageOperation
YYWebImageOperation 继承自 ,每一个图片的下载都是一个 YYWebImageOperation 任务对象。对象包含一个 NSURLRequest 请求属性、一个 NSURLResponse 响应属性、一个 YYImageCache 缓存属性、一个 NSString 类型的缓存key、一个 NSURLCredential 认证属性。
初始化方法为:
/**
Creates and returns a new operation.
You should call `start` to execute this operation, or you can add the operation
to an operation queue.
@param request The Image request. This value should not be nil.
@param options A mask to specify options to use for this operation.
@param cache An image cache. Pass nil to avoid image cache.
@param cacheKey An image cache key. Pass nil to avoid image cache.
@param progress A block invoked in image fetch progress.
The block will be invoked in background thread. Pass nil to avoid it.
@param transform A block invoked before image fetch finished to do additional image process.
The block will be invoked in background thread. Pass nil to avoid it.
@param completion A block invoked when image fetch finished or cancelled.
The block will be invoked in background thread. Pass nil to avoid it.
@return The image request opeartion, or nil if an error occurs.
*/
// 实例化一个 请求任务对象
- (instancetype)initWithRequest:(NSURLRequest *)request
options:(YYWebImageOptions)options
cache:(nullable YYImageCache *)cache
cacheKey:(nullable NSString *)cacheKey
progress:(nullable YYWebImageProgressBlock)progress
transform:(nullable YYWebImageTransformBlock)transform
completion:(nullable YYWebImageCompletionBlock)completion NS_DESIGNATED_INITIALIZER;
内部私有属性包括
@interface YYWebImageOperation()
// 是否正在执行任务
@property (readwrite, getter=isExecuting) BOOL executing;
// 是否完成
@property (readwrite, getter=isFinished) BOOL finished;
// 是否取消
@property (readwrite, getter=isCancelled) BOOL cancelled;
// 是否开始
@property (readwrite, getter=isStarted) BOOL started;
// 递归锁
@property (nonatomic, strong) NSRecursiveLock *lock;
// 请求任务
@property (nonatomic, strong) NSURLConnection *connection;
// 接收数据对象
@property (nonatomic, strong) NSMutableData *data;
// 预料内容大小
@property (nonatomic, assign) NSInteger expectedSize;
// 任务ID
@property (nonatomic, assign) UIBackgroundTaskIdentifier taskID;
// 最后时间戳
@property (nonatomic, assign) NSTimeInterval lastProgressiveDecodeTimestamp;
// 图片解码对象
@property (nonatomic, strong) YYImageDecoder *progressiveDecoder;
// 是否解码
@property (nonatomic, assign) BOOL progressiveIgnored;
//
@property (nonatomic, assign) BOOL progressiveDetected;
// 已解码的长度
@property (nonatomic, assign) NSUInteger progressiveScanedLength;
//
@property (nonatomic, assign) NSUInteger progressiveDisplayCount;
// 下载进度
@property (nonatomic, copy) YYWebImageProgressBlock progress;
// 图片变化
@property (nonatomic, copy) YYWebImageTransformBlock transform;
// 图片下载结束
@property (nonatomic, copy) YYWebImageCompletionBlock completion;
@end
NSURLConnection
YYWebImageOperation 使用的是 NSURLConnection 类进行图片下载,在内部一个单例子线程进行图片下载,下载完成然后进行图片进行回调。NSURLConnection 的代理对象为 NSProxy 的子类 对象,通过消息转发给 YYWebImageOperation
源码
// 开始请求任务
- (void)_startRequest:(id)object {
// 判断是否已经取消了任务
if ([self isCancelled]) return;
// 进入自动释放池
@autoreleasepool {
// 容错处理,如果参数存在错误,则结束任务
if ((_options & YYWebImageOptionIgnoreFailedURL) && URLBlackListContains(_request.URL)) {
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:@{ NSLocalizedDescriptionKey : @"Failed to load URL, blacklisted." }];
[_lock lock];
if (![self isCancelled]) {
if (_completion) _completion(nil, _request.URL, YYWebImageFromNone, YYWebImageStageFinished, error);
}
[self _finish];
[_lock unlock];
return;
}
// 如果请求的url 是一个文件路径,做一步转化
if (_request.URL.isFileURL) {
NSArray *keys = @[NSURLFileSizeKey];
NSDictionary *attr = [_request.URL resourceValuesForKeys:keys error:nil];
NSNumber *fileSize = attr[NSURLFileSizeKey];
_expectedSize = (fileSize != nil) ? fileSize.unsignedIntegerValue : -1;
}
// request image from web
[_lock lock];
// 判断是否已经取消了任务
if (![self isCancelled]) {
// 初始化一个下载任务,并把接收数据的对象指想 self。
_connection = [[NSURLConnection alloc] initWithRequest:_request delegate:[_YYWebImageWeakProxy proxyWithTarget:self]];
if (![_request.URL isFileURL] && (_options & YYWebImageOptionShowNetworkActivity)) {
// 开始加载动画
[YYWebImageManager incrementNetworkActivityCount];
}
}
[_lock unlock];
}
}
认证信息
// NSURLConnectionDelegate 的代理方法
// 用户认证
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection {
return _shouldUseCredentialStorage;
}
// 将要发送请求,需要的认证信息
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
@autoreleasepool {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if (!(_options & YYWebImageOptionAllowInvalidSSLCertificates) &&
[challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) {
[challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
} else {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
}
} else {
if ([challenge previousFailureCount] == 0) {
if (_credential) {
[[challenge sender] useCredential:_credential forAuthenticationChallenge:challenge];
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
}
}
下载进度
这里需要从收到服务端响应开始
// 接收到服务端的响应
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
@autoreleasepool {
NSError *error = nil;
// 类型判断
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
// 类型转换
NSHTTPURLResponse *httpResponse = (id) response;
NSInteger statusCode = httpResponse.statusCode;
// 判断状态吗,大于400 说明是请求错误,304 表示包体内部不包含实体数据
if (statusCode >= 400 || statusCode == 304) {
error = [NSError errorWithDomain:NSURLErrorDomain code:statusCode userInfo:nil];
}
}
// 错误对象存在,说明请求失败
if (error) {
// 取消链接
[_connection cancel];
// 抛出错误
[self connection:_connection didFailWithError:error];
} else {
// 判断内容的长度
if (response.expectedContentLength) {
// 预期大小
_expectedSize = (NSInteger)response.expectedContentLength;
if (_expectedSize < 0) _expectedSize = -1;
}
// 初始化一个预期大小的 可变 Data 对象
_data = [NSMutableData dataWithCapacity:_expectedSize > 0 ? _expectedSize : 0];
// 回调进度
if (_progress) {
[_lock lock];
if (![self isCancelled]) _progress(0, _expectedSize);
[_lock unlock];
}
}
}
}
// 陆续接收服务端返回的数据
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
@autoreleasepool {
[_lock lock];
BOOL canceled = [self isCancelled];
[_lock unlock];
// 判断是否取消了下载任务
if (canceled) return;
// 拼接数据
if (data) [_data appendData:data];
// 回调进度
if (_progress) {
[_lock lock];
if (![self isCancelled]) {
_progress(_data.length, _expectedSize);
}
[_lock unlock];
}
/*--------------------------- progressive ----------------------------*/
BOOL progressive = (_options & YYWebImageOptionProgressive) > 0;
BOOL progressiveBlur = (_options & YYWebImageOptionProgressiveBlur) > 0;
if (!_completion || !(progressive || progressiveBlur)) return;
if (data.length <= 16) return;
// 如果下载图片的进度 大于 99% 就结束方法
if (_expectedSize > 0 && data.length >= _expectedSize * 0.99) return;
// 是否提前解码
if (_progressiveIgnored) return;
NSTimeInterval min = progressiveBlur ? MIN_PROGRESSIVE_BLUR_TIME_INTERVAL : MIN_PROGRESSIVE_TIME_INTERVAL;
// 获得当前时间
NSTimeInterval now = CACurrentMediaTime();
// 获得最后一次刷新时间长
if (now - _lastProgressiveDecodeTimestamp < min) return;
// 创建解码器。没有就创建一个图片解码器
if (!_progressiveDecoder) {
_progressiveDecoder = [[YYImageDecoder alloc] initWithScale:[UIScreen mainScreen].scale];
}
// 更新解码器中的数据内容
[_progressiveDecoder updateData:_data final:NO];
if ([self isCancelled]) return;
// 判断图片的格式,如果为 YYImageTypeUnknown、YYImageTypeWebP、YYImageTypeOther中的其中一个,则不用解码
if (_progressiveDecoder.type == YYImageTypeUnknown ||
_progressiveDecoder.type == YYImageTypeWebP ||
_progressiveDecoder.type == YYImageTypeOther) {
_progressiveDecoder = nil;
_progressiveIgnored = YES;
return;
}
// 如果是非 JPEG 或者 PNG 格式的图片,则取消提前解码
if (progressiveBlur) { // only support progressive JPEG and interlaced PNG
if (_progressiveDecoder.type != YYImageTypeJPEG &&
_progressiveDecoder.type != YYImageTypePNG) {
_progressiveDecoder = nil;
_progressiveIgnored = YES;
return;
}
}
// 帧数为 0 则结束方法
if (_progressiveDecoder.frameCount == 0) return;
//
if (!progressiveBlur) {
YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
if (frame.image) {
[_lock lock];
if (![self isCancelled]) {
_completion(frame.image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
_lastProgressiveDecodeTimestamp = now;
}
[_lock unlock];
}
return;
} else {
if (_progressiveDecoder.type == YYImageTypeJPEG) {
if (!_progressiveDetected) {
NSDictionary *dic = [_progressiveDecoder framePropertiesAtIndex:0];
NSDictionary *jpeg = dic[(id)kCGImagePropertyJFIFDictionary];
NSNumber *isProg = jpeg[(id)kCGImagePropertyJFIFIsProgressive];
if (!isProg.boolValue) {
_progressiveIgnored = YES;
_progressiveDecoder = nil;
return;
}
_progressiveDetected = YES;
}
NSInteger scanLength = (NSInteger)_data.length - (NSInteger)_progressiveScanedLength - 4;
if (scanLength <= 2) return;
NSRange scanRange = NSMakeRange(_progressiveScanedLength, scanLength);
NSRange markerRange = [_data rangeOfData:JPEGSOSMarker() options:kNilOptions range:scanRange];
_progressiveScanedLength = _data.length;
if (markerRange.location == NSNotFound) return;
if ([self isCancelled]) return;
} else if (_progressiveDecoder.type == YYImageTypePNG) {
if (!_progressiveDetected) {
NSDictionary *dic = [_progressiveDecoder framePropertiesAtIndex:0];
NSDictionary *png = dic[(id)kCGImagePropertyPNGDictionary];
NSNumber *isProg = png[(id)kCGImagePropertyPNGInterlaceType];
if (!isProg.boolValue) {
_progressiveIgnored = YES;
_progressiveDecoder = nil;
return;
}
_progressiveDetected = YES;
}
}
// 解码图片的第一帧
YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
UIImage *image = frame.image;
if (!image) return;
if ([self isCancelled]) return;
if (!YYCGImageLastPixelFilled(image.CGImage)) return;
_progressiveDisplayCount++;
CGFloat radius = 32;
if (_expectedSize > 0) {
radius *= 1.0 / (3 * _data.length / (CGFloat)_expectedSize + 0.6) - 0.25;
} else {
radius /= (_progressiveDisplayCount);
}
// 图片遮罩
image = [image yy_imageByBlurRadius:radius tintColor:nil tintMode:0 saturation:1 maskImage:nil];
if (image) {
[_lock lock];
if (![self isCancelled]) {
// 回调图片
_completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
// 更新最后一次解码时间
_lastProgressiveDecodeTimestamp = now;
}
[_lock unlock];
}
}
}
}
如果响应的状态码大于 400(发出请求的客户端存在错误)或者 304(服务端资源未改变,可以直接使用客户端为过期的缓存资源,不包含任何响应主体部分),结束请求,抛出请求错误。
connection: didReceiveData: 代理方法中进行实际的进度回调,前提是下载进度小于 99% 。
RunLoop(运行循环)
YYWebImage 在下载网络图片框架,同众多网络框架一样采用类 runloop ,取保子线程不会退出
源码
/// Network thread entry point.
// 在当前线程初始化一个 运行循环
+ (void)_networkThreadMain:(id)object {
@autoreleasepool {
// 设置当前线程的别名
[[NSThread currentThread] setName:@"com.ibireme.webimage.request"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 在运行循环中加入一个 port(端口),使得运行循环不会退出
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 开始跑运行循环
[runLoop run];
}
}
/// Global image request network thread, used by NSURLConnection delegate.
// 初始化一个单例子线程
+ (NSThread *)_networkThread {
static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 实例化一个线程
thread = [[NSThread alloc] initWithTarget:self selector:@selector(_networkThreadMain:) object:nil];
// qualityOfService 属性是 iOS 8.0 版本才新增的 API,所以这里需要做一个判断
if ([thread respondsToSelector:@selector(setQualityOfService:)]) {
// 线程启动后是只读的,设置线程的类型为后台服务线程
thread.qualityOfService = NSQualityOfServiceBackground;
}
[thread start];
});
return thread;
}
图片解码 YYImage(解码器 YYImageDecoder)
- 下载图片解码
网络图片的加载使用的是渐进式加载方式。源码中有这样一段:
// 创建解码器。没有就创建一个图片解码器
if (!_progressiveDecoder) {
_progressiveDecoder = [[YYImageDecoder alloc] initWithScale:[UIScreen mainScreen].scale];
}
// 更新解码器中的数据内容
[_progressiveDecoder updateData:_data final:NO];
if ([self isCancelled]) return;
// 判断图片的格式,如果为 YYImageTypeUnknown、YYImageTypeWebP、YYImageTypeOther中的其中一个,则不用解码
if (_progressiveDecoder.type == YYImageTypeUnknown ||
_progressiveDecoder.type == YYImageTypeWebP ||
_progressiveDecoder.type == YYImageTypeOther) {
_progressiveDecoder = nil;
_progressiveIgnored = YES;
return;
}
// 如果是非 JPEG 或者 PNG 格式的图片,则取消提前解码
if (progressiveBlur) { // only support progressive JPEG and interlaced PNG
if (_progressiveDecoder.type != YYImageTypeJPEG &&
_progressiveDecoder.type != YYImageTypePNG) {
_progressiveDecoder = nil;
_progressiveIgnored = YES;
return;
}
}
// 帧数为 0 则结束方法
if (_progressiveDecoder.frameCount == 0) return;
//
if (!progressiveBlur) {
YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
if (frame.image) {
[_lock lock];
if (![self isCancelled]) {
_completion(frame.image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
_lastProgressiveDecodeTimestamp = now;
}
[_lock unlock];
}
return;
} else {
if (_progressiveDecoder.type == YYImageTypeJPEG) {
if (!_progressiveDetected) {
NSDictionary *dic = [_progressiveDecoder framePropertiesAtIndex:0];
NSDictionary *jpeg = dic[(id)kCGImagePropertyJFIFDictionary];
NSNumber *isProg = jpeg[(id)kCGImagePropertyJFIFIsProgressive];
if (!isProg.boolValue) {
_progressiveIgnored = YES;
_progressiveDecoder = nil;
return;
}
_progressiveDetected = YES;
}
NSInteger scanLength = (NSInteger)_data.length - (NSInteger)_progressiveScanedLength - 4;
if (scanLength <= 2) return;
NSRange scanRange = NSMakeRange(_progressiveScanedLength, scanLength);
NSRange markerRange = [_data rangeOfData:JPEGSOSMarker() options:kNilOptions range:scanRange];
_progressiveScanedLength = _data.length;
if (markerRange.location == NSNotFound) return;
if ([self isCancelled]) return;
} else if (_progressiveDecoder.type == YYImageTypePNG) {
if (!_progressiveDetected) {
NSDictionary *dic = [_progressiveDecoder framePropertiesAtIndex:0];
NSDictionary *png = dic[(id)kCGImagePropertyPNGDictionary];
NSNumber *isProg = png[(id)kCGImagePropertyPNGInterlaceType];
if (!isProg.boolValue) {
_progressiveIgnored = YES;
_progressiveDecoder = nil;
return;
}
_progressiveDetected = YES;
}
}
// 解码图片的第一帧
YYImageFrame *frame = [_progressiveDecoder frameAtIndex:0 decodeForDisplay:YES];
UIImage *image = frame.image;
if (!image) return;
if ([self isCancelled]) return;
if (!YYCGImageLastPixelFilled(image.CGImage)) return;
_progressiveDisplayCount++;
CGFloat radius = 32;
if (_expectedSize > 0) {
radius *= 1.0 / (3 * _data.length / (CGFloat)_expectedSize + 0.6) - 0.25;
} else {
radius /= (_progressiveDisplayCount);
}
// 图片遮罩
image = [image yy_imageByBlurRadius:radius tintColor:nil tintMode:0 saturation:1 maskImage:nil];
if (image) {
[_lock lock];
if (![self isCancelled]) {
// 回调图片
_completion(image, _request.URL, YYWebImageFromRemote, YYWebImageStageProgress, nil);
// 更新最后一次解码时间
_lastProgressiveDecodeTimestamp = now;
}
[_lock unlock];
}
}
如果图片的帧数等于 0 则取消解码工作,如果图片帧数 大于 0 ,取出第一帧图片数据,并解码返回,如果图片的格式为 静态的 JPEG 或者 PNG格式,则使用 系统的方法进行解码,解码完成进行返回。解码器(YYImageDecoder)
- 异步解码
在图片下载完成的 connectionDidFinishLoading: 方法中进行了对图片的整体解码。这个解码过程是在内建的队列池中完成。
解码源码
// 下载图片完成
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// 进入自动释放池
@autoreleasepool {
[_lock lock];
_connection = nil;
// 判断是否取消了任务
if (![self isCancelled]) {
__weak typeof(self) _self = self;
// 异步解码图片
dispatch_async([self.class _imageQueue], ^{
__strong typeof(_self) self = _self;
if (!self) return;
// 解码图片
BOOL shouldDecode = (self.options & YYWebImageOptionIgnoreImageDecoding) == 0;
BOOL allowAnimation = (self.options & YYWebImageOptionIgnoreAnimatedImage) == 0;
UIImage *image;
BOOL hasAnimation = NO;
if (allowAnimation) {
image = [[YYImage alloc] initWithData:self.data scale:[UIScreen mainScreen].scale];
if (shouldDecode) image = [image yy_imageByDecoded];
if ([((YYImage *)image) animatedImageFrameCount] > 1) {
hasAnimation = YES;
}
} else {
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:self.data scale:[UIScreen mainScreen].scale];
image = [decoder frameAtIndex:0 decodeForDisplay:shouldDecode].image;
}
/*
If the image has animation, save the original image data to disk cache.
If the image is not PNG or JPEG, re-encode the image to PNG or JPEG for
better decoding performance.
*/
YYImageType imageType = YYImageDetectType((__bridge CFDataRef)self.data);
switch (imageType) {
case YYImageTypeJPEG:
case YYImageTypeGIF:
case YYImageTypePNG:
case YYImageTypeWebP: { // save to disk cache
if (!hasAnimation) {
if (imageType == YYImageTypeGIF ||
imageType == YYImageTypeWebP) {
self.data = nil; // clear the data, re-encode for disk cache
}
}
} break;
default: {
self.data = nil; // clear the data, re-encode for disk cache
} break;
}
// 判断是否取消了任务
if ([self isCancelled]) return;
// 处理图片
if (self.transform && image) {
UIImage *newImage = self.transform(image, self.request.URL);
if (newImage != image) {
self.data = nil;
}
image = newImage;
if ([self isCancelled]) return;
}
// 图片解码完成,回到自定线程把图片回传回去
[self performSelector:@selector(_didReceiveImageFromWeb:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];
});
// 更新下载任务个数
if (![self.request.URL isFileURL] && (self.options & YYWebImageOptionShowNetworkActivity)) {
[YYWebImageManager decrementNetworkActivityCount];
}
}
[_lock unlock];
}
}
队列池源码
/// Global image queue, used for image reading and decoding.
// 获取全局图片队列
+ (dispatch_queue_t)_imageQueue {
// 同时最多只能有 16 个队列
#define MAX_QUEUE_COUNT 16
static int queueCount;
// 定义一个 长度为 16 的队列数组
static dispatch_queue_t queues[MAX_QUEUE_COUNT];
static dispatch_once_t onceToken;
static int32_t counter = 0;
dispatch_once(&onceToken, ^{
// CPU 个数/ 多核 CPU
queueCount = (int)[NSProcessInfo processInfo].activeProcessorCount;
// 如果小于 1 则赋值为 1,如果大于 16 则赋值为 16,否则值不变
queueCount = queueCount < 1 ? 1 : queueCount > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : queueCount;
// 创建 queueCount 个串行队列,也就是说这里会创建 queueCount 个线程,每一个 串行队列会开辟一个新的线程
if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
for (NSUInteger i = 0; i < queueCount; i++) {
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
queues[i] = dispatch_queue_create("com.ibireme.image.decode", attr);
}
} else {
for (NSUInteger i = 0; i < queueCount; i++) {
queues[i] = dispatch_queue_create("com.ibireme.image.decode", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queues[i], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0));
}
}
});
// 系统原子级别操作,使 counter 自增 1; 并赋值给 画cur
int32_t cur = OSAtomicIncrement32(&counter);
if (cur < 0) cur = -cur;
// 通过对 queueCount 取余拿到对应的队列
return queues[(cur) % queueCount];
#undef MAX_QUEUE_COUNT
}
解码完成后跳转到开启了 runloop 的全局线程中进行返回
// 图片解码完成,回到自定线程把图片回传回去
[self performSelector:@selector(_didReceiveImageFromWeb:) onThread:[self.class _networkThread] withObject:image waitUntilDone:NO];
多线程安全
缓存图片线程安全
缓存图片的本身底层 YYCache 是线程安全。YYCache 内部使用的信号量(dispatch_semaphore_t)来完成
下载图片线程安全
下载拖的线程安全,通过递归锁(NSRecursiveLock)来保证
上层接口
YYWebImageManager
YYWebImageManager 是为 UIKit 开放给接口的类。
// 网络图片管理类
@interface YYWebImageManager : NSObject
/**
Returns global YYWebImageManager instance.
@return YYWebImageManager shared instance.
*/
// 单例对象
+ (instancetype)sharedManager;
/**
Creates a manager with an image cache and operation queue.
@param cache Image cache used by manager (pass nil to avoid image cache).
@param queue The operation queue on which image operations are scheduled and run
(pass nil to make the new operation start immediately without queue).
@return A new manager.
*/
// 缓存对象
- (instancetype)initWithCache:(nullable YYImageCache *)cache
queue:(nullable NSOperationQueue *)queue NS_DESIGNATED_INITIALIZER;
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
/**
Creates and returns a new image operation, the operation will start immediately.
@param url The image url (remote or local file path).
@param options The options to control image operation.
@param progress Progress block which will be invoked on background thread (pass nil to avoid).
@param transform Transform block which will be invoked on background thread (pass nil to avoid).
@param completion Completion block which will be invoked on background thread (pass nil to avoid).
@return A new image operation.
*/
// 获取一张网络图片
- (nullable YYWebImageOperation *)requestImageWithURL:(NSURL *)url
options:(YYWebImageOptions)options
progress:(nullable YYWebImageProgressBlock)progress
transform:(nullable YYWebImageTransformBlock)transform
completion:(nullable YYWebImageCompletionBlock)completion;
/**
The image cache used by image operation.
You can set it to nil to avoid image cache.
*/
// 图片缓存对象
@property (nullable, nonatomic, strong) YYImageCache *cache;
/**
The operation queue on which image operations are scheduled and run.
You can set it to nil to make the new operation start immediately without queue.
You can use this queue to control maximum number of concurrent operations, to obtain
the status of the current operations, or to cancel all operations in this manager.
*/
// 任务队列
@property (nullable, nonatomic, strong) NSOperationQueue *queue;
/**
The shared transform block to process image. Default is nil.
When called `requestImageWithURL:options:progress:transform:completion` and
the `transform` is nil, this block will be used.
*/
// 图片形变回调块
@property (nullable, nonatomic, copy) YYWebImageTransformBlock sharedTransformBlock;
/**
The image request timeout interval in seconds. Default is 15.
*/
// 超时时间
@property (nonatomic) NSTimeInterval timeout;
/**
The username used by NSURLCredential, default is nil.
*/
// 验证信息中的用户名
@property (nullable, nonatomic, copy) NSString *username;
/**
The password used by NSURLCredential, default is nil.
*/
// 认证信息中的密码
@property (nullable, nonatomic, copy) NSString *password;
/**
The image HTTP request header. Default is "Accept:image/webp,image/\*;q=0.8".
*/
// 请求体头部,默认为 "Accept:image/webp,image/\*;q=0.8".
@property (nullable, nonatomic, copy) NSDictionary *headers;
/**
A block which will be invoked for each image HTTP request to do additional
HTTP header process. Default is nil.
Use this block to add or remove HTTP header field for a specified URL.
*/
// 请求头部过滤器
@property (nullable, nonatomic, copy) NSDictionary *(^headersFilter)(NSURL *url, NSDictionary * _Nullable header);
/**
A block which will be invoked for each image operation. Default is nil.
Use this block to provide a custom image cache key for a specified URL.
*/
// 缓存过滤器
@property (nullable, nonatomic, copy) NSString *(^cacheKeyFilter)(NSURL *url);
/**
Returns the HTTP headers for a specified URL.
@param url A specified URL.
@return HTTP headers.
*/
// 根据url 获取请求头部信息
- (nullable NSDictionary *)headersForURL:(NSURL *)url;
/**
Returns the cache key for a specified URL.
@param url A specified URL
@return Cache key used in YYImageCache.
*/
// 通过url 获取 缓存 key
- (NSString *)cacheKeyForURL:(NSURL *)url;
/**
Increments the number of active network requests.
If this number was zero before incrementing, this will start animating the
status bar network activity indicator.
This method is thread safe.
This method has no effect in App Extension.
*/
// 请求任务加 1
+ (void)incrementNetworkActivityCount;
/**
Decrements the number of active network requests.
If this number becomes zero after decrementing, this will stop animating the
status bar network activity indicator.
This method is thread safe.
This method has no effect in App Extension.
*/
// 请求任务减 1
+ (void)decrementNetworkActivityCount;
/**
Get current number of active network requests.
This method is thread safe.
This method has no effect in App Extension.
*/
// 当前图片下载活动数量。
+ (NSInteger)currentNetworkActivityCount;
@end
内部实现子线程到主线程的回调
Categories
Categories 对上层图片的展示
_YYWebImageSetter
UIKit 控件需要加载网络图片,都是由 _YYWebImageSetter对图片下载的监听,以及对上层的回调,同一控件加载重复加载图片的时候做容错,取消去之前的url的下载,下载新的 url