1. 开源代码信息

SDWebImage Github 库
SDWebImage Main Repo
SDWebImage 图片Coder插件列表

想必每个接触过iOS开发的开发者都接触过SDWebImage吧,它是一个带有缓存支持的异步图片下载工具。我们常用的UIImageView, *UIButton, MKAnnotationView,这些UI元素都有它为我们提供的专门分类用于异步图片的加载。

特性

  • 为UIImageView, UIButton, MKAnnotationView这些类提供了用于图片加载以及缓存管理的分类。
  • 带有一个异步的图片下载器以及一个缓存过期自动处理的异步内存图片缓存管理。
  • 支持后台图片解压
  • 支持渐进式图片加载功能
  • 可扩展的图像编码器以支持大图片格式,例如WebP
  • 动画图像的全栈解决方案能够做到在CPU和内存之间保持性能平衡
  • 能够支持对下载后图片进行自定义且可组合的转换
  • 可定制的多缓存系统
  • 支持图片加载指示器
  • 支持图片加载过渡动画
  • 同一个URL不会下载多次,虚假的URL不会一直重试
  • 保证主线程不会被阻塞
  • 支持Objective-C 以及 Swift
  • 高性能
  • 支持JPEG, PNG, HEIC图片格式,包含GIF/APNG/HEIC动画格式
  • 支持WebP,以及动画WebP格式
  • 支持可扩张的图片编码器插件,可以通过它来添加更多新的格式。
2. 源码解析
2.1 总入口

我们先来看下给UIImageView设置网络图片的流程的代码,这是我们最常用的一个功能,这部分代码位于:
UIView+WebCache.h其他部分涉及到图片加载的最终都是调用这个方法:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {

context = [context copy]; // copy to avoid mutable object
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
validOperationKey = NSStringFromClass([self class]);
}
self.sd_latestOperationKey = validOperationKey;

//取消当前运行的同样请求
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

//设置本次加载的URL
self.sd_imageURL = url;

//立刻设置占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
//设置占位图
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}

if (url) {
// 重置进度状态
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}

// 开始图片加载指示器
[self sd_startImageIndicator];
// 图片指示器
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;

//图片管理器
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
}

//更新进度
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};

//加载图片
@weakify(self);
id <SDWebImageOperation> operation = [manager loadImageWithURL:url
options:options
context:context
progress:combinedProgressBlock
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
@strongify(self);
if (!self) { return; }
// if the progress not been updated, mark it to complete state
if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) {
imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
}

// 加载结束后停止图片指示器
if (finished) {
[self sd_stopImageIndicator];
}
// 是否自动调用complete Block
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
// 是否不设置Image
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));

SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!self) { return; }
if (!shouldNotSetImage) {
[self sd_setNeedsLayout];
}
// 是否调用completeBlock
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, data, error, cacheType, finished, url);
}
};

// 到这里有两种可能,一种是我们获得到了图片,但是SDWebImageAvoidAutoSetImage = 1 或者 我们没拿到图片,并且SDWebImageDelayPlaceholder = 0
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}

UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// 获得到额了图片,SDWebImageAvoidAutoSetImage = 0,这是正常流程。
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// 没得到图片,但是设置了SDWebImageDelayPlaceholder
targetImage = placeholder;
targetData = nil;
}

// 检查是否使用图片transition
SDWebImageTransition *transition = nil;
if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
transition = self.sd_imageTransition;
}
dispatch_main_async_safe(^{
[self sd_setImage:targetImage
imageData:targetData
basedOnClassOrViaCustomSetImageBlock:setImageBlock
transition:transition
cacheType:cacheType
imageURL:imageURL];
callCompletedBlockClojure();
});
}];
//设置当前的operation
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
//如果url为空则停止加载指示器,并抛出异常。
[self sd_stopImageIndicator];
dispatch_main_async_safe(^{
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
}
});
}
}

这个方法主要完成了三大块任务:

1. 图片指示器控制
2. 通过图片加载器加载图片
3. 下载完成后给控件设置图片

其他包括缓存,后期图片转换的都在第二部分中处理。这里有些需要注意的细节简单过一下,都是一些option:

SDWebImageDelayPlaceholder :
是否延迟加载占位图,默认一旦开始加载就会显示占位图,如果设置成延迟占位图,那么只有在图片没下载成功的时候设置。
SDWebImageContextCustomManager:
可以外部注入图片管理器,负责图片的缓存控制和图片的下载。默认是没有注入的。
SDWebImageAvoidAutoSetImage
图片下载成功后是否不自动设置图片

2.2 图片加载
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

//如果传入的URL是字符串则将其转换为NSURL
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}

// 如果不是NSURL则设置url为空,避免崩溃
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}

SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;

// 判断是否是失败的URL
BOOL isFailedUrl = NO;
if (url) {
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}

//如果地址为空,或者是失败的url则取消请求
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
return operation;
}

//将当前的operation添加到self.runningOperations 中表示该请求已经发起
SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);

// 对options和context参数进行预先处理,这些会影响manager加载的最后处理结果
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

// 启动图片加载
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];

return operation;
}

在图片加载开始的时候会先对传入的url类型进行判断,这里的url可能会有两种类型一种是NSString,一种是NSURL这个阶段会分别对它做一个判断,并最终转换为NSURL,然后判断当前的url是否在failedURLs中,如果在的话会根据SDWebImageRetryFailed的配置来进行选择接下来的处理,如果SDWebImageRetryFailed = 0的话 就表示不对失败的url进行重新尝试,所以这种情况会取消请求,failedURLs会在每次加载失败的时候添加。

紧接着会调用processedResultForURL对options和context参数进行预先处理,这些会影响manager加载的最后处理结果:

- (SDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context {
SDWebImageOptionsResult *result;
SDWebImageMutableContext *mutableContext = [SDWebImageMutableContext dictionary];

// 用于对下载后的图片进行转换后存储到缓存中
if (!context[SDWebImageContextImageTransformer]) {
id<SDImageTransformer> transformer = self.transformer;
[mutableContext setValue:transformer forKey:SDWebImageContextImageTransformer];
}
// 用于将URL转换为它对应的缓存key的组件
if (!context[SDWebImageContextCacheKeyFilter]) {
id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
[mutableContext setValue:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter];
}
// 用于将图片缓存到disk缓存中
if (!context[SDWebImageContextCacheSerializer]) {
id<SDWebImageCacheSerializer> cacheSerializer = self.cacheSerializer;
[mutableContext setValue:cacheSerializer forKey:SDWebImageContextCacheSerializer];
}

if (mutableContext.count > 0) {
if (context) {
[mutableContext addEntriesFromDictionary:context];
}
context = [mutableContext copy];
}

// 对当前的options 以及 context 进行进一步处理
if (self.optionsProcessor) {
result = [self.optionsProcessor processedResultForURL:url options:options context:context];
}
if (!result) {
// 创建默认的SDWebImageOptionsResult
result = [[SDWebImageOptionsResult alloc] initWithOptions:options context:context];
}
return result;
}

它实际上是在context中加入了SDImageTransformerSDWebImageContextCacheKeyFilter,以及SDWebImageContextCacheSerializer,然后通过optionsProcessor 对当前对options 以及 context进行统一处理后返回,这些组件会在加载过程中被提取出来使用。

2.2.1 查询缓存还是从网络下载
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {

// 检查当前是否需要查询缓存内的图片
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
//通过url转缓存key的组件,将url转换为对应的缓存key
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
@weakify(operation);
//异步查询,缓存中与当前url对应key的缓存
operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
//用户取消查找
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 继续开始下载
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// 继续开始下载
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}

在将下载过程需要的重要组件放置到options和context中后就开始加载图片,这里无外乎两种方式一种是从网络上下载资源,一种是使用本地资源,具体使用哪一种,在加载之前会先根据用户的设置,判断是否只使用缓存的图片,这取决于SDWebImageFromLoaderOnly,如果SDWebImageFromLoaderOnly = 1 会先将URL转换为缓存的key,再使用这个key调用self.imageCache queryImageForKey,异步查询是否有对应的缓存。,如果SDWebImageFromLoaderOnly = 0 则会通过callDownloadProcessForOperation从网络上下载资源。我们接下来分别看下这两个分支的代码:

2.2.1.1 使用缓存资源
- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
//将配置从options搬到cacheOptions
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
if (options & SDWebImageMatchAnimatedImageClass) cacheOptions |= SDImageCacheMatchAnimatedImageClass;

return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
}

这里就不多说了就是将缓存相关的配置添加到cacheOptions中调用queryCacheOperationForKey

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
//如果key为空则直接返回
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
//从context取出图片转换器
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer) {
// 取出我们存储在context中的transformer的transformerKey
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}

//首先从内存缓存中查找
UIImage *image = [self imageFromMemoryCacheForKey:key];

if (image) {
// 默认情况下我们会对动画图片整个进行解码,SDImageCacheDecodeFirstFrameOnly表示强迫只解码第一帧图片形成一个静态图
if (options & SDImageCacheDecodeFirstFrameOnly) {
// Ensure static image
Class animatedImageClass = image.class;
if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
}
} else if (options & SDImageCacheMatchAnimatedImageClass) {
// Check image class matching
Class animatedImageClass = image.class;
Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
image = nil;
}
}
}

//是否只使用内存缓存的图片
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}

// 检查磁盘缓存中的资源
NSOperation *operation = [NSOperation new];
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));

void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}

@autoreleasepool {
//从磁盘中查找对应的缓存图片
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// 使用内存缓存中的图片
// the image is from in-memory cache, but need image data
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
//使用磁盘缓存中的图片
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
// 在内存缓存没有命中的时候解压从磁盘中获取到的文件
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
//是否需要存储到内存缓存中
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}

//交付数据
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};

// Query in ioQueue to keep IO-safe
// 开始查询,这里分同步和异步查询两种
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}

return operation;
}

queryCacheOperationForKey中做的工作和其他图片加载库类似,只不过在细节会有点差异:

  1. 按照key的规则构建出缓存key
  2. 优先从缓存中查找缓存,如果只使用内存缓存的则在这个步骤不论是否能够在内存中找到缓存,都通过block返回结果。
  3. 如果在内存缓存中没有找到对应的图片缓存,则继续查找磁盘缓存,如果在磁盘中找到了,再看是否需要同步到内存缓存。
  4. 最后交付数据到应用层

下面将针对上面几点展开介绍:

  • 按照key的规则构建出缓存key

SDWebImage中的缓存key和转换器是有关联,所以我们在构建key的时候需要将转换器从context中取出。

id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer) {
// 取出我们存储在context中的transformer的transformerKey
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}

缓存key的具体构建过程在SDTransformedKeyForKey方法中,这里会在原先的缓存key后面添加不同的转换器信息,用于区分同一个url 对应不同转换器的情况,比如我们有一个图片为image.png,它经过flip(YES,NO) 也就是水平翻转,再经过旋转45度处理,这样最终生成的key就是image-SDImageFlippingTransformer(1,0)-SDImageRotationTransformer(0.78539816339,1).png:

NSString * _Nullable SDTransformedKeyForKey(NSString * _Nullable key, NSString * _Nonnull transformerKey) {
if (!key || !transformerKey) {
return nil;
}
// Find the file extension
NSURL *keyURL = [NSURL URLWithString:key];
NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension;
if (ext.length > 0) {
// For non-file URL
if (keyURL && !keyURL.isFileURL) {
// keep anything except path (like URL query)
NSURLComponents *component = [NSURLComponents componentsWithURL:keyURL resolvingAgainstBaseURL:NO];
component.path = [[[component.path.stringByDeletingPathExtension stringByAppendingString:SDImageTransformerKeySeparator] stringByAppendingString:transformerKey] stringByAppendingPathExtension:ext];
return component.URL.absoluteString;
} else {
// file URL
return [[[key.stringByDeletingPathExtension stringByAppendingString:SDImageTransformerKeySeparator] stringByAppendingString:transformerKey] stringByAppendingPathExtension:ext];
}
} else {
return [[key stringByAppendingString:SDImageTransformerKeySeparator] stringByAppendingString:O];
}
}

在拿到key后key后我们会先从内存图片缓存中去查找对与当前key匹配的图片。

//首先从内存缓存中查找
UIImage *image = [self imageFromMemoryCacheForKey:key];

--->

- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memoryCache objectForKey:key];
}

如果指定只使用内存中的缓存图片并且内存中有找到对应的图片就直接返回:

//是否只使用内存缓存的图片
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}

如果shouldQueryMemoryOnly = NO,也就是不单单使用内存的图片缓存,就会调用diskImageDataBySearchingAllPathsForKey在磁盘中查找对应的缓存图片,如同内存中有则优先使用内存的,其实这里有个需要优化的地方,如果image不为空的情况下就不需要调用diskImageDataBySearchingAllPathsForKey,如果内存没有,那么会将从磁盘缓存中获取到的缓存数据,经过解码后存放到内存缓存,供下次使用。

BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));

void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}
@autoreleasepool {
//从磁盘中查找对应的缓存图片
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// 使用内存缓存中的图片
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
//使用磁盘缓存中的图片
cacheType = SDImageCacheTypeDisk;
// 在内存缓存没有命中的时候解压从磁盘中获取到的文件
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
//是否需要存储到内存缓存中
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
//交付数据
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
// 开始查询,这里分同步和异步查询两种
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
2.2.1.2 使用网络资源

上面小节我们了解了从缓存中加载图片的流程,接下来我们看下从网络上对图片进行加载的流程:

在SDWebImage默认情况下使用SDWebImageDownloader来完成下载任务,我们在分析下载流程之前我们先看下SDWebImageDownloader数据流是怎样的:

  • SDWebImageDownloader初始化阶段

在SDWebImageDownloader 第一次调用的时候会调用initialize方法

+ (void)initialize {
if (NSClassFromString(@"SDNetworkActivityIndicator")) {

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
#pragma clang diagnostic pop

// Remove observer in case it was previously added.
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil];

[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
selector:NSSelectorFromString(@"startActivity")
name:SDWebImageDownloadStartNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:activityIndicator
selector:NSSelectorFromString(@"stopActivity")
name:SDWebImageDownloadStopNotification object:nil];
}
}

在这里主要是对网络状态指示器通知进行监听。

- (nonnull instancetype)init {
return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}
- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
self = [super init];
if (self) {
if (!config) {
config = SDWebImageDownloaderConfig.defaultDownloaderConfig;
}
_config = [config copy];
//指定并行下载数目
[_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];

//创建下载队列_downloadQueue
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
_downloadQueue.name = @"com.hackemist.SDWebImageDownloader";

//创建用于管理NSOperation的字典,key为url,value为对应的NSOperation
_URLOperations = [NSMutableDictionary new];
NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
NSString *userAgent = nil;
// User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
//UA设置
if (userAgent) {
if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
NSMutableString *mutableUserAgent = [userAgent mutableCopy];
if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
userAgent = mutableUserAgent;
}
}
headerDictionary[@"User-Agent"] = userAgent;
}
//Accept context设置
headerDictionary[@"Accept"] = @"image/*,*/*;q=0.8";
_HTTPHeaders = headerDictionary;
_HTTPHeadersLock = dispatch_semaphore_create(1);
_operationsLock = dispatch_semaphore_create(1);
NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration;
if (!sessionConfiguration) {
sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
//创建NSURLSession
_session = [NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:self
delegateQueue:nil];
}
return self;
}

在初始化方法中主要负责重要组件的创建****_downloadQueue_URLOperations_session****。

  • SDWebImageDownloader下载流程

SDWebImage的下载是从callDownloadProcessForOperation开始的:

- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {

BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
shouldDownload &= [self.imageLoader canRequestImageForURL:url];

if (shouldDownload) {
//在需要强制刷新缓存的情况下先调用callCompletionBlockForOperation将缓存图片交给上层,然后继续开始下载
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}

@weakify(operation);
// 开始发起图片加载的请求
operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation);
//用户取消请求
if (!operation || operation.isCancelled) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
// 强制刷新缓存的时候,在前面先将缓存图片通过completeBlock 将数据传递给上层,到这里将数据加载完毕后,不做任何操作
} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
// 将错误交给上层
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
} else if (error) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
//是否需要将错误的URL添加到failedURLs
BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];

if (shouldBlockFailedURL) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
SD_UNLOCK(self.failedURLsLock);
}
} else {
// 成功
//如果SDWebImageRetryFailed = 1 表示错误的url需要重试,这时候就要将当前的url从failedURLs移除
if ((options & SDWebImageRetryFailed)) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
}

[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
if (finished) {
//加载成功后将操作从Running队列中移除
[self safelyRemoveOperationFromRunning:operation];
}
}];
} else if (cachedImage) {
//使用缓存的情况下将缓存数据传递到上层,并将请求从Running队列中移除
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
//缓存没有图片,并且不允许下载
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}

是否需要从网络上下载图片需要根据如下条件进行判断:

  1. SDWebImageFromCacheOnly = 0,表示不单单只允许使用缓存的数据
  2. 上一步没有找到缓存cachedImage
  3. SDWebImageRefreshCached = 1 强制刷新缓存
  4. imageManager:shouldDownloadImageForURL:决定当图片在缓存内没有找到的时候是否需要下载
  5. [self.imageLoader canRequestImageForURL:url] 决定是否能够下载,它会在每次imageloader请求到来的时候,对URL进行检查,
    如果返回YES,则会开始调用requestImageWithURL发起请求,否则将会将当前的图片加载标记为加载失败。

这里需要强调的是如果是需要强制刷新的时候会先将缓存图片通过block上传给业务层,但是会继续下载任务。

下载是调用****self.imageLoader requestImageWithURL:****来执行的,我们来看下:

- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock {
if (!url) {
return nil;
}
NSArray<id<SDImageLoader>> *loaders = self.loaders;
for (id<SDImageLoader> loader in loaders.reverseObjectEnumerator) {
if ([loader canRequestImageForURL:url]) {
return [loader requestImageWithURL:url options:options context:context progress:progressBlock completed:completedBlock];
}
}
return nil;
}

这里会遍历每个注册的SDImageLoader,对每个loader调用canRequestImageForURL 来判断当前loader能否对当前url请求做处理,如果能就进行请求,这种插件化处理是很多通用库采用的套路。我们看下SDImageLoadersManager

- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock {
UIImage *cachedImage = context[SDWebImageContextLoaderCachedImage];

SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveLoad) downloaderOptions |= SDWebImageDownloaderProgressiveLoad;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage) downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
if (options & SDWebImageMatchAnimatedImageClass) downloaderOptions |= SDWebImageDownloaderMatchAnimatedImageClass;

if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveLoad;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];
}

和查找缓存一样在执行真正的下载任务之前,会先重建一份options,然后再开始真正的请求:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
//..........
SD_LOCK(self.operationsLock);
id downloadOperationCancelToken;
// 通过当前的url去self.URLOperations 中去取NSOperation
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
// 如果当前url对应的operation为空,并且operation.isFinished = YES 或者 operation.isCancelled = YES,就调用createDownloaderOperationWithUrl新建一个NSOperation。
// operation.isFinished = YES 或者 operation.isCancelled = YES 会发生在operation被取消或者被标记为结束,但是并没有将它从self.URLOperations移除
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
// .......
//operation 执行结束后会将当前url对应的operation从URLOperations中移除
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
//......
SD_LOCK(self.operationsLock);
[self.URLOperations removeObjectForKey:url];
SD_UNLOCK(self.operationsLock);
};
//添加到self.URLOperations中
self.URLOperations[url] = operation;
// 将operation 添加到下载队列
[self.downloadQueue addOperation:operation];
//为当前的operation添加下载进度block以及下载完成的block
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
} else {
// So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
// 走到这里表示之前有对应的下载了,我们可以复用已经存在的下载,并添加progressBlock 以及completedBlock,这样在原先的下载任务完成后也会通知到当前的请求
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
if (!operation.isExecuting) {
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
} else {
operation.queuePriority = NSOperationQueuePriorityNormal;
}
}
}
SD_UNLOCK(self.operationsLock);
//这里返回的是一个SDWebImageDownloadToken,后面可以通过它来取消这个下载任务
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}

下载请求是基于NSOperation的,每个url对应一个NSOperation,这些NSOperation放在self.URLOperations 中统一管理,每次发起一个未存在的图片请求的时候就会新建一个NSOperation并添加到self.URLOperations,一旦完成下载这个url对应的NSOperation就会从self.URLOperations中移除。如果发起一个已经存在的url请求,那么就会复用当前已经开始的NSOperation,将当前的progressBlockcompletedBlock 添加到 已经存在的NSOperation中,这样在原先的下载任务完成后也会通知到当前的请求响应的对应block。

我们接下来看下NSOperation的创建方法createDownloaderOperationWithUrl,这里主要是构建出NSRequest并通过请求修改器,对请求进行拦截修改,然后将返回体修改器,解密器以及Request 添加到NSOperation中。

- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {

//配置网络超时时间,缓存策略等
NSTimeInterval timeoutInterval = self.config.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}

// In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
SD_LOCK(self.HTTPHeadersLock);
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
SD_UNLOCK(self.HTTPHeadersLock);

// 取出context
SDWebImageMutableContext *mutableContext;
if (context) {
mutableContext = [context mutableCopy];
} else {
mutableContext = [NSMutableDictionary dictionary];
}

// 从context中取出requestModifier,它会对当前的URL进行修改。
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
requestModifier = self.requestModifier;
}

NSURLRequest *request;
if (requestModifier) {
NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
// If modified request is nil, early return
if (!modifiedRequest) {
return nil;
} else {
request = [modifiedRequest copy];
}
} else {
request = [mutableRequest copy];
}


// Response 修改器件
id<SDWebImageDownloaderResponseModifier> responseModifier;
if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
} else {
responseModifier = self.responseModifier;
}
if (responseModifier) {
mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
}
// 解密器
id<SDWebImageDownloaderDecryptor> decryptor;
if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
} else {
decryptor = self.decryptor;
}
if (decryptor) {
mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
}
context = [mutableContext copy];


// 取出指定的NSOperation class
Class operationClass = self.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
// Custom operation class
} else {
operationClass = [SDWebImageDownloaderOperation class];
}
//将处理过的request,以及 Response 修改器件,解密器放置到NSOperation
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];

//设置认证信息
if ([operation respondsToSelector:@selector(setCredential:)]) {
if (self.config.urlCredential) {
operation.credential = self.config.urlCredential;
} else if (self.config.username && self.config.password) {
operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
}
}

//设置最小的进度
if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
}

//设置优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}

if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation
// This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations
// Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder
for (NSOperation *pendingOperation in self.downloadQueue.operations) {
[pendingOperation addDependency:operation];
}
}
return operation;
}

看到这里大家可能会有疑问,上面只是构建NSOperation然后添加到downloadQueue中,那么什么时候开始呢?我们看到了NSOperation创建过程如下:

NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];

这里持有了NSSession,我们知道网络数据的下载都是依靠NSSession的,所以下载的触发应该也是在NSOperation中。所以我们带着这个问题来看下SDWebImageDownloaderOperation从初始化方法中我们可以看到它包含很多重要的组件,具体的见下面注释。

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
inSession:(nullable NSURLSession *)session
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context {
if ((self = [super init])) {
_request = [request copy]; //当前下载请求NSURLRequest
_options = options; //下载选项
_context = [context copy]; //包含一系列组件的上下文
_callbackBlocks = [NSMutableArray new];//进度通知block和下载完成处理block
_responseModifier = context[SDWebImageContextDownloadResponseModifier];//返回内容修改器
_decryptor = context[SDWebImageContextDownloadDecryptor];//解码器
_executing = NO; //当前状态
_finished = NO; //当前状态
_expectedSize = 0;
_unownedSession = session; //NSSession
_coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);//解码队列
_backgroundTaskId = UIBackgroundTaskInvalid;//后台id
}
return self;
}

接下来我们带着大家来解决如下几个问题:

  1. SDWebImageDownloaderOperation是如何使用NSSession发起下载请求的?
  2. 请求后通过代理返回的数据怎么传到SDWebImageDownloaderOperation中?在SDWebImageDownloaderOperation又是怎么做处理的。
  3. 图片下载后是怎么通知给上层的,下载进度又是如何传递的?

我们先来看下第一个问题:

  • SDWebImageDownloaderOperation是如何使用NSSession发起下载请求的?

这个比较好回答,我们知道NSOperation运行会调用它的start方法,所以我们要回答这个问题也应该从这个方法入手:

- (void)start {
@synchronized (self) {

//开始之前就已经被取消了
if (self.isCancelled) {
self.finished = YES;
// Operation cancelled by user before sending the request
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil]];
[self reset];
return;
}
//退出后台后会停止该下载任务
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak typeof(self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
[wself cancel];
}];
}
//构建session
NSURLSession *session = self.unownedSession;
//......
//通过session 以及request 创建出一个 dataTask
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}

if (self.dataTask) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
}
//启动下载任务
[self.dataTask resume];
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
//发出启动下载通知
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
});
} else {
//.....
}
}

整个过程还是拿传入的request使用NSSession创建出dataTask,然后调用dataTask的resume方法启动下载。下载后数据会从相应的代理方法中获得,但是代理是SDWebImageDownloader,如何将它传给SDWebImageDownloaderOperation呢?我们接下来就来回答这第二个问题,如果看过AFNetWorking源码的对这个流程会比较熟悉,大致相同。

  • *请求后通过代理返回的数据怎么传到SDWebImageDownloaderOperation中?在SDWebImageDownloaderOperation又是怎么做处理的

我们在delegate中拿到的是task,所以我们要通过task找到它所属的NSOperation:

- (NSOperation<SDWebImageDownloaderOperation> *)operationWithTask:(NSURLSessionTask *)task {
NSOperation<SDWebImageDownloaderOperation> *returnOperation = nil;
for (NSOperation<SDWebImageDownloaderOperation> *operation in self.downloadQueue.operations) {
if ([operation respondsToSelector:@selector(dataTask)]) {
if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
returnOperation = operation;
break;
}
}
}
return returnOperation;
}

嗯,就是通过遍历NSOperation拿当前的task的taskIdentifier与self.downloadQueue.operations中task的taskIdentifier进行匹配,找到所属的NSOperation。

我们下下面的一个代理方法,它就是通过operationWithTask找到对应的NSOperation,然后再调用NSOperation中相对应的方法处理:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {

// Identify the operation that runs this task and pass it the delegate method
NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
[dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
}

我们接下来看下具体代理的处理:

收到服务端回复的时候会调用didReceiveResponse代理方法,这里主要是通过使用返回体修改器对返回的请求进行处理,然后再进行状态码进行校验。具体见如下代码注释:

- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow;

// Check response modifier, if return nil, will marked as cancelled.
BOOL valid = YES;
// 使用返回体修改器对返回的请求进行处理
if (self.responseModifier && response) {
response = [self.responseModifier modifiedResponseWithResponse:response];
if (!response) {
valid = NO;
self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:nil];
}
}

NSInteger expected = (NSInteger)response.expectedContentLength;
expected = expected > 0 ? expected : 0;
self.expectedSize = expected;
self.response = response;

//状态码检查
NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
// Status code should between [200,400)
BOOL statusCodeValid = statusCode >= 200 && statusCode < 400;
if (!statusCodeValid) {
valid = NO;
self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}];
}
//......
//进度提交给应用层上层
if (valid) {
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected, self.request.URL);
}
} else {
disposition = NSURLSessionResponseCancel;
}
//发出通知
__block typeof(self) strongSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:strongSelf];
});

if (completionHandler) {
completionHandler(disposition);
}
}

在收到图像数据后会回调didReceiveData代理,在didReceiveData中回将收到的图片数据追加到imageData,这里比较关键的是渐进式图片解码,顾名思义就是拿到多少数据就立刻解码交给上层展示,最后将当前下载进度通知给业务层,对应的关键部分看下面的注解:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (!self.imageData) {
self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize];
}
//将数据追加到self.imageData
[self.imageData appendData:data];
self.receivedSize = self.imageData.length;
if (self.expectedSize == 0) {
// Unknown expectedSize, immediately call progressBlock and return
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
}
return;
}
// 获取当前的下载状态,是否结束
BOOL finished = (self.receivedSize >= self.expectedSize);
// 计算当前下载进度
double currentProgress = (double)self.receivedSize / (double)self.expectedSize;
double previousProgress = self.previousProgress;
double progressInterval = currentProgress - previousProgress;
// 只有在大于最小的进度间隔才会通知上层block
if (!finished && (progressInterval < self.minimumProgressInterval)) {
return;
}
self.previousProgress = currentProgress;
//如果使用数据解码器将会禁止渐进式解码,
BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
if (supportProgressive) {
//获得当前的图片数据
NSData *imageData = [self.imageData copy];
//在解码队列进行渐进式解码
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
if (image) {
//将数据交给业务层,注意这里的finished为NO,因为还没下载完
[self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
}
}
});
}
//从callbacksForKey中取出对应的进度回调block,将进度上传给业务层
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
}
}

我们重点看下解码部分:

UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished,  id<SDWebImageOperation> _Nonnull operation, SDWebImageOptions options, SDWebImageContext * _Nullable context) {

//.......

//获取缓存key
UIImage *image;
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *cacheKey;
if (cacheKeyFilter) {
cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL];
} else {
cacheKey = imageURL.absoluteString;
}

//获取scale
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);

//构建解码Option
SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)};
if (context) {
SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy];
[mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext];
coderOptions = [mutableCoderOptions copy];
}

//获取对应的渐进式解码器
id<SDProgressiveImageCoder> progressiveCoder = objc_getAssociatedObject(operation, SDImageLoaderProgressiveCoderKey);
if (!progressiveCoder) {
// We need to create a new instance for progressive decoding to avoid conflicts
for (id<SDImageCoder>coder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) {
if ([coder conformsToProtocol:@protocol(SDProgressiveImageCoder)] &&
[((id<SDProgressiveImageCoder>)coder) canIncrementalDecodeFromData:imageData]) {
//通过Options来创建渐进式解码器
progressiveCoder = [[[coder class] alloc] initIncrementalWithOptions:coderOptions];
break;
}
}
objc_setAssociatedObject(operation, SDImageLoaderProgressiveCoderKey, progressiveCoder, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//......
//将图片数据更新到渐进式解码器
[progressiveCoder updateIncrementalData:imageData finished:finished];
//..........
if (!image) {
//进行解码
image = [progressiveCoder incrementalDecodedImageWithOptions:coderOptions];
}
if (image) {
//是否需要解码
BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
//如果需要解码则调用decodedImageWithImage进行解码
if (shouldDecode) {
image = [SDImageCoderHelper decodedImageWithImage:image];
}
image.sd_isIncremental = YES;
}

return image;
}

上面最关键的部分在于:

[progressiveCoder updateIncrementalData:imageData finished:finished];
image = [progressiveCoder incrementalDecodedImageWithOptions:coderOptions];
if (image) {
//是否需要解码
BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
//如果需要解码则调用decodedImageWithImage进行解码
if (shouldDecode) {
image = [SDImageCoderHelper decodedImageWithImage:image];
}
// mark the image as progressive (completionBlock one are not mark as progressive)
image.sd_isIncremental = YES;
}

这里渐进式解码器也有好多种,我们仅仅以其中的一种SDImageIOAnimatedCoder为例子:

- (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
//.......
//更新数据
_imageData = data;
_finished = finished;
//通过接口更新图片数据
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
if (_width + _height == 0) {
//读取图片属性
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
if (properties) {
//获取到图片的宽高
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
CFRelease(properties);
}
}
//检查帧的有效性
[self scanAndCheckFramesValidWithImageSource:_imageSource];
}

- (BOOL)scanAndCheckFramesValidWithImageSource:(CGImageSourceRef)imageSource {
//......
//获取图片数据的帧数
NSUInteger frameCount = CGImageSourceGetCount(imageSource);
NSUInteger loopCount = [self.class imageLoopCountWithSource:imageSource];
NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray array];

for (size_t i = 0; i < frameCount; i++) {
SDImageIOCoderFrame *frame = [[SDImageIOCoderFrame alloc] init];
//获取帧序列
frame.index = i;
//获取帧时长
frame.duration = [self.class frameDurationAtIndex:i source:imageSource];
//将其添加到frames
[frames addObject:frame];
}

_frameCount = frameCount;
_loopCount = loopCount;
_frames = [frames copy];

return YES;
}
- (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
UIImage *image;
if (_width + _height > 0) {
// 取出当前的第一帧
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
if (partialImageRef) {
CGFloat scale = _scale;
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
if (scaleFactor != nil) {
scale = MAX([scaleFactor doubleValue], 1);
}
image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:UIImageOrientationUp];
CGImageRelease(partialImageRef);
image.sd_imageFormat = self.class.imageFormat;
}
}
return image;
}

上面的代码主要就是在数据帧来的时候更新数据源,然后解码数据,取出当前帧的第一帧构建后返回。

紧接着就会根据当前的情况除了动图都会对其进行解码,我们来看下解码部分的代码:

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
if (![self shouldDecodeImage:image]) {
return image;
}

CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
if (!imageRef) {
return image;
}
UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(imageRef);
decodedImage.sd_isDecoded = YES;
decodedImage.sd_imageFormat = image.sd_imageFormat;
return decodedImage;
}

嗯,这块不是很难,过一下就可以了。

最后我们看下下载完成后的处理:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {

//......
if (error) {
//.........
} else {

if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
NSData *imageData = [self.imageData copy];
self.imageData = nil;
// 使用数据解密器对图片数据进行解密
if (imageData && self.decryptor) {
imageData = [self.decryptor decryptedDataWithData:imageData response:self.response];
}
if (imageData) {
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
//...........
} else {
// 对图像数据进行解码
dispatch_async(self.coderQueue, ^{
@autoreleasepool {
UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
CGSize imageSize = image.size;
if (imageSize.width == 0 || imageSize.height == 0) {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
} else {
[self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
}
[self done];
}
});
}
} else {
//......
}
} else {
[self done];
}
}
}

下载完成后首先会对数据进行解密,然后再对解密后的数据进行解码,最后将数据通过block传递到业务层。
解密部分是通过SDImageLoaderDecodeImageData方法,这部分代码和上面介绍渐进式解码的类似,这里只注重流程,先不对这部分细节进行展开了。

UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context) {

//......
//调用解码器进行解码
if (!image) {
image = [[SDImageCodersManager sharedManager] decodedImageWithData:imageData options:coderOptions];
}
if (image) {
BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}

if (shouldDecode) {
BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
if (shouldScaleDown) {
image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0];
} else {
image = [SDImageCoderHelper decodedImageWithImage:image];
}
}
}

return image;
}

到目前为止已经带大家过了一遍整个图片下载的流程,在解码结束后通过completeBlock交给上层后还没结束,我们看下completeBlock到底做了啥,让我们回到callDownloadProcessForOperation:

下面是completeBlock代码:

@strongify(operation);
//用户取消请求
if (!operation || operation.isCancelled) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:nil] url:url];
} else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
// 强制刷新缓存的时候,在前面先将缓存图片通过completeBlock 将数据传递给上层,到这里将数据加载完毕后,不做任何操作
} else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
// 将错误交给上层
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
} else if (error) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
//是否需要将错误的URL添加到failedURLs
BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];
if (shouldBlockFailedURL) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
SD_UNLOCK(self.failedURLsLock);
}
} else {
// 成功
//如果SDWebImageRetryFailed = 1 表示错误的url需要重试,这时候就要将当前的url从failedURLs移除
if ((options & SDWebImageRetryFailed)) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
}
//成功后会对当前数据进行缓存
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
if (finished) {
//加载成功后将操作从Running队列中移除
[self safelyRemoveOperationFromRunning:operation];
}

completeBlock里面有很多的情况会调到,比如NSOperation被取消,发生错误,成功等,下面我们只强调如下几种情况:

  • 下载失败的处理方式
[self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
//是否需要将错误的URL添加到failedURLs
BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];
if (shouldBlockFailedURL) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs addObject:url];
SD_UNLOCK(self.failedURLsLock);
}

如果下载失败那么会将错误通过callCompletionBlockForOperation将错误返回给业务层,并且会将错误的url添加到failedURLs,如果我们配置不重试错误的url,那么在failedURLs数组中的url将不会发起请求。

  • 下载成功的处理方式
//如果SDWebImageRetryFailed = 1 表示错误的url需要重试,这时候就要将当前的url从failedURLs移除
if ((options & SDWebImageRetryFailed)) {
SD_LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
SD_UNLOCK(self.failedURLsLock);
}
//成功后会对当前数据进行缓存
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];

和上面相反,如果下载成功后会将之前添加到failedURLs的url移除,然后调用callStoreCacheProcessForOperation将数据添加到缓存中。

- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {

// 获取缓存类型
SDImageCacheType storeCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextStoreCacheType]) {
storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
}

SDImageCacheType originalStoreCacheType = SDImageCacheTypeNone;
if (context[SDWebImageContextOriginalStoreCacheType]) {
originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue];
}

//生成缓存key
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
//图片转换器
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
//缓存图片处理器
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];

//是否需要对图片进行处理
BOOL shouldTransformImage = downloadedImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer;
//是否缓存原始图片
BOOL shouldCacheOriginal = downloadedImage && finished;

//原图缓存
//需要缓存原始图片的情况下
if (shouldCacheOriginal) {
//正常情况下我们使用cache type,但是如果目标图片是转换过的,我们会使用original 存储缓存类型来缓存
SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
//将数据通过cacheSerializer处理后返回
NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
//将数据添加到缓存中,具体缓存到哪里根据targetStoreCacheType类型
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType completion:nil];
}
});
} else {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType completion:nil];
}
}
// 转换后图片缓存
if (shouldTransformImage) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
//对数据进行转换
UIImage *transformedImage = [transformer transformedImageWithImage:downloadedImage forKey:key];
if (transformedImage && finished) {
NSString *transformerKey = [transformer transformerKey];
//缓存的图片带转换信息
NSString *cacheKey = SDTransformedKeyForKey(key, transformerKey);
//判断图片是否被转换成功
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
//将数据通过cacheSerializer处理后返回
cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : downloadedData) imageURL:url];
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
//缓存图片数据
[self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil];
}
//将结果传递到业务层
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
});
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
2.3 设置图片

设置图片环节比较简单就是根据view的类型来设置对应的finalSetImageBlock,然后看是否有过场动画,如果有则播放过场动画后执行finalSetImageBlock。

- (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL {
// 根据view的类型来设置对应的finalSetImageBlock
UIView *view = self;
SDSetImageBlock finalSetImageBlock;
if (setImageBlock) {
finalSetImageBlock = setImageBlock;
} else if ([view isKindOfClass:[UIImageView class]]) {
UIImageView *imageView = (UIImageView *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
imageView.image = setImage;
};
}
else if ([view isKindOfClass:[UIButton class]]) {
UIButton *button = (UIButton *)view;
finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) {
[button setImage:setImage forState:UIControlStateNormal];
};
}
//播放过场动画
if (transition) {
[UIView transitionWithView:view duration:0 options:0 animations:^{
// 0 duration to let UIKit render placeholder and prepares block
if (transition.prepares) {
transition.prepares(view, image, imageData, cacheType, imageURL);
}
} completion:^(BOOL finished) {
[UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
if (finalSetImageBlock && !transition.avoidAutoSetImage) {
//执行finalSetImageBlock
finalSetImageBlock(image, imageData, cacheType, imageURL);
}
if (transition.animations) {
transition.animations(view, image);
}
} completion:transition.completion];
}];
} else {
//如果没有过场动画直接执行finalSetImageBlock
if (finalSetImageBlock) {
finalSetImageBlock(image, imageData, cacheType, imageURL);
}
}
}

简要流程如下图所示:

2.4 整体架构总结

上面几个章节我们将整个流程过了一遍,接下来我们将会从宏观的角度来浏览下整个架构,整个SDWebImage代码量还是蛮庞大的,所以如果没有缕清整个结构的话很容易陷入代码之中,SDWebImage之所以大主要是因为它整个架构可定制的部件多,并且每个部件都可以有不同的选择,这种框架最适合采用Core+Plugin形式,SDWebImage 有个不足的地方就是代码的目录管理做得不好,整个庞大的项目都堆到一个目录下,让刚接触SDWebImage会显得毫无头绪,下面是重新组织的目录结构:

下面是SDWebImage官方的架构图,以及关键的类图:



下面我们结合上面三张图来过一遍整个项目:

1. 常用控件的分类
SDWebImage为UIImageView,NSButton,SDAnimatedImageView这些类都创建了对应都分类,方便这些类加载网络图片资源,大家可以通过搜索+WebCache或者+HightlightedWebCache可以找到这些分类。这些分类中除了提供设置网络图片的方法外,还提供了设置下载指示器,以及图片转换处理器等方法。UIView+WebCache.h是这些分类的基础。

2. 图片预取器

SDWebImagePrefetcher 用于图片的预取:

- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls;

这个是专门供外部使用的,SDWebImageView内部并没有使用到这个类.图片预取顾名思义就是在没使用图片之前就先将图片给下载到缓存中,这样使用的时候加载速度就会块很多,这一般是针对比较常用的图片资源。SDWebImagePrefetcherDelegate 会在全部下载结束后,或者单个资源下载完成后进行回调。

3. 图片管理器

SDWebImageManager是整个SDWebImage的核心它持有:

* imageLoader(遵循SDImageLoader)用于从网络上加载图片
* mageCache(遵循SDImageCache)用于从缓存中获取图片
* cacheKeyFilter(遵循SDWebImageCacheKeyFilter)用于缓存key的生成
* cacheSerializer(遵循SDWebImageCacheSerializer)用于对解码后的图片数据进行序列化
* transformer(遵循SDImageTransformer)用于图片后期处理
* optionsProcessor(遵循SDWebImageOptionsProcessor)用于提供全局的option设置。

它通过:

- (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock;

加载图片,这里面包含了全部的加载策略,是整个库的核心

还提供了取消下载的接口:

- (void)cancelAll;

以及查询key的接口:

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url;

代理SDWebImageManagerDelegate

用于判断某个URL是否需要下载
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nonnull NSURL *)imageURL;
用于判断某个URL发生某个错误是否将其标记为失败的URL
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldBlockFailedURL:(nonnull NSURL *)imageURL withError:(nonnull NSError *)error;

3.1 图片转换器

图片转换器是存放在context中的,key为SDWebImageContextImageTransformer,与图片转换器有关的文件如下:

UIImage+Transform.h/.m
SDImageTransformer.h/.m

UIImage+Transform主要是图片转换的实际处理分类,而SDImageTransformer则是负责上层调度的,它会调度UIImage+Transform中的方法对图片进行处理。

我们看下SDWebImage都提供了哪些图片转换器:

SDImagePipelineTransformer 串行流水线图片处理器
之前一直有疑问,context中只能存放一个图片处理器,如果我们需要连续应用多个处理呢?这个对图片库来说是很常见的一个功能,SDImagePipelineTransformer就是用于这种用途,我们只要将所有的处理器添加到transformers数组中就会被应用到当前图片上。
SDImageRoundCornerTransformer 图片圆角处理器
可以指定圆角半径,边界线宽,边界线颜色,圆角位置
SDImageResizingTransformer 缩放处理器
SDImageCroppingTransformer 裁剪处理器
SDImageFlippingTransformer 翻转处理器
SDImageRotationTransformer 旋转处理器
SDImageTintTransformer 颜色修改处理器
SDImageBlurTransformer 模糊处理器
SDImageFilterTransformer 滤镜处理器

3.2 图片加载器

涉及到的类主要有:

SDImageLoader.h/.m
SDImageLoadersManager.h/.m

SDImageLoader里面主要定义了两个解码相关的静态方法SDImageLoaderDecodeImageDataSDImageLoaderDecodeProgressiveImageData,以及SDImageLoader协议。而SDImageLoadersManager则负责管理全部的loader,每个SDWebImageManager都只有一个SDImageLoadersManager,但是SDImageLoadersManager里面却管理着一系列的SDImageLoader,所以整个SDWebImage相当于由多个loader构成。
我们这里看下SDImageLoader

//判断当前url是否需要下载
- (BOOL)canRequestImageForURL:(nullable NSURL *)url;

//请求图片下载
- (nullable id<SDWebImageOperation>)requestImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDImageLoaderCompletedBlock)completedBlock;

//判断某个URL发生某个错误的时候是否需要将它判定为失败的url
- (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
error:(nonnull NSError *)error;

3.2.1 图片下载器

我们知道iOS中图片的下载使用的还是URLSession,而这里的图片下载器就是通过URLSession进行下载的,这个在上面介绍流程源码的时候已经介绍过了,下面将从宏观的角度对这些类进行分析,整个下载器的组成,这里不追究细节。

SDWebImageDownloaderOptions
下载全局选项
SDWebImageDownloadToken
用于指示某个下载的Token,包括url,request,response,以及取消的方法,在启动下载的时候会返回一个DownloadToken,用它可以进行取消任务。
SDWebImageDownloaderConfig
下载器配置类,包括:

  • 最大并发数,默认为6
  • 下载超时时间,默认为15秒
  • 最小进度间隔,指的是上一次进度和本次进度的差值不能小于这个数,如果小于这个数就不通知进度更新
  • operationClass 下载指定的NSOperation具体实现类
  • executionOrder operations执行顺序,默认是先入先出
  • 认证需要的username,password
  • sessionConfiguration

SDWebImageDownloaderRequestModifier
全局请求拦截修改器,如果返回nil表示某个请求将会被取消,如果requestModifier = nil 表示不修改原始的下载请求。
SDWebImageDownloaderResponseModifier
全局返回体拦截修改器,如果返回nil表示当前下载被取消,如果responseModifier = nil 表示不修改原始返回体
SDWebImageDownloaderDecryptor
解密器,它会在图片解码之前对某些加密的图片进行解密,比如某些图片会进行Base64加密,这时候就需要使用解密器进行解密,如果返回nil表示下载失败
SDWebImageDownloaderOperation

除了上面的几个重要类外,还具备了下载器状态查询接口/属性:suspended是否暂停下载,当前下载数currentDownloadCount。

以及下载,取消下载接口:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

- (void)cancelAllDownloads;

SDWebImageDownloaderOperation
SDWebImageDownloaderOperation是这里的重头戏,它的输入是NSURLRequest,NSURLSession,SDWebImageDownloaderOptions,SDWebImageContext。
NSURLRequest,NSURLSession不用多说它是用于创建NSURLSessionTask,SDWebImageDownloaderOptions是用于下载的配置选项,SDWebImageContext用于存放重要组件。

下面是一些比较重要的属性:

//请求体
@property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
//返回体
@property (strong, nonatomic, readonly, nullable) NSURLResponse *response;
//返回异常
@property (strong, nonatomic, nullable) NSError *responseError;
//执行下载请求的dataTask
@property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;
//urlsession
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
//包括进度更新block以及completeblock
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
//下载选项
@property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options;
//重要部件上下文
@property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context;
//图片数据
@property (strong, nonatomic, nullable) NSMutableData *imageData;
//收发数据大小
@property (assign, nonatomic) NSUInteger expectedSize;
@property (assign, nonatomic) NSUInteger receivedSize;
@property (assign, nonatomic) double previousProgress;
//code队列
@property (strong, nonatomic, nonnull) dispatch_queue_t coderQueue;
//当前NSOperation状态
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;
//返回体修改器
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderResponseModifier> responseModifier;
//解密器
@property (strong, nonatomic, nullable) id<SDWebImageDownloaderDecryptor> decryptor;

3.3 图片下载缓存

SDImageCache
SDImageCache是管理图片下载缓存的类,最初的时候个人以为是SDImageCachesManager管理全部的缓存,但是实际上是这货,并且SDImageCachesManager从源码上看并没有实际的用途。我们先来看下SDImageCache,它的重要元素包含:
缓存配置:SDImageCacheConfig
内存缓存:memoryCache
磁盘缓存:diskCache,磁盘缓存根路径 diskCachePath

SDMemoryCache

基于NSCache的内存缓存,主要对象包括缓存配置SDImageCacheConfig,以及缓存对象的增删改查。

SDDiskCache
基于文件的内存缓存,主要对象包括缓存配置SDImageCacheConfig,以及缓存对象的增删改查。

SDWebImageCacheKeyFilter

全局根据url映射到缓存key的类。

SDWebImageCacheSerializer

在缓存之前对图像数据进行处理

SDImageCacheConfig
SDImageCacheOptions

缓存配置

SDImageCacheDefine

缓存模块相关的定义

SDImageCachesManager

用于管理缓存的管理器,但是目前在项目中没有使用

SDImageCachesManagerOperation
用在SDImageCachesManager也是一个NSOperation

同时需要强调的是SDWebImage支持以插件的形式加入YYImage以及YYCache.具体见:SDWebImageYYPlugin
后面也会对这个开源库进行分析。

3.4 图片编解码器

SDImageCodersManager

SDImageCodersManager 顾名思义就是用于全局得管理编解码器,后面添加的编解码器有最高的优先级

SDImageCoderOption
解码器配置选项

SDImageCoder/SDProgressiveImageCoder

SDImageCoder 是一个比较重要的协议,是所有编解码器都需要遵循的协议:

@protocol SDImageCoder <NSObject>

@required
//给定的数据能否被当前编解码器解码
- (BOOL)canDecodeFromData:(nullable NSData *)data;

//使用当前解码器对图像数据进行解码
- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data
options:(nullable SDImageCoderOptions *)options;

#pragma mark - Encoding

//当前编解码器是否能够编码到指定的格式
- (BOOL)canEncodeToFormat:(SDImageFormat)format NS_SWIFT_NAME(canEncode(to:));

//使用当前编码器对图像数据进行编码
- (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image
format:(SDImageFormat)format
options:(nullable SDImageCoderOptions *)options;
@end

@protocol SDProgressiveImageCoder <SDImageCoder>

@required

//是否能够渐进解码
- (BOOL)canIncrementalDecodeFromData:(nullable NSData *)data;

- (nonnull instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options;

//更新增量图片数据
- (void)updateIncrementalData:(nullable NSData *)data finished:(BOOL)finished;

//进行渐进式解码
- (nullable UIImage *)incrementalDecodedImageWithOptions:(nullable SDImageCoderOptions *)options;

@end

SDAnimatedImageProvider
用于提供基本动图所需要遵循的协议,目前SDAnimatedImage以及SDAnimatedImageCoder会遵循这个协议。

SDImageIOCoder

用于对PNG, JPEG, TIFF 这些格式进行编解码,它也支持GIF以及HEIC这些动图格式,但是GIF只支持第一帧,HEIC需要看各个系统的支持情况。对于这些动图建议使用SDAnimatedImageView

SDAnimatedImageCoder
SDAnimatedImageCoder是所有动图编解码器的基类,下面将要介绍的SDImageAPNGCoder,SDImageGIFCoder,SDImageHEICCoder都是这个类的子类。

SDImageAPNGCoder
APNG动画格式编解码器,关于APNG格式介绍,大家可以查看如下文章:

SDImageCoderHelper
SDImageCoderHelper是上层编解码器的公共方法类。

3.5 动图相关类

SDAnimatedImage
SDAnimatedImage 为动图数据类和UIImage对应
SDAnimatedImageView
SDAnimatedImageView 为动图显示View和UIImageView类对应,内部持有SDAnimatedImagePlayer
SDAnimatedImagePlayer
SDAnimatedImagePlayer 用于播放动画
SDWebImageTransition
SDWebImageTransition为图片加载结束后呈现图片时候使用的转场动画,目前支持fadeTransition,flipFromLeftTransition,flipFromRightTransition,flipFromTopTransition,flipFromBottomTransition,curlUpTransition,curlDownTransition几种。

****3.6 SDWebImageOptionsProcessor ****

全局Options处理器,用于针对所有的Options设置进行过滤处理。

****3.7 SDWebImageIndicator ****

进度指示器,目前主要支持两种:SDWebImageActivityIndicator和SDWebImageProgressIndicator

开源代码信息

开源地址

YTKNetwork是基于AFNetworking封装的iOS网络库,它在AFNetworking的基础上提供了如下的功能:

  • 支持按时间缓存网络请求内容
  • 支持按版本号缓存网络请求内容
  • 支持统一设置服务器和 CDN 的地址
  • 支持检查返回 JSON 内容的合法性
  • 支持文件的断点续传
  • 支持 block 和 delegate 两种模式的回调方式
  • 支持批量的网络请求发送,并统一设置它们的回调(实现在 YTKBatchRequest 类中)
  • 支持方便地设置有相互依赖的网络请求的发送,例如:发送请求 A,根据请求 A 的结果,选择性的发送请求 B 和 C,再根据 B 和 C 的结果,选择性的发送请求 D。(实现在 YTKChainRequest 类中)
  • 支持网络请求 URL 的 filter,可以统一为网络请求加上一些参数,或者修改一些路径。
  • 定义了一套插件机制,可以很方便地为 YTKNetwork 增加功能。猿题库官方现在提供了一个插件,可以在某些网络请求发起时,在界面上显示“正在加载”的 HUD。

源码分析

YTKNetWork 整个代码量不是很大,整个代码结构如下所示:

在整个请求过程中都是围绕着Request展开,这里需要注意的是YTKBatchRequest以及YTKChainRequest并不继承自YTKBaseRequest,它其实是通过请求数组requestArray来管理请求的形式,所以个人觉得这两个类的命名不是很合理,很容易让人以为它们和YTKRequest一样都是继承自YTKBaseRequest。另一个个人觉得不是很合理的地方是它将全部的数据都放到YTKRequest,会显得庞大且乱。

这篇博客将通过如下几个方面对YTKNetWork源码进行解析:

* YTKRequest && YTKBaseRequest 的组成
* 基本请求过程
* 批量请求
* 链式请求
* 缓存机制
* 断点续传
* URL filter
* 插件机制
1. YTKRequest && YTKBaseRequest 的组成

YTKRequest


@interface YTKBaseRequest : NSObject

#pragma mark - Request and Response Information
///=============================================================================
/// @name Request and Response Information
///=============================================================================

/// 当前请求执行所使用的NSURLSessionTask,在请求start之前不应该调用这个值,因为这时候这个值为nil
@property (nonatomic, strong, readonly) NSURLSessionTask *requestTask;
/// requestTask.currentRequest的快捷访问方式
@property (nonatomic, strong, readonly) NSURLRequest *currentRequest;
/// requestTask.originalRequest的快捷访问方式
@property (nonatomic, strong, readonly) NSURLRequest *originalRequest;
/// requestTask.response的快捷访问方式.
@property (nonatomic, strong, readonly) NSHTTPURLResponse *response;
/// 返回体状态码
@property (nonatomic, readonly) NSInteger responseStatusCode;
/// 头信息
@property (nonatomic, strong, readonly, nullable) NSDictionary *responseHeaders;
/// response的原始信息,在失败的时候值为nil
@property (nonatomic, strong, readonly, nullable) NSData *responseData;
/// response的字符串形式信息,在失败的时候值为nil
@property (nonatomic, strong, readonly, nullable) NSString *responseString;
/// 序列化后的结果对象,在下载的情景下并且在使用resumableDownloadPath的时候,这个值代表文件存放的地址
@property (nonatomic, strong, readonly, nullable) id responseObject;
/// JSON形式的结果
@property (nonatomic, strong, readonly, nullable) id responseJSONObject;
// 错误信息
@property (nonatomic, strong, readonly, nullable) NSError *error;
/// 当前请求是否被取消
@property (nonatomic, readonly, getter=isCancelled) BOOL cancelled;
/// 当前请求是否正在执行
@property (nonatomic, readonly, getter=isExecuting) BOOL executing;


#pragma mark - Request Configuration
///=============================================================================
/// @name Request Configuration
///=============================================================================

/// 用于标示一个请求的 tag
@property (nonatomic) NSInteger tag;
/// 用于存储关于请求的额外信息
@property (nonatomic, strong, nullable) NSDictionary *userInfo;
/// 请求代理,如果选择用block方式来交付数据可以忽略这个值
@property (nonatomic, weak, nullable) id<YTKRequestDelegate> delegate;
/// 请求成功后将会调用的Block,如果delegate以及block都指定都话会先调用delegate然后再调用block,并且这个block会在主线程调用
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock successCompletionBlock;
/// 请求失败都时候调用的Block,规则同successCompletionBlock
@property (nonatomic, copy, nullable) YTKRequestCompletionBlock failureCompletionBlock;
/// 当前请求所添加的插件
@property (nonatomic, strong, nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;
/// 用于POST形式下组装请求body的block
@property (nonatomic, copy, nullable) AFConstructingBlock constructingBodyBlock;
/// 这个用于断点续传的情况,用于表示下载的临时文件的存放路径。在请求start之前将会删除这些文件,如果下载成功文件将会自动下载到这个路径,否则下载结果将会保存到
/// responseData,responseString。
@property (nonatomic, strong, nullable) NSString *resumableDownloadPath;
/// 用于跟踪文件的下载进度
@property (nonatomic, copy, nullable) AFURLSessionTaskProgressBlock resumableDownloadProgressBlock;
/// 请求优先级
@property (nonatomic) YTKRequestPriority requestPriority;
/// 请求完成的回调block
- (void)setCompletionBlockWithSuccess:(nullable YTKRequestCompletionBlock)success
failure:(nullable YTKRequestCompletionBlock)failure;
/// 清除回调完成后结果交付block
- (void)clearCompletionBlock;
/// 添加插件
- (void)addAccessory:(id<YTKRequestAccessory>)accessory;

#pragma mark - Request Action
///=============================================================================
/// @name Request Action
///=============================================================================

/// 开始请求
- (void)start;
/// 停止请求
- (void)stop;
/// 开始请求
- (void)startWithCompletionBlockWithSuccess:(nullable YTKRequestCompletionBlock)success
failure:(nullable YTKRequestCompletionBlock)failure;

#pragma mark - Subclass Override
///=============================================================================
/// @name Subclass Override
///=============================================================================

/// 在后台获得到数据但是尚未切换到主线程之前调用这个方法。
- (void)requestCompletePreprocessor;
/// 在请求成功并且切换到主线程后调用这个方法
- (void)requestCompleteFilter;
/// 在后台请求失败但是尚未切换到主线程之前调用这个方法。
- (void)requestFailedPreprocessor;
/// 请求失败并且切换到主线程后调用这个方法
- (void)requestFailedFilter;
/// URL的host部分 比如 http://www.example.com.
- (NSString *)baseUrl;
/// 它可以有两种形式,一种是全路径形式,这时候就会忽略baseUrl直接使用requestUrl,
/// 另一种是这个值只包含url的路径信息比如/v1/user,这时候会将requestUrl与baseUrl进行拼接
- (NSString *)requestUrl;
/// cdn地址
- (NSString *)cdnUrl;
/// 请求的超时时间默认是60s
- (NSTimeInterval)requestTimeoutInterval;
/// 额外的请求参数,GET的时候添加到路径上,POST的时候添加到body
- (nullable id)requestArgument;
/// HTTP 请求方式
- (YTKRequestMethod)requestMethod;
/// 请求序列化类型
- (YTKRequestSerializerType)requestSerializerType;
/// 结构体序列化类型
- (YTKResponseSerializerType)responseSerializerType;
/// HTTP 认证参数数组
- (nullable NSArray<NSString *> *)requestAuthorizationHeaderFieldArray;
/// 额外的请求头
- (nullable NSDictionary<NSString *, NSString *> *)requestHeaderFieldValueDictionary;
/// 用于构建自定义请求,如果这个返回值为非空,那么上面所有用于构建请求的就全部被忽略,而统一使用这个请求
- (nullable NSURLRequest *)buildCustomUrlRequest;
/// 是否使用cdn
- (BOOL)useCDN;
/// 是否允许使用蜂窝网络
- (BOOL)allowsCellularAccess;
/// 用于校验responseJSONObject结构
- (nullable id)jsonValidator;
/// 用于校验responseStatusCode
- (BOOL)statusCodeValidator;

@end

YTKRequest

YTKRequest主要是在YTKBaseRequest添加了缓存的相关管理。

@interface YTKRequest : YTKBaseRequest

/// 是否忽略缓存结果,需要注意的是cacheTimeInSeconds默认值为-1所以如果我们没有明确给出一个cacheTimeInSeconds值的话实际上我们还是会忽略缓存
@property (nonatomic) BOOL ignoreCache;

/// 判断当前返回的数据是否是从缓存中获取而来
- (BOOL)isDataFromCache;

///手动从存储中加载缓存
- (BOOL)loadCacheWithError:(NSError * __autoreleasing *)error;

///以忽略缓存的形式启动请求,这个一般用于更新缓存数据
- (void)startWithoutCache;

// 缓存返回数据到缓存文件
- (void)saveResponseDataToCacheFile:(NSData *)data;

#pragma mark - Subclass Override

/// 缓存时间
- (NSInteger)cacheTimeInSeconds;

/// 缓存版本
- (long long)cacheVersion;

- (nullable id)cacheSensitiveData;

/// 是否异步写缓存
- (BOOL)writeCacheAsynchronously;

@end

2. 基本请求过程

基本请求这块代码都放在YTKBaseRequest以及YTKNetworkAgent中,YTKBaseRequest会将实际的处理封装在YTKNetworkAgent,我们就来看下这部分代码:

请求的发起

一般请求我们会通过startWithCompletionBlockWithSuccess来执行:

- (void)startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success
failure:(YTKRequestCompletionBlock)failure {
//设置successBlock 以及failureBlock
[self setCompletionBlockWithSuccess:success failure:failure];
//启动请求
[self start];
}

- (void)start {
//通知插件将要开始请求
[self toggleAccessoriesWillStartCallBack];
//将请求添加到YTKNetworkAgent
[[YTKNetworkAgent sharedAgent] addRequest:self];
}

请求过程中会先设置成功,失败结果返回的block,然后通知插件请求将要开始了,插件可以根据实际需求做对应处理,比如显示加载动画等,然后就将请求传递给YTKNetworkAgent。

- (void)addRequest:(YTKBaseRequest *)request {

NSError * __autoreleasing requestSerializationError = nil;
//如果请求中有自定义的url则使用自定义的NSURLRequest创建对应的NSURLSessionDataTask
NSURLRequest *customUrlRequest= [request buildCustomUrlRequest];
if (customUrlRequest) {
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {
[self handleRequestResult:dataTask responseObject:responseObject error:error];
}];
request.requestTask = dataTask;
} else {
//直接通过sessionTaskForRequest获取NSURLSessionDataTask
request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError];
}

if (requestSerializationError) {
[self requestDidFailWithRequest:request error:requestSerializationError];
return;
}

// 设置请求的优先级
if ([request.requestTask respondsToSelector:@selector(priority)]) {
switch (request.requestPriority) {
case YTKRequestPriorityHigh:
request.requestTask.priority = NSURLSessionTaskPriorityHigh;
break;
case YTKRequestPriorityLow:
request.requestTask.priority = NSURLSessionTaskPriorityLow;
break;
case YTKRequestPriorityDefault:
/*!!fall through*/
default:
request.requestTask.priority = NSURLSessionTaskPriorityDefault;
break;
}
}
// Retain request
[self addRequestToRecord:request];
//启动请求
[request.requestTask resume];
}

在YTKNetworkAgent中我们首先会看下是否开发者在对应等request中重写了buildCustomUrlRequest方法,如果是的话则通过这个返回的request 生成NSURLSessionDataTask,否则使用传入的request通过调用sessionTaskForRequest生成NSURLSessionDataTask,紧接着给请求设置优先级,并且保持请求引用。然后开启请求。


- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error {

// 取出请求方法
YTKRequestMethod method = [request requestMethod];
// 构建URL
NSString *url = [self buildRequestUrl:request];
// 准备请求参数
id param = request.requestArgument;
// 构建POST请求体Block
AFConstructingBlock constructingBlock = [request constructingBodyBlock];
// 通过请求来构建请求序列化器
AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request];

//通过请求方法来构建出不同的NSURLSessionTask
switch (method) {
case YTKRequestMethodGET:
if (request.resumableDownloadPath) {
return [self downloadTaskWithDownloadPath:request.resumableDownloadPath
requestSerializer:requestSerializer
URLString:url
parameters:param
progress:request.resumableDownloadProgressBlock
error:error];
} else {
return [self dataTaskWithHTTPMethod:@"GET"
requestSerializer:requestSerializer
URLString:url
parameters:param
error:error];
}
case YTKRequestMethodPOST:
return [self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error];
case YTKRequestMethodHEAD:
return [self dataTaskWithHTTPMethod:@"HEAD" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodPUT:
return [self dataTaskWithHTTPMethod:@"PUT" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodDELETE:
return [self dataTaskWithHTTPMethod:@"DELETE" requestSerializer:requestSerializer URLString:url parameters:param error:error];
case YTKRequestMethodPATCH:
return [self dataTaskWithHTTPMethod:@"PATCH" requestSerializer:requestSerializer URLString:url parameters:param error:error];
}
}

在sessionTaskForRequest方法会将YTKBaseRequest中的一系列配置拆开,通过在YTKBaseRequest中的配置来创建不同请求方法下的dataTask。首先看下请求url的构建策略:

请求url的构建是通过buildRequestUrl来实现的


- (NSString *)buildRequestUrl:(YTKBaseRequest *)request {

//获取请求的requestUrl使用它来构建出一个NSURL对象
NSString *detailUrl = [request requestUrl];
NSURL *temp = [NSURL URLWithString:detailUrl];
// 检查构建出来的URL是否是完整的URL请求
if (temp && temp.host && temp.scheme) {
return detailUrl;
}
// 对构建出来的URL通过Config中预设的urlFilters 一一对它进行处理,比如添加公共字段等
NSArray *filters = [_config urlFilters];
for (id<YTKUrlFilterProtocol> f in filters) {
detailUrl = [f filterUrl:detailUrl withRequest:request];
}

//构建baseUrl 如果使用CDN,并且cdnUrl有值,则baseUrl为cdnUrl,
//如果不使用CDN则优先使用当前request的baseUrl否则使用公共配置中的baseUrl
NSString *baseUrl;
if ([request useCDN]) {
if ([request cdnUrl].length > 0) {
baseUrl = [request cdnUrl];
} else {
baseUrl = [_config cdnUrl];
}
} else {
if ([request baseUrl].length > 0) {
baseUrl = [request baseUrl];
} else {
baseUrl = [_config baseUrl];
}
}
// URL slash compability
NSURL *url = [NSURL URLWithString:baseUrl];

if (baseUrl.length > 0 && ![baseUrl hasSuffix:@"/"]) {
url = [url URLByAppendingPathComponent:@""];
}
//将当前的baseUrl与detailUrl进行拼接
return [NSURL URLWithString:detailUrl relativeToURL:url].absoluteString;
}

首先会先取requestUrl看下是不是一个完整的URL请求,如果是的话就直接将这个作为请求返回,紧接着会从YTKNetWorkConfig中获取预设的urlFilters,一一对它进行处理,比如添加公共字段等。最终形成一个处理后的不包括scheme,host的URL。

接下来就是构建baseUrl的过程,这个顺序如下:

request --> cdnUrl  >  _config ---> cdnUrl  > request---> baseUrl > _config ---> baseUrl

总结来说就是能使用cdn的优先使用cdn的地址,不能使用的时候再选用baseUrl中的地址,能使用request的优先使用request的,没有的话使用config中的。最后将detailUrl和baseUrl进行拼接形成最终的地址。所以整个地址包含三个部分,一个是baseUrl,一个是requestUrl,然后是各个filter添加的信息。

接下来看下dataTask的创建这里分成三类,一个是GET方式,一个是POST方式,一个是download方式:

GET

[self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error];

POST

[self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error];

DOWNLOAD

[self downloadTaskWithDownloadPath:request.resumableDownloadPath
requestSerializer:requestSerializer
URLString:url parameters:param
progress:request.resumableDownloadProgressBlock
error:error];

上面GET POST 最终都走到下面的方法中,无非就是constructingBodyWithBlock空与非空的区别,

- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
constructingBodyWithBlock:(nullable void (^)(id <AFMultipartFormData> formData))block
error:(NSError * _Nullable __autoreleasing *)error {
NSMutableURLRequest *request = nil;

//如果constructingBodyWithBlock非空的话使用multipartFormRequestWithMethod
if (block) {
request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error];
} else {
request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error];
}
__block NSURLSessionDataTask *dataTask = nil;
dataTask = [_manager dataTaskWithRequest:request
completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) {
[self handleRequestResult:dataTask responseObject:responseObject error:_error];
}];

return dataTask;
}

constructingBodyWithBlock有值的时候我们会使用请求序列化中的multipartFormRequestWithMethod进行处理,如果无值的话就会调用请求序列化器中的requestWithMethod进行处理。对请求处理后就可以使用该请求对象生成dataTask了,这部分没什么特殊的地方,我们关键来看下下载的部分:

- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
error:(NSError * _Nullable __autoreleasing *)error {
// add parameters to URL;
NSMutableURLRequest *urlRequest = [requestSerializer requestWithMethod:@"GET" URLString:URLString parameters:parameters error:error];

NSString *downloadTargetPath;
BOOL isDirectory;
if(![[NSFileManager defaultManager] fileExistsAtPath:downloadPath isDirectory:&isDirectory]) {
isDirectory = NO;
}
// If targetPath is a directory, use the file name we got from the urlRequest.
// Make sure downloadTargetPath is always a file, not directory.
// 构建下载路径downloadTargetPath
if (isDirectory) {
//如果给定的downloadpath是目录路径,那么将会将下载url下的最后部分作为文件名添加到最后,形成最终的下载文件地址
NSString *fileName = [urlRequest.URL lastPathComponent];
downloadTargetPath = [NSString pathWithComponents:@[downloadPath, fileName]];
} else {
downloadTargetPath = downloadPath;
}

// 在下载前如果文件存在先直接删除? 直接删除不先下到缓存再?
if ([[NSFileManager defaultManager] fileExistsAtPath:downloadTargetPath]) {
[[NSFileManager defaultManager] removeItemAtPath:downloadTargetPath error:nil];
}

BOOL resumeSucceeded = NO;
__block NSURLSessionDownloadTask *downloadTask = nil;
//未下载完成存储路径
NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:downloadPath];
if (localUrl != nil) {
// 是否存在未下载完成的文件
BOOL resumeDataFileExists = [[NSFileManager defaultManager] fileExistsAtPath:localUrl.path];
// 加载数据
NSData *data = [NSData dataWithContentsOfURL:localUrl];
// 数据是否合法
BOOL resumeDataIsValid = [YTKNetworkUtils validateResumeData:data];

//是否可以恢复
BOOL canBeResumed = resumeDataFileExists && resumeDataIsValid;
// Try to resume with resumeData.
// Even though we try to validate the resumeData, this may still fail and raise excecption.
if (canBeResumed) {
@try {
downloadTask = [_manager downloadTaskWithResumeData:data progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
return [NSURL fileURLWithPath:downloadTargetPath isDirectory:NO];
} completionHandler:
^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
[self handleRequestResult:downloadTask responseObject:filePath error:error];
}];
resumeSucceeded = YES;
} @catch (NSException *exception) {
YTKLog(@"Resume download failed, reason = %@", exception.reason);
resumeSucceeded = NO;
}
}
}
//重新开始下载
if (!resumeSucceeded) {
downloadTask = [_manager downloadTaskWithRequest:urlRequest progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
return [NSURL fileURLWithPath:downloadTargetPath isDirectory:NO];
} completionHandler:
^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
[self handleRequestResult:downloadTask responseObject:filePath error:error];
}];
}
return downloadTask;
}
  1. 首先会先通过判断传进来的downloadPath是否是目录名称,如果是则将url最后的部分作为文件名添加到path最后,如果是文件名,则直接作为目标下载地址
  2. 紧接着如果要下载的文件已经存在那么在下载之前先删除这个文件
  3. 然后再去临时目录去看是否有之前下载一半没有下载结束的缓存文件,临时目录下的这些缓存文件命名规则大家可以看下这部分代码:
- (NSURL *)incompleteDownloadTempPathForDownloadPath:(NSString *)downloadPath {
NSString *tempPath = nil;
//通过下载路径生成MD5字符串 最终生成 temp路径 + Incomplete + MD5[downloadPath]
NSString *md5URLString = [YTKNetworkUtils md5StringFromString:downloadPath];
tempPath = [[self incompleteDownloadTempCacheFolder] stringByAppendingPathComponent:md5URLString];
return tempPath == nil ? nil : [NSURL fileURLWithPath:tempPath];
}
  1. 如果有上一次下载一半的数据,就先加载到内存,验证数据的合法性,如果数据存在,并且合法,就通过downloadTaskWithResumeData创建dataTask,否则通过downloadTaskWithRequest来创建。

请求结束处理

请求结束统一调用handleRequestResult进行处理:

- (void)handleRequestResult:(NSURLSessionTask *)task responseObject:(id)responseObject error:(NSError *)error {

//取出请求
Lock();
YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)];
Unlock();

// When the request is cancelled and removed from records, the underlying
// AFNetworking failure callback will still kicks in, resulting in a nil `request`.
//
// Here we choose to completely ignore cancelled tasks. Neither success or failure
// callback will be called.
if (!request) {
return;
}

YTKLog(@"Finished Request: %@", NSStringFromClass([request class]));

NSError * __autoreleasing serializationError = nil;
NSError * __autoreleasing validationError = nil;

NSError *requestError = nil;
BOOL succeed = NO;

//将结果传递给responseObject
request.responseObject = responseObject;

//通过对应的response序列化器进行处理
if ([request.responseObject isKindOfClass:[NSData class]]) {

request.responseData = responseObject;
request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];

switch (request.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Default serializer. Do nothing.
break;
case YTKResponseSerializerTypeJSON:
request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError];
request.responseJSONObject = request.responseObject;
break;
case YTKResponseSerializerTypeXMLParser:
request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError];
break;
}
}
if (error) {
succeed = NO;
requestError = error;
} else if (serializationError) {
succeed = NO;
requestError = serializationError;
} else {
succeed = [self validateResult:request error:&validationError];
requestError = validationError;
}

//通知到外部
if (succeed) {
[self requestDidSucceedWithRequest:request];
} else {
[self requestDidFailWithRequest:request error:requestError];
}

dispatch_async(dispatch_get_main_queue(), ^{
//清除成功失败回调
[self removeRequestFromRecord:request];
[request clearCompletionBlock];
});
}

根据上面加载的请求发起的流程来看,不难猜出下载结束后的工作应该是,将请求数据通过response 序列化器进行处理后存放到YTKBaseRequest中,然后通过对应的block以及delegte将数据交付到应用层,然后将delegate以及block清除,但是我们知道请求返回后AFNetWorking实际上返回的是task,responseOject,这时候并不知道具体是哪个YTKBaseRequest,这个问题是怎么解决的呢?我们知道在addRequest的时候会调用****[self addRequestToRecord:request]****

- (void)addRequestToRecord:(YTKBaseRequest *)request {
Lock();
_requestsRecord[@(request.requestTask.taskIdentifier)] = request;
Unlock();
}

它会以dataTask的taskIdentifier为key YTKBaseRequest为value建立一个映射表,当请求结束的时候我们得到的是一个dataTask,那么就可以通过它的taskIdentifier作为key,获得对应的YTKBaseRequest,然后将resonse序列化器处理后的结果塞到YTKBaseRequest中。这一切都结束后再将映射关系从映射表中删除。

我们再继续看下请求成功和失败后会怎么处理接下来的流程:


- (void)requestDidSucceedWithRequest:(YTKBaseRequest *)request {
@autoreleasepool {
[request requestCompletePreprocessor];
}
dispatch_async(dispatch_get_main_queue(), ^{

//通过外部的插件将要停止请求
[request toggleAccessoriesWillStopCallBack];
[request requestCompleteFilter];

// 通过代理通知请求已经结束
if (request.delegate != nil) {
[request.delegate requestFinished:request];
}
// 通过block通知请求已经结束
if (request.successCompletionBlock) {
request.successCompletionBlock(request);
}
//通知外部的插件已经结束
[request toggleAccessoriesDidStopCallBack];
});
}

请求成功后在还没切回主线程之前会先调用requestCompletePreprocessor,在这里相当于埋下一个勾子,外面可以在这里实现类似统计之类的功能,紧接着切换到主线程,通知外部插件请求将要结束了,然后再调用requestCompleteFilter,这个和requestCompletePreprocessor功能类似,区别是一个在后台线程一个在主线程,紧接着就是通过代理和block将数据交付给上层,最后通知插件该请求结束。

- (void)requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error {
request.error = error;
YTKLog(@"Request %@ failed, status code = %ld, error = %@",
NSStringFromClass([request class]), (long)request.responseStatusCode, error.localizedDescription);

// 从userInfo 中取出下载没完成的数据
NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData];
NSURL *localUrl = nil;
if (request.resumableDownloadPath) {
localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath];
}
//存放到指定的路径
if (incompleteDownloadData && localUrl != nil) {
[incompleteDownloadData writeToURL:localUrl atomically:YES];
}

// 将文件从url中取出,如果是文件的话就删除下载失败的文件
if ([request.responseObject isKindOfClass:[NSURL class]]) {
NSURL *url = request.responseObject;
if (url.isFileURL && [[NSFileManager defaultManager] fileExistsAtPath:url.path]) {
request.responseData = [NSData dataWithContentsOfURL:url];
request.responseString = [[NSString alloc] initWithData:request.responseData encoding:[YTKNetworkUtils stringEncodingWithRequest:request]];
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
}
request.responseObject = nil;
}

@autoreleasepool {
[request requestFailedPreprocessor];
}

//在主线程通过代理,block通知外部下载失败
dispatch_async(dispatch_get_main_queue(), ^{
[request toggleAccessoriesWillStopCallBack];
[request requestFailedFilter];

if (request.delegate != nil) {
[request.delegate requestFailed:request];
}
if (request.failureCompletionBlock) {
request.failureCompletionBlock(request);
}
[request toggleAccessoriesDidStopCallBack];
});
}

请求失败的话大体上和请求成功在流程上是一致的,只不过会在失败的时候通过****error.userInfo[NSURLSessionDownloadTaskResumeData]****拿到已经获取的数据,这时候就可以将这数据存储起来供下一次继续请求的时候实现断点续传。

最后看下取消请求的流程:

- (void)cancelRequest:(YTKBaseRequest *)request {
NSParameterAssert(request != nil);

//如果有指定恢复下载文件的路径则取消后将数据写到恢复下载文件的地方
if (request.resumableDownloadPath && [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] != nil) {
NSURLSessionDownloadTask *requestTask = (NSURLSessionDownloadTask *)request.requestTask;
[requestTask cancelByProducingResumeData:^(NSData *resumeData) {
NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath];
[resumeData writeToURL:localUrl atomically:YES];
}];
} else {
//直接调用NSURLSessionDataTask cancel
[request.requestTask cancel];
}
[self removeRequestFromRecord:request];
//清除请求成功失败的block
[request clearCompletionBlock];
}

和请求失败处理一样可以做临时数据缓存,只不过需要通过NSURLSessionDownloadTask 的 cancelByProducingResumeData方法拿到已经获取的数据。再将数据写到临时目录下。

3. 批量请求

批量请求没啥需要介绍的,就是用一个数组将YTKRequst存放起来,在start的时候一个个调用YTKRequest的start方法:

- (void)start {
if (_finishedCount > 0) {
YTKLog(@"Error! Batch request has already started.");
return;
}
_failedRequest = nil;
[[YTKBatchRequestAgent sharedAgent] addBatchRequest:self];
[self toggleAccessoriesWillStartCallBack];
for (YTKRequest * req in _requestArray) {
req.delegate = self;
[req clearCompletionBlock];
[req start];
}
}
4. 链式请求

链式请求是一系列有依赖关系的请求,它会在某个请求执行完毕后,进行下一个请求的启动。接下来我们以添加请求启动请求请求结束后启动下一个请求三个部分进行简单展开:

1. 添加请求

- (void)addRequest:(YTKBaseRequest *)request callback:(YTKChainCallback)callback {
[_requestArray addObject:request];
if (callback != nil) {
[_requestCallbackArray addObject:callback];
} else {
[_requestCallbackArray addObject:_emptyCallback];
}
}

添加请求是通过addRequest:callback方法来完成的,每个请求与callback是一一对应的,如果外面传进来一个nil callback,那么_requestCallbackArray就会添加一个空请求,作为占位。

2. 启动请求

- (void)start {
if (_nextRequestIndex > 0) {
YTKLog(@"Error! Chain request has already started.");
return;
}
if ([_requestArray count] > 0) {
[self toggleAccessoriesWillStartCallBack];
[self startNextRequest];
[[YTKChainRequestAgent sharedAgent] addChainRequest:self];
} else {
YTKLog(@"Error! Chain request array is empty.");
}
}

- (BOOL)startNextRequest {
if (_nextRequestIndex < [_requestArray count]) {
YTKBaseRequest *request = _requestArray[_nextRequestIndex];
_nextRequestIndex++;
request.delegate = self;
[request clearCompletionBlock];
[request start];
return YES;
} else {
return NO;
}
}

这个会按照请求数组的顺序一个一个取出来,调用start方法,这个就不做过多介绍了。

请求结束后启动下一个请求

- (void)requestFinished:(YTKBaseRequest *)request {
NSUInteger currentRequestIndex = _nextRequestIndex - 1;
YTKChainCallback callback = _requestCallbackArray[currentRequestIndex];
callback(self, request);
if (![self startNextRequest]) {
[self toggleAccessoriesWillStopCallBack];
if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) {
[_delegate chainRequestFinished:self];
[[YTKChainRequestAgent sharedAgent] removeChainRequest:self];
}
[self toggleAccessoriesDidStopCallBack];
}
}

请求结束后会从_requestCallbackArray中获取对应的YTKChainCallback进行处理,然后调用startNextRequest开启下一个请求。

5. 缓存机制

前面已经提到YTKNetWork的缓存机制都保存在YTKRequest中,所以这部分我们来看下YTKReques:

依旧从启动请求开始:

- (void)start {
if (self.ignoreCache) {
[self startWithoutCache];
return;
}

// Do not cache download request.
if (self.resumableDownloadPath) {
[self startWithoutCache];
return;
}

if (![self loadCacheWithError:nil]) {
[self startWithoutCache];
return;
}

_dataFromCache = YES;

dispatch_async(dispatch_get_main_queue(), ^{
[self requestCompletePreprocessor];
[self requestCompleteFilter];
YTKRequest *strongSelf = self;
[strongSelf.delegate requestFinished:strongSelf];
if (strongSelf.successCompletionBlock) {
strongSelf.successCompletionBlock(strongSelf);
}
[strongSelf clearCompletionBlock];
});
}

在启动的时候会首先根据当前情况选择是否使用缓存数据,如果我们在YTKRequest中配置了ignoreCache,或者在下载数据的情景中并且配置了resumableDownloadPath,再或者缓存加载失败都会启动无缓存请求模式,
除了上述情况会先通过*loadCacheWithError**将缓存再本地的数据加载到内存中:

- (BOOL)loadCacheWithError:(NSError * _Nullable __autoreleasing *)error {
// 检查缓存时间
if ([self cacheTimeInSeconds] < 0) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheTime userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache time"}];
}
return NO;
}

// 加载缓存信息数据
if (![self loadCacheMetadata]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidMetadata userInfo:@{ NSLocalizedDescriptionKey:@"Invalid metadata. Cache may not exist"}];
}
return NO;
}

// 检查缓存数据
if (![self validateCacheWithError:error]) {
return NO;
}

// 加载缓存
if (![self loadCacheData]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheData userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache data"}];
}
return NO;
}

return YES;
}

在加载数据之前会先检查下我们配置的缓存时间,如果cacheTimeInSeconds小于0的话就不再继续加载了,这时候会抛出错误。缓存数据加载失败走正常数据请求流程。否则再紧接着缓存加载的流程:

加载缓存信息数据loadCacheMetadata,检查缓存数据的合法性validateCacheWithError,加载数据loadCacheData,下面就针对这些环节进行介绍:

loadCacheMetadata

在加载缓存数据之前会先加载缓存的信息文件

- (BOOL)loadCacheMetadata {
NSString *path = [self cacheMetadataFilePath];
NSFileManager * fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
@try {
_cacheMetadata = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
return YES;
} @catch (NSException *exception) {
YTKLog(@"Load cache metadata failed, reason = %@", exception.reason);
return NO;
}
}
return NO;
}

那么缓存信息文件存在哪里呢?我们怎么拿现有的配置取到缓存的信息文件?带着这个疑问继续看下面的代码:

- (NSString *)cacheFileName {
NSString *requestUrl = [self requestUrl];
NSString *baseUrl = [YTKNetworkConfig sharedConfig].baseUrl;
id argument = [self cacheFileNameFilterForRequestArgument:[self requestArgument]];
NSString *requestInfo = [NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",
(long)[self requestMethod], baseUrl, requestUrl, argument];
NSString *cacheFileName = [YTKNetworkUtils md5StringFromString:requestInfo];
return cacheFileName;
}

- (NSString *)cacheMetadataFilePath {
NSString *cacheMetadataFileName = [NSString stringWithFormat:@"%@.metadata", [self cacheFileName]];
NSString *path = [self cacheBasePath];
path = [path stringByAppendingPathComponent:cacheMetadataFileName];
return path;
}

- (NSString *)cacheBasePath {
NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache"];

// Filter cache base path
NSArray<id<YTKCacheDirPathFilterProtocol>> *filters = [[YTKNetworkConfig sharedConfig] cacheDirPathFilters];
if (filters.count > 0) {
for (id<YTKCacheDirPathFilterProtocol> f in filters) {
path = [f filterCacheDirPath:path withRequest:self];
}
}

[self createDirectoryIfNeeded:path];
return path;
}


我们先来看下requestInfo这个字符串是怎么来的:

[NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@",(long)[self requestMethod], baseUrl, requestUrl, argument];

requestInfo包含着Method,URL(Host:Url),Argument,也就是说缓存文件信息文件名里面包含着一个请求的全部信息。在通过对这个字符串进行md5加密,生成的字符串作为缓存信息文件名。
最终文件存放在:

/[系统缓存路径]/LazyRequestCache[这个名称会经过cacheDirPathFilters进行处理,默认是LazyRequestCache]/xxxxxxxxxxxx.metadata

可以通过cacheDirPathFilters添加版本信息等公共的处理。

加载出来的对象结构如下:


@interface YTKCacheMetadata : NSObject<NSSecureCoding>

@property (nonatomic, assign) long long version;
@property (nonatomic, strong) NSString *sensitiveDataString;
@property (nonatomic, assign) NSStringEncoding stringEncoding;
@property (nonatomic, strong) NSDate *creationDate;
@property (nonatomic, strong) NSString *appVersionString;

@end

包括缓存版本,关键字段,字符编码格式,创建时间,app版本信息。后面将会重点看下怎么利用这些信息来实现缓存策略。

validateCacheWithError

上面已经成功地加载了缓存信息文件,接下来就是对这些进行缓存信息进行校验了:

- (BOOL)validateCacheWithError:(NSError * _Nullable __autoreleasing *)error {
// Date
NSDate *creationDate = self.cacheMetadata.creationDate;
NSTimeInterval duration = -[creationDate timeIntervalSinceNow];
if (duration < 0 || duration > [self cacheTimeInSeconds]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey:@"Cache expired"}];
}
return NO;
}
// Version
long long cacheVersionFileContent = self.cacheMetadata.version;
if (cacheVersionFileContent != [self cacheVersion]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache version mismatch"}];
}
return NO;
}
// Sensitive data
NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString;
NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
if (sensitiveDataString || currentSensitiveDataString) {
// If one of the strings is nil, short-circuit evaluation will trigger
if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}];
}
return NO;
}
}
// App version
NSString *appVersionString = self.cacheMetadata.appVersionString;
NSString *currentAppVersionString = [YTKNetworkUtils appVersionString];
if (appVersionString || currentAppVersionString) {
if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) {
if (error) {
*error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"App version mismatch"}];
}
return NO;
}
}
return YES;
}

首先是对缓存是否过期进行校验,这一步主要主要通过YTKCacheMetadata中的creationDate与当前时间计算差值,再与cacheTimeInSeconds对比判断缓存数据是否过期。
紧接着看缓存版本和我们request中的cacheVersion是否匹配。紧接着对比缓存的关键信息。然后再是应用版本的匹配,这些信息都匹配的时候才说明缓存是可用的,才会去加载缓存数据。

loadCacheData

紧接着会将本地缓存文件根据序列化形式加载到****_cacheData_cacheString_cacheXML****,

- (BOOL)loadCacheData {
NSString *path = [self cacheFilePath];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error = nil;

if ([fileManager fileExistsAtPath:path isDirectory:nil]) {
NSData *data = [NSData dataWithContentsOfFile:path];
_cacheData = data;
_cacheString = [[NSString alloc] initWithData:_cacheData encoding:self.cacheMetadata.stringEncoding];
switch (self.responseSerializerType) {
case YTKResponseSerializerTypeHTTP:
// Do nothing.
return YES;
case YTKResponseSerializerTypeJSON:
_cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions)0 error:&error];
return error == nil;
case YTKResponseSerializerTypeXMLParser:
_cacheXML = [[NSXMLParser alloc] initWithData:_cacheData];
return YES;
}
}
return NO;
}

向应用层交付缓存数据

dispatch_async(dispatch_get_main_queue(), ^{
[self requestCompletePreprocessor];
[self requestCompleteFilter];
YTKRequest *strongSelf = self;
[strongSelf.delegate requestFinished:strongSelf];
if (strongSelf.successCompletionBlock) {
strongSelf.successCompletionBlock(strongSelf);
}
[strongSelf clearCompletionBlock];
});

这里照样会通过delegate以及block进行交付,但是刚开始会感到奇怪我们刚刚加载数据的时候是加载到****_cacheString这些地方,但是这里交付却是strongSelf****,那么我们通过strongSelf的responseString这些对象访问的时候怎么转到_cacheString中的呢?

- (NSString *)responseString {
if (_cacheString) {
return _cacheString;
}
return [super responseString];
}

嗯,就是在getter方法中拦截,所以及时重置_cacheString非常关键。

缓存数据

还有最后一个问题?这些缓存数据是怎么来的呢?


- (void)requestCompletePreprocessor {

[super requestCompletePreprocessor];
if (self.writeCacheAsynchronously) {
dispatch_async(ytkrequest_cache_writing_queue(), ^{
[self saveResponseDataToCacheFile:[super responseData]];
});
} else {
[self saveResponseDataToCacheFile:[super responseData]];
}
}

我们知道请求结束的时候会调用requestCompletePreprocessor 这时候会调用 *saveResponseDataToCacheFile**。

- (void)saveResponseDataToCacheFile:(NSData *)data {
if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) {
if (data != nil) {
@try {
// New data will always overwrite old data.
[data writeToFile:[self cacheFilePath] atomically:YES];

YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init];
metadata.version = [self cacheVersion];
metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description;
metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self];
metadata.creationDate = [NSDate date];
metadata.appVersionString = [YTKNetworkUtils appVersionString];
[NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]];
} @catch (NSException *exception) {
YTKLog(@"Save cache failed, reason = %@", exception.reason);
}
}
}
}

在saveResponseDataToCacheFile方法中会将服务端返回的数据写到缓存目录下,然后再更新缓存信息。

6. 断点续传

断点续传其实上面已经介绍过了,既然这里已经将它拎出来了,就做个总结把:

这里最关键在于两个方面:
请求中断的时候怎么缓存数据,请求开始的时候怎么续传:

YTKNetWork中会在请求失败,以及取消请求的时候对数据进行缓存:

失败时候进行缓存

- (void)requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error {

//......

NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData];
NSURL *localUrl = nil;
if (request.resumableDownloadPath) {
localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath];
}

if (incompleteDownloadData && localUrl != nil) {
[incompleteDownloadData writeToURL:localUrl atomically:YES];
}

//........
}

取消请求后的缓存


- (void)cancelRequest:(YTKBaseRequest *)request {

//如果有指定恢复下载文件的路径则取消后将数据写到恢复下载文件的地方
if (request.resumableDownloadPath && [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] != nil) {
NSURLSessionDownloadTask *requestTask = (NSURLSessionDownloadTask *)request.requestTask;
[requestTask cancelByProducingResumeData:^(NSData *resumeData) {
NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath];
[resumeData writeToURL:localUrl atomically:YES];
}];
} else {
//直接调用NSURLSessionDataTask cancel
[request.requestTask cancel];
}
[self removeRequestFromRecord:request];
//清除请求成功失败的block
[request clearCompletionBlock];
}

启动请求之前会先查看下载缓存地址,如果有数据则通过downloadTaskWithResumeData继续:

- (NSURLSessionDownloadTask *)downloadTaskWithDownloadPath:(NSString *)downloadPath
requestSerializer:(AFHTTPRequestSerializer *)requestSerializer
URLString:(NSString *)URLString
parameters:(id)parameters
progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
error:(NSError * _Nullable __autoreleasing *)error {
// ......
// 在下载前如果文件存在先直接删除
if ([[NSFileManager defaultManager] fileExistsAtPath:downloadTargetPath]) {
[[NSFileManager defaultManager] removeItemAtPath:downloadTargetPath error:nil];
}

BOOL resumeSucceeded = NO;
__block NSURLSessionDownloadTask *downloadTask = nil;
//未下载完成存储路径
NSURL *localUrl = [self incompleteDownloadTempPathForDownloadPath:downloadPath];
if (localUrl != nil) {
// 是否存在未下载完成的文件
BOOL resumeDataFileExists = [[NSFileManager defaultManager] fileExistsAtPath:localUrl.path];
// 加载数据
NSData *data = [NSData dataWithContentsOfURL:localUrl];
// 数据是否合法
BOOL resumeDataIsValid = [YTKNetworkUtils validateResumeData:data];
//是否可以恢复
BOOL canBeResumed = resumeDataFileExists && resumeDataIsValid;
// Try to resume with resumeData.
// Even though we try to validate the resumeData, this may still fail and raise excecption.
if (canBeResumed) {
@try {
downloadTask = [_manager downloadTaskWithResumeData:data progress:downloadProgressBlock destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {
return [NSURL fileURLWithPath:downloadTargetPath isDirectory:NO];
} completionHandler:
^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
[self handleRequestResult:downloadTask responseObject:filePath error:error];
}];
resumeSucceeded = YES;
} @catch (NSException *exception) {
YTKLog(@"Resume download failed, reason = %@", exception.reason);
resumeSucceeded = NO;
}
}
}

//......

return downloadTask;
}

7. Filter

在YTKNetworkConfig中有两个Filter,一个是_urlFilters,一个是_cacheDirPathFilters

@implementation YTKNetworkConfig {
NSMutableArray<id<YTKUrlFilterProtocol>> *_urlFilters;
NSMutableArray<id<YTKCacheDirPathFilterProtocol>> *_cacheDirPathFilters;
}

url filter它会对所有发起请求的requestUrl进行处理:

- (NSString *)buildRequestUrl:(YTKBaseRequest *)request {

//.......

// 对构建出来的URL通过Config中预设的urlFilters 一一对它进行处理,比如添加公共字段等
NSArray *filters = [_config urlFilters];
for (id<YTKUrlFilterProtocol> f in filters) {
detailUrl = [f filterUrl:detailUrl withRequest:request];
}

//......
}

cacheDirPathFilters filter会在创建缓存路径的时候调用:

- (NSString *)cacheBasePath {
NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache"];

// Filter cache base path
NSArray<id<YTKCacheDirPathFilterProtocol>> *filters = [[YTKNetworkConfig sharedConfig] cacheDirPathFilters];
if (filters.count > 0) {
for (id<YTKCacheDirPathFilterProtocol> f in filters) {
path = [f filterCacheDirPath:path withRequest:self];
}
}
[self createDirectoryIfNeeded:path];
return path;
}
8. 插件机制

插件机制,这个在很多场合都会使用,其实就是一种观察者模式,将观察者添加到要观察对象的通知列表中,一旦某个关键环节发生了,就便利这些观察者,调用他们的方法通知他们。这时候观察者就可以插入自己想要的操作:
YTKNetWork中目前有三个类支持插件机制,分别是YTKBaseRequestYTKBatchRequestYTKChainRequest


@interface YTKBaseRequest (RequestAccessory)

- (void)toggleAccessoriesWillStartCallBack;
- (void)toggleAccessoriesWillStopCallBack;
- (void)toggleAccessoriesDidStopCallBack;

@end

@interface YTKBatchRequest (RequestAccessory)

- (void)toggleAccessoriesWillStartCallBack;
- (void)toggleAccessoriesWillStopCallBack;
- (void)toggleAccessoriesDidStopCallBack;

@end

@interface YTKChainRequest (RequestAccessory)

- (void)toggleAccessoriesWillStartCallBack;
- (void)toggleAccessoriesWillStopCallBack;
- (void)toggleAccessoriesDidStopCallBack;

@end

他们都持有requestAccessories:

@property (nonatomic, strong, nullable) NSMutableArray<id<YTKRequestAccessory>> *requestAccessories;

- (void)addAccessory:(id<YTKRequestAccessory>)accessory;

其实个人认为这里的requestAccessories权限应该限制为readonly比较合适,只是一个小建议。

触发时机,这里以YTKBaseRequest为例子,嗯,不解释看代码。

- (void)start {
[self toggleAccessoriesWillStartCallBack];
[[YTKNetworkAgent sharedAgent] addRequest:self];
}

- (void)stop {
[self toggleAccessoriesWillStopCallBack];
self.delegate = nil;
[[YTKNetworkAgent sharedAgent] cancelRequest:self];
[self toggleAccessoriesDidStopCallBack];
}

好了 YTKNetWork的解析就到这里,今天周六,祝大家周末愉快!

开源库信息

CocoaLumberjack

架构概览

1. DDLog

上面是CocoaLumberjack的架构图,从整体来看主要由DDLog这个是总的入口,用于管理DDLogger,下面是DDLog的一些关键方法与属性:


@property (class, nonatomic, copy, readonly) NSArray<id<DDLogger>> *allLoggers;
@property (class, nonatomic, copy, readonly) NSArray<DDLoggerInformation *> *allLoggersWithLevel;
@property (class, nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggingQueue;

+ (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level;
+ (void)removeLogger:(id <DDLogger>)logger;
+ (void)removeAllLoggers;
+ (void)flushLog;

2. DDLogger

DDLogger是所有Logger都需要遵循的协议,我们先来看下DDLogger协议:

@protocol DDLogger <NSObject>

// 用于格式化输出Log的格式
@property (nonatomic, strong) id <DDLogFormatter> logFormatter;

//每个logger相对于其他的logger都是并行执行的,所以每个logger都有一个专门的分发队列。
@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue;

//如果logger实现方法没有提供它专有的队列,那么我们会自动为它创建一个,队列的名称将会通过这个方法获取
@property (nonatomic, readonly) NSString *loggerName;

//由于log是异步的,并且添加和删除logger也是异步的,在logger被添加到logger系统之前或者被移除之后都不会收到消息,logger可以通过这些方法做一些初始化,善后操作等
- (void)didAddLogger;
- (void)didAddLoggerInQueue:(dispatch_queue_t)queue;
- (void)willRemoveLogger;

// 执行Log输出的方法
- (void)logMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(log(message:));

//一些logger可能为了优化性能会使用缓存,这些缓存必须在退出应用之前进行flush操作将数据写到对应的目标。
- (void)flush;

@end

2.1 DDAbstractLogger

DDAbstractLogger 是所有Logger 的基类,它为我们提供了默认的logger格式化方法以及默认logger队列的创建,并提供了当前logger所使用队列的情况。

@interface DDAbstractLogger : NSObject <DDLogger> {
@public
id <DDLogFormatter> _logFormatter;
dispatch_queue_t _loggerQueue;
}
@property (nonatomic, strong, nullable) id <DDLogFormatter> logFormatter;
@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE) dispatch_queue_t loggerQueue;
//当前logger使用global queue作为logger输出队列
@property (nonatomic, readonly, getter=isOnGlobalLoggingQueue) BOOL onGlobalLoggingQueue;
//当前logger使用内部设计的queue作为logger输出队列
@property (nonatomic, readonly, getter=isOnInternalLoggerQueue) BOOL onInternalLoggerQueue;

@end

2.2 Loggers

DDASLLogger:ASL是Apple System Log的缩写,它用于发送日志到苹果的日志系统,以便它们显示在Console.app上。
DDTTYLogger:用于将日志发送到Xcode控制台。
DDFileLogger:用于将日志写入到日志文件下。
DDOSLogger:用于将日志写入到os_log
DDAbstractDatabaseLogger:是数据库日志的基础类,用于方便我们进行扩展。
一般我们如果只需要NSLog类似的功能,只需要添加DDASLLogger和DDTTYLogger Logger,只有要生成日志文件的情况下才需要DDFileLogger

2.3 DDLogMessage

Logger消息类封装:

@interface DDLogMessage : NSObject <NSCopying>
{
@public
NSString *_message; //具体的消息
DDLogLevel _level; //消息等级
DDLogFlag _flag; //等级flag
NSInteger _context; //上下文标记
NSString *_file; //文件路径
NSString *_fileName; //文件名
NSString *_function; //方法名
NSUInteger _line; //logger所在的行数
id _tag; //tag标记
DDLogMessageOptions _options; //消息选项
NSDate *_timestamp; //消息时间戳
NSString *_threadID; //线程id
NSString *_threadName; //线程名
NSString *_queueLabel; //队列标记
}

2.4 DDLoggerInformation

DDLoggerInformation用于标示单个logger的信息,包括对应的logger以及对应的level

2.5 DDLogFormatter

DDLogFormatter用于序列化某个logger的输出格式,

@protocol DDLogFormatter <NSObject>
@required
- (NSString * __nullable)formatLogMessage:(DDLogMessage *)logMessage NS_SWIFT_NAME(format(message:));
@optional
- (void)didAddToLogger:(id <DDLogger>)logger;
- (void)didAddToLogger:(id <DDLogger>)logger inQueue:(dispatch_queue_t)queue;
- (void)willRemoveFromLogger:(id <DDLogger>)logger;
@end

DDMultiFormatter: 链式log格式化器,可以添加多个格式化器,添加后每个DDLogMessage 都需要经过这个链式log格式化器的每个格式化器处理
DDContextWhitelistFilterLogFormatter:提供黑白名单处理,只有在白名单中的logger才会被输出
DDDispatchQueueLogFormatter
:这个序列化器主要用于方便查看dispatch_queue,它会将dispatch_queue label 替代mach_thread_id作为输出。

源码解析

我们以官方的样例入手进行源码解析:


DDFileLogger *fileLogger = [[DDFileLogger alloc] init];
fileLogger.logFormatter = fileFormatter
[DDLog addLogger:fileLogger];

DDLogVerbose(@"Verbose");

DDLogVerbose其实是一个宏定义,我们先来看下这个宏定义

#define DDLogVerbose(frmt, ...) LOG_OBJC_MAYBE(LOG_ASYNC_VERBOSE, LOG_LEVEL_DEF, LOG_FLAG_VERBOSE, 0, frmt, ##__VA_ARGS__)
#define LOG_OBJC_MAYBE(async, lvl, flg, ctx, frmt, ...) \
LOG_MAYBE(async, lvl, flg, ctx, __PRETTY_FUNCTION__, frmt, ## __VA_ARGS__)


#define LOG_MAYBE(async, lvl, flg, ctx, fnct, frmt, ...) \
do { if(lvl & flg) LOG_MACRO(async, lvl, flg, ctx, nil, fnct, frmt, ##__VA_ARGS__); } while(0)

#define LOG_MACRO(isAsynchronous, lvl, flg, ctx, atag, fnct, frmt, ...) \
[DDLog log : isAsynchronous \
level : lvl \
flag : flg \
context : ctx \
file : __FILE__ \
function : fnct \
line : __LINE__ \
tag : atag \
format : (frmt), ## __VA_ARGS__]

我们开始分析上面的宏定义:

#define LOG_ASYNC_ENABLED YES
#define LOG_ASYNC_VERBOSE (YES && LOG_ASYNC_ENABLED)

根据LOG_ASYNC_ENABLED 以及LOG_ASYNC_VERBOSE的定义我们可以看出默认的LOG_ASYNC_VERBOSE为YES

我们再来看下LOG_OBJC_MAYBE的第二个参数LOG_LEVEL_DEF,我们的等级可以通过两种方式来定义,一种是定义LOG_LEVEL_DEF,这种情况下,就直接使用LOG_LEVEL_DEF的值,一种是不定义LOG_LEVEL_DEF只定义ddLogLevel,这种情况下使用ddLogLevel的值,

#ifndef LOG_LEVEL_DEF
#define LOG_LEVEL_DEF ddLogLevel
#endif

接下来是第三个参数LOG_FLAG_VERBOSE,它主要用于过滤某个等级的log的消息。

#define LOG_FLAG_VERBOSE  DDLogFlagVerbose

在LOG_MAYBE宏中会对LOG_LEVEL_DEF和LOG_FLAG_VERBOSE进行一个并运算,判断当前的LOG_FLAG 是否在LOG_LEVEL_DEF范围内,如果是的话则调用LOG_MACRO输出log。

我们来看下log方法,我们这里不再去层层看消息是怎么拼接的,我们只看最终调用的那个log方法:

- (void)log:(BOOL)asynchronous          //根据上面分析,默认这里为YES
message:(NSString *)message //这是要输出的log消息
level:(DDLogLevel)level //消息等级
flag:(DDLogFlag)flag //消息flag
context:(NSInteger)context //附加context 这里为0
file:(const char *)file //当前文件
function:(const char *)function //方法名
line:(NSUInteger)line //输出log的行
tag:(id)tag { //tag标记
DDLogMessage *logMessage = [[DDLogMessage alloc] initWithMessage:message
level:level
flag:flag
context:context
file:[NSString stringWithFormat:@"%s", file]
function:[NSString stringWithFormat:@"%s", function]
line:line
tag:tag
options:(DDLogMessageOptions)0
timestamp:nil];
[self queueLogMessage:logMessage asynchronously:asynchronous];
}

log方法中使用调用方法的参数构建出一个DDLogMessage,然后通过queueLogMessage将消息添加到队列,这里需要注意的是:

在队列尺寸小于maximumQueueSize的时候,只是简单地将消息添加到队列,这时候我们不会使用任何锁来锁住操作,但是如果队列尺寸超过了maximumQueueSize那么就会锁住操作。一旦有有log出队列了,被锁住的操作将会被解锁,CocoaLumberjack,使用计数信号量来作为锁,最大的队列尺寸被定义为DDLOG_MAX_QUEUE_SIZE

- (void)queueLogMessage:(DDLogMessage *)logMessage asynchronously:(BOOL)asyncFlag {

dispatch_block_t logBlock = ^{
dispatch_semaphore_wait(_queueSemaphore, DISPATCH_TIME_FOREVER);
@autoreleasepool {
[self lt_log:logMessage];
}
};

if (asyncFlag) {
dispatch_async(_loggingQueue, logBlock);
} else {
dispatch_sync(_loggingQueue, logBlock);
}
}
- (void)lt_log:(DDLogMessage *)logMessage {

//......
if (_numProcessors > 1) {
for (DDLoggerNode *loggerNode in self._loggers) {
// skip the loggers that shouldn't write this message based on the log level
if (!(logMessage->_flag & loggerNode->_level)) {
continue;
}
dispatch_group_async(_loggingGroup, loggerNode->_loggerQueue, ^{ @autoreleasepool {
[loggerNode->_logger logMessage:logMessage];
} });
}
dispatch_group_wait(_loggingGroup, DISPATCH_TIME_FOREVER);
} else {
for (DDLoggerNode *loggerNode in self._loggers) {
if (!(logMessage->_flag & loggerNode->_level)) {
continue;
}
dispatch_sync(loggerNode->_loggerQueue, ^{ @autoreleasepool {
[loggerNode->_logger logMessage:logMessage];
} });
}
}
dispatch_semaphore_signal(_queueSemaphore);
}

lt_log 会首先判断当前环境是否是多核环境,如果是多核处理器环境下,每个logger的log任务都会被加到自己的队列中,并行执行,但是在多核处理器的环境下,这些任务还会被加到同一个组中,并且会等到组内所有的log都执行完毕后往下执行。如果在单核环境下每个logger只是简单地将log任务添加到自己的队列中,然后看是否超过最大队列长度,如果超过了就会等待。直到有任务完成并出队列。

我们先回头看下DDLog的初始化以及logger是怎么添加的。

DDLog 初始化

这部分我们先来看下DDLog长啥样:

DDLog有如下几个重要的变量:

有关DDLog所有的操作都放在_loggingQueue,以保证它的所有操作都串行化,后续所有的addLogger,removeLogger,allLoggers等等都放在这个队列中执行,这里需要注意的是这里的_loggingQueue与每个Logger自身的_loggerQueue不是一个东西。要注意区分。

static dispatch_queue_t _loggingQueue;

每个单独的logger都并行得执行在自己的队列,_loggingGroup用于这些并行队列的同步

static dispatch_group_t _loggingGroup;

为了保证队列不会无限制地增加这里使用了一个信号量来控制队列的长度

static dispatch_semaphore_t _queueSemaphore;

// 处理器的数量

static NSUInteger _numProcessors;

在第一次初始化DDLog的时候会调用initialize,在这里初始化了_loggingQueue,_loggingGroup,_queueSemaphore,_numProcessors。

+ (void)initialize {
static dispatch_once_t DDLogOnceToken;
dispatch_once(&DDLogOnceToken, ^{
_loggingQueue = dispatch_queue_create("cocoa.lumberjack", NULL);
_loggingGroup = dispatch_group_create();
void *nonNullValue = GlobalLoggingQueueIdentityKey; // Whatever, just not null
dispatch_queue_set_specific(_loggingQueue, GlobalLoggingQueueIdentityKey, nonNullValue, NULL);
_queueSemaphore = dispatch_semaphore_create(DDLOG_MAX_QUEUE_SIZE);
_numProcessors = MAX([NSProcessInfo processInfo].processorCount, (NSUInteger) 1);
});
}

每次初始化的时候都会调用init,但是由于DDLog以单例的形式存在所以init也只会调用一次。在这个方法中主要创建了self._loggers数组,后续添加的logger都存放在这个列表中。然后添加通知,监听applicationWillTerminate,也就是在应用快要退出的时候调用flushLog

- (id)init {
if (self = [super init]) {
self._loggers = [[NSMutableArray alloc] initWithCapacity:4];
NSString *notificationName = @"UIApplicationWillTerminateNotification";
if (notificationName) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillTerminate:)
name:notificationName
object:nil];
}

return self;
}

- (void)applicationWillTerminate:(NSNotification * __attribute__((unused)))notification {
[self flushLog];
}

DDLog 添加 Logger

addLogger也是DDLog中的方法所以它跑在_loggingQueue队列上:

- (void)addLogger:(id <DDLogger>)logger withLevel:(DDLogLevel)level {
//.....
dispatch_async(_loggingQueue, ^{ @autoreleasepool {
[self lt_addLogger:logger level:level];
} });
}
- (void)lt_addLogger:(id <DDLogger>)logger level:(DDLogLevel)level {
// Add to loggers array.
// Need to create loggerQueue if loggerNode doesn't provide one.

//判断是否已经存在了
for (DDLoggerNode* node in self._loggers) {
if (node->_logger == logger
&& node->_level == level) {
return;
}
}

NSAssert(dispatch_get_specific(GlobalLoggingQueueIdentityKey),
@"This method should only be run on the logging thread/queue");

dispatch_queue_t loggerQueue = NULL;

//判断当前logger是否有自己的队列
if ([logger respondsToSelector:@selector(loggerQueue)]) {
loggerQueue = [logger loggerQueue];
}
//如果没有自己的队列,则使用logger提供的loggerName方法返回的名字,创建队列
if (loggerQueue == nil) {
const char *loggerQueueName = NULL;
if ([logger respondsToSelector:@selector(loggerName)]) {
loggerQueueName = [[logger loggerName] UTF8String];
}
loggerQueue = dispatch_queue_create(loggerQueueName, NULL);
}

//将logger,logger队列,等级封装到DDLoggerNode后添加到_loggers
DDLoggerNode *loggerNode = [DDLoggerNode nodeWithLogger:logger loggerQueue:loggerQueue level:level];
[self._loggers addObject:loggerNode];

//通知外部当前logger已经被添加了
if ([logger respondsToSelector:@selector(didAddLoggerInQueue:)]) {
dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool {
[logger didAddLoggerInQueue:loggerNode->_loggerQueue];
} });
} else if ([logger respondsToSelector:@selector(didAddLogger)]) {
dispatch_async(loggerNode->_loggerQueue, ^{ @autoreleasepool {
[logger didAddLogger];
} });
}
}

lt_addLogger中将会以异步的方式将logger添加到DDLog.由于是异步方式添加,所以每个logger被添加到self._loggers中的时候每个logger的****didAddLoggerInQueue:didAddLogger:****将会被调用,在这里可以做相应的处理。每个logger都拥有一个队列,可以通过两种方式指定:

一种是通过loggerQueue指定:

@property (nonatomic, DISPATCH_QUEUE_REFERENCE_TYPE, readonly) dispatch_queue_t loggerQueue;

另一种是指定loggerName,就会将loggerName作为队列名称创建logger队列

@property (nonatomic, readonly) NSString *loggerName;

后续当前logger的log都会通过这个队列来完成log的输出。

Logger 结构

我们在介绍架构的时候详细介绍过Logger的组成,这里简单重新介绍下,CocoaLumberjack 总共有 DDASLLogger,DDTTYLogger,DDFileLogger,DDOSLogger这几种Logger,并且提供了一个抽象的DDAbstractDatabaseLogger用于我们自己根据实际的业务需求进行扩展数据库Logger,这些Logger都继承自DDAbstractLogger。DDAbstractLogger比较简单主要负责每个logger Queue的创建,以及提供LoggerFormater的设置。至于log的输出形式都交给具体的Logger子类实现。

我们这里以比较常用的DDFileLogger作为重点给大家介绍下Logger的功能。

对于一个文件日志来说它要能够自动管理日志文件,DDFileLogger在日志超过指定时间(age)后删除,大小超过最大日志文件大小的时候将文件打包成achive文件。并且能够自定义日志文件名,以及日志文件log格式:

我们先来看下DDFileLogger的组成:

@interface DDFileLogger : DDAbstractLogger <DDLogger> {
DDLogFileInfo *_currentLogFileInfo;
}
//构造函数
- (instancetype)init;
- (instancetype)initWithLogFileManager:(id <DDLogFileManager>)logFileManager NS_DESIGNATED_INITIALIZER;
//在将要写日志的时候会被调用
- (void)willLogMessage NS_REQUIRES_SUPER;
//当前日志写完后会被调用
- (void)didLogMessage NS_REQUIRES_SUPER;
//是否对传进来的日志文件进行打包成achive文件
- (BOOL)shouldArchiveRecentLogFileInfo:(DDLogFileInfo *)recentLogFileInfo;
//日志文件的最大尺寸,超过这个大小将会被回滚,如果maximumFileSize设置为0则只有rollingFrequency会影响到文件的回滚
@property (readwrite, assign) unsigned long long maximumFileSize;
// 回滚的频率,超过这个时间就会被回滚,maximumFileSize设置为0或者任何负数则只有maximumFileSize会影响到文件的回滚
@property (readwrite, assign) NSTimeInterval rollingFrequency;
// 在每次启动的时候会自动创建新的日志文件,不会重用旧的
@property (readwrite, assign, atomic) BOOL doNotReuseLogFiles;
// 日志文件管理类
@property (strong, nonatomic, readonly) id <DDLogFileManager> logFileManager;
// 会在每条日志的最后添加\n
@property (nonatomic, readwrite, assign) BOOL automaticallyAppendNewlineForCustomFormatters;
// 强制roll当前日志
- (void)rollLogFileWithCompletionBlock:(void (^)(void))completionBlock NS_SWIFT_NAME(rollLogFile(withCompletion:));
// 日志文件格式化
- (id <DDLogFormatter>)logFormatter;
// 当前日志文件信息
@property (nonatomic, readonly, strong) DDLogFileInfo *currentLogFileInfo;

DDFileLogger有两个重要的对象:DDLogFileManagerDefault,以及DDLogFileFormatterDefault.前者负责日志文件的管理,后者负责日志的输出格式,这两者都可以自己实现来达到自定义的目的:

LogFileManager是所有日志文件都必须遵循的协议,它负责日志文件的创建,删除,日志文件命名等所有日志文件的管理,默认情况下每个日志文件的最大尺寸为1M,超过这个大小,日志文件将会被roll,默认情况下每个日志文件的有效期为24小时,超过24小时也会被roll,默认最多保存5个日志文件,整个日志磁盘配额最大为20M.

unsigned long long const kDDDefaultLogMaxFileSize      = 1024 * 1024;      // 1 MB
NSTimeInterval const kDDDefaultLogRollingFrequency = 60 * 60 * 24; // 24 Hours
NSUInteger const kDDDefaultLogMaxNumLogFiles = 5; // 5 Files
unsigned long long const kDDDefaultLogFilesDiskQuota = 20 * 1024 * 1024; // 20 MB
@protocol DDLogFileManager <NSObject>
@required

// Public properties

// 磁盘上能够存储的最多archived log文件数量,
// 比如我们将这个值设置为3,那么LogFileManager只会保存当前日志文件在内的3个日志文件,如果要禁止这个功能可以将这个值设置为0
@property (readwrite, assign, atomic) NSUInteger maximumNumberOfLogFiles;

// 日志文件所能占用的空间大小
@property (readwrite, assign, atomic) unsigned long long logFilesDiskQuota;

// Public methods
// 日志文件目录
@property (nonatomic, readonly, copy) NSString *logsDirectory;

//未排序的日志文件信息
@property (nonatomic, readonly, strong) NSArray<NSString *> *unsortedLogFilePaths;
@property (nonatomic, readonly, strong) NSArray<NSString *> *unsortedLogFileNames;
@property (nonatomic, readonly, strong) NSArray<DDLogFileInfo *> *unsortedLogFileInfos;

//排序后的日志文件信息
@property (nonatomic, readonly, strong) NSArray<NSString *> *sortedLogFilePaths;
@property (nonatomic, readonly, strong) NSArray<NSString *> *sortedLogFileNames;
@property (nonatomic, readonly, strong) NSArray<DDLogFileInfo *> *sortedLogFileInfos;

// Private methods (only to be used by DDFileLogger)
// 创建新的Log文件
- (NSString *)createNewLogFile;

@optional

// Notifications from DDFileLogger
// 在日志文件被archieved的时候被调用
- (void)didArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME(didArchiveLogFile(atPath:));

// 在日志文件被roll的时候被调用
- (void)didRollAndArchiveLogFile:(NSString *)logFilePath NS_SWIFT_NAME(didRollAndArchiveLogFile(atPath:));

@end

我们来看下默认提供的DDLogFileManagerDefault的具体实现:

初始化

- (instancetype)initWithLogsDirectory:(NSString *)aLogsDirectory {
if ((self = [super init])) {
_maximumNumberOfLogFiles = kDDDefaultLogMaxNumLogFiles;
_logFilesDiskQuota = kDDDefaultLogFilesDiskQuota;
if (aLogsDirectory) {
_logsDirectory = [aLogsDirectory copy];
} else {
_logsDirectory = [[self defaultLogsDirectory] copy];
}
NSKeyValueObservingOptions kvoOptions = NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew;
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(maximumNumberOfLogFiles)) options:kvoOptions context:nil];
[self addObserver:self forKeyPath:NSStringFromSelector(@selector(logFilesDiskQuota)) options:kvoOptions context:nil];
}
return self;
}

初始化方法中会指定日志的最大文件数量以及日志文件所占用的最大空间,以及日志文件存储目录。以及使用KVO检测maximumNumberOfLogFiles,logFilesDiskQuota的设置。一旦我们对这两个值进行设置就会触发下面的方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSNumber *old = change[NSKeyValueChangeOldKey];
NSNumber *new = change[NSKeyValueChangeNewKey];
if ([old isEqual:new]) {
return;
}
if ([keyPath isEqualToString:NSStringFromSelector(@selector(maximumNumberOfLogFiles))] ||
[keyPath isEqualToString:NSStringFromSelector(@selector(logFilesDiskQuota))]) {
dispatch_async([DDLog loggingQueue], ^{ @autoreleasepool {
[self deleteOldLogFiles];
} });
}
}

在设置最大磁盘空间以及最大文件数的时候会对磁盘空间情况做个扫描,将超过的部分删除。

- (void)deleteOldLogFiles {

//将缓存目录下的所有日志进行排序形成sortedLogFileInfos数组
NSArray *sortedLogFileInfos = [self sortedLogFileInfos];

NSUInteger firstIndexToDelete = NSNotFound;

const unsigned long long diskQuota = self.logFilesDiskQuota;
const NSUInteger maxNumLogFiles = self.maximumNumberOfLogFiles;

//先看磁盘占用空间
if (diskQuota) {
unsigned long long used = 0;
for (NSUInteger i = 0; i < sortedLogFileInfos.count; i++) {
DDLogFileInfo *info = sortedLogFileInfos[i];
used += info.fileSize;
if (used > diskQuota) {
firstIndexToDelete = i;
break;
}
}
}
//再看最大日志文件数
if (maxNumLogFiles) {
if (firstIndexToDelete == NSNotFound) {
firstIndexToDelete = maxNumLogFiles;
} else {
firstIndexToDelete = MIN(firstIndexToDelete, maxNumLogFiles);
}
}
// 删除超过磁盘空间和最大文件数的文件
if (firstIndexToDelete != NSNotFound) {
for (NSUInteger i = firstIndexToDelete; i < sortedLogFileInfos.count; i++) {
DDLogFileInfo *logFileInfo = sortedLogFileInfos[i];
[[NSFileManager defaultManager] removeItemAtPath:logFileInfo.filePath error:nil];
}
}
}

我们来看下是怎么对文件进行排序的?

- (NSArray *)sortedLogFileInfos {
return [[self unsortedLogFileInfos] sortedArrayUsingComparator:^NSComparisonResult(DDLogFileInfo * _Nonnull obj1, DDLogFileInfo * _Nonnull obj2) {
NSDate *date1 = [NSDate new];
NSDate *date2 = [NSDate new];

NSArray<NSString *> *arrayComponent = [[obj1 fileName] componentsSeparatedByString:@" "];
if (arrayComponent.count > 0) {
NSString *stringDate = arrayComponent.lastObject;
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
date1 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj1 creationDate];
}

arrayComponent = [[obj2 fileName] componentsSeparatedByString:@" "];
if (arrayComponent.count > 0) {
NSString *stringDate = arrayComponent.lastObject;
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".log" withString:@""];
stringDate = [stringDate stringByReplacingOccurrencesOfString:@".archived" withString:@""];
date2 = [[self logFileDateFormatter] dateFromString:stringDate] ?: [obj2 creationDate];
}
return [date2 compare:date1 ?: [NSDate new]];
}];

}
- (NSArray *)unsortedLogFileInfos {
NSArray *unsortedLogFilePaths = [self unsortedLogFilePaths];
NSMutableArray *unsortedLogFileInfos = [NSMutableArray arrayWithCapacity:[unsortedLogFilePaths count]];
for (NSString *filePath in unsortedLogFilePaths) {
DDLogFileInfo *logFileInfo = [[DDLogFileInfo alloc] initWithFilePath:filePath];
[unsortedLogFileInfos addObject:logFileInfo];
}
return unsortedLogFileInfos;
}
- (NSArray *)unsortedLogFilePaths {
NSString *logsDirectory = [self logsDirectory];
NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:logsDirectory error:nil];
NSMutableArray *unsortedLogFilePaths = [NSMutableArray arrayWithCapacity:[fileNames count]];
for (NSString *fileName in fileNames) {
if ([self isLogFile:fileName]) {
NSString *filePath = [logsDirectory stringByAppendingPathComponent:fileName];
[unsortedLogFilePaths addObject:filePath];
}
}
return unsortedLogFilePaths;
}

- (BOOL)isLogFile:(NSString *)fileName {
NSString *appName = [self applicationName];
BOOL hasProperPrefix = [fileName hasPrefix:[appName stringByAppendingString:@" "]];
BOOL hasProperSuffix = [fileName hasSuffix:@".log"];
return (hasProperPrefix && hasProperSuffix);
}

首先会进入日志目录,对日志目录下对文件进行判断,通过isLogFile判断当前文件是否是日志文件,将日志文件添加到unsortedLogFilePaths,这里的日志文件被定义为”应用名 xxxxxxxx.log”形式的文件,这个定义是可以定制的我们在介绍日志文件的创建的时候会对这部分进行介绍。上面排序就是扫描日志文件夹下的符合日志格式的文件,将其添加到数组,再对数组的文件根据创建时间进行排序。

我们再来看下日志文件的创建:

- (NSString *)newLogFileName {
NSString *appName = [self applicationName];
NSDateFormatter *dateFormatter = [self logFileDateFormatter];
NSString *formattedDate = [dateFormatter stringFromDate:[NSDate date]];
return [NSString stringWithFormat:@"%@ %@.log", appName, formattedDate];
}

- (NSDateFormatter *)logFileDateFormatter {
NSMutableDictionary *dictionary = [[NSThread currentThread]
threadDictionary];
NSString *dateFormat = @"yyyy'-'MM'-'dd'--'HH'-'mm'-'ss'-'SSS'";
NSString *key = [NSString stringWithFormat:@"logFileDateFormatter.%@", dateFormat];
NSDateFormatter *dateFormatter = dictionary[key];
if (dateFormatter == nil) {
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]];
[dateFormatter setDateFormat:dateFormat];
[dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
dictionary[key] = dateFormatter;
}
return dateFormatter;
}

- (NSString *)createNewLogFile {
NSString *fileName = [self newLogFileName];
NSString *logsDirectory = [self logsDirectory];
NSUInteger attempt = 1;
do {
NSString *actualFileName = fileName;
if (attempt > 1) {
NSString *extension = [actualFileName pathExtension];
actualFileName = [actualFileName stringByDeletingPathExtension];
actualFileName = [actualFileName stringByAppendingFormat:@" %lu", (unsigned long)attempt];
if (extension.length) {
actualFileName = [actualFileName stringByAppendingPathExtension:extension];
}
}
NSString *filePath = [logsDirectory stringByAppendingPathComponent:actualFileName];
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSDictionary *attributes = nil;
[[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:attributes];
[self deleteOldLogFiles];
return filePath;
} else {
attempt++;
}
} while (YES);
}

从上面可以看出日志文件的格式为“应用名 yyyy-MM-dd–HH-mm-ss-SSS.log”存储在缓存目录下的”Log”目录,再createNewLogFile主要处理同名的情况下会追加一个数字用于避免文件名同名,比如“应用名 yyyy-MM-dd–HH-mm-ss-SSS 1.log”,“应用名 yyyy-MM-dd–HH-mm-ss-SSS 2.log”等。创建一个新的文件之后还会调用deleteOldLogFiles对当前日志文件目录下对文件进行一遍检查。

我们在介绍DDFileLogger之前对DDLogFileManagerDefault做个总结,DDLogFileManagerDefault总地来说就是负责日志文件的创建以及根据我们为日志文件设置的存储空间,以及日志文件数量进行管控,一旦超过空间大小,或者日志文件数量大于我们设定的最大数量都会删除掉最旧的那个。DDLogFileManager整体就是为了DDFileLogger服务的所以我们接下来看下DDFileLogger是怎么通过它来管理日志文件的。

初始化:

- (instancetype)init {
DDLogFileManagerDefault *defaultLogFileManager = [[DDLogFileManagerDefault alloc] init];
return [self initWithLogFileManager:defaultLogFileManager];
}

- (instancetype)initWithLogFileManager:(id <DDLogFileManager>)aLogFileManager {
if ((self = [super init])) {
_maximumFileSize = kDDDefaultLogMaxFileSize;
_rollingFrequency = kDDDefaultLogRollingFrequency;
_automaticallyAppendNewlineForCustomFormatters = YES;
logFileManager = aLogFileManager;
self.logFormatter = [DDLogFileFormatterDefault new];
}
return self;
}

在初始化方法中主要初始化了logFileManager,logFormatter以及_rollingFrequency,紧接着我们从最核心的日志输出入手看下整个DDFileLogger的细节。

- (void)logMessage:(DDLogMessage *)logMessage {
NSString *message = logMessage->_message;
BOOL isFormatted = NO;
if (_logFormatter) {
//使用文件日志格式化器对传入的日志进行输出前格式化
message = [_logFormatter formatLogMessage:logMessage];
isFormatted = message != logMessage->_message;
}
if (message) {
//如果_automaticallyAppendNewlineForCustomFormatters = YES,就会在格式化日志的后面追加\n
if ((!isFormatted || _automaticallyAppendNewlineForCustomFormatters) &&
(![message hasSuffix:@"\n"])) {
message = [message stringByAppendingString:@"\n"];
}
//将数据转换为NSData
NSData *logData = [message dataUsingEncoding:NSUTF8StringEncoding];
@try {
//通知外部将要写日志了,这样可以在开始写日志前做一些处理
[self willLogMessage];
//写入日志
[[self currentLogFileHandle] writeData:logData];
//通知外部日志写入成功了
[self didLogMessage];
} @catch (NSException *exception) {
//如果出问题则重试10次
exception_count++;
if (exception_count <= 10) {
if (exception_count == 10) {
NSLogError(@"DDFileLogger.logMessage: Too many exceptions -- will not log any more of them.");
}
}
}
}
}

我们先看下默认的文件日志格式化器是怎么对消息进行格式化的:

- (NSString *)formatLogMessage:(DDLogMessage *)logMessage {
NSString *dateAndTime = [_dateFormatter stringFromDate:(logMessage->_timestamp)];
//时间格式为yyyy/MM/dd HH:mm:ss:SSS
return [NSString stringWithFormat:@"%@ %@", dateAndTime, logMessage->_message];
}

formatLogMessage比较简单,就是将日志时间和日志内容进行拼接后返回。没有太大的特别处理,然后根据_automaticallyAppendNewlineForCustomFormatters的值判断是否需要在格式化后的消息尾部添加\n,在日志将要输出以及输出之后会分别调用willLogMessage,以及didLogMessage告诉外界将要开始写文件了,以及日志已经写到日志文件了。

[[self currentLogFileHandle] writeData:logData];
- (NSFileHandle *)currentLogFileHandle {
if (_currentLogFileHandle == nil) {
//获取到当前可用的日志文件
NSString *logFilePath = [[self currentLogFileInfo] filePath];
//创建NSFileHandle
_currentLogFileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath];
//将文件光标移到文件的最后
[_currentLogFileHandle seekToEndOfFile];
if (_currentLogFileHandle) {
//开始定时roll
[self scheduleTimerToRollLogFileDueToAge];
_currentLogFileVnode = dispatch_source_create(
DISPATCH_SOURCE_TYPE_VNODE,
[_currentLogFileHandle fileDescriptor],
DISPATCH_VNODE_DELETE | DISPATCH_VNODE_RENAME,
self.loggerQueue
);
dispatch_source_set_event_handler(_currentLogFileVnode, ^{ @autoreleasepool {
[self rollLogFileNow];
} });

dispatch_resume(_currentLogFileVnode);
}
}
return _currentLogFileHandle;
}

- (DDLogFileInfo *)currentLogFileInfo {
if (_currentLogFileInfo == nil) {
//拿到已经排序好的日志文件
NSArray *sortedLogFileInfos = [logFileManager sortedLogFileInfos];
if ([sortedLogFileInfos count] > 0) {
//拿到排序中的第一个文件
DDLogFileInfo *mostRecentLogFileInfo = sortedLogFileInfos[0];
BOOL shouldArchiveMostRecent = NO;
//如果已经archived则shouldArchiveMostRecent = NO
if (mostRecentLogFileInfo.isArchived) {
shouldArchiveMostRecent = NO;
//通过外部方法拦截判断当前日志文件是否需要archive
} else if ([self shouldArchiveRecentLogFileInfo:mostRecxentLogFileInfo]) {
shouldArchiveMostRecent = YES;
} else if (_maximumFileSize > 0 && mostRecentLogFileInfo.fileSize >= _maximumFileSize) {
//如果当前文件尺寸大于_maximumFileSize则需要archive
shouldArchiveMostRecent = YES;
} else if (_rollingFrequency > 0.0 && mostRecentLogFileInfo.age >= _rollingFrequency) {
//如果当前文件age超过了_rollingFrequency则需要archive
shouldArchiveMostRecent = YES;
}
//.......
// 如果是未achive的日志文件表示,当前可用的,将其赋给_currentLogFileInfo
if (!_doNotReuseLogFiles && !mostRecentLogFileInfo.isArchived && !shouldArchiveMostRecent) {
_currentLogFileInfo = mostRecentLogFileInfo;
} else {
//如果需要achive当前文件则将当前文件设置为achive并通知外部
if (shouldArchiveMostRecent) {
mostRecentLogFileInfo.isArchived = YES;
if ([logFileManager respondsToSelector:@selector(didArchiveLogFile:)]) {
[logFileManager didArchiveLogFile:(mostRecentLogFileInfo.filePath)];
}
}
}
}
//如果到这里还是空则只能新建了
if (_currentLogFileInfo == nil) {
NSString *currentLogFilePath = [logFileManager createNewLogFile];
_currentLogFileInfo = [[DDLogFileInfo alloc] initWithFilePath:currentLogFilePath];
}
}
return _currentLogFileInfo;
}

整个文件的创建过程都放在currentLogFileHandle中,首先会从日志文件夹中取出最新一个文件,然后判断它是否需要achive了,判断的标准先看它是否已经achive了,然后再调用外部的shouldArchiveRecentLogFileInfo来判断是否需要achive,最后再看日志文件的大小以及age是否超过限制,如果超过限制则achive。achive大家理解吗?比较难解释,就是某个日志文件不用了就要将其归档起来,上传日志的时候就上传这些归档后的日志。如果当前第一个文件不是achive文件,那么就将它赋给_currentLogFileInfo,将文件光标指向文件最后,继续往这个日志文件下写日志。这里还有一个比较重要的方法scheduleTimerToRollLogFileDueToAge,它用于定时扫描查看文件是否age过期了,每个日志文件都只能保留指定的时间,默认的情况下是24小时。我们来看下这个方法:

- (void)scheduleTimerToRollLogFileDueToAge {
if (_rollingTimer) {
dispatch_source_cancel(_rollingTimer);
_rollingTimer = NULL;
}

if (_currentLogFileInfo == nil || _rollingFrequency <= 0.0) {
return;
}

NSDate *logFileCreationDate = [_currentLogFileInfo creationDate];
NSTimeInterval ti = [logFileCreationDate timeIntervalSinceReferenceDate];
ti += _rollingFrequency;
//计算下一次检查的定时时间
NSDate *logFileRollingDate = [NSDate dateWithTimeIntervalSinceReferenceDate:ti];
_rollingTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue);
dispatch_source_set_event_handler(_rollingTimer, ^{ @autoreleasepool {
[self maybeRollLogFileDueToAge];
} });
uint64_t delay = (uint64_t)([logFileRollingDate timeIntervalSinceNow] * (NSTimeInterval) NSEC_PER_SEC);
dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, delay);
dispatch_source_set_timer(_rollingTimer, fireTime, DISPATCH_TIME_FOREVER, 1ull * NSEC_PER_SEC);
dispatch_resume(_rollingTimer);
}
- (void)maybeRollLogFileDueToAge {
if (_rollingFrequency > 0.0 && _currentLogFileInfo.age >= _rollingFrequency) {
[self rollLogFileNow];
} else {
[self scheduleTimerToRollLogFileDueToAge];
}
}

- (void)rollLogFileNow {

if (_currentLogFileHandle == nil) {
return;
}

[_currentLogFileHandle synchronizeFile];
[_currentLogFileHandle closeFile];
_currentLogFileHandle = nil;

_currentLogFileInfo.isArchived = YES;

if ([logFileManager respondsToSelector:@selector(didRollAndArchiveLogFile:)]) {
[logFileManager didRollAndArchiveLogFile:(_currentLogFileInfo.filePath)];
}

_currentLogFileInfo = nil;

if (_currentLogFileVnode) {
dispatch_source_cancel(_currentLogFileVnode);
_currentLogFileVnode = NULL;
}

if (_rollingTimer) {
dispatch_source_cancel(_rollingTimer);
_rollingTimer = NULL;
}
}

scheduleTimerToRollLogFileDueToAge会查看文件的创建时间,然后在它的基础上加上_rollingFrequency,将它作为定时器的值定时,在这之后检查当前日志文件是否过期了,如果没有过期则再次进入下一轮。rollLogFileNow就是用于将文件归档。它会关闭文件句柄,_currentLogFileInfo.isArchived其实是这里最为关键的代码。

- (void)setIsArchived:(BOOL)flag {
if (flag) {
[self addExtendedAttributeWithName:kDDXAttrArchivedName];
} else {
[self removeExtendedAttributeWithName:kDDXAttrArchivedName];
}
}

- (void)addExtensionAttributeWithName:(NSString *)attrName {
// This method is only used on the iPhone simulator, where normal extended attributes are broken.
// See full explanation in the header file.
if ([attrName length] == 0) {
return;
}
// Example:
// attrName = "archived"
//
// "mylog.txt" -> "mylog.archived.txt"
// "mylog" -> "mylog.archived"
NSArray *components = [[self fileName] componentsSeparatedByString:@"."];
NSUInteger count = [components count];
NSUInteger estimatedNewLength = [[self fileName] length] + [attrName length] + 1;
NSMutableString *newFileName = [NSMutableString stringWithCapacity:estimatedNewLength];
if (count > 0) {
[newFileName appendString:components.firstObject];
}
NSString *lastExt = @"";
NSUInteger i;
for (i = 1; i < count; i++) {
NSString *attr = components[i];
if ([attr length] == 0) {
continue;
}
if ([attrName isEqualToString:attr]) {
return;
}
if ([lastExt length] > 0) {
[newFileName appendFormat:@".%@", lastExt];
}
lastExt = attr;
}
[newFileName appendFormat:@".%@", attrName];
if ([lastExt length] > 0) {
[newFileName appendFormat:@".%@", lastExt];
}
[self renameFile:newFileName];
}

它就是在文件名的最末尾添加一个archived标记,下一次再次调用currentLogFileInfo的时候就会略过这个文件。

到此为止已经带大家过了一遍CocoaLumberjack的核心源码了,至于DDASLLogger,DDOSLogger,以及DDTTYLogger这里就不再继续展开了,代码都很简单,但是这里还是要简单提下:

DDASLLogger
DDASLLogger会在初始化后通过asl_open来建立连接,通过asl_new创建一个新的消息,然后通过asl_set来设置一系列的属性,最后通过asl_send将日志发送出去。

DDOSLogger
DDOSLogger则是通过os_log_error,os_log_info,os_log_debug将日志发送出去的。

DDTTYLogger
DDTTYLogger是通过writev(STDERR_FILENO,…,….);将日志发送到StdError终端的。

开篇叨叨

FBRetainCycleDetector 是FaceBook推出的用于检测对象是否有循环引用的一个开源库.下面是一个简单的用法:

#import <FBRetainCycleDetector/FBRetainCycleDetector.h>

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:myObject];
NSSet *retainCycles = [detector findRetainCycles];
NSLog(@"%@", retainCycles);

它先创建一个FBRetainCycleDetector循环引用检测器,然后将待检查循环引用关系的对象通过addCandidate传入,检测器通过findRetainCycles查找这个对象中的循环引用。它能通过添加Filters对某些进行过滤,并且支持检测Block,Timmer,Associations这些对象的内存泄漏。但是它不能单独在项目中使用必须借助其他工具比如FBAllocationTracker,MLeaksFinders 查找Candidates,下面这篇博客主要是对FBRetainCycleDetector源码进行分析,不对使用做过多介绍,具体使用可以在GitHub主页查看。

FBRetainCycleDetector

项目结构

下图是FBRetainCycleDetector目录结构:

源码解析

FBRetainCycleDetector主要任务有两个:

1. 找出****Object********Timer类型Object********Object associate类型Object****,****Block类型Object**** 这几种类型的 Strong Reference。
2. 最开始把Self作为根节点,沿着找出的各个Reference进行深度遍历,如果形成了环,则存在循环依赖。

OK 我们现在开始FBRetainCycleDetector的解析:

FBRetainCycleDetector 对象初始化

首先我们来看下FBRetainCycleDetector:

@implementation FBRetainCycleDetector {
NSMutableArray *_candidates;
FBObjectGraphConfiguration *_configuration;
NSMutableSet *_objectSet;
}

这里最重要有两个对象,_candidates是一系列待检测的对象,_configuration为配置对象,在这里可以配置需要过滤的对象,检测属性的配置等等。

为了简化代码的分析过程,我们以最简单的初始化形式入手看下初始化过程我们做了什么:

- (instancetype)init {
return [self initWithConfiguration:
[[FBObjectGraphConfiguration alloc] initWithFilterBlocks:FBGetStandardGraphEdgeFilters()
shouldInspectTimers:YES]];
}
- (instancetype)initWithConfiguration:(FBObjectGraphConfiguration *)configuration {
if (self = [super init]) {
_configuration = configuration;
_candidates = [NSMutableArray new];
_objectSet = [NSMutableSet new];
}
return self;
}

在初始化过程中FBRetainCycleDetector调用了initWithConfiguration:初始化了一个标准的过滤器,过滤器过滤了引用循环中的一些类和方法,并检查NSTimer循环引用情况。

添加检测对象

待检测的对象在addCandidate方法中被封装为FBObjectiveCGraphElement对象,然后添加到_candidates中。

- (void)addCandidate:(id)candidate {
FBObjectiveCGraphElement *graphElement = FBWrapObjectGraphElement(nil, candidate, _configuration);
if (graphElement) {
[_candidates addObject:graphElement];
}
}

查找循环引用

FBRetainCycleDetector 通过调用findRetainCycles开始查找循环引用,如果某个对象比较复杂可能检测时间会比较久,所以在FBRetainCycleDetector中默认设置了一个检测深度,默认情况下为10.可以通过

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length

来指定更深的层级。我们继续看findRetainCycles,findRetainCycles内部只是简单得转调了findRetainCyclesWithMaxCycleLength方法。

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCycles {
return [self findRetainCyclesWithMaxCycleLength:kFBRetainCycleDetectorDefaultStackDepth];
}

我们继续跟进findRetainCyclesWithMaxCycleLength方法:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)findRetainCyclesWithMaxCycleLength:(NSUInteger)length {
NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *allRetainCycles = [NSMutableSet new];
for (FBObjectiveCGraphElement *graphElement in _candidates) {
NSSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [self _findRetainCyclesInObject:graphElement
stackDepth:length];
[allRetainCycles unionSet:retainCycles];
}
[_candidates removeAllObjects];
[_objectSet removeAllObjects];

// Filter cycles that have been broken down since we found them.
// These are false-positive that were picked-up and are transient cycles.
NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *brokenCycles = [NSMutableSet set];
for (NSArray<FBObjectiveCGraphElement *> *itemCycle in allRetainCycles) {
for (FBObjectiveCGraphElement *element in itemCycle) {
if (element.object == nil) {
// At least one element of the cycle has been removed, thus breaking
// the cycle.
[brokenCycles addObject:itemCycle];
break;
}
}
}
[allRetainCycles minusSet:brokenCycles];

return allRetainCycles;
}

findRetainCyclesWithMaxCycleLength方法中主要可以分成两个阶段,第一个阶段通过_findRetainCyclesInObject查找被加入到_candidates中对象的循环引用放到allRetainCycles集合中 ,第二阶段是查看这些元素中哪些是已经释放了的从allRetainCycles中移除。

对于对象的相互引用情况可以看做一个有向图,对象之间的引用就是图的的连线,每一个对象就是有向图的顶点,查找循环引用的过程就是在整个有向图中查找闭环的过程,FBRetainCycleDetector是通过DFS深度遍历的方式遍历整个对象有向图的,整个遍历查找过程真正的实现位于
_findRetainCyclesInObject方法中,在分析_findRetainCyclesInObject实现之前我们先通过下面的视频看下整个算法的大概流程:

- (NSSet<NSArray<FBObjectiveCGraphElement *> *> *)_findRetainCyclesInObject:(FBObjectiveCGraphElement *)graphElement
stackDepth:(NSUInteger)stackDepth {

NSMutableSet<NSArray<FBObjectiveCGraphElement *> *> *retainCycles = [NSMutableSet new];

// 查找循环引用是通过深度遍历整个对象图来实现的
// 首先初始化深度搜索树中的一个节点
FBNodeEnumerator *wrappedObject = [[FBNodeEnumerator alloc] initWithObject:graphElement];

// stack 用于保存当前DFS搜索树中的搜索路径
NSMutableArray<FBNodeEnumerator *> *stack = [NSMutableArray new];

// objectsOnPath 保存搜索路径中访问过的对象
NSMutableSet<FBNodeEnumerator *> *objectsOnPath = [NSMutableSet new];

// 增加根节点,从根节点开始搜索
[stack addObject:wrappedObject];

//判断是否已经搜索完毕
while ([stack count] > 0) {
// 算法会创建许多生命周期非常短的对象,这个会造成很大的内存抖动,所以这里使用了自动释放池来缓解这个问题。
@autoreleasepool {
// 取出stack中的最上面的节点,并标记该节点已读
FBNodeEnumerator *top = [stack lastObject];

// 这里不重复便利同样的子树
if (![objectsOnPath containsObject:top]) {
if ([_objectSet containsObject:@([top.object objectAddress])]) {
[stack removeLastObject];
continue;
}
// 这里之所以只保留对象的地址是为了避免不必要的对象持有
[_objectSet addObject:@([top.object objectAddress])];
}
// 记录已经访问过的节点
[objectsOnPath addObject:top];

// Take next adjecent node to that child. Wrapper object can
// persist iteration state. If we see that node again, it will
// give us new adjacent node unless it runs out of them
//取top节点的next节点,也就是这个object可能持有的对象。
FBNodeEnumerator *firstAdjacent = [top nextObject];
if (firstAdjacent) {
//如果存在未访问到的节点
// Current node still has some adjacent not-visited nodes

BOOL shouldPushToStack = NO;

// 检查是否已经访问过了
// Check if child was already seen in that path
if ([objectsOnPath containsObject:firstAdjacent]) {
// We have caught a retain cycle
//如果该节点已经存在被访问过的对象中,说明构成了retain cycle
// Ignore the first element which is equal to firstAdjacent, use firstAdjacent
// we're doing that because firstAdjacent has set all contexts, while its
// first occurence could be a root without any context
NSUInteger index = [stack indexOfObject:firstAdjacent];
NSInteger length = [stack count] - index;

if (index == NSNotFound) {
// Object got deallocated between checking if it exists and grabbing its index
shouldPushToStack = YES;
} else {
//计算出firstAdj出现的位置,同时计算出路径的长度,将这一系列的节点(object),也就是环,存放在array里面。
//将这个array存放到retainCycles集合中。
NSRange cycleRange = NSMakeRange(index, length);
NSMutableArray<FBNodeEnumerator *> *cycle = [[stack subarrayWithRange:cycleRange] mutableCopy];
[cycle replaceObjectAtIndex:0 withObject:firstAdjacent];

// 1. Unwrap the cycle
// 2. Shift to lowest address (if we omit that, and the cycle is created by same class,
// we might have duplicates)
// 3. Shift by class (lexicographically)

[retainCycles addObject:[self _shiftToUnifiedCycle:[self _unwrapCycle:cycle]]];
}
} else {
// Node is clear to check, add it to stack and continue
shouldPushToStack = YES;
}

if (shouldPushToStack) {
if ([stack count] < stackDepth) {
[stack addObject:firstAdjacent];
}
}
} else {
// Node has no more adjacent nodes, it itself is done, move on
[stack removeLastObject];
[objectsOnPath removeObject:top];
}
}
}
return retainCycles;
}

_findRetainCyclesInObject方法中有四个比较重要的对象:

* retainCycles用于存放循环引用环的集合;
* wrappedObject图的根起点
* stack是在图中当前的路径
* objectsOnPath用于记录以前访问过的节点。

在遍历开始前首先用传进来的graphElement来初始化FBNodeEnumerator对象,FBNodeEnumerator 是整个遍历的关键,但是这里我们先注重整个流程,开始遍历之前会将用当前graphElement初始化的FBNodeEnumerator添加到stack,然后通过FBNodeEnumerator 的 nextObject 取出下一个节点,通过 [objectsOnPath containsObject:firstAdjacent] 来判断该节点是否已经被访问过了,如果该节点已经存在被访问过的对象中,说明构成了retain cycle,这时候计算出firstAdj出现的位置,同时计算出路径的长度,将这一系列的节点组成的环,存放在array里面。将这个array存放到retainCycles集合中。

这里还有两个比较重要的方法_unwrapCycle和_shiftToUnifiedCycle:

  • _unwrapCycle: 的作用是将数组中的每一个 FBNodeEnumerator 实例解压成 FBObjectiveCGraphElement
  • _shiftToUnifiedCycle: 方法用于将每一个环中的元素按照地址递增以及字母顺序来排序,来避免把相同环的不同表示方式当作两个不同的环。

获取强引用对象

我们上面介绍addCandidate方法的时候看到FBRetainCycleDetector会把所有的对象封装在FBObjectiveCGraphElement中,FBObjectiveCGraphElement类中有个十分重要的方法allRetainedObjects,它用于返回某个对象所持有的全部强引用对象数组。获取数组之后,再把其中的对象包装成新的FBNodeEnumerator实例,作为下一个有向图的顶点。

- (NSSet *)allRetainedObjects {
NSArray *retainedObjectsNotWrapped = [FBAssociationManager associationsForObject:_object];
NSMutableSet *retainedObjects = [NSMutableSet new];

for (id obj in retainedObjectsNotWrapped) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self,
obj,
_configuration,
@[@"__associated_object"]);
if (element) {
[retainedObjects addObject:element];
}
}
return retainedObjects;
}

在最开始我们会通过[FBAssociationManager associationsForObject:]获取该对象所有通过objc_setAssociatedObject关联的对象。FBAssociationManager 是 object associations的一个跟踪器,通过指定对象它能够返回该指定对象通过objc_setAssociatedObject retain策略添加的关联对象。它只有三个方法:

/**
开始跟踪 object associations,这里使用的是fishhook来hook objc_(set/remove)AssociatedObject 这些C方法,并插入一些跟踪代码
*/
+ (void)hook;

/**
停止跟踪 object associations
*/
+ (void)unhook;

/**
返回指定对象的 objects associated
*/
+ (nullable NSArray *)associationsForObject:(nullable id)object;

要跟踪关联对象必须在main.m中调用[FBAssociationManager hook]通过fishhook Hook 对应方法。

我们接下来继续看下allRetainedObjects方法。allRetainedObjects方法中会遍历FBAssociationManager获取到的_object关联的对象。然后通过FBWrapObjectGraphElementWithContext创建FBObjectiveCGraphElement。并添加到retainedObjects数组返回。

FBObjectiveCGraphElement *FBWrapObjectGraphElementWithContext(FBObjectiveCGraphElement *sourceElement,
id object,
FBObjectGraphConfiguration *configuration,
NSArray<NSString *> *namePath) {
//通过FBObjectGraphConfiguration中添加的过滤器对当前的object进行一次过滤
if (_ShouldBreakGraphEdge(configuration, sourceElement, [namePath firstObject], object_getClass(object))) {
return nil;
}
FBObjectiveCBlock *newElement;
//如果是Block类型则返回FBObjectiveCBlock
if (FBObjectIsBlock((__bridge void *)object)) {
newElement = [[FBObjectiveCBlock alloc] initWithObject:object
configuration:configuration
namePath:namePath];
} else {
//如果是NSTimer的子类并且配置类中shouldInspectTimers 为 YES 返回FBObjectiveCNSCFTimer
if ([object_getClass(object) isSubclassOfClass:[NSTimer class]] &&
configuration.shouldInspectTimers) {
newElement = [[FBObjectiveCNSCFTimer alloc] initWithObject:object
configuration:configuration
namePath:namePath];
} else {
//否则返回FBObjectiveCObject
newElement = [[FBObjectiveCObject alloc] initWithObject:object
configuration:configuration
namePath:namePath];
}
}
return (configuration && configuration.transformerBlock) ? configuration.transformerBlock(newElement) : newElement;
}

FBWrapObjectGraphElementWithContext会根据传入的object的对象判断,如果是Block类型则返回FBObjectiveCBlock,如果是NSTimer的子类并且配置类中shouldInspectTimers 为 YES 返回FBObjectiveCNSCFTimer,如果其他类型返回FBObjectiveCObject。

FBObjectiveCBlock,FBObjectiveCNSCFTimer,FBObjectiveCObject都是FBObjectiveCGraphElement的子类,都是一种对象图元素。

我们回到FBNodeEnumerator类,我们上面提到深度遍历对象树靠的就是FBNodeEnumerator的nextObject方法。我们看下nextObject方法:

- (FBNodeEnumerator *)nextObject {
if (!_object) {
return nil;
} else if (!_retainedObjectsSnapshot) {
_retainedObjectsSnapshot = [_object allRetainedObjects];
_enumerator = [_retainedObjectsSnapshot objectEnumerator];
}
FBObjectiveCGraphElement *next = [_enumerator nextObject];
if (next) {
return [[FBNodeEnumerator alloc] initWithObject:next];
}
return nil;
}

在这里最关键的就是调用了上面提到的allRetainedObjects方法。也就是说通过allRetainedObjects可以对当前节点进行展开递归。

我们针对上面提到的三种对象图元素一一看下怎么获取到某个对象的全部强引用属性:

FBObjectiveCBlock

- (NSSet *)allRetainedObjects {

// ......
//获取一个对象的所有强引用属性
NSArray *strongIvars = FBGetObjectStrongReferences(self.object, self.configuration.layoutCache);
//通过super方法获取当前类的关联对象属性
NSMutableArray *retainedObjects = [[[super allRetainedObjects] allObjects] mutableCopy];

//将全部的强引用对象封装成FBObjectiveCGraphElement
for (id<FBObjectReference> ref in strongIvars) {
id referencedObject = [ref objectReferenceFromObject:self.object];
if (referencedObject) {
NSArray<NSString *> *namePath = [ref namePath];
FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self,
referencedObject,
self.configuration,
namePath);
if (element) {
[retainedObjects addObject:element];
}
}
}

if ([NSStringFromClass(aCls) hasPrefix:@"__NSCF"]) {
/**
If we are dealing with toll-free bridged collections, we are not guaranteed that the collection
will hold only Objective-C objects. We are not able to check in runtime what callbacks it uses to
retain/release (if any) and we could easily crash here.
*/
return [NSSet setWithArray:retainedObjects];
}

if (class_isMetaClass(aCls)) {
// If it's a meta-class it can conform to following protocols,
// but it would crash when trying enumerating
return nil;
}

//获取集合类的引用
if ([aCls conformsToProtocol:@protocol(NSFastEnumeration)]) {
BOOL retainsKeys = [self _objectRetainsEnumerableKeys];
BOOL retainsValues = [self _objectRetainsEnumerableValues];

BOOL isKeyValued = NO;
if ([aCls instancesRespondToSelector:@selector(objectForKey:)]) {
isKeyValued = YES;
}

/**
This codepath is prone to errors. When you enumerate a collection that can be mutated while enumeration
we fall into risk of crash. To save ourselves from that we will catch such exception and try again.
We should not try this endlessly, so at some point we will simply give up.
*/
NSInteger tries = 10;
for (NSInteger i = 0; i < tries; ++i) {
// If collection is mutated we want to rollback and try again - let's keep refs in temporary set
NSMutableSet *temporaryRetainedObjects = [NSMutableSet new];
@try {
for (id subobject in self.object) {
if (retainsKeys) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, subobject, self.configuration);
if (element) {
[temporaryRetainedObjects addObject:element];
}
}
if (isKeyValued && retainsValues) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self,
[self.object objectForKey:subobject],
self.configuration);
if (element) {
[temporaryRetainedObjects addObject:element];
}
}
}
}
@catch (NSException *exception) {
// mutation happened, we want to try enumerating again
continue;
}

// If we are here it means no exception happened and we want to break outer loop
[retainedObjects addObjectsFromArray:[temporaryRetainedObjects allObjects]];
break;
}
}

return [NSSet setWithArray:retainedObjects];
}

这里最关键的就是FBGetObjectStrongReferences方法:它能从类中获取它的所有引用,无论是强引用或者是弱引用。

NSArray<id<FBObjectReference>> *FBGetObjectStrongReferences(id obj,
NSMutableDictionary<Class, NSArray<id<FBObjectReference>> *> *layoutCache) {
NSMutableArray<id<FBObjectReference>> *array = [NSMutableArray new];

__unsafe_unretained Class previousClass = nil;
__unsafe_unretained Class currentClass = object_getClass(obj);

while (previousClass != currentClass) {
NSArray<id<FBObjectReference>> *ivars;

if (layoutCache && currentClass) {
ivars = layoutCache[currentClass];
}

if (!ivars) {
ivars = FBGetStrongReferencesForClass(currentClass);
if (layoutCache && currentClass) {
layoutCache[currentClass] = ivars;
}
}
[array addObjectsFromArray:ivars];
previousClass = currentClass;
currentClass = class_getSuperclass(currentClass);
}

return [array copy];
}

在FBGetObjectStrongReferences方法中遍历本类以及所有父指针强引用,并且加入了缓存以加速查找强引用的过程,在这里会对所有遍历的类调用FBGetStrongReferencesForClass获取ivars,同时过滤弱引用。

static NSArray<id<FBObjectReference>> *FBGetStrongReferencesForClass(Class aCls) {
NSArray<id<FBObjectReference>> *ivars = [FBGetClassReferences(aCls) filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
if ([evaluatedObject isKindOfClass:[FBIvarReference class]]) {
FBIvarReference *wrapper = evaluatedObject;
return wrapper.type != FBUnknownType;
}
return YES;
}]];

const uint8_t *fullLayout = class_getIvarLayout(aCls);

if (!fullLayout) {
return @[];
}

NSUInteger minimumIndex = FBGetMinimumIvarIndex(aCls);
NSIndexSet *parsedLayout = FBGetLayoutAsIndexesForDescription(minimumIndex, fullLayout);

NSArray<id<FBObjectReference>> *filteredIvars =
[ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject,
NSDictionary *bindings) {
return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]];
}]];

return filteredIvars;
}

我们来看下FBGetClassReferences方法:

NSArray<id<FBObjectReference>> *FBGetClassReferences(Class aCls) {
NSMutableArray<id<FBObjectReference>> *result = [NSMutableArray new];

unsigned int count;
Ivar *ivars = class_copyIvarList(aCls, &count);

for (unsigned int i = 0; i < count; ++i) {
Ivar ivar = ivars[i];
FBIvarReference *wrapper = [[FBIvarReference alloc] initWithIvar:ivar];

if (wrapper.type == FBStructType) {
std::string encoding = std::string(ivar_getTypeEncoding(wrapper.ivar));
NSArray<FBObjectInStructReference *> *references = FBGetReferencesForObjectsInStructEncoding(wrapper, encoding);

[result addObjectsFromArray:references];
} else {
[result addObject:wrapper];
}
}
free(ivars);

return [result copy];
}

这里主要通过class_copyIvarList方法获取到Ivar然后将所有的Ivar封装到FBIvarReference对象。FBIvarReference对象其实是Ivar的面向对象封装,包括属性的名称、类型、偏移量以及索引。

@interface FBIvarReference : NSObject <FBObjectReference>

@property (nonatomic, copy, readonly, nullable) NSString *name;
@property (nonatomic, readonly) FBType type;
@property (nonatomic, readonly) ptrdiff_t offset;
@property (nonatomic, readonly) NSUInteger index;
@property (nonatomic, readonly, nonnull) Ivar ivar;

- (nonnull instancetype)initWithIvar:(nonnull Ivar)ivar;

@end

到目前为止我们直到了如何获取到某个对象的全部属性,这里包括强引用和弱引用,下面我们还要知道如何过滤弱引用。

为了弄明白怎么过滤弱引用,我们要先了解Ivar Layout,获取Ivar Layout 是通过 FBGetLayoutAsIndexesForDescription方法获取的。
Ivar Layout 是一系列的字符,每两个一组,比如\xmn,每一组 Ivar Layout 中第一位表示有m个非强属性,第二位表示接下来有n个强属性。
FBGetLayoutAsIndexesForDescription 返回的就是所有强引用的Ivar Layout。

static NSIndexSet *FBGetLayoutAsIndexesForDescription(NSUInteger minimumIndex, const uint8_t *layoutDescription) {
NSMutableIndexSet *interestingIndexes = [NSMutableIndexSet new];
NSUInteger currentIndex = minimumIndex;

while (*layoutDescription != '\x00') {
int upperNibble = (*layoutDescription & 0xf0) >> 4;
int lowerNibble = *layoutDescription & 0xf;

currentIndex += upperNibble;
[interestingIndexes addIndexesInRange:NSMakeRange(currentIndex, lowerNibble)];
currentIndex += lowerNibble;

++layoutDescription;
}

return interestingIndexes;
}

接下来我们就可以拿着上面找到的parsedLayout对ivars进行过滤,留下所有强引用的ivars。

NSArray<id<FBObjectReference>> *filteredIvars =
[ivars filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id<FBObjectReference> evaluatedObject,
NSDictionary *bindings) {
return [parsedLayout containsIndex:[evaluatedObject indexInIvarLayout]];
}]];

FBObjectiveCBlock

我们来看下Block对象对应的图节点FBObjectiveCBlock:

- (NSSet *)allRetainedObjects {

//获取关联对象的全部强引用
NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy];

// Grab a strong reference to the object, otherwise it can crash while doing
// nasty stuff on deallocation
__attribute__((objc_precise_lifetime)) id anObject = self.object;

void *blockObjectReference = (__bridge void *)anObject;
NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference);

for (id object in allRetainedReferences) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration);
if (element) {
[results addObject:element];
}
}

return [NSSet setWithArray:results];
}

这里最重要的是FBGetBlockStrongReferences方法:

NSArray *FBGetBlockStrongReferences(void *block) {
if (!FBObjectIsBlock(block)) {
return nil;
}
NSMutableArray *results = [NSMutableArray new];
void **blockReference = block;
NSIndexSet *strongLayout = _GetBlockStrongLayout(block);
[strongLayout enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
void **reference = &blockReference[idx];

if (reference && (*reference)) {
id object = (id)(*reference);

if (object) {
[results addObject:object];
}
}
}];

return [results autorelease];
}

在FBGetBlockStrongReferences方法中通过_GetBlockStrongLayout获得Block所持有的强引用。在理解如何Block强引用之前我们先来回顾下Block的一些基础知识:

struct BlockLiteral {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, …);
struct BlockDescriptor *descriptor;
// imported variables
};

首先Block对于它所捕获的Objective-C对象实例,都会在block 结构体的下面存放这些持有的对象,并且会将强引用的对象排放在弱引用对象的前面。当Block将要释放时,会调用dispose_helper函数,该函数会调用所有需要进行内存管理的所捕获的对象,如Block、block变量、__attribute((NSObject))变量或有constructor/destructor的C++ const对象。所以我们可以创建一系列的fake对象来模拟捕获的Objective-C对象实例,然后主动调用Block的dispose_helper方法,该方法会调用对象实例的release方法,我们只需要在fake对象中实现release方法,如果release方法被调用,所以该fake对象对应的真实变量为Objective-C对象实例。

static NSIndexSet *_GetBlockStrongLayout(void *block) {

struct BlockLiteral *blockLiteral = block;

/**
BLOCK_HAS_CTOR - Block has a C++ constructor/destructor, which gives us a good chance it retains
objects that are not pointer aligned, so omit them.

!BLOCK_HAS_COPY_DISPOSE - Block doesn't have a dispose function, so it does not retain objects and
we are not able to blackbox it.
*/
if ((blockLiteral->flags & BLOCK_HAS_CTOR)
|| !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) {
return nil;
}

//获取当前Block的dispose_helper方法
void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;

//指针大小
const size_t ptrSize = sizeof(void *);

//计算出需要填充的fake对象数量
// Figure out the number of pointers it takes to fill out the object, rounding up.
const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;

// 创建fack对象
// Create a fake object of the appropriate length.
void *obj[elements];
void *detectors[elements];

for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
obj[i] = detectors[i] = detector;
}

//调用dispose_helper,这时候会调用每个FBBlockStrongRelationDetector的release方法,这时候会将strong设置为YES
@autoreleasepool {
dispose_helper(obj);
}
// Run through the release detectors and add each one that got released to the object's
// strong ivar layout.
NSMutableIndexSet *layout = [NSMutableIndexSet indexSet];
//判断strong是否为YES,如果为YES表示为强引用,则将其添加到layout
for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]);
if (detector.isStrong) {
[layout addIndex:i];
}
// Destroy detectors
[detector trueRelease];
}
return layout;
}

FBObjectiveCNSCFTimer*

FBObjectiveCNSCFTimer比较简单它会通过runloop去获取CFRunLoopTimerGetContext,然后通过context查看是否有target或者,userInfo如果有假设它强持有这些对象的引用。

- (NSSet *)allRetainedObjects {
// Let's retain our timer
__attribute__((objc_precise_lifetime)) NSTimer *timer = self.object;

if (!timer) {
return nil;
}
NSMutableSet *retained = [[super allRetainedObjects] mutableCopy];
CFRunLoopTimerContext context;
//通过runloop去获取CFRunLoopTimerGetContext
CFRunLoopTimerGetContext((CFRunLoopTimerRef)timer, &context);
// If it has a retain function, let's assume it retains strongly
if (context.info && context.retain) {
_FBNSCFTimerInfoStruct infoStruct = *(_FBNSCFTimerInfoStruct *)(context.info);
if (infoStruct.target) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, infoStruct.target, self.configuration, @[@"target"]);
if (element) {
[retained addObject:element];
}
}
if (infoStruct.userInfo) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElementWithContext(self, infoStruct.userInfo, self.configuration, @[@"userInfo"]);
if (element) {
[retained addObject:element];
}
}
}

return retained;
}

FBAllocationTracker

FBAllocationTracker我们上面给大家分析了FBRetainCycleDetector,它主要的功能是查找到待检测对象的全部强引用对象,这些强引用对象便是搜索循环引用的有向图节点, FBRetainCycleDetector通过深度优先原则遍历整个对象图,如果在整个路径中发现有存在重复访问的情况,便认为是一个循环引用,FBRetainCycleDetector一般先通过MLeaksFinder或者FBAllocationTracker这些开源库找到可疑的对象,然后再通过FBRetainCycleDetector对这个对象进行检查是否存在循环引用。
接下来我们给大家介绍下FBAllocationTracker,下面是它的最基本用法:

#import <FBAllocationTracker/FBAllocationTrackerManager.h>

int main(int argc, char * argv[]) {
[[FBAllocationTrackerManager sharedManager] startTrackingAllocations];
[[FBAllocationTrackerManager sharedManager] enableGenerations];
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
- (void)startTrackingAllocations {
FB::AllocationTracker::beginTracking();
}

- (void)enableGenerations {
dispatch_sync(_queue, ^{
if (self->_generationsClients == 0) {
FB::AllocationTracker::enableGenerations();
FB::AllocationTracker::markGeneration();
}
self->_generationsClients += 1;
});
}

不可变集合类型

下面的方法适用于如下类型:

NSArray
NSDictionary
NSIndexSet
NSSet
NSOrderedSet
- (void)bk_each:(void (^)(id obj))block;

对数组内的每个元素进行block指定的处理

- (void)bk_apply:(void (^)(id obj))block;

这个功能上和bk_each其实是一致的,但是它是异步的,速度上会比bk_each快,特别是在双核处理器上,但是必须注意线程安全,并且它不是按照顺序处理的。

- (id)bk_match:(BOOL (^)(id obj))block;

会找到第一个经过block处理后返回YES的元素

- (NSArray *)bk_select:(BOOL (^)(id obj))block;

对所有对元素进行block处理,将所有返回YES的元素放到一个数组中返回。

- (NSArray *)bk_reject:(BOOL (^)(id obj))block;

这个会剔除掉不匹配的

- (NSArray *)bk_map:(id (^)(id obj))block;

对数组进行通用block进行处理

- (id)bk_reduce:(id)initial withBlock:(id (^)(id sum, id obj))block;

- (NSInteger)bk_reduceInteger:(NSInteger)initial withBlock:(NSInteger(^)(NSInteger result, id obj))block;

- (CGFloat)bk_reduceFloat:(CGFloat)inital withBlock:(CGFloat(^)(CGFloat result, id obj))block;

对所有的元素进行递归处理,不断累加效果。

- (BOOL)bk_any:(BOOL (^)(id obj))block;

查看数组内部是否有满足条件的元素

- (BOOL)bk_none:(BOOL (^)(id obj))block;

查看数组内是否没有满足条件的元素

- (BOOL)bk_all:(BOOL (^)(id obj))block;

查看数组内是否全部原始都满足

- (BOOL)bk_corresponds:(NSArray *)list withBlock:(BOOL (^)(id obj1, id obj2))block;

查看当前数组和list的对应元素是否对应“相等”。

可变集合类型

下面的方法适用于如下几种可变集合类型:

NSMutableArray
NSMutableDictionary
NSMutableIndexSet
NSMutableOrderedSet
NSMutableSet
- (void)bk_performSelect:(BOOL (^)(id obj))block;

找出满足条件的

- (void)bk_performReject:(BOOL (^)(id obj))block;

剔除不满足条件的

- (void)bk_performMap:(id (^)(id obj))block;

对各个元素进行map处理

关联对象

- (void)bk_associateValue:(id)value withKey:(const void *)key;
+ (void)bk_associateValue:(id)value withKey:(const void *)key;
- (void)bk_atomicallyAssociateValue:(id)value withKey:(const void *)key;
+ (void)bk_atomicallyAssociateValue:(id)value withKey:(const void *)key;
- (void)bk_associateCopyOfValue:(id)value withKey:(const void *)key;
+ (void)bk_associateCopyOfValue:(id)value withKey:(const void *)key;
- (void)bk_atomicallyAssociateCopyOfValue:(id)value withKey:(const void *)key;
+ (void)bk_atomicallyAssociateCopyOfValue:(id)value withKey:(const void *)key;
- (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key;
+ (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key;
- (id)bk_associatedValueForKey:(const void *)key;
+ (id)bk_associatedValueForKey:(const void *)key;
- (void)bk_removeAllAssociatedObjects;
+ (void)bk_removeAllAssociatedObjects;

NSInvocation

NSInvocation *invocation = [NSInvocation bk_invocationWithTarget:self block:^(id target) {
[target testMethod];
}];

[invocation invoke];

- (void)testMethod {
NSLog(@"===>");
}

这个用于触发某个target的对应方法,在block中传递进来target对象,直接使用该对象调用方法。

performSelector 的简化

它的最大优点在于能够取消已经对应的block。

- (id)bk_performBlock:(void (^)(id obj))block afterDelay:(NSTimeInterval)delay;
+ (id)bk_performBlock:(void (^)(void))block afterDelay:(NSTimeInterval)delay;
- (id)bk_performBlockInBackground:(void (^)(id obj))block afterDelay:(NSTimeInterval)delay;
+ (id)bk_performBlockInBackground:(void (^)(void))block afterDelay:(NSTimeInterval)delay;
+ (id)bk_performBlock:(void (^)(void))block onQueue:(dispatch_queue_t)queue afterDelay:(NSTimeInterval)
delay;
- (id)bk_performBlock:(void (^)(id obj))block onQueue:(dispatch_queue_t)queue afterDelay:(NSTimeInterval)delay;
+ (void)bk_cancelBlock:(id)block;

KVO

- (NSString *)bk_addObserverForKeyPath:(NSString *)keyPath task:(void (^)(id target))task;
- (NSString *)bk_addObserverForKeyPaths:(NSArray *)keyPaths task:(void (^)(id obj, NSString *keyPath))task;
- (NSString *)bk_addObserverForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options task:(void (^)(id obj, NSDictionary *change))task;
- (NSString *)bk_addObserverForKeyPaths:(NSArray *)keyPaths options:(NSKeyValueObservingOptions)options task:(void (^)(id obj, NSString *keyPath, NSDictionary *change))task;
- (void)bk_addObserverForKeyPath:(NSString *)keyPath identifier:(NSString *)token options:(NSKeyValueObservingOptions)options task:(void (^)(id obj, NSDictionary *change))task;
- (void)bk_addObserverForKeyPaths:(NSArray *)keyPaths identifier:(NSString *)token options:(NSKeyValueObservingOptions)options task:(void (^)(id obj, NSString *keyPath, NSDictionary *change))task;
- (void)bk_removeObserverForKeyPath:(NSString *)keyPath identifier:(NSString *)token;
- (void)bk_removeObserversWithIdentifier:(NSString *)token;
- (void)bk_removeAllBlockObservers;

NSTimer

+ (NSTimer *)bk_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))inBlock repeats:(BOOL)inRepeats;
+ (NSTimer *)bk_timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))inBlock repeats:(BOOL)inRepeats;

UIView

- (void)bk_whenTouches:(NSUInteger)numberOfTouches tapped:(NSUInteger)numberOfTaps handler:(void (^)(void))block;

numberOfTouches 多少个手指
numberOfTaps 点击多少次

规定多少个手指,多少点击多少次才会触发该handler Block进行处理。

- (void)bk_whenTapped:(void (^)(void))block;
- (void)bk_whenDoubleTapped:(void (^)(void))block;

单击双击UIView对象会执行指定的block

- (void)bk_eachSubview:(void (^)(UIView *subview))block;

非递归方式遍历当前View的子View

UIGestureRecognizer

UITapGestureRecognizer *singleTap = [UITapGestureRecognizer recognizerWithHandler:^(id sender) {
NSLog(@"Single tap.");
} delay:0.18];
[self addGestureRecognizer:singleTap];

UITapGestureRecognizer *doubleTap = [UITapGestureRecognizer recognizerWithHandler:^(id sender) {
[singleTap cancel];
NSLog(@"Double tap.");
}];
doubleTap.numberOfTapsRequired = 2;
[self addGestureRecognizer:doubleTap];

UIControl

- (void)bk_addEventHandler:(void (^)(id sender))handler forControlEvents:(UIControlEvents)controlEvents;
- (void)bk_removeEventHandlersForControlEvents:(UIControlEvents)controlEvents;
- (BOOL)bk_hasEventHandlersForControlEvents:(UIControlEvents)controlEvents;

部分源码解析:

UIControl
- (void)bk_addEventHandler:(void (^)(id sender))handler forControlEvents:(UIControlEvents)controlEvents{

NSParameterAssert(handler);

//获取对象绑定到的事件
NSMutableDictionary *events = objc_getAssociatedObject(self, BKControlHandlersKey);
if (!events) {
events = [NSMutableDictionary dictionary];
//如果当前为首次给对象添加事件,则新建一个事件字典,并动态关联到对象
objc_setAssociatedObject(self, BKControlHandlersKey, events, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

//取出该事件的处理者
NSNumber *key = @(controlEvents);
NSMutableSet *handlers = events[key];
if (!handlers) {
handlers = [NSMutableSet set];
events[key] = handlers;
}
//将处理的Block 以及处理类型存储到BKControlWrapper对象
BKControlWrapper *target = [[BKControlWrapper alloc] initWithHandler:handler forControlEvents:controlEvents];
[handlers addObject:target];
//触发BKControlWrapper的invoke方法。
[self addTarget:target action:@selector(invoke:) forControlEvents:controlEvents];
}

UIControl+BlocksKit 中是通过为每个UIControl对象添加一个以UIControlEvents为key,value为BKControlWrapper集合的字典,当我们要为某个UIControlEvents添加一个事件的时候只要找到对应的handlers集合,然后将handler和UIControlEvents封装到BKControlWrapper,然后再调用[self addTarget:target action:@selector(invoke:) forControlEvents:controlEvents];
这样一旦发生controlEvents事件,就会触发BKControlWrapper的invoke方法,在invoke方法中就会执行对应的block。

UIGestureRecognizer
+ (id)bk_recognizerWithHandler:(void (^)(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location))block delay:(NSTimeInterval)delay {
return [[[self class] alloc] bk_initWithHandler:block delay:delay];
}

- (id)bk_initWithHandler:(void (^)(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location))block delay:(NSTimeInterval)delay {
self = [self initWithTarget:self action:@selector(bk_handleAction:)];
if (!self) return nil;
self.bk_handler = block;
self.bk_handlerDelay = delay;
return self;
}

初始化的时候会将delay以及block保存到bk_handler以及bk_handlerDelay,并指定当前处理的Action为bk_handleAction

- (void)bk_handleAction:(UIGestureRecognizer *)recognizer{
void (^handler)(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location) = recognizer.bk_handler;
if (!handler) return;

NSTimeInterval delay = self.bk_handlerDelay;
CGPoint location = [self locationInView:self.view];
void (^block)(void) = ^{
if (!self.bk_shouldHandleAction) return;
handler(self, self.state, location);
};

self.bk_shouldHandleAction = YES;

if (!delay) {
block();
return;
}

dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), block);
}

在对应的手势事件触发的时候会调用bk_handleAction 这时候取出bk_handler,然后根据是否需要延迟执行来执行对应的bk_handler

介绍了UIGestureRecognizer+BlocksKit这里顺带介绍下UIView+BlocksKit,它实际上也是在UIView的基础上添加手势识别器来实现的:

- (void)bk_whenTouches:(NSUInteger)numberOfTouches tapped:(NSUInteger)numberOfTaps handler:(void (^)(void))block {
if (!block) return;

UITapGestureRecognizer *gesture = [UITapGestureRecognizer bk_recognizerWithHandler:^(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location) {
if (state == UIGestureRecognizerStateRecognized) block();
}];

gesture.numberOfTouchesRequired = numberOfTouches;
gesture.numberOfTapsRequired = numberOfTaps;

[self.gestureRecognizers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (![obj isKindOfClass:[UITapGestureRecognizer class]]) return;

UITapGestureRecognizer *tap = obj;
BOOL rightTouches = (tap.numberOfTouchesRequired == numberOfTouches);
BOOL rightTaps = (tap.numberOfTapsRequired == numberOfTaps);
if (rightTouches && rightTaps) {
[gesture requireGestureRecognizerToFail:tap];
}
}];

[self addGestureRecognizer:gesture];
}

- (void)bk_whenTapped:(void (^)(void))block{
[self bk_whenTouches:1 tapped:1 handler:block];
}
关联对象

这部分没太多新颖的内容,比较有意思的是实现weak方式的关联,它实际上使用了一个中间对象,来做过渡,它持有一个weak类型的属性value来存储实际所赋的值。在获取关联对象时就判断其是否为_BKWeakAssociatedObject类型的对象,如果是就返回该对象value的属性值。

+ (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key {
_BKWeakAssociatedObject *assoc = objc_getAssociatedObject(self, key);
if (!assoc) {
assoc = [_BKWeakAssociatedObject new];
[self bk_associateValue:assoc withKey:key];
}
assoc.value = value;
}
NSObject+BKBlockExecution
- (id)bk_performBlock:(void (^)(id obj))block onQueue:(dispatch_queue_t)queue afterDelay:(NSTimeInterval)delay {
NSParameterAssert(block != nil);

__block BOOL cancelled = NO;

void (^wrapper)(BOOL) = ^(BOOL cancel) {
if (cancel) {
cancelled = YES;
return;
}
if (!cancelled) block(self);
};

dispatch_after(BKTimeDelay(delay), queue, ^{
wrapper(NO);
});

return [wrapper copy];
}

+ (void)bk_cancelBlock:(id)block {
NSParameterAssert(block != nil);
void (^wrapper)(BOOL) = block;
wrapper(YES);
}

这里使用了一个__block的cacelled标记,标记无效时才执行block。默认情况下就传入NO,使其能正常工作。将要取消时传入YES即可,所以在取消的时候就直接调用了wrapper(YES);

动态代理

BlocksKit的动态代理部分是整个BlocksKit的精华所在,它用于解决什么问题呢?
我们先来看下我们要给一个对象设置代理一般有如下步骤:

1. 创建代理对象
2. 代理对象遵守协议;
3. 代理对象实现协议
4. 将代理对象赋给delegate属性

这有个不好的地方就是代码会显得比较散乱,必须要新建一个代理类,并且代理实现方法和设置代理的方法是分开的,这种问题之前我们会采用rac_signalForSelector来将代理实现聚合在设置代理的方法地方。但是这还不算是十分优雅的方式:

在BlocksKit中给出下面的一个例子:

UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:@"Hello World!"
message:@"This alert's delegate is implemented using blocks. That's so cool!"
delegate:nil
cancelButtonTitle:@"Meh."
otherButtonTitles:@"Woo!", nil];

// Get the dynamic delegate
A2DynamicDelegate *dd = alertView.bk_dynamicDelegate;

// Implement -alertViewShouldEnableFirstOtherButton:
[dd implementMethod:@selector(alertViewShouldEnableFirstOtherButton:) withBlock:^(UIAlertView *alertView) {
NSLog(@"Message: %@", alertView.message);
return YES;
}];

// Implement -alertView:willDismissWithButtonIndex:
[dd implementMethod:@selector(alertView:willDismissWithButtonIndex:) withBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
NSLog(@"You pushed button #%d (%@)", buttonIndex, [alertView buttonTitleAtIndex:buttonIndex]);
}];

// Set the delegate
alertView.delegate = dd;

[alertView show];

这个就是BlocksKit动态代理的使用过程:

首先通过bk_dynamicDelegate来获得UIAlertView的动态代理,然后通过implementMethod将代理方法selector与对应的block绑定,最后将动态代理设置给UIAlertView。
整个代码一气呵成。但是背后实际上还是存在代理类的创建,后面会一一向大家介绍整个流程,我们先来看下涉及到的关键类有哪些:

A2BlockInvocation 的主要用于存储和转发block
A2DynamicDelegate 用来实现类的代理和数据源
NSObject+A2DynamicDelegate 负责为返回 bk_dynamicDelegate bk_dynamicDataSource 等 A2DynamicDelegate 类型的实例,
NSObject+A2BlockDelegate 提供了一系列接口将代理方法映射到 block

我们接下来顺着上面的例子过一遍代码,看下如何实现动态代理的功能。

在载入UIAlertView+BlocksKit 分类的时候会调用load方法,这里主要做了两件事情,一个是通过bk_registerDynamicDelegate来注册动态代理,另一个是通过bk_linkDelegateMethods将指定的Block与对应的Delegate方法链接起来。

+ (void)load{
@autoreleasepool {
[self bk_registerDynamicDelegate];
[self bk_linkDelegateMethods:@{
@"bk_willShowBlock": @"willPresentAlertView:",
@"bk_didShowBlock": @"didPresentAlertView:",
@"bk_willDismissBlock": @"alertView:willDismissWithButtonIndex:",
@"bk_didDismissBlock": @"alertView:didDismissWithButtonIndex:",
@"bk_shouldEnableFirstOtherButtonBlock": @"alertViewShouldEnableFirstOtherButton:"
}];
}
}

注册动态代理

注册动态代理相关代码如下:

+ (void)bk_registerDynamicDelegate {
[self bk_registerDynamicDelegateNamed:@"delegate" forProtocol:a2_delegateProtocol(self/*UIAlertView*/)/*UIAlertViewDelegate*/];
}
Protocol *a2_delegateProtocol(Class cls) {
/*获取到UIActionSheetDelegate协议*/
return a2_classProtocol(cls, @"Delegate", @"delegate");
}

static Protocol *a2_classProtocol(Class _cls/*UIAlertView*/, NSString *suffix/*Delegate*/, NSString *description/*delegate*/) {
Class cls = _cls;
while (cls) {
NSString *className = NSStringFromClass(cls);
//UIAlertViewDelegate
NSString *protocolName = [className stringByAppendingString:suffix];
//获得UIAlertViewDelegate协议
Protocol *protocol = objc_getProtocol(protocolName.UTF8String);
if (protocol) return protocol;
cls = class_getSuperclass(cls);
}
//............
}
+ (void)bk_registerDynamicDelegateNamed:(NSString *)delegateName {
[self bk_registerDynamicDelegateNamed:delegateName forProtocol:a2_delegateProtocol(self)];
}
+ (void)bk_registerDynamicDelegateNamed:(NSString *)delegateName/*delegate*/ forProtocol:(Protocol *)protocol/*UIAlertViewDelegate*/ {
NSMapTable *propertyMap = [self bk_delegateInfoByProtocol:YES];
A2BlockDelegateInfo *infoAsPtr = (__bridge void *)[propertyMap objectForKey:protocol];
if (infoAsPtr != NULL) { return; }

const char *name = delegateName.UTF8String; /*delegate*/
objc_property_t property = class_getProperty(self, name); /*获得UIAlertView 的 delegate property属性*/
SEL setter = setterForProperty(property, name); /*setDelegate:*/
SEL a2_setter = prefixedSelector(setter); /*a2_SetDelegate:*/
SEL getter = getterForProperty(property, name); /*delegate*/

//A2BlockDelegateInfo 包着setter,a2_setter,getter 三个Selector
A2BlockDelegateInfo info = {
setter, a2_setter, getter
};

//这里将A2BlockDelegateInfo设置propertyMap到
[propertyMap setObject:(__bridge id)&info forKey:protocol];
infoAsPtr = (__bridge void *)[propertyMap objectForKey:protocol];

//交换delegate setter的selector,也就是说在XXXX.delegate 的时候就会将delegte值赋给dynamicDelegate.realDelegate
IMP setterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject, id delegate) {
A2DynamicDelegate *dynamicDelegate = getDynamicDelegate(delegatingObject, protocol, infoAsPtr, YES);
//如果dynamicDelegate 等于 delegate的话也不使用delegate
if ([delegate isEqual:dynamicDelegate]) {
delegate = nil;
}
//dynamicDelegate 表示真实的delegate
dynamicDelegate.realDelegate = delegate;
});

//将设置方法setter设为a2_SetDelegate
if (!swizzleWithIMP(self, setter, a2_setter, setterImplementation, "v@:@", YES)) {
bzero(infoAsPtr, sizeof(A2BlockDelegateInfo));
return;
}

if (![self instancesRespondToSelector:getter]) {
IMP getterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject) {
//获取到对应的动态代理对象
return [delegatingObject bk_dynamicDelegateForProtocol:a2_protocolForDelegatingObject(delegatingObject, protocol)];
});
addMethodWithIMP(self, getter, NULL, getterImplementation, "@@:", NO);
}
}
static inline A2DynamicDelegate *getDynamicDelegate(NSObject *delegatingObject,/*被代理对象*/ Protocol *protocol, const A2BlockDelegateInfo *info, BOOL ensuring) {
A2DynamicDelegate *dynamicDelegate = [delegatingObject bk_dynamicDelegateForProtocol:a2_protocolForDelegatingObject(delegatingObject, protocol)/*UIAlertViewDelegate*/];
//......
return dynamicDelegate;
}
- (id)bk_dynamicDelegateForProtocol:(Protocol *)protocol{
Class class = [A2DynamicDelegate class];
/*UIAlertViewDelegate*/
NSString *protocolName = NSStringFromProtocol(protocol);
if ([protocolName hasSuffix:@"Delegate"]) {
class = a2_dynamicDelegateClass([self class], @"Delegate");
} else if ([protocolName hasSuffix:@"DataSource"]) {
class = a2_dynamicDelegateClass([self class], @"DataSource");
}
/*A2DynamicUIAlertViewDelegate*/
return [self bk_dynamicDelegateWithClass:class forProtocol:protocol];
}
static Class a2_dynamicDelegateClass(Class cls, NSString *suffix) {
while (cls) {
//A2DynamicUIAlertViewDelegate
NSString *className = [NSString stringWithFormat:@"A2Dynamic%@%@", NSStringFromClass(cls), suffix];
Class ddClass = NSClassFromString(className);
if (ddClass) return ddClass;
cls = class_getSuperclass(cls);
}

return [A2DynamicDelegate class];
}
- (id)bk_dynamicDelegateWithClass:(Class)cls forProtocol:(Protocol *)protocol{

__block A2DynamicDelegate *dynamicDelegate;

dispatch_sync(a2_backgroundQueue(), ^{
dynamicDelegate = objc_getAssociatedObject(self, (__bridge const void *)protocol);

if (!dynamicDelegate) {
//将当前代理与当前对象关联,这里可以起到缓存作用
dynamicDelegate = [[cls alloc] initWithProtocol:protocol];
objc_setAssociatedObject(self, (__bridge const void *)protocol, dynamicDelegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
});
return dynamicDelegate;
}

最为关键的在于:

+ (void)bk_registerDynamicDelegateNamed:(NSString *)delegateName forProtocol:(Protocol *)protocol;

以及

- (id)bk_dynamicDelegateForProtocol:(Protocol *)protocol

在bk_registerDynamicDelegateNamed方法中会将通过bk_dynamicDelegateForProtocol方法获取到的代理对象通过关联对象与当前对象产生关联,也就是说将A2DynamicUIAlertViewDelegate对象作为当前的dynamicDelegate属性添加到当前类中,并且原本的delegate存到dynamicDelegate 的 realDelegate对象中,获取delegate的时候,返回就是dynamicDelegate这个管理对象。从而达到替换的目的。也就是说在我们调用delegate的时候实际上是返回A2DynamicUIAlertViewDelegate,这时候系统调用delegate方法的时候会转调A2DynamicUIAlertViewDelegate对应的方法,

我们接下来看下A2DynamicUIAlertViewDelegate中的具体一个代理方法:

- (BOOL)alertViewShouldEnableFirstOtherButton:(UIAlertView *)alertView {
BOOL should = YES;
id realDelegate = self.realDelegate;
if (realDelegate && [realDelegate respondsToSelector:@selector(alertViewShouldEnableFirstOtherButton:)])
should &= [realDelegate alertViewShouldEnableFirstOtherButton:alertView];

BOOL (^block)(UIAlertView *) = [self blockImplementationForMethod:_cmd];
if (block)
should &= block(alertView);

return should;
}

在这里调用了blockImplementationForMethod,通过这个方法取出这个代理对应的Block执行。我们看下blockImplementationForMethod方法:

- (id)blockImplementationForMethod:(SEL)selector{
A2BlockInvocation *invocation = nil;
if ((invocation = [self.invocationsBySelectors bk_objectForSelector:selector]))
return invocation.block;
return NULL;
}

这个方法会通过selector 从 A2BlockInvocation中获取对应的invocation,而block就存在这里,这个接下来会进行介绍。

这里做个简单的总结:

首先在load方法中通过bk_registerDynamicDelegate方法将delegate替换为我们已经实现好的A2DynamicUIAlertViewDelegate,这样在系统事件发生需要调用delegate方法的时候就会将流程转到A2DynamicUIAlertViewDelegate,在这里会通过当前selector从A2BlockInvocation中获取到对应的invocation,block就存在这个invocation中。那么这里就会产生一个问题,selector怎么和invocation产生关联的?我们接下来介绍这部分。

如果我们还有印象的话在load方法中还调用了bk_linkDelegateMethods

+ (void)load{
@autoreleasepool {
[self bk_registerDynamicDelegate];
[self bk_linkDelegateMethods:@{
@"bk_willShowBlock": @"willPresentAlertView:",
@"bk_didShowBlock": @"didPresentAlertView:",
@"bk_willDismissBlock": @"alertView:willDismissWithButtonIndex:",
@"bk_didDismissBlock": @"alertView:didDismissWithButtonIndex:",
@"bk_shouldEnableFirstOtherButtonBlock": @"alertViewShouldEnableFirstOtherButton:"
}];
}
}
+ (void)bk_linkDelegateMethods:(NSDictionary *)dictionary{
[self bk_linkProtocol:a2_delegateProtocol(self) methods:dictionary];
}

bk_linkProtocol是这部分最为关键的方法,会将block与delegate中的selector进行关联。

+ (void)bk_linkProtocol:(Protocol *)protocol methods:(NSDictionary *)dictionary {

[dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *selectorName, BOOL *stop) {

//......
IMP getterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject) {
A2DynamicDelegate *delegate = getDynamicDelegate(delegatingObject, protocol, info, NO);
return [delegate blockImplementationForMethod:selector];
});

if (!class_addMethod(self, getter, getterImplementation, "@@:")) {
NSCAssert(NO, @"Could not implement getter for \"%@\" property.", propertyName);
}

IMP setterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject, id block) {
A2DynamicDelegate *delegate = getDynamicDelegate(delegatingObject, protocol, info, YES);
[delegate implementMethod:selector withBlock:block];
});

if (!class_addMethod(self, setter, setterImplementation, "v@:@")) {
NSCAssert(NO, @"Could not implement setter for \"%@\" property.", propertyName);
}
}];
}

我们看下implementMethod

- (void)implementMethod:(SEL)selector withBlock:(id)block {
BOOL isClassMethod = self.isClassProxy;
//.......
struct objc_method_description methodDescription = protocol_getMethodDescription(self.protocol, selector, YES, !isClassMethod);
if (!methodDescription.name) methodDescription = protocol_getMethodDescription(self.protocol, selector, NO, !isClassMethod);

A2BlockInvocation *inv = nil;
if (methodDescription.name) {
NSMethodSignature *protoSig = [NSMethodSignature signatureWithObjCTypes:methodDescription.types];
inv = [[A2BlockInvocation alloc] initWithBlock:block methodSignature:protoSig];
} else {
inv = [[A2BlockInvocation alloc] initWithBlock:block];
}
[self.invocationsBySelectors bk_setObject:inv forSelector:selector];
}

在这里会新建一个A2BlockInvocation并将block 保存到 A2BlockInvocation中,并以selector为key A2BlockInvocation为value 存到self.invocationsBySelectors中。这样上一步就可以通过selector找到A2BlockInvocation进而找到关联的block了,这部分逻辑在blockImplementationForMethod中。

- (id)blockImplementationForMethod:(SEL)selector {
A2BlockInvocation *invocation = nil;
if ((invocation = [self.invocationsBySelectors bk_objectForSelector:selector]))
return invocation.block;
return NULL;
}

但是你们想过没为什么要通过A2BlockInvocation而不是直接将block与selector关联?这其实涉及到下面这些部分,往分类添加的属性,这些属性分类中并没有设置它的getter方法和setter方法。

@property (nonatomic, copy, setter = bk_setCancelBlock:) void (^bk_cancelBlock)(void);

@property (nonatomic, copy, setter = bk_setWillShowBlock:) void (^bk_willShowBlock)(UIAlertView *alertView);

@property (nonatomic, copy, setter = bk_setDidShowBlock:) void (^bk_didShowBlock)(UIAlertView *alertView);

@property (nonatomic, copy, setter = bk_setWillDismissBlock:) void (^bk_willDismissBlock)(UIAlertView *alertView, NSInteger buttonIndex);

@property (nonatomic, copy, setter = bk_setDidDismissBlock:) void (^bk_didDismissBlock)(UIAlertView *alertView, NSInteger buttonIndex);

@property (nonatomic, copy, setter = bk_SetShouldEnableFirstOtherButtonBlock:) BOOL (^bk_shouldEnableFirstOtherButtonBlock)(UIAlertView *alertView) NS_AVAILABLE_IOS(5_0);

这样做的目的就是为了通过消息分发机制来找到对应的block。

@implementation UIAlertView (BlocksKit)
@dynamic bk_willShowBlock, bk_didShowBlock, bk_willDismissBlock, bk_didDismissBlock, bk_shouldEnableFirstOtherButtonBlock;

整个分发过程在A2BlockInvocation类中。

到此为止整个流程已经结束,相关细节可以查看代码中的具体实现。

1. 开篇叨叨

在iOS内存优化总结那篇博客中提到过MLeaksFinder,并简要介绍了它的实现原理,这里主要从源码角度来看下MLeaksFinder是怎样做到内存泄漏检测的。

2. 代码结构与整体思路

我们先来看下整个源码的目录结构:

代码主要分成三类:

1. 各种对象的分类

NSObject+MemoryLeak.h
UIApplication+MemoryLeak.h
UINavigationController+MemoryLeak.h
UIPageViewController+MemoryLeak.h
UISplitViewController+MemoryLeak.h
UITabBarController+MemoryLeak.h
UITouch+MemoryLeak.h
UIView+MemoryLeak.h
UIViewController+MemoryLeak.h

这些分类主要完成触发对象回收方法的Hook,以及NSObject+MemoryLeak方法的覆盖。

2. MLeaksMessenger 发生泄漏时候的提醒执行对象

3. MLeakedObjectProxy 堆栈信息管理对象

再进一步往细地讲首先MemoryLeak分类会Hook对应触发回收的方法,一旦触发回收,就会递归地调用NSObject+MemoryLeak 中对应的willDealloc方法,再这个方法中会延迟2秒调用assertNotDealloc,如果这2秒内对象被销毁那么assertNotDealloc将不会被调用,否则将调用assertNotDealloc,在assertNotDealloc将会通过MLeakedObjectProxy构建泄漏堆栈,然后通过MLeaksMessenger发出泄漏提醒信息。

整个MLeaksFinder的代码量不是很大,顺着上面介绍的思路不会有太大理解上的问题:

3. 各个类的分类

首先我们看下Hook部分。我们先来看下每个分类都Hook了哪些方法:

  • NSObject+MemoryLeak 方法没有Hook任何方法,它只不过作为基类提供一些公共的方法。
  • UIApplication+MemoryLeak 方法,Hook了sendAction:to:from:forEvent:在这个方法中将sender存储起来,至于用来干啥用先卖个关子,后面介绍NSObject+MemoryLeak的时候介绍。
  • UINavigationController+MemoryLeak 主要是Hook了:
pushViewController:animated:
popViewControllerAnimated:
popToViewController:animated:
popToRootViewControllerAnimated:

它的主要工作是对每个pop出来对ViewController调用willDealloc

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
  • UIPageViewController+MemoryLeak,UISplitViewController+MemoryLeak,UITabBarController+MemoryLeak,UIView+MemoryLeak

里面都只有一个方法,主要是对自己对子ViewController进行判断是否泄漏。

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.viewControllers];
return YES;
}
  • UITouch+MemoryLeak中只Hook了setView:方法在这里将view保存为sender

  • UIViewController+MemoryLeak Hook了

viewDidDisappear:
viewWillAppear:
dismissViewControllerAnimated:completion:

并重写了:

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];

if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}

在这里对self.presentedViewController ,self.childViewControllers,self.view检测是否有泄漏。

4. 情景分析

接下来我们以通过push进入一个界面到pop出来为情景看下MLeaksFinder是怎样检测内存泄漏的:

首先是push方法:

- (void)swizzled_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
if (self.splitViewController) {
id detailViewController = objc_getAssociatedObject(self, kPoppedDetailVCKey);
if ([detailViewController isKindOfClass:[UIViewController class]]) {
[detailViewController willDealloc];
objc_setAssociatedObject(self, kPoppedDetailVCKey, nil, OBJC_ASSOCIATION_RETAIN);
}
}
[self swizzled_pushViewController:viewController animated:animated];
}

对于普通的viewController self.splitViewController 为 nil所以这里实际上是没做什么工作,我们直接略过。

这时候UIViewController 将会被push进来这时候会调用viewWillAppear,由于viewWillAppear被Hook了,所以实际上调用的是:

- (void)swizzled_viewWillAppear:(BOOL)animated {
[self swizzled_viewWillAppear:animated];
objc_setAssociatedObject(self, kHasBeenPoppedKey, @(NO), OBJC_ASSOCIATION_RETAIN);
}

这里会将kHasBeenPoppedKey设置为NO,这个有什么作用我们往下看:

在我们pop UIViewController的时候会调用swizzled_popViewControllerAnimated

- (UIViewController *)swizzled_popViewControllerAnimated:(BOOL)animated {
UIViewController *poppedViewController = [self swizzled_popViewControllerAnimated:animated];

//.....

// VC is not dealloced until disappear when popped using a left-edge swipe gesture
extern const void *const kHasBeenPoppedKey;
objc_setAssociatedObject(poppedViewController, kHasBeenPoppedKey, @(YES), OBJC_ASSOCIATION_RETAIN);

return poppedViewController;
}

在swizzled_popViewControllerAnimated方法中只是将kHasBeenPoppedKey设置为YES,但是并没调用willDealloc,这是因为在使用左边缘滑动关闭的时候,要等到UIViewController disappear的时候才开始销毁,所以这里只是设置一个标记延迟调用willDealloc。

在然后UIViewController的swizzled_viewDidDisappear会被调用,由于上面已经设置kHasBeenPoppedKey为YES,所以willDealloc就会被调用。

- (void)swizzled_viewDidDisappear:(BOOL)animated {
[self swizzled_viewDidDisappear:animated];
if ([objc_getAssociatedObject(self, kHasBeenPoppedKey) boolValue]) {
[self willDealloc];
}
}
- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}

willDealloc 中会先调用NSObject+MemoryLeak的willDealloc

- (BOOL)willDealloc {
NSString *className = NSStringFromClass([self class]);
if ([[NSObject classNamesWhitelist] containsObject:className])
return NO;

NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;

__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong id strongSelf = weakSelf;
[strongSelf assertNotDealloc];
});

return YES;
}

NSObject+MemoryLeak的willDealloc 中会先判断当前类是否在白名单中。

+ (NSMutableSet *)classNamesWhitelist {
static NSMutableSet *whitelist = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
whitelist = [NSMutableSet setWithObjects:
@"UIFieldEditor", // UIAlertControllerTextField
@"UINavigationBar",
@"_UIAlertControllerActionView",
@"_UIVisualEffectBackdropView",
nil];

// System's bug since iOS 10 and not fixed yet up to this ci.
NSString *systemVersion = [UIDevice currentDevice].systemVersion;
if ([systemVersion compare:@"10.0" options:NSNumericSearch] != NSOrderedAscending) {
[whitelist addObject:@"UISwitch"];
}
});
return whitelist;
}

也就是说UIFieldEditor,UINavigationBar,_UIAlertControllerActionView,_UIAlertControllerActionView以及10.0以后的UISwitch都在白名单中,如果是这些对象将不做检查。

下一步:

NSNumber *senderPtr = objc_getAssociatedObject([UIApplication sharedApplication], kLatestSenderKey);
if ([senderPtr isEqualToNumber:@((uintptr_t)self)])
return NO;

是否还记得在讲UIApplication+MemoryLeak中的Hook的时候有讲到UIApplication+MemoryLeak会将当前sender保存起来,这里就得提一下iOS 的 Target-Action机制了。当一个事件发生的时候,UIControl会调用sendAction:to:forEvent:将行为消息转发到UIApplication对象,再由UIApplication对象调用其sendAction:to:fromSender:forEvent:方法来将消息分发到指定的target上,而如果我们没有指定target,则会将事件分发到响应链上第一个想处理消息的对象上。而如果子类想监控或修改这种行为的话,则可以重写这个方法。在UIApplication+MemoryLeak将最近正在执行的sender存储起来就是为了在这个地方与self进行对比,也就是说如果当前对象正在执行action那么就不再对该对象进行内存检测。否则就会延迟两秒调用assertNotDealloc方法。

假设发生了内存泄漏,当前对象没有被释放,那么assertNotDealloc将会被调用:

- (void)assertNotDealloc {
if ([MLeakedObjectProxy isAnyObjectLeakedAtPtrs:[self parentPtrs]]) {
return;
}
[MLeakedObjectProxy addLeakedObject:self];
NSString *className = NSStringFromClass([self class]);
NSLog(@"Possibly Memory Leak.\nIn case that %@ should not be dealloced, override -willDealloc in %@ by returning NO.\nView-ViewController stack: %@", className, className, [self viewStack]);
}

在assertNotDealloc中将会检查是否已经在泄漏名单中了,如果已经在了就不再添加直接放回,如果不在那么就调用addLeakedObject将当前对象添加到泄漏名单。

+ (BOOL)isAnyObjectLeakedAtPtrs:(NSSet *)ptrs {
NSAssert([NSThread isMainThread], @"Must be in main thread.");
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
leakedObjectPtrs = [[NSMutableSet alloc] init];
});
if (!ptrs.count) {
return NO;
}
if ([leakedObjectPtrs intersectsSet:ptrs]) {
return YES;
} else {
return NO;
}
}

紧接着就会通过MLeaksMessenger 弹出弹窗提示用户泄漏堆栈:

+ (void)addLeakedObject:(id)object {
NSAssert([NSThread isMainThread], @"Must be in main thread.");

MLeakedObjectProxy *proxy = [[MLeakedObjectProxy alloc] init];
proxy.object = object;
proxy.objectPtr = @((uintptr_t)object);
proxy.viewStack = [object viewStack];
static const void *const kLeakedObjectProxyKey = &kLeakedObjectProxyKey;
objc_setAssociatedObject(object, kLeakedObjectProxyKey, proxy, OBJC_ASSOCIATION_RETAIN);

[leakedObjectPtrs addObject:proxy.objectPtr];

#if _INTERNAL_MLF_RC_ENABLED
[MLeaksMessenger alertWithTitle:@"Memory Leak"
message:[NSString stringWithFormat:@"%@", proxy.viewStack]
delegate:proxy
additionalButtonTitle:@"Retain Cycle"];
#else
[MLeaksMessenger alertWithTitle:@"Memory Leak"
message:[NSString stringWithFormat:@"%@", proxy.viewStack]];
#endif
}

如果我们点击弹窗上的按钮查看具体的循环引用信息那么将会调用下面的方法:

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (!buttonIndex) {
return;
}

id object = self.object;
if (!object) {
return;
}

#if _INTERNAL_MLF_RC_ENABLED
dispatch_async(dispatch_get_global_queue(0, 0), ^{
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self.object];
NSSet *retainCycles = [detector findRetainCyclesWithMaxCycleLength:20];

BOOL hasFound = NO;
for (NSArray *retainCycle in retainCycles) {
NSInteger index = 0;
for (FBObjectiveCGraphElement *element in retainCycle) {
if (element.object == object) {
NSArray *shiftedRetainCycle = [self shiftArray:retainCycle toIndex:index];

dispatch_async(dispatch_get_main_queue(), ^{
[MLeaksMessenger alertWithTitle:@"Retain Cycle"
message:[NSString stringWithFormat:@"%@", shiftedRetainCycle]];
});
hasFound = YES;
break;
}

++index;
}
if (hasFound) {
break;
}
}
if (!hasFound) {
dispatch_async(dispatch_get_main_queue(), ^{
[MLeaksMessenger alertWithTitle:@"Retain Cycle"
message:@"Fail to find a retain cycle"];
});
}
});
#endif
}

在alertView方法中对通过FBRetainCycleDetector来检测循环引用,然后通过弹窗进行展示,这部分将会在FBRetainCycleDetector源码解析中进行详细介绍。

除了对UIViewController 自身调用willDealloc还需要对当前UIViewController的self.presentedViewController,self.childViewControllers以及self.view调用willDealloc判断是否有泄漏发生。

- (BOOL)willDealloc {
if (![super willDealloc]) {
return NO;
}
[self willReleaseChildren:self.childViewControllers];
[self willReleaseChild:self.presentedViewController];
if (self.isViewLoaded) {
[self willReleaseChild:self.view];
}
return YES;
}

如果某个对象被检测到2秒内还没被释放,但是在2秒之后还是调用了dealloc释放了,那么这种不算是内存泄漏,所以会弹出Object Deallocated提示该对象已经被释放了,不属于内存泄漏。

- (void)dealloc {
NSNumber *objectPtr = _objectPtr;
NSArray *viewStack = _viewStack;
dispatch_async(dispatch_get_main_queue(), ^{
[leakedObjectPtrs removeObject:objectPtr];
[MLeaksMessenger alertWithTitle:@"Object Deallocated"
message:[NSString stringWithFormat:@"%@", viewStack]];
});
}

开源库资料

源码地址

接触过Web前端开发的同学一定知道Promise这个东西,Promise其实就是一个封装着异步操作的一个对象,它可以通过resolve以及reject来控制整个分支的流程。个人理解这个就相当于一个没有办法立即给予答复的承诺,只知道这个地方会在恰当的时机会得到一个答复,至于什么时候得到怎样的答复,新建promise的时候并不确定。

下面是官方给出的一个定义:

A promise represents the future value of a task.

每个Promises都有一个状态,新建的每个Promises都处于pending状态,而后会转到resolve状态,resolve状态可以是fulfilled 或者 rejected ,如果是rejected 则会收到一个NSError,如果是fulfilled将会收到任何形式的对象。这个具体放在下面介绍。

下面的使用是基于PromiseKit 6.11.0 版本,可以通过下面的pod 方式导入:

use_frameworks!
pod 'PromiseKit', '~> 6.11.0'

一个简单的例子:

[self promise_registerWithName:@"linxiaohai"].then(^(NSString *userName){
NSLog(@"Regist Successfull %@ !!!",userName);
return [self promise_loginWithName:userName];
}).then(^(NSString *userName) {
NSLog(@"Login Successfull %@ !!!",userName);
}).catch(^(){
NSLog(@"Login Failed !!!");
}).finally(^(){
NSLog(@"Finally !!!");
});

- (PMKPromise *)promise_registerWithName:(NSString *)userName {
return [PMKPromise promiseWithAdapter:^(PMKAdapter adapter) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
adapter(userName,nil);
});
}];
}

- (PMKPromise *)promise_loginWithName:(NSString *)userName {
return [PMKPromise promiseWithAdapter:^(PMKAdapter adapter) {
adapter(userName,nil);
}];
}

如果我们没有使用PromiseKit那么我们会在注册成功的block里面嵌套调用登录的请求,如果一个流程只是这两步还可以容忍,但是如果超过三步整个代码就会显得非常乱。因此如果在实际项目中遇到一个请求依赖上一个请求成功后才发起请求,也就是存在链式请求的话就可以考虑使用PromiseKit来规避Callback Hell.

PromiseKit的创建:

+ (instancetype)promiseWithValue:(id)value 

将某个值封装成Promise返回

[AnyPromise promiseWithValue:@"linxiaohai"].then(^(id value){
NSLog(@"%@",value);
});
+ (instancetype)promiseWithResolverBlock:(void (^)(PMKResolver _Nonnull))resolveBlock

当我们想将我们的非Promise异步代码添加到Promise链中的时候就可以使用上面这种方式来封装(需要注意的是这个方法仅仅适用于非Promise异步代码)这里的PMKResolver block参数是id类型,如果传入的是非NSError类型的话那么就会走到then流程,如果是NSError类型的话就会走catch流程。经过封装后的异步代码就可以使用then来链式调用了。下面是一个非常简单的一个例子:

[AnyPromise promiseWithResolverBlock:^(PMKResolver _Nonnull resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
resolver(@"linxiaohai");
});
}].then(^(id value){
NSLog(@"%@",value);
});

下面是使用PMKAdapter来包装异步请求

+ (instancetype)promiseWithAdapter:(void (^)(PMKAdapter adapter))block;
+ (instancetype)promiseWithIntegerAdapter:(void (^)(PMKIntegerAdapter adapter))block;
+ (instancetype)promiseWithBooleanAdapter:(void (^)(PMKBooleanAdapter adapter))block;

PMKAdapter Block有两个参数,第一个参数是id类型,第二个参数是NSError类型:

typedef void (^PMKAdapter)(id, NSError *);

具体走哪个流程取决于两个值的情况,可以参照下表:

下面是一个例子:

[AnyPromise promiseWithAdapterBlock:^(PMKAdapter  _Nonnull adapter) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
adapter(@"linxiaohai",[NSError errorWithDomain:@"test" code:100 userInfo:nil]);
});
}].then(^(id value){
NSLog(@"%@",value);
}).catch(^(NSError *error){
NSLog(@"%@",error);
});

PMKWhen:

PMKWhen的参数是一个Promise数组,或者字典,它会等待所有的Promise执行结束,或者有一个Error发生,也就是说如果这些异步的Promise都没有Error的时候,会等到都Resolve后才执行then,并且then Block 传递回来的是各个Promise执行的结果,如果中途有Error发生就会中断,并且走到catch流程。下面是一个简单例子:

- (void)onCreate {
[super onCreate];

PMKWhen(@[[self promise_delay_1s],[self promise_delay_error],[self promise_delay_3s],[self promise_delay_8s]]).then(^(id value){
NSLog(@"PMKWhen %@",value);
}).catch(^(NSError *error){
NSLog(@"PMKWhen %@",error);
});
}

- (AnyPromise *)promise_delay_1s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_1s");
resolver(@"PMKWhen promise_delay_1s");
});
}];
}

- (AnyPromise *)promise_delay_3s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_3s");
resolver(@"PMKWhen promise_delay_3s");
});
}];
}

- (AnyPromise *)promise_delay_8s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_8s");
resolver(@"PMKWhen promise_delay_3s");
});
}];
}

- (AnyPromise *)promise_delay_error {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_error");
resolver([NSError errorWithDomain:@"test" code:100 userInfo:nil]);
});
}];
}

输出结果如下:

2019-11-04 14:19:46.896521+0800 IDLFundationTest[91339:11794858] PMKWhen promise_delay_1s
2019-11-04 14:19:48.994048+0800 IDLFundationTest[91339:11794858] PMKWhen promise_delay_3s
2019-11-04 14:19:52.477392+0800 IDLFundationTest[91339:11794858] PMKWhen promise_delay_error
2019-11-04 14:19:52.477859+0800 IDLFundationTest[91339:11794858] PMKWhen Error Domain=test Code=100 "(null)" UserInfo={NSUnderlyingError=0x600000c64450 {Error Domain=test Code=100 "(null)"}, PMKFailingPromiseIndexKey=0}
2019-11-04 14:19:54.694791+0800 IDLFundationTest[91339:11794858] PMKWhen promise_delay_8s

这里需要注意的是6s发生错误之后走catch分支,之后就不再走then分支了,但是error发生之后promise_delay_8s还会继续进行,并不会中止。

PMKJoin

这个和PMKWhen有类似的地方,就是接受的参数是字典或者数组,但是它会等待所有的Promise都解决后才走then或者catch分支,不像PMKWhen那样一旦有错误发生就catch,下面是一个例子可以对比下:

- (void)onCreate {
[super onCreate];

PMKJoin(@[[self promise_delay_1s],[self promise_delay_error],[self promise_delay_3s],[self promise_delay_8s]]).then(^(id value){
NSLog(@"PMKWhen %@",value);
}).catch(^(NSError *error){
NSLog(@"PMKWhen %@",error);
});
}

- (AnyPromise *)promise_delay_1s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_1s");
resolver(@"PMKWhen promise_delay_1s");
});
}];
}

- (AnyPromise *)promise_delay_3s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_3s");
resolver(@"PMKWhen promise_delay_3s");
});
}];
}

- (AnyPromise *)promise_delay_8s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_8s");
resolver(@"PMKWhen promise_delay_8s");
});
}];
}

- (AnyPromise *)promise_delay_error {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_error");
resolver([NSError errorWithDomain:@"test" code:100 userInfo:nil]);
});
}];
}

下面是执行的结果:

2019-11-04 14:23:53.419624+0800 IDLFundationTest[94348:11805962] PMKWhen promise_delay_1s
2019-11-04 14:23:55.419830+0800 IDLFundationTest[94348:11805962] PMKWhen promise_delay_3s
2019-11-04 14:23:59.018698+0800 IDLFundationTest[94348:11805962] PMKWhen promise_delay_error
2019-11-04 14:24:01.218821+0800 IDLFundationTest[94348:11805962] PMKWhen promise_delay_8s
2019-11-04 14:24:01.221100+0800 IDLFundationTest[94348:11805962] PMKWhen Error Domain=PMKErrorDomain Code=10 "(null)" UserInfo={PMKJoinPromisesKey=(
"AnyPromise(PMKWhen promise_delay_1s)",
"AnyPromise(PMKWhen promise_delay_3s)",
"AnyPromise(PMKWhen promise_delay_8s)"

可以看出catch是在全部Promise完成后执行的,并且在UserInfo中可以看出哪些是成功的Promise.

PMKRace

PMKRace 会在第一个resolve(不论是fullfill还是reject)的时候执行then或者catch。

- (void)onCreate {
[super onCreate];

PMKRace(@[[self promise_delay_1s],[self promise_delay_error],[self promise_delay_3s],[self promise_delay_8s]]).then(^(id value){
NSLog(@"PMKWhen %@",value);
}).catch(^(NSError *error){
NSLog(@"PMKWhen %@",error);
});
}

- (AnyPromise *)promise_delay_1s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_1s");
resolver(@"PMKWhen promise_delay_1s");
});
}];
}

- (AnyPromise *)promise_delay_3s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_3s");
resolver(@"PMKWhen promise_delay_3s");
});
}];
}

- (AnyPromise *)promise_delay_8s {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_8s");
resolver(@"PMKWhen promise_delay_8s");
});
}];
}

- (AnyPromise *)promise_delay_error {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolver) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(6 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"PMKWhen promise_delay_error");
resolver([NSError errorWithDomain:@"test" code:100 userInfo:nil]);
});
}];
}

下面是执行结果:

2019-11-04 14:34:06.333085+0800 IDLFundationTest[2020:11831274] PMKWhen promise_delay_1s
2019-11-04 14:34:06.333917+0800 IDLFundationTest[2020:11831274] PMKWhen PMKWhen promise_delay_1s
2019-11-04 14:34:08.532678+0800 IDLFundationTest[2020:11831274] PMKWhen promise_delay_3s
2019-11-04 14:34:11.834737+0800 IDLFundationTest[2020:11831274] PMKWhen promise_delay_error
2019-11-04 14:34:14.034667+0800 IDLFundationTest[2020:11831274] PMKWhen promise_delay_8s

Present ViewController 后返回结果:

在刚接触iOS开发的时候感觉这部分没有Android来得便捷,在Android中直接通过onActivityResult回调就可以拿到上一个页面传递回来的值,而PromiseKit提供了这个便捷方法,下面是一个例子:

页面A,开启一个页面并等待页面的返回值

[self.viewHolder.presentViewControllerBtn bk_whenTapped:^{
[self promiseViewController:[IDLListKitViewController new] animated:YES completion:nil].then(^(id value){
NSLog(@"Return Value from last ViewController %@",value);
});
}];

页面B,经过一系列处理后返回值给上一个界面,并关闭当前界面

[PMKPromise when:@{@"task1":[self promise_delay_1s],@"task2":[self promise_delay_8s],@"task3":@"linxiaohai"}].then(^(id value) {
NSLog(@"%@",value);
[self fulfill:value];
});

UIView 视图动画:

[UIView promiseWithDuration:3 animations:^{
self.viewHolder.pushViewControllerBtn.alpha = 0;
}].then(^(){

});

获取Promise值:

Promise值是通过then来获取的,PromiseKit目前有三个方法用于Promise值的获取,分别是then,thenInBackground,以及thenOn,分别在主线程,后台线程,指定线程获取Promise值。

- (AnyPromise * __nonnull (^ __nonnull)(id __nonnull))then NS_REFINED_FOR_SWIFT;
- (AnyPromise * __nonnull(^ __nonnull)(id __nonnull))thenInBackground NS_REFINED_FOR_SWIFT;
- (AnyPromise * __nonnull(^ __nonnull)(dispatch_queue_t __nonnull, id __nonnull))thenOn NS_REFINED_FOR_SWIFT;

捕获异常:

在Promise reject 的时候,流程会走到catch分支,PromiseKit有两种catch方式来catch抛出的异常,一种在主线程,一种需要指定处理异常的线程。

- (AnyPromise * __nonnull(^ __nonnull)(id __nonnull))catch NS_REFINED_FOR_SWIFT;
- (AnyPromise * __nonnull(^ __nonnull)(id __nonnull))catchInBackground NS_REFINED_FOR_SWIFT;
- (AnyPromise * __nonnull(^ __nonnull)(dispatch_queue_t __nonnull, id __nonnull))catchOn NS_REFINED_FOR_SWIFT;

善后处理:

在Promise 被 resolve的时候,不论结果是fullfill还是reject都会走到ensure这个分支。


- (AnyPromise * __nonnull(^ __nonnull)(dispatch_block_t __nonnull))ensure NS_REFINED_FOR_SWIFT;
- (AnyPromise * __nonnull(^ __nonnull)(dispatch_queue_t __nonnull, dispatch_block_t __nonnull))ensureOn NS_REFINED_FOR_SWIFT;

异步处理:

AnyPromise *dispatch_promise_on(dispatch_queue_t queue, id block);

在某个队列下执行block操作

AnyPromise *dispatch_promise(id block);
dispatch_promise(^{
return md5(input);
}).then(^(NSString *md5){
NSLog(@"md5: %@", md5);
});

在后台队列下执行block操作,如果要取消某个操作可以return NSError.一旦返回一个id对象就相当于启动了一个Promise链。

开源代码信息

YYCache 开源库地址

我们在分析YTKNetWork,SDWebImage的时候涉及到缓存的实现,缓存一般都包含内存缓存和磁盘缓存,并且提供磁盘空间控制,缓存内容有效期控制等功能,我们接下来也从这几个方面对YYCache进行分析。YYCache代码量不多很精简,建议大家都可以尝试阅读下,因为一旦看惯代码了,就不会觉得畏惧了。下面先给出一个之前已经绘制好的一个YYCache构成图:

整个开源库也只有8个文件四个类:

  • YYCache: YYCache 的顶层类,业务主要和它进行交互获取实现对应的缓存任务。

  • YYMemoryCache: 内存缓存,它是基于双向链表结构,它和NSCache的区别在于,YYMemoryCache 剔除过期对象是基于LRU方法,而NSCache剔除对象的方法是不确定的。
    YYMemoryCache可以根据缓存的cost,count,age来控制缓存,而NSCache不能。在收到内存缓存不足警告,或者应用进入后台的时候可以指定剔除某些缓存。

  • YYDiskCache: 它是基于文件以及sqlite数据的磁盘缓存,它会根据不同的情况自动选择使用文件还是数据库来存储缓存,和YYMemoryCache类似它也是给予LRU原则来移除无用的缓存的,也可以根据缓存的cost,count,age来控制缓存。并且能够在磁盘空间不足的时候自动剔除无用的数据。

  • YYKVStorage 是YYDiskCache执行存储的重要对象。后面介绍源码的时候我们会详细看下这部分实现。

我们先来看下YYCache,YYCache我们这里只会做简单介绍,因为它大部分的功能都依托于YYMemoryCache和YYDiskCache。它使用YYMemoryCache用于存储小而快的内存缓存,使用YYDiskCache来存储大的对象。它提供了对缓存进行增删改查的操作,并且每种操作都提供了同步和异步两种方式。

这里仅仅列出同步的增删改查代码其实上层的逻辑和其他的缓存没有太多的差别,重点是底层的实现:

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
[_memoryCache setObject:object forKey:key];
[_diskCache setObject:object forKey:key];
}

- (void)removeObjectForKey:(NSString *)key {
[_memoryCache removeObjectForKey:key];
[_diskCache removeObjectForKey:key];
}

- (id<NSCoding>)objectForKey:(NSString *)key {
id<NSCoding> object = [_memoryCache objectForKey:key];
if (!object) {
object = [_diskCache objectForKey:key];
if (object) {
[_memoryCache setObject:object forKey:key];
}
}
return object;
}

- (BOOL)containsObjectForKey:(NSString *)key {
return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}

YYMemoryCache

YYMemoryCache 内部是基于 _YYLinkedMap 双向链表实现的。我们看下YYMemoryCache初始化:

- (instancetype)init {
self = super.init;
pthread_mutex_init(&_lock, NULL);
//创建_YYLinkedMap
_lru = [_YYLinkedMap new];
//创建串行队列
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
//最大缓存对象数量限制
_countLimit = NSUIntegerMax;
//最大空间消耗限制
_costLimit = NSUIntegerMax;
//最大过期时间限制
_ageLimit = DBL_MAX;
//自动缓存清理触发时间
_autoTrimInterval = 5.0;
//在收到内存警告的时候是否移除全部缓存对象
_shouldRemoveAllObjectsOnMemoryWarning = YES;
//在进入后台的时候是否移除全部缓存对象
_shouldRemoveAllObjectsWhenEnteringBackground = YES;

//注册内存警告通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
//注册进入后台的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];

//间隔_autoTrimInterval时间定期清除过期,超过限制的缓存
[self _trimRecursively];
return self;
}

_YYLinkedMap是一个双向链表,这部分代码不展开介绍,但是给出了代码的相关注释:

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
//计算_totalCost
_totalCost += node->_cost;
//计算_totalCount
_totalCount++;
if (_head) {
//将node插入到head之前
node->_next = _head;
_head->_prev = node;
_head = node;
} else {
//空的双向链表
_head = _tail = node;
}
}

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
if (_head == node) return;

// 将node从原来的双向链表中分离开来
if (_tail == node) {
_tail = node->_prev;
_tail->_next = nil;
} else {
node->_next->_prev = node->_prev;
node->_prev->_next = node->_next;
}
//将node插入到head
node->_next = _head;
node->_prev = nil;
_head->_prev = node;
_head = node;
}

- (void)removeNode:(_YYLinkedMapNode *)node {
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
//计算_totalCost
_totalCost -= node->_cost;
//计算_totalCount
_totalCount--;
//移除节点
if (node->_next) node->_next->_prev = node->_prev;
if (node->_prev) node->_prev->_next = node->_next;
if (_head == node) _head = node->_next;
if (_tail == node) _tail = node->_prev;
}

- (_YYLinkedMapNode *)removeTailNode {
if (!_tail) return nil;
_YYLinkedMapNode *tail = _tail;
CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
_totalCost -= _tail->_cost;
_totalCount--;
if (_head == _tail) {
_head = _tail = nil;
} else {
_tail = _tail->_prev;
_tail->_next = nil;
}
return tail;
}

- (void)removeAll {
_totalCost = 0;
_totalCount = 0;
_head = nil;
_tail = nil;
if (CFDictionaryGetCount(_dic) > 0) {
CFMutableDictionaryRef holder = _dic;
_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
if (_releaseAsynchronously) {
dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
CFRelease(holder); // hold and release in specified queue
});
} else if (_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
CFRelease(holder); // hold and release in specified queue
});
} else {
CFRelease(holder);
}
}
}

增/改缓存

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
// 如果设置的object对象是空的那么就会移除这个对象
if (!object) {
[self removeObjectForKey:key];
return;
}
pthread_mutex_lock(&_lock);
//创建一个节点
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();

if (node) {
//要设置的节点在缓存链表中,用新的值来更新节点内容
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
//节点移动到缓存链表的头部,
[_lru bringNodeToHead:node];
} else {
//要设置的节点不在缓存链表中,新建一个node将值传入并插入到双向链表的头部
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
[_lru insertNodeAtHead:node];
}
//如果缓存双向链表总的空间超过了限制大小调用trimToCost清除缓存
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
//如果缓存数量超过了限制,移除最后的节点
if (_lru->_totalCount > _countLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}

setObject方法会首先检查传入的当前对象是否是nil,如果是nil会从缓存链表中删除传入key的元素。如果不是nil,则会在缓存链表中查看是否已经存在,如果是的话,就使用传入的object更新节点内容,然后将其移到链表的最前面。如果不存在就新建一个node插入到缓存链表头部,然后会对链表的空间以及链表节点数量进行检查,剔除掉不用的对象。

删除缓存

这个不做过多介绍大家直接看代码:

- (void)removeObjectForKey:(id)key {
if (!key) return;
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
[_lru removeNode:node];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}

查找缓存

- (id)objectForKey:(id)key {
if (!key) return nil;
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
//每次查找之前都会将找到的对象移到缓存链表的最前面
if (node) {
node->_time = CACurrentMediaTime();
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}

- (BOOL)containsObjectForKey:(id)key {
if (!key) return NO;
pthread_mutex_lock(&_lock);
BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
pthread_mutex_unlock(&_lock);
return contains;
}

这部分最关键的是它会在每次查找缓存,找到的时候将找到的对象移动到缓存链表最前面。

缓存清理

我们上面提到了在初始化的时候会触发定期清理缓存操作,默认每隔5秒进行一次。并且在每次添加缓存的时候还会触发一次。

- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
//如果costLimit被设置为0那么就将所有的缓存都删除
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;

NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
//如果缓存的存储空间还是超过了限制,就从缓存尾部移除一个暂时添加到holder,避免频繁对内存读写,导致锁被占用,
if (_lru->_totalCost > costLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
//最后将待清除的对象所占用的空间清除。
if (holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
}
}

_trimToCost 中如果有超过限制就会不断从缓存链表队尾移除一个对象,然后先将待移除的对象暂时添加到holder,避免频繁对内存读写,导致锁被占用,直到没有超过限制的时候在对应的队列中将这些对象的空间释放。****_trimToCount_trimToAge**** 和 _trimToCost 代码类似,只不过判断的标准不一样而已,这里就不展开介绍了,有兴趣的同学可以查看对应的代码。

YYDiskCache

磁盘缓存和内存缓存对外的接口大体类似,只有一点不大一致的:

  • inlineThreshold

它多出了inlineThreshold属性,它是一个阈值,最开始的时候磁盘缓存是存储在sqlite上的,一旦存储在sqlite的磁盘缓存超过这个值的时候后续的对象就会被存储到文件,如果设置为0的话表示所有的缓存都会被存储到文件,NSUIntegerMax表示所有的对象都会被存储到sqlite,默认这个阈值是20KB

@property (readonly) NSUInteger inlineThreshold;
  • 自定义归档/反归档处理

多出了归档对象的方式,默认情况下YYCache是通过NSKeyedArchiver和NSKeyedUnarchiver进行归档和反归档,但是这要求对象必须遵循NSCoding协议,如果某些对象没法做到这一点,那么可以通过指定customArchiveBlock以及customUnarchiveBlock来自定义归档和反归档将要存储的对象:

@property (nullable, copy) NSData *(^customArchiveBlock)(id object);
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data);
  • 自定义缓存文件名规范

默认缓存文件名是通过md5(key)计算出来的,但是我们可以通过customFileNameBlock来自定义key与对它对应的缓存文件名字的命名规范。

@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key);
  • 可以扩张缓存对象的数据

在对缓存对象进行缓存之前,还可以通过下面的方法对缓存数据进行扩展,它实际上是通过关联对象的方式来实现的。

+ (nullable NSData *)getExtendedDataFromObject:(id)object;
+ (void)setExtendedData:(nullable NSData *)extendedData toObject:(id)object;

我们接下来开始分析YYDiskCache

YYDiskCache的初始化

- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}

- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;

//我们在项目中可能会有多个YYDiskCache,我们会将这些YYDiskCache的实例以path为key,缓存到类型为NSMapTable的_globalInstances中。
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
//如果缓存中有就先用缓存中的,避免重复创建
if (globalCache) return globalCache;

//根据threshold来设定缓存的存储方式:
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}

//新建一个YYKVStorage,所有的磁盘存储都由YYKVStorage来完成
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;

_kv = kv;
_path = path;
_lock = dispatch_semaphore_create(1);
//磁盘操作队列
_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
//一系列的限制
_inlineThreshold = threshold;
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_freeDiskSpaceLimit = 0;
//缓存整理主动触发时间默认为1分钟
_autoTrimInterval = 60;

//触发缓存整理
[self _trimRecursively];

//将当前YYDiskCache缓存起来
_YYDiskCacheSetGlobal(self);

//在退出应用的时候会将kv设置为nil
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
return self;
}

我们在项目中可能会有多个YYDiskCache,我们会将这些YYDiskCache的实例以path为key,缓存到类型为NSMapTable的_globalInstances中。所以我们在最初的时候会先从globalInstances中先看下是否有已经创建好的,如果没有再新建YYDiskCache并缓存起来。YYDiskCache中最关键的就是YYKVStorage了,所有的磁盘存储都由YYKVStorage来完成。

YYDiskCache上层比较简单,主要的逻辑在YYKVStorage,但是在介绍YYKVStorage之前我们先简单浏览下YYDiskCache:

增/改缓存

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
//如果object为空表示从磁盘缓存中删除改对象,这个和YYMemoryCache是一致的
if (!object) {
[self removeObjectForKey:key];
return;
}

//获取某个对象额外需要追加到对象上的数据
NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
//如果指定了自定义的归档方式,那么使用自定义的归档方式来对要缓存的对象归档为NSData存储到磁盘缓存中
if (_customArchiveBlock) {
value = _customArchiveBlock(object);
} else {
//否则使用NSKeyedArchiver进行归档
@try {
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (!value) return;
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}

//磁盘缓存
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}

上面代码注释得比较详细了,大家看代码就能看懂,就不展开了。

删除缓存

- (void)removeObjectForKey:(NSString *)key {
if (!key) return;
Lock();
[_kv removeItemForKey:key];
Unlock();
}

查找缓存

- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}

- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block {
if (!block) return;
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
BOOL contains = [self containsObjectForKey:key];
block(key, contains);
});
}

- (id<NSCoding>)objectForKey:(NSString *)key {
if (!key) return nil;
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
if (!item.value) return nil;

id object = nil;
if (_customUnarchiveBlock) {
object = _customUnarchiveBlock(item.value);
} else {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
return object;
}

还是不大想解释,代码不难。

缓存清理


- (void)_trimToCost:(NSUInteger)costLimit {
if (costLimit >= INT_MAX) return;
[_kv removeItemsToFitSize:(int)costLimit];
}

- (void)_trimToCount:(NSUInteger)countLimit {
if (countLimit >= INT_MAX) return;
[_kv removeItemsToFitCount:(int)countLimit];
}

- (void)_trimToAge:(NSTimeInterval)ageLimit {
if (ageLimit <= 0) {
[_kv removeAllItems];
return;
}
long timestamp = time(NULL);
if (timestamp <= ageLimit) return;
long age = timestamp - ageLimit;
if (age >= INT_MAX) return;
[_kv removeItemsEarlierThanTime:(int)age];
}

YYKVStorage

存储对象

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
//.......
if (filename.length) {
//如果filename不为空,表示使用文件作为磁盘缓存,这时候会先将缓存写到文件中
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
//如果缓存成功写入到缓存,那么就更新缓存记录数据
if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {
//如果缓存记录数据存储失败,那么会将缓存文件同时删除以保证缓存文件与缓存记录的同步
[self _fileDeleteWithName:filename];
return NO;
}
return YES;
} else {
if (_type != YYKVStorageTypeSQLite) {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
//由于filename为nil那么会将缓存数据缓存到inline_data字段。
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}

saveItemWithKey方法中会先判断filename是否为空,如果是有指定缓存文件名,那么就会先将缓存数据写入到文件缓存起来,同时将缓存记录写入到数据库中,作为缓存记录。如果filename为nil,那么就直接将缓存数据缓存到数据库inline_data字段。接下来我们来看下_dbSaveWithKey:


- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;

int timestamp = (int)time(NULL);
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
sqlite3_bind_int(stmt, 3, (int)value.length);
if (fileName.length == 0) {
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
sqlite3_bind_int(stmt, 5, timestamp);
sqlite3_bind_int(stmt, 6, timestamp);
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);

int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}

我们看这个方法主要是了解这个缓存记录表的结构设计:这个缓存记录表有7个字段分别列举如下:

* key               缓存的key
* filename 使用文件缓存的情况下该文件存储的路径
* size 缓存数据大小
* inline_data 数据库缓存的地方
* modification_time 缓存修改时间
* last_access_time 上一次访问时间
* extended_data 额外追加数据

删除对象

- (BOOL)removeItemForKey:(NSString *)key {
if (key.length == 0) return NO;
switch (_type) {
case YYKVStorageTypeSQLite: {
//如果是YYKVStorageTypeSQLite 则只删除数据库记录就可以了,因为这时候数据是存储在数据库记录上的
return [self _dbDeleteItemWithKey:key];
} break;
case YYKVStorageTypeFile:
case YYKVStorageTypeMixed: {
//如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed的形式,会同时删除缓存文件以及数据库缓存记录上的内容
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
return [self _dbDeleteItemWithKey:key];
} break;
default: return NO;
}
}

如果了解了上面的表结构,其实在逻辑层面上就比较好理解了,这里就不展开介绍了。下面是一些条件删除,只是删除数据库的时候条件不一样罢了,没有啥区别:

- (BOOL)removeItemsLargerThanSize:(int)size {
if (size == INT_MAX) return YES;
if (size <= 0) return [self removeAllItems];

switch (_type) {
case YYKVStorageTypeSQLite: {
if ([self _dbDeleteItemsWithSizeLargerThan:size]) {
[self _dbCheckpoint];
return YES;
}
} break;
case YYKVStorageTypeFile:
case YYKVStorageTypeMixed: {
NSArray *filenames = [self _dbGetFilenamesWithSizeLargerThan:size];
for (NSString *name in filenames) {
[self _fileDeleteWithName:name];
}
if ([self _dbDeleteItemsWithSizeLargerThan:size]) {
[self _dbCheckpoint];
return YES;
}
} break;
}
return NO;
}

- (BOOL)removeItemsEarlierThanTime:(int)time {
if (time <= 0) return YES;
if (time == INT_MAX) return [self removeAllItems];

switch (_type) {
case YYKVStorageTypeSQLite: {
if ([self _dbDeleteItemsWithTimeEarlierThan:time]) {
[self _dbCheckpoint];
return YES;
}
} break;
case YYKVStorageTypeFile:
case YYKVStorageTypeMixed: {
NSArray *filenames = [self _dbGetFilenamesWithTimeEarlierThan:time];
for (NSString *name in filenames) {
[self _fileDeleteWithName:name];
}
if ([self _dbDeleteItemsWithTimeEarlierThan:time]) {
[self _dbCheckpoint];
return YES;
}
} break;
}
return NO;
}
- (BOOL)removeItemsToFitSize:(int)maxSize {
if (maxSize == INT_MAX) return YES;
if (maxSize <= 0) return [self removeAllItems];

int total = [self _dbGetTotalItemSize];
if (total < 0) return NO;
if (total <= maxSize) return YES;

NSArray *items = nil;
BOOL suc = NO;
do {
int perCount = 16;
items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
for (YYKVStorageItem *item in items) {
if (total > maxSize) {
if (item.filename) {
[self _fileDeleteWithName:item.filename];
}
suc = [self _dbDeleteItemWithKey:item.key];
total -= item.size;
} else {
break;
}
if (!suc) break;
}
} while (total > maxSize && items.count > 0 && suc);
if (suc) [self _dbCheckpoint];
return suc;
}

- (BOOL)removeItemsToFitCount:(int)maxCount {
if (maxCount == INT_MAX) return YES;
if (maxCount <= 0) return [self removeAllItems];

int total = [self _dbGetTotalItemCount];
if (total < 0) return NO;
if (total <= maxCount) return YES;

NSArray *items = nil;
BOOL suc = NO;
do {
int perCount = 16;
items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
for (YYKVStorageItem *item in items) {
if (total > maxCount) {
if (item.filename) {
[self _fileDeleteWithName:item.filename];
}
suc = [self _dbDeleteItemWithKey:item.key];
total--;
} else {
break;
}
if (!suc) break;
}
} while (total > maxCount && items.count > 0 && suc);
if (suc) [self _dbCheckpoint];
return suc;
}

获取对象

这里也不想介绍了,大家看注释吧,其实了解了数据库表结构,以及存储的思路,删除和获取其实很好理解的。

- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
//通过key 从数据库或者到当前缓存对象的缓存记录。
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
//更新下缓存访问时间
[self _dbUpdateAccessTimeWithKey:key];
//如果缓存记录的filename不为空,则表示是文件缓存
if (item.filename) {
//读取文件,将数据添加到value字段
item.value = [self _fileReadWithName:item.filename];
if (!item.value) {
//如果缓存文件数据为空,删除缓存记录,保持缓存数据记录与缓存文件同步
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}

- (YYKVStorageItem *)getItemInfoForKey:(NSString *)key {
if (key.length == 0) return nil;
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:YES];
return item;
}

- (NSData *)getItemValueForKey:(NSString *)key {
if (key.length == 0) return nil;
NSData *value = nil;
switch (_type) {
case YYKVStorageTypeFile: {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
value = [self _fileReadWithName:filename];
if (!value) {
[self _dbDeleteItemWithKey:key];
value = nil;
}
}
} break;
case YYKVStorageTypeSQLite: {
value = [self _dbGetValueWithKey:key];
} break;
case YYKVStorageTypeMixed: {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
value = [self _fileReadWithName:filename];
if (!value) {
[self _dbDeleteItemWithKey:key];
value = nil;
}
} else {
value = [self _dbGetValueWithKey:key];
}
} break;
}
if (value) {
[self _dbUpdateAccessTimeWithKey:key];
}
return value;
}

最后看下磁盘缓存的目录结构:

/path/
/manifest.sqlite
/manifest.sqlite-shm
/manifest.sqlite-wal
/data/
/e10adc3949ba59abbe56e057f20f883e
/e10adc3949ba59abbe56e057f20f883e
/trash/
/unused_file_or_folder

manifest.sqlite,manifest.sqlite-shm,manifest.sqlite-wal 是数据库缓存记录相关的sqlite数据库文件,/data/目录是用于存储对应的文件缓存,/trash/则是和我们电脑的垃圾箱一样。

总结

其实大家看文章开始的那张图就啥都明白了,不明白抽我,抖机~~~~~~

开源库信息

YYModel

与同类开源库的性能对比:

YYModel是一个序列化和反序列化库,说得更直白一点就是将NSString,NSData,NSDictionary形式的数据转换为具体类型的对象,以及将具体类型的对象转换为NSDictionary。

我们在没有开始看源码之前,我们猜测下如果这件事情要做应该怎么做?

  • 将NSString,NSData,NSDictionary形式的数据转换为具体类型的对象

首先我们需要将这些对象统一转换为NSDictionary,然后获取目标对象类的所有属性,创建一个目标对象,以目标对象的属性名为key到NSDictionary中去取值设置到目标对象上。最后返回这个对象。

  • 将具体类型的对象转换为NSDictionary

这个其实是上一个过程的逆过程,首先需要获取当前对象类的信息,以及各个属性的getter/setter方法,使用getter方法取出当前对象当前属性的值,然后再将属性名为key,属性值为value存到NSDictionary中返回。

当然这仅仅是大体的构想,过程中还有很多细节需要考虑到,比如key和属性名的映射,容器类型属性的元素类型的指定方式等,我们接下来就来看下这部分代码,YYModel代码量不大,只要记住上面两点就可以把握大致的一个方向,如果对runtime机制比较熟悉会有更多的帮助,但是即使不知道也不妨碍你对整个代码的理解。

源码解析
1.JSON to Model

1.1 转换前准备工作 – 将JSON统一成NSDictionary

将JSON结构转换为Model是通过yy_modelWithJSON方法转换的,这里的json可以是NSString,NSData,NSDictionary

+ (nullable instancetype)yy_modelWithJSON:(id)json;

yy_modelWithJSON作为总的入口会将这些类型统一通过_yy_dictionaryWithJSON转换为NSDictionary,再调用yy_modelWithDictionary将NSDictionary转换为Model返回。

+ (instancetype)yy_modelWithJSON:(id)json {
//将所有类型的数据都转换为NSDictionary类型
NSDictionary *dic = [self _yy_dictionaryWithJSON:json];
//将NSDictionary类型转换为model
return [self yy_modelWithDictionary:dic];
}
+ (NSDictionary *)_yy_dictionaryWithJSON:(id)json {
if (!json || json == (id)kCFNull) return nil;
NSDictionary *dic = nil;
NSData *jsonData = nil;
if ([json isKindOfClass:[NSDictionary class]]) {
dic = json;
} else if ([json isKindOfClass:[NSString class]]) {
jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding];
} else if ([json isKindOfClass:[NSData class]]) {
jsonData = json;
}
//不论传入的json是NSDictionary,NSString还是NSData都将结果都转换为dic
if (jsonData) {
dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
if (![dic isKindOfClass:[NSDictionary class]]) dic = nil;
}
return dic;
}

_yy_dictionaryWithJSON 这块没什么好介绍的如果是NSDictionary就直接返回,如果是NSString或者NSData都通过JSONObjectWithData 转化为 NSDictionary对象,如果是其他类型就直接返回nil。

1.2 将NSDictionary 转换为Model对象

接下来的yy_modelWithDictionary开始就比较重要了,这里就开始对Model类进行分析,提取出它的全部属性,以及各种信息。

+ (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary {

if (!dictionary || dictionary == (id)kCFNull) return nil;
if (![dictionary isKindOfClass:[NSDictionary class]]) return nil;

Class cls = [self class];
//获取当前类的属性
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls];
if (modelMeta->_hasCustomClassFromDictionary) {
//根据dictionary来决定最后的cls
cls = [cls modelCustomClassForDictionary:dictionary] ?: cls;
}

// 上面是获取当前类有哪些属性,哪些方法,以及实例方法,相当于建立了一个模版,
// 就比方说我们定义了一个类,到上面为止,我们知道了它有a,b,c三个属性,每个属性的类型分别是NSString,NSInteger,BOOL 仅此而已,
// 下面要做的就是创建出一个当前类的对象,并将传进来的字典里面的值赋给它的每个元素。这个在yy_modelSetWithDictionary中实现的。
NSObject *one = [cls new];
if ([one yy_modelSetWithDictionary:dictionary]) return one;
return nil;
}

1.2.1 提取Model信息

在上一个阶段将各种JSON格式转换为NSDictionary后,接下啦就从Model入手提取它的各种信息,我们看下_YYModelMeta的结构:

@interface _YYModelMeta : NSObject {
@package
YYClassInfo *_classInfo; //Model类的信息
/// Key:mapped key and key path, Value:_YYModelPropertyMeta.
NSDictionary *_mapper; //所有属性的key和keyPath
/// Array<_YYModelPropertyMeta>, all property meta of this model.
NSArray *_allPropertyMetas; //所有属性的信息
/// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path.
NSArray *_keyPathPropertyMetas; //属于keyPath的属性列表
/// Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys.
NSArray *_multiKeysPropertyMetas; //一个属性多个key的属性列表
/// The number of mapped key (and key path), same to _mapper.count.
NSUInteger _keyMappedCount; //全部属性数量
/// Model class type.
YYEncodingNSType _nsType; //Model类的对象

//这个类实现覆盖方法的情况
BOOL _hasCustomWillTransformFromDictionary;
BOOL _hasCustomTransformFromDictionary;
BOOL _hasCustomTransformToDictionary;
BOOL _hasCustomClassFromDictionary;
}
@end

从_YYModelMeta结构中我们可以看到_YYModelMeta既有key与属性的映射关系,又有各个属性的Meta信息,所以基本上就可以通过明确知道字典中的每个key对应的value值对应的属性。只要有这个关系就可以将NSDictionary中的值映射到Model属性上。我们接下来看下这部分源码:

+ (instancetype)metaWithClass:(Class)cls {
if (!cls) return nil;
static CFMutableDictionaryRef cache;
static dispatch_once_t onceToken;
static dispatch_semaphore_t lock;
//建立缓存
dispatch_once(&onceToken, ^{
cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
lock = dispatch_semaphore_create(1);
});
//通过cls从缓存中取出meta
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
_YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls));
dispatch_semaphore_signal(lock);
//如果之前没有缓存过或者缓存的信息需要更新
if (!meta || meta->_classInfo.needUpdate) {
//通过cls 创建出一个 _YYModelMeta
meta = [[_YYModelMeta alloc] initWithClass:cls];
if (meta) {
//将新建的_YYModelMeta添加到缓存供下次使用
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta));
dispatch_semaphore_signal(lock);
}
}
return meta;
}

在第一调用metaWithClass的时候会创建一个缓存,并且为这个缓存添加一个dispatch_semaphor信号量,为了避免每次都进行查询,这里会将每次查询的数据缓存到新建的缓存中,以加快查询速度。


- (instancetype)initWithClass:(Class)cls {

//用于存储类的信息 包括当前类,父类,当前属性,实例变量,方法
YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls];
if (!classInfo) return nil;
self = [super init];

// 获取属性黑名单
NSSet *blacklist = nil;
if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) {
NSArray *properties = [(id<YYModel>)cls modelPropertyBlacklist];
if (properties) {
blacklist = [NSSet setWithArray:properties];
}
}

// 获取属性白名单
NSSet *whitelist = nil;
if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
NSArray *properties = [(id<YYModel>)cls modelPropertyWhitelist];
if (properties) {
whitelist = [NSSet setWithArray:properties];
}
}

// 获取容器的元素对象,存储到genericMapper key为属性名 value为该容器类里面元素的类型
NSDictionary *genericMapper = nil;
if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
genericMapper = [(id<YYModel>)cls modelContainerPropertyGenericClass];
if (genericMapper) {
NSMutableDictionary *tmp = [NSMutableDictionary new];
[genericMapper enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:[NSString class]]) return;
Class meta = object_getClass(obj);
if (!meta) return;
if (class_isMetaClass(meta)) {
//key为属性名 value为该容器类里面元素的类型
tmp[key] = obj;
} else if ([obj isKindOfClass:[NSString class]]) {
Class cls = NSClassFromString(obj);
if (cls) {
tmp[key] = cls;
}
}
}];
genericMapper = tmp;
}
}


NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
YYClassInfo *curClassInfo = classInfo;
//遍历当前类及其父类的属性,
while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy)
for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
if (!propertyInfo.name) continue;
//只有不在黑名单并且再白名单中的属性才会被加到allPropertyMetas
if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
_YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
propertyInfo:propertyInfo
generic:genericMapper[propertyInfo.name]];
//meta必须有值,并且有getter/setters
if (!meta || !meta->_name) continue;
if (!meta->_getter || !meta->_setter) continue;
//已经存在allPropertyMetas中了则不再继续
if (allPropertyMetas[meta->_name]) continue;
//将meta存到allPropertyMetas中
allPropertyMetas[meta->_name] = meta;
}
curClassInfo = curClassInfo.superClassInfo;
}
if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy;
//将所有的类属性都添加到allPropertyMetas



// 建立映射关系
NSMutableDictionary *mapper = [NSMutableDictionary new];
NSMutableArray *keyPathPropertyMetas = [NSMutableArray new];
NSMutableArray *multiKeysPropertyMetas = [NSMutableArray new];

//处理映射
if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
//获得自定义映射字典,它用于解决json文件中关键字和定义的类的属性不一致的问题。
/*
+ (NSDictionary *) modelCustomPropertyMapper {
return @{@"errnoTest"类属性 : @"errno"json中的字段};
}*/
NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
[customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) {
//取出原来key对应的属性信息
_YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
if (!propertyMeta) return;
//从allPropertyMetas移除
[allPropertyMetas removeObjectForKey:propertyName];
//要被映射到的属性名字
if ([mappedToKey isKindOfClass:[NSString class]]) {
if (mappedToKey.length == 0) return;
//只有一个key
propertyMeta->_mappedToKey = mappedToKey;

//去掉空keyPath
NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."];
for (NSString *onePath in keyPath) {
if (onePath.length == 0) {
NSMutableArray *tmp = keyPath.mutableCopy;
[tmp removeObject:@""];
keyPath = tmp;
break;
}
}

if (keyPath.count > 1) {
//有多个path的key,例如xxx.xxxx.xxx
propertyMeta->_mappedToKeyPath = keyPath;
[keyPathPropertyMetas addObject:propertyMeta];
}
propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;

} else if ([mappedToKey isKindOfClass:[NSArray class]]) {

NSMutableArray *mappedToKeyArray = [NSMutableArray new];
for (NSString *oneKey in ((NSArray *)mappedToKey)) {
if (![oneKey isKindOfClass:[NSString class]]) continue;
if (oneKey.length == 0) continue;

NSArray *keyPath = [oneKey componentsSeparatedByString:@"."];
if (keyPath.count > 1) {
[mappedToKeyArray addObject:keyPath];
} else {
[mappedToKeyArray addObject:oneKey];
}

if (!propertyMeta->_mappedToKey) {
propertyMeta->_mappedToKey = oneKey;
propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil;
}
}
if (!propertyMeta->_mappedToKey) return;

propertyMeta->_mappedToKeyArray = mappedToKeyArray;
[multiKeysPropertyMetas addObject:propertyMeta];

propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;
}
}];
}

//这些是上面还没处理的,处理后添加到mapper,所以mapper包含了全部的属性
[allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
propertyMeta->_mappedToKey = name;
//如果有其他值绑定在同一个key上则将它赋给next
propertyMeta->_next = mapper[name] ?: nil;
//将属性添加到mapper
mapper[name] = propertyMeta;
}];

//将上述的属性信息添加到_mapper以及_keyPathPropertyMetas,_multiKeysPropertyMetas
if (mapper.count) _mapper = mapper;
if (keyPathPropertyMetas) _keyPathPropertyMetas = keyPathPropertyMetas;
if (multiKeysPropertyMetas) _multiKeysPropertyMetas = multiKeysPropertyMetas;

//赋值到对应的属性上
_classInfo = classInfo;
_keyMappedCount = _allPropertyMetas.count;
_nsType = YYClassGetNSType(cls);
_hasCustomWillTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomWillTransformFromDictionary:)]);
_hasCustomTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformFromDictionary:)]);
_hasCustomTransformToDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformToDictionary:)]);
_hasCustomClassFromDictionary = ([cls respondsToSelector:@selector(modelCustomClassForDictionary:)]);

return self;
}

嗯 是的 上面是_YYModelMeta的构造方法,哈哈,长吧。在_YYModelMeta的构造方法中主要是负责填充前面介绍过的_YYModelMeta中的属性。我们接下来将它拆成一段一段进行介绍:

  • 获取当前类的属性信息
//用于存储类的信息 包括当前类,父类,当前属性,实例变量,方法
YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls];
if (!classInfo) return nil;

如果看过runtime源码的同学应该对接下来的这部分代码理解会比较透彻,如果不属性也无防在这里先了解下,后面看runtime的代码的时候会恍然大悟,看源码其实很多时候第一遍可能理解的只是一个大概,不要太去纠结细节,后面在某项技术补上后,或者在项目上遇到类似的问题的时候会有更深的理解,所以看不懂埋着头看下去,一遍不行,再来一遍,就是这种傻办法慢慢积累。

@interface YYClassInfo : NSObject
@property (nonatomic, assign, readonly) Class cls; ///< //当前YYClassInfo 所对应的cls
@property (nullable, nonatomic, assign, readonly) Class superCls; ///< 当前类的父类
@property (nullable, nonatomic, assign, readonly) Class metaCls; ///< 当前类的meta对象
@property (nonatomic, readonly) BOOL isMeta; ///< 当前类是否是meta类
@property (nonatomic, strong, readonly) NSString *name; ///< 类名
@property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< 父类信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassIvarInfo *> *ivarInfos; ///< 实例变量信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassMethodInfo *> *methodInfos; ///< 方法信息
@property (nullable, nonatomic, strong, readonly) NSDictionary<NSString *, YYClassPropertyInfo *> *propertyInfos; ///< 属性信息

//.......
@end

接下来看下classInfoWithClass:

+ (instancetype)classInfoWithClass:(Class)cls {
if (!cls) return nil;
//........

//创建缓存
dispatch_once(&onceToken, ^{
classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
lock = dispatch_semaphore_create(1);
});
//获取class信息
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls));
//查看是否需要更新,如果需要更新的话则调用info的_update方法
if (info && info->_needUpdate) {
[info _update];
}
dispatch_semaphore_signal(lock);
//如果缓存没有则新建后添加到缓存中
if (!info) {
info = [[YYClassInfo alloc] initWithClass:cls];
if (info) {
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info));
dispatch_semaphore_signal(lock);
}
}
return info;
}

嗯,熟悉吧,还是使用了缓存,很明显数据的序列化其实是一个特别频繁的任务,基本上每次网络请求回来后都需要经过序列化后进行交付,如果每次都进行类信息的提取将会是很影响性能的任务。

- (instancetype)initWithClass:(Class)cls {
if (!cls) return nil;
self = [super init];
_cls = cls;
_superCls = class_getSuperclass(cls);
_isMeta = class_isMetaClass(cls);
if (!_isMeta) {
_metaCls = objc_getMetaClass(class_getName(cls));
}
_name = NSStringFromClass(cls);
[self _update];
_superClassInfo = [self.class classInfoWithClass:_superCls];
return self;
}

- (void)_update {
//.....

Class cls = self.cls;
unsigned int methodCount = 0;
//获取当前类的方法数
Method *methods = class_copyMethodList(cls, &methodCount);
if (methods) {
NSMutableDictionary *methodInfos = [NSMutableDictionary new];
_methodInfos = methodInfos;
for (unsigned int i = 0; i < methodCount; i++) {
YYClassMethodInfo *info = [[YYClassMethodInfo alloc] initWithMethod:methods[i]];
//key --> 方法名 value 方法信息
if (info.name) methodInfos[info.name] = info;
}
free(methods);
}
//获取当前的属性
unsigned int propertyCount = 0;
objc_property_t *properties = class_copyPropertyList(cls, &propertyCount);
if (properties) {
NSMutableDictionary *propertyInfos = [NSMutableDictionary new];
_propertyInfos = propertyInfos;
for (unsigned int i = 0; i < propertyCount; i++) {
YYClassPropertyInfo *info = [[YYClassPropertyInfo alloc] initWithProperty:properties[i]];
//key --> 属性名 value 属性信息
if (info.name) propertyInfos[info.name] = info;
}
free(properties);
}
//获取当前对象的实例变量
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList(cls, &ivarCount);
if (ivars) {
NSMutableDictionary *ivarInfos = [NSMutableDictionary new];
_ivarInfos = ivarInfos;
for (unsigned int i = 0; i < ivarCount; i++) {
YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]];
if (info.name) ivarInfos[info.name] = info;
}
free(ivars);
}

//......
//更新结束
_needUpdate = NO;
}

上面一整段代码就是负责使用runtime方法提取class中的属性信息,实例遍历信息,方法信息,父类信息等,

  • 通过属性黑白名单对属性进行过滤
// 获取属性黑名单
NSSet *blacklist = nil;
if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) {
NSArray *properties = [(id<YYModel>)cls modelPropertyBlacklist];
if (properties) {
blacklist = [NSSet setWithArray:properties];
}
}

// 获取属性白名单
NSSet *whitelist = nil;
if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) {
NSArray *properties = [(id<YYModel>)cls modelPropertyWhitelist];
if (properties) {
whitelist = [NSSet setWithArray:properties];
}
}

// 获取容器的元素对象,存储到genericMapper key为属性名 value为该容器类里面元素的类型
NSDictionary *genericMapper = nil;
if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) {
genericMapper = [(id<YYModel>)cls modelContainerPropertyGenericClass];
if (genericMapper) {
NSMutableDictionary *tmp = [NSMutableDictionary new];
[genericMapper enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![key isKindOfClass:[NSString class]]) return;
Class meta = object_getClass(obj);
if (!meta) return;
if (class_isMetaClass(meta)) {
//key为属性名 value为该容器类里面元素的类型
tmp[key] = obj;
} else if ([obj isKindOfClass:[NSString class]]) {
Class cls = NSClassFromString(obj);
if (cls) {
tmp[key] = cls;
}
}
}];
genericMapper = tmp;
}
}

NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new];
YYClassInfo *curClassInfo = classInfo;
//遍历当前类及其父类的属性,
while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy)
for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) {
if (!propertyInfo.name) continue;
//只有不在黑名单并且再白名单中的属性才会被加到allPropertyMetas
if (blacklist && [blacklist containsObject:propertyInfo.name]) continue;
if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue;
_YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo
propertyInfo:propertyInfo
generic:genericMapper[propertyInfo.name]];
//meta必须有值,并且有getter/setters
if (!meta || !meta->_name) continue;
if (!meta->_getter || !meta->_setter) continue;
//已经存在allPropertyMetas中了则不再继续
if (allPropertyMetas[meta->_name]) continue;
//将meta存到allPropertyMetas中
allPropertyMetas[meta->_name] = meta;
}
curClassInfo = curClassInfo.superClassInfo;
}
if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy;
//将所有的类属性都添加到allPropertyMetas

上面代码虽然长但是完成的工作却很简单就是从类中提取出黑白名单,然后对当前类的全部属性,包括父对象在内的全部属性进行过滤,对于不在白名单,在黑名单的属性进行过滤。当中穿插了一个容器类元素对象类型的获取过程,如果我们的某个属性是容器对象:比如数组,元素类型这部分其实信息是丢失的。需要我们通过外部方式指定,YYmodel中会通过覆写modelContainerPropertyGenericClass方法,指定某个属性的元素类型。最终存放在genericMapper,在初始化_YYModelPropertyMeta的时候就可以通过属性最为key,查到对应的generic。添加到_YYModelProperty中。这里还必须注意每个属性都必须具备getter/Setter 方法。最终将各个属性添加到_allPropertyMetas上。

1.2.2 提取Model信息


// 建立映射关系
NSMutableDictionary *mapper = [NSMutableDictionary new];
NSMutableArray *keyPathPropertyMetas = [NSMutableArray new];
NSMutableArray *multiKeysPropertyMetas = [NSMutableArray new];

//处理映射
if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) {
//获得自定义映射字典,它用于解决json文件中关键字和定义的类的属性不一致的问题。
NSDictionary *customMapper = [(id <YYModel>)cls modelCustomPropertyMapper];
[customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) {
//取出原来key对应的属性信息
_YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName];
if (!propertyMeta) return;
//从allPropertyMetas移除
[allPropertyMetas removeObjectForKey:propertyName];
if ([mappedToKey isKindOfClass:[NSString class]]) {
if (mappedToKey.length == 0) return;
// 1.最简单的形式
propertyMeta->_mappedToKey = mappedToKey;
//去掉空keyPath
NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."];
for (NSString *onePath in keyPath) {
if (onePath.length == 0) {
NSMutableArray *tmp = keyPath.mutableCopy;
[tmp removeObject:@""];
keyPath = tmp;
break;
}
}
if (keyPath.count > 1) {
//2. 有多个path的key,例如xxx.xxxx.xxx
propertyMeta->_mappedToKeyPath = keyPath;
//添加到keyPathPropertyMetas
[keyPathPropertyMetas addObject:propertyMeta];
}
propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;

} else if ([mappedToKey isKindOfClass:[NSArray class]]) {
//3.映射的对象是数组类型的时候
NSMutableArray *mappedToKeyArray = [NSMutableArray new];
for (NSString *oneKey in ((NSArray *)mappedToKey)) {
if (![oneKey isKindOfClass:[NSString class]]) continue;
if (oneKey.length == 0) continue;

NSArray *keyPath = [oneKey componentsSeparatedByString:@"."];
if (keyPath.count > 1) {
[mappedToKeyArray addObject:keyPath];
} else {
[mappedToKeyArray addObject:oneKey];
}

if (!propertyMeta->_mappedToKey) {
propertyMeta->_mappedToKey = oneKey;
propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil;
}
}
if (!propertyMeta->_mappedToKey) return;
//添加到multiKeysPropertyMetas
propertyMeta->_mappedToKeyArray = mappedToKeyArray;
[multiKeysPropertyMetas addObject:propertyMeta];

propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;
}
}];
}

我们结合下面的例子来解析上面的代码:

{
"n":"Harry Pottery",
"ext" : {
"desc" : "A book written by J.K.Rowing."
},
"ID" : 100010
}

+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"desc" : @"ext.desc",
@"bookID" : @[@"id",@"ID",@"book_id"]};
}

上面的例子三个属性分别对应代码中标出的1,2,3 三种例子,我们分别看这三种情况:

情况一 最简单的映射:

这种没啥好介绍的就是将modelCustomPropertyMapper对应的value存到propertyMeta->_mappedToKey

// 1.最简单的形式
propertyMeta->_mappedToKey = mappedToKey;

情况二 keyPath的映射:

NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."];
for (NSString *onePath in keyPath) {
if (onePath.length == 0) {
NSMutableArray *tmp = keyPath.mutableCopy;
[tmp removeObject:@""];
keyPath = tmp;
break;
}
}
if (keyPath.count > 1) {
//2. 有多个path的key,例如xxx.xxxx.xxx
propertyMeta->_mappedToKeyPath = keyPath;
//添加到keyPathPropertyMetas
[keyPathPropertyMetas addObject:propertyMeta];
}
propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;

这里和上面的区别是它会将类似ext.desc这种形式的路径转化为数组@[@”ext”,@”desc”],然后存到_mappedToKeyPath中并添加到keyPathPropertyMetas,以后如果要找到这种带路径的属性可以直接从keyPathPropertyMetas找。

情况三 多映射类型key的映射:

//3.映射的对象是数组类型的时候
NSMutableArray *mappedToKeyArray = [NSMutableArray new];
for (NSString *oneKey in ((NSArray *)mappedToKey)) {
//........
NSArray *keyPath = [oneKey componentsSeparatedByString:@"."];
if (keyPath.count > 1) {
[mappedToKeyArray addObject:keyPath];
} else {
[mappedToKeyArray addObject:oneKey];
}
//先填充_mappedToKey和_mappedToKeyPath
if (!propertyMeta->_mappedToKey) {
propertyMeta->_mappedToKey = oneKey;
propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil;
}
}
if (!propertyMeta->_mappedToKey) return;
//添加到multiKeysPropertyMetas
propertyMeta->_mappedToKeyArray = mappedToKeyArray;
[multiKeysPropertyMetas addObject:propertyMeta];

propertyMeta->_next = mapper[mappedToKey] ?: nil;
mapper[mappedToKey] = propertyMeta;

我们拿 @”bookID” : @[@”id”,@”ID”,@”book_id”]作为例子,经过上面处理后propertyMeta->_mappedToKey = @”ID”,propertyMeta->_mappedToKeyPath = nil, propertyMeta->_mappedToKeyArray = @[@”id”,@”ID”,@”book_id”] 最后propertyMeta会被添加到multiKeysPropertyMetas数组中。

情况四 其余不在modelCustomPropertyMapper中指定的映射:

[allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
propertyMeta->_mappedToKey = name;
//如果有其他值绑定在同一个key上则将它赋给next
propertyMeta->_next = mapper[name] ?: nil;
//将属性添加到mapper
mapper[name] = propertyMeta;
}];

上面三种情况都是处理在modelCustomPropertyMapper中特别指定的属性,而上面的这种情况是没有特殊指定的属性,这种情况下就是将属性名赋给_mappedToKey。

综上几种情况,所有的映射关系都存在于_mappedToKey,_mappedToKeyPath,_mappedToKeyArray这三个属性中,比如下面这种情况:

// Model:
@interface Book : NSObject
@property NSString *name;
@property NSString *test;
@property NSString *desc;
@property NSString *bookID;
@end
@implementation Book
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"desc" : @"ext.desc",
@"bookID" : @[@"id",@"ID",@"book_id"]};
}
@end

属性name的_mappedToKey = @”n”,_mappedToKeyPath = nil,_mappedToKeyArray = nil
属性test的_mappedToKey = @”test”,_mappedToKeyPath = nil,_mappedToKeyArray = nil
属性desc的_mappedToKey = @”ext.desc”,_mappedToKeyPath = @[@”ext”,@”desc”],_mappedToKeyArray = nil
属性bookID的_mappedToKey = @”id”,_mappedToKeyPath = nil ,_mappedToKeyArray = @[@”id”,@”ID”,@”book_id”]

1.2.3 使用NSDictionary的数据填充Model

我们前面已经获取到了Model类的class结构信息,并且完成了属性黑白名单的过滤,以及属性名和JSON中字段名的对应关系,接下来我们就可以使用Model 类创建出一个Model,并从JSON (NSDictionary)中取出对应的值,对Model对象进行填充,最后再将生成的model对象返回就完成了整个序列化过程,这部分代码位于yy_modelSetWithDictionary

- (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic {
//.....
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)];
//.....
//构建context 上下文中包括modelMeta model的各种映射信息,model 要填充的model对象, dictionary 包含数据的字典
ModelSetContext context = {0};
context.modelMeta = (__bridge void *)(modelMeta);
context.model = (__bridge void *)(self);
context.dictionary = (__bridge void *)(dic);
//开始将dictionary数据填充到model上,这里最关键的就是ModelSetWithPropertyMetaArrayFunction方法。
if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) {

CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);

if (modelMeta->_keyPathPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}

if (modelMeta->_multiKeysPropertyMetas) {
CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas,
CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
} else {
CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas,
CFRangeMake(0, modelMeta->_keyMappedCount),
ModelSetWithPropertyMetaArrayFunction,
&context);
}
//........
return YES;
}

我们先来看下:

CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context);

它实际上是对dic,也就是当前JSON所对应的NSDictionary的所有元素,应用ModelSetWithDictionaryFunction方法,并在每次调用中将context传递进去,下面是ModelSetWithDictionaryFunction的定义:CFDictionaryApplyFunction 会将NSDictionary中的每个元素的key,作为ModelSetWithDictionaryFunction第一个参数,value作为第二个参数,最后是一个context用户存对应的公共数据。比如这里的modelMeta,model,dictionary,ModelSetWithDictionaryFunction就是将dictionary各个元素取出来,使用key的映射关系来找到modelMeta中的属性meta,取出dictionary中的值通过属性meta中的setter将值设置到model中。我们来看下这部分代码:

static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) {
ModelSetContext *context = _context;
__unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta);
//取出单个属性
__unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)];
//取出model
__unsafe_unretained id model = (__bridge id)(context->model);
while (propertyMeta) {
if (propertyMeta->_setter) {
//将从dictionary中获取到的某个值赋给某个model对象的某个属性
ModelSetValueForProperty(model/*对象*/, (__bridge __unsafe_unretained id)_value/*某个属性的值*/, propertyMeta/*属性信息*/);
}
propertyMeta = propertyMeta->_next;
};
}

对于_keyPathPropertyMetas 以及 _multiKeysPropertyMetas通过调用ModelSetWithPropertyMetaArrayFunction来设置的,最终也是归到ModelSetValueForProperty

static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) {
ModelSetContext *context = _context;
__unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary);
__unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta *)(_propertyMeta);
if (!propertyMeta->_setter) return;
id value = nil;

//通过属性信息里面的key的映射关系拿到字典里面对应的value值。
if (propertyMeta->_mappedToKeyArray) {
value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray);
} else if (propertyMeta->_mappedToKeyPath) {
value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath);
} else {
value = [dictionary objectForKey:propertyMeta->_mappedToKey];
}
//将value赋给model中的属性
if (value) {
__unsafe_unretained id model = (__bridge id)(context->model);
ModelSetValueForProperty(model, value, propertyMeta);
}
}

ModelSetValueForProperty是整个库中算得上是比较长的代码了,之所以长是因为类型比较多?我们取其中简单的一种作为例子—- JSON中的某个值为字符类型,Model中的某个属性为NSString,或者NSMutableString类型,这时候通过objc_msgSend 完 model中调用对应属性的setter方法将值设置到model上。

static void ModelSetValueForProperty(__unsafe_unretained id model,
__unsafe_unretained id value,
__unsafe_unretained _YYModelPropertyMeta *meta) {

//获取meta的的类型,也就是要将字典里面的值,转成的目标类型。这里为什么用objc_msgSend,是因为可能有些属性会自定义不同的setter。
if (meta->_isCNumber) {
//.....
} else if (meta->_nsType) {
if (value == (id)kCFNull) {
//......
} else {
switch (meta->_nsType) {
case YYEncodingTypeNSString:
case YYEncodingTypeNSMutableString: {
if ([value isKindOfClass:[NSString class]]) {
if (meta->_nsType == YYEncodingTypeNSString) {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value);
} else {
((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy);
}
}
//......
default: break;
}
}
}
}
}
2. Model to JSON

在yy_modelToJSONObject方法的开头有下面一段注释:

Apple said:
The top level object is an NSArray or NSDictionary.
All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull.
All dictionary keys are instances of NSString.
Numbers are not NaN or infinity.

翻译成中文就是,标准的JSON顶层是一个NSArray或者NSDictionary,并且所有的对象都是NSString,NSNumber,NSArray,NSDictionary,NSNull这些类的实例,并且字典中的key都是NSString类型的实例,数值都是非NaN或者无穷,这其实大家都比较清楚这里强调下对后续代码的理解会有一定的帮助,我们继续看下怎么将Model转换为NSDictionary Model,入口是yy_modelToJSONObject

- (id)yy_modelToJSONObject {
id jsonObject = ModelToJSONObjectRecursive(self);
if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject;
if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject;
return nil;
}

在yy_modelToJSONObject方法中就先调用ModelToJSONObjectRecursive来递归地将当前对象转换为jsonObject,只有顶层对象是NSArray和NSDictionary才返回,否则都是nil。因此最关键的转换应该在ModelToJSONObjectRecursive方法中。


static id ModelToJSONObjectRecursive(NSObject *model) {
// 对于简单的NSString,NSNumber,nill类型
if (!model || model == (id)kCFNull) return model;
if ([model isKindOfClass:[NSString class]]) return model;
if ([model isKindOfClass:[NSNumber class]]) return model;
if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model];
if ([model isKindOfClass:[NSData class]]) return nil;

//如果当前对象是NSDictionary
if ([model isKindOfClass:[NSDictionary class]]) {
// 简单的字典
if ([NSJSONSerialization isValidJSONObject:model]) return model;
// 对象是复杂的对象
NSMutableDictionary *newDic = [NSMutableDictionary new];
[((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description;
if (!stringKey) return;
//复杂对象需要递归转换
id jsonObj = ModelToJSONObjectRecursive(obj);
if (!jsonObj) jsonObj = (id)kCFNull;
newDic[stringKey] = jsonObj;
}];
return newDic;
}
//如果当前对象是NSSet
if ([model isKindOfClass:[NSSet class]]) {
// 简单的NSSet
NSArray *array = ((NSSet *)model).allObjects;
if ([NSJSONSerialization isValidJSONObject:array]) return array;

// 对象是复杂的对象
NSMutableArray *newArray = [NSMutableArray new];
for (id obj in array) {
if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
[newArray addObject:obj];
} else {
id jsonObj = ModelToJSONObjectRecursive(obj);
if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
}
}
return newArray;
}
//如果当前对象是NSArray
if ([model isKindOfClass:[NSArray class]]) {
// 简单的NSArray
if ([NSJSONSerialization isValidJSONObject:model]) return model;

// 对象是复杂的对象
NSMutableArray *newArray = [NSMutableArray new];
for (id obj in (NSArray *)model) {
if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) {
[newArray addObject:obj];
} else {
id jsonObj = ModelToJSONObjectRecursive(obj);
if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj];
}
}
return newArray;
}

//获取Model信息
_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]];
if (!modelMeta || modelMeta->_keyMappedCount == 0) return nil;

NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64];
__unsafe_unretained NSMutableDictionary *dic = result; // avoid retain and release in block
[modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
if (!propertyMeta->_getter) return;
// 通过属性信息中的_getter方法使用objc_msgSend从model中获取当前对应属性的值,值获取到了,mapkey也知道了就可以构造返回的dictionary了。
id value = nil;
if (propertyMeta->_isCNumber) {
value = ModelCreateNumberFromProperty(model, propertyMeta);
} else if (propertyMeta->_nsType) {
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = ModelToJSONObjectRecursive(v);
} else {
switch (propertyMeta->_type & YYEncodingTypeMask) {
case YYEncodingTypeObject: {
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = ModelToJSONObjectRecursive(v);
if (value == (id)kCFNull) value = nil;
} break;
case YYEncodingTypeClass: {
Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromClass(v) : nil;
} break;
case YYEncodingTypeSEL: {
SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromSelector(v) : nil;
} break;
default: break;
}
}
if (!value) return;

//根据是否有_mappedToKeyPath将上面获取到的值加入到字典中。
if (propertyMeta->_mappedToKeyPath) {
NSMutableDictionary *superDic = dic;
NSMutableDictionary *subDic = nil;
for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) {
NSString *key = propertyMeta->_mappedToKeyPath[i];
if (i + 1 == max) { // end
if (!superDic[key]) superDic[key] = value;
break;
}
subDic = superDic[key];
if (subDic) {
if ([subDic isKindOfClass:[NSDictionary class]]) {
subDic = subDic.mutableCopy;
superDic[key] = subDic;
} else {
break;
}
} else {
subDic = [NSMutableDictionary new];
superDic[key] = subDic;
}
superDic = subDic;
subDic = nil;
}
} else {
if (!dic[propertyMeta->_mappedToKey]) {
dic[propertyMeta->_mappedToKey] = value;
}
}
}];
if (modelMeta->_hasCustomTransformToDictionary) {
BOOL suc = [((id<YYModel>)model) modelCustomTransformToDictionary:dic];
if (!suc) return nil;
}
return result;
}

ModelToJSONObjectRecurs代码很长我们分段进行解析:

简单类型

 if (!model || model == (id)kCFNull) return model;
if ([model isKindOfClass:[NSString class]]) return model;
if ([model isKindOfClass:[NSNumber class]]) return model;
if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString;
if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string;
if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model];
if ([model isKindOfClass:[NSData class]]) return nil;

这部分就不介绍了。

NSDictionary, NSSet,NSArray 容器类型

if ([model isKindOfClass:[NSDictionary class]]) {
// 简单的字典
if ([NSJSONSerialization isValidJSONObject:model]) return model;
// 对象是复杂的对象
NSMutableDictionary *newDic = [NSMutableDictionary new];
[((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) {
NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description;
if (!stringKey) return;
//复杂对象需要递归转换
id jsonObj = ModelToJSONObjectRecursive(obj);
if (!jsonObj) jsonObj = (id)kCFNull;
newDic[stringKey] = jsonObj;
}];
return newDic;
}
//如果当前对象是NSSet
if ([model isKindOfClass:[NSSet class]]) {
//.....
}
//如果当前对象是NSArray
if ([model isKindOfClass:[NSArray class]]) {
//.....
}

对于NSDictionary, NSSet,NSArray 容器类型处理的过程是类似的,我们这里以NSDictionary为例子进行分析,如果model是NSDictionary会遍历每个元素,对每个值递归调用ModelToJSONObjectRecursive后存储到newDic中作为value。最后返回newDic。

其他对象类型

对于我们自定义的类型我们处理逻辑如下:首先我们会先通过metaWithClass进行抽取Model的详细信息,然后对model全部属性进行遍历,通过调用每个属性的getter方法提取出属性值,并通过属性名与NSDictionary key的映射关系确定key后将值存到key对应的value中,从而完成整个过程。


_YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]];

NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64];
__unsafe_unretained NSMutableDictionary *dic = result; // avoid retain and release in block
[modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) {
if (!propertyMeta->_getter) return;
// 通过属性信息中的_getter方法使用objc_msgSend从model中获取当前对应属性的值,值获取到了,mapkey也知道了就可以构造返回的dictionary了。
id value = nil;
if (propertyMeta->_isCNumber) {
value = ModelCreateNumberFromProperty(model, propertyMeta);
} else if (propertyMeta->_nsType) {
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = ModelToJSONObjectRecursive(v);
} else {
switch (propertyMeta->_type & YYEncodingTypeMask) {
case YYEncodingTypeObject: {
id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = ModelToJSONObjectRecursive(v);
if (value == (id)kCFNull) value = nil;
} break;
case YYEncodingTypeClass: {
Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromClass(v) : nil;
} break;
case YYEncodingTypeSEL: {
SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter);
value = v ? NSStringFromSelector(v) : nil;
} break;
default: break;
}
}
if (!value) return;

//根据是否有_mappedToKeyPath将上面获取到的值加入到字典中。
if (propertyMeta->_mappedToKeyPath) {
NSMutableDictionary *superDic = dic;
NSMutableDictionary *subDic = nil;
for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) {
NSString *key = propertyMeta->_mappedToKeyPath[i];
if (i + 1 == max) { // end
if (!superDic[key]) superDic[key] = value;
break;
}
subDic = superDic[key];
if (subDic) {
if ([subDic isKindOfClass:[NSDictionary class]]) {
subDic = subDic.mutableCopy;
superDic[key] = subDic;
} else {
break;
}
} else {
subDic = [NSMutableDictionary new];
superDic[key] = subDic;
}
superDic = subDic;
subDic = nil;
}
} else {
if (!dic[propertyMeta->_mappedToKey]) {
dic[propertyMeta->_mappedToKey] = value;
}
}
}
3. 其他JSON 转 Model 方法

除了上面介绍的JSON 转 Model 方法,适用于如下类型:

{
"key1":"value1",
"key2":"value2",
"key3":"value3",
"key4":"value4",
"key5":"value5",
}

当然YYModel还适合序列化对象数组类型的JSON比如:

[
{
"key1":"value1",
"key2":"value2",
},
{
"key3":"value3",
"key4":"value4",
},
{
"key5":"value5",
"key6":"value6",
}
]

这里仅仅贴出代码,有了上面的介绍理解下面的代码应该不会有任何困难。

+ (NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json {
if (!json) return nil;
NSArray *arr = nil;
NSData *jsonData = nil;
if ([json isKindOfClass:[NSArray class]]) {
arr = json;
} else if ([json isKindOfClass:[NSString class]]) {
jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding];
} else if ([json isKindOfClass:[NSData class]]) {
jsonData = json;
}
if (jsonData) {
arr = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
if (![arr isKindOfClass:[NSArray class]]) arr = nil;
}
return [self yy_modelArrayWithClass:cls array:arr];
}

+ (NSArray *)yy_modelArrayWithClass:(Class)cls array:(NSArray *)arr {
if (!cls || !arr) return nil;
NSMutableArray *result = [NSMutableArray new];
for (NSDictionary *dic in arr) {
if (![dic isKindOfClass:[NSDictionary class]]) continue;
NSObject *obj = [cls yy_modelWithDictionary:dic];
if (obj) [result addObject:obj];
}
return result;
}

还可以解析下面类型的JSON,

{
"obj1":
{
"key1":"value1",
"key2":"value2",
},
"obj2":
{
"key3":"value3",
"key4":"value4",
},
"obj3":
{
"key5":"value5",
"key6":"value6",
}
}

代码如下和上面的那个其实是类似的,只不过是顶层元素换成字典罢了,这类个人在项目中用得不多。

+ (NSDictionary *)yy_modelDictionaryWithClass:(Class)cls json:(id)json {
if (!json) return nil;
NSDictionary *dic = nil;
NSData *jsonData = nil;
if ([json isKindOfClass:[NSDictionary class]]) {
dic = json;
} else if ([json isKindOfClass:[NSString class]]) {
jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding];
} else if ([json isKindOfClass:[NSData class]]) {
jsonData = json;
}
if (jsonData) {
dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL];
if (![dic isKindOfClass:[NSDictionary class]]) dic = nil;
}
return [self yy_modelDictionaryWithClass:cls dictionary:dic];
}

+ (NSDictionary *)yy_modelDictionaryWithClass:(Class)cls dictionary:(NSDictionary *)dic {
if (!cls || !dic) return nil;
NSMutableDictionary *result = [NSMutableDictionary new];
for (NSString *key in dic.allKeys) {
if (![key isKindOfClass:[NSString class]]) continue;
NSObject *obj = [cls yy_modelWithDictionary:dic[key]];
if (obj) result[key] = obj;
}
return result;
}

除了上面介绍的方法外,YYModel还提供了极大减轻个人工作量的方法:

// 深度拷贝
- (id)yy_modelCopy;
// NSCoding
- (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder;
- (id)yy_modelInitWithCoder:(NSCoder *)aDecoder;
// Hash值生成
- (NSUInteger)yy_modelHash;
// 判断是否一致
- (BOOL)yy_modelIsEqual:(id)model;
// 当前对象的描述
- (NSString *)yy_modelDescription;
4. 总结

YYModel代码就解析到这里,其实大体的思路就是,解析Model对象信息,明确NSDictionary key 与 Model属性名之间的映射关系,通过这种映射关系,新建一个model对象,使用model对象的setter方法将NSDictionary中的值设置到model中,从而完成JSON转Model 的过程。Model 转 JSON是通过model的getter方法,取出对应属性的值,然后通过NSDictionary key和model属性名的映射关系,确定对应的key,然后将model值设置到NSDictionary中。

源码信息

开源地址: YYDispatchQueuePool

源码解析

按照惯例我们还是先看它的用法:

// 从全局队列中队列池中获取到一个串行队列
dispatch_queue_t queue = YYDispatchQueueGetForQOS(NSQualityOfServiceUtility);

// 获得一个指定类型的串行队列池
YYDispatchQueuePool *pool = [[YYDispatchQueuePool alloc] initWithName:@"file.read" queueCount:5 qos:NSQualityOfServiceBackground];
// 从队列池中获取一个串行队列
dispatch_queue_t queue = [pool queue];

我们接下来就从这两个情景进行分析:

**** 情景一 ****

好了先贴代码:

dispatch_queue_t YYDispatchQueueGetForQOS(NSQualityOfService qos) {
return YYDispatchContextGetQueue(YYDispatchContextGetForQOS(qos));
}

Queality of Service 是 iOS8 提出的一个新的概念,一个高质量的服务就意味着更多的资源得以提供来更快的完成操作, 它可以是如下值:

typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} NS_ENUM_AVAILABLE(10_10, 8_0);
  • UserInteractive QoS 用于直接参与提供一个交互式UI,如处理事件或对屏幕的绘制。
  • UserInitiated QoS 用于表示执行工作已经被用户显示提出并且要求结果能够立即展示以便进行进一步的用户交互。
  • Utility QoS 用于表述执行一项工作后,用户并不需要立即得到结果。这一工作通常用户已经请求过或者在初始化的时候已经自动执行,不会阻碍用户用户的进一步交互,通常在用户可见的时间尺度和可能由一个非模态的进度指示器展示给用户。
  • Background QoS 用于那些非用户初始化或可见的工作。通常说来,用户甚至不知道这想工作已经发生,但是它会以最有效的方法运行同时会树丛那些高优先级的QoS。例如:内容抓取,搜索索引,备份,同步与外部系统的数据。
  • Default QoS 表明QoS信息缺失。尽可能的从其它资源推断可能的QoS信息。如果这一推断不成立,一个位于UserInitiated和Utility之间的QoS将得以使用。

这里创建的QoS 为 NSQualityOfServiceUtility:


static YYDispatchContext *YYDispatchContextGetForQOS(NSQualityOfService qos) {
static YYDispatchContext *context[5] = {0};
switch (qos) {
//.........
case NSQualityOfServiceUtility: {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
int count = (int)[NSProcessInfo processInfo].activeProcessorCount;
count = count < 1 ? 1 : count > MAX_QUEUE_COUNT ? MAX_QUEUE_COUNT : count;
context[2] = YYDispatchContextCreate("com.ibireme.yykit.utility", count, qos);
});
return context[2];
} break;
//.........
}
}

创建的串行队列数和处理器的内核数一致,这里还有一个需要注意的是这里创建了五个YYDispatchContext,也就是每次使用一种QoS都会创建出和内核数相等的当前类型QoS的串行队列。我们看下整个创建过程:

static YYDispatchContext *YYDispatchContextCreate(const char *name,
uint32_t queueCount,
NSQualityOfService qos) {
YYDispatchContext *context = calloc(1, sizeof(YYDispatchContext));
if (!context) return NULL;
context->queues = calloc(queueCount, sizeof(void *));
if (!context->queues) {
free(context);
return NULL;
}
if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
dispatch_qos_class_t qosClass = NSQualityOfServiceToQOSClass(qos);
for (NSUInteger i = 0; i < queueCount; i++) {
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, qosClass, 0);
dispatch_queue_t queue = dispatch_queue_create(name, attr);
context->queues[i] = (__bridge_retained void *)(queue);
}
} else {
long identifier = NSQualityOfServiceToDispatchPriority(qos);
for (NSUInteger i = 0; i < queueCount; i++) {
dispatch_queue_t queue = dispatch_queue_create(name, DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(queue, dispatch_get_global_queue(identifier, 0));
context->queues[i] = (__bridge_retained void *)(queue);
}
}
context->queueCount = queueCount;
if (name) {
context->name = strdup(name);
}
return context;
}

这个和YYAsyncDisplay里面显示串行队列的创建代码相一致。

YYDispatchContextGetQueue中会从YYDispatchContext中取出对应的queue,这里用到了OSAtomicIncrement32,每次调用的时候会将context->counter递增1,所以返回的queue会在context->queues中循环顺序取出。

static dispatch_queue_t YYDispatchContextGetQueue(YYDispatchContext *context) {
uint32_t counter = (uint32_t)OSAtomicIncrement32(&context->counter);
void *queue = context->queues[counter % context->queueCount];
return (__bridge dispatch_queue_t)(queue);
}

**** 情景二 ****

有了上面介绍后看下面代码就比较简单了,它通过YYDispatchQueuePool的初始化方法创建一个指定类型Qos的串行队列池,再通过YYDispatchContextGetQueue从前面创建的YYDispatchContext中的queues中去除一个串行队列

- (instancetype)initWithName:(NSString *)name queueCount:(NSUInteger)queueCount qos:(NSQualityOfService)qos {
if (queueCount == 0 || queueCount > MAX_QUEUE_COUNT) return nil;
self = [super init];
_context = YYDispatchContextCreate(name.UTF8String, (uint32_t)queueCount, qos);
if (!_context) return nil;
_name = name;
return self;
}

- (dispatch_queue_t)queue {
return YYDispatchContextGetQueue(_context);
}

下面是根据源码绘制的结构图: