今天有个朋友问我一个问题:
#import "MyView.h"
#import <SDWebImage/UIImageView+WebCache.h>
@interface MyView()
@end
@implementation MyView
- (void)dealloc{
NSLog(@"myView Dealloc");
}
- (instancetype)init
{
self = [super init];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit{
UIImageView *imageView = [[UIImageView alloc] init];
[imageView setBackgroundColor:[UIColor yellowColor]];
imageView.frame = CGRectMake(0, 0, 100, 100);
imageView.center = self.center;
[self addSubview:imageView];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/rs/SDWebImage/master/SDWebImage_logo.png"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
imageView.frame = CGRectMake(100, 100, 200, 200);
NSLog(@"image download done");
}];
}
@end
其中self
是一个UIView
。这样会造成循环引用吗?
答案是不会!
2018-01-16 19:38:31.678455+0800 SDWebImageTest[30248:2155555] myView Dealloc
为什么不会?这里留一个思考题哈。
提示一下看看SDWebImage
的源码,到底谁持有的block
====================答案分析====================
如果你产生了困惑,我想可能是因为下面这个方法。
- (void)sd_setImageWithURL:(nullable NSURL *)url
completed:(nullable SDExternalCompletionBlock)completedBlock;
这个方法看上去是imageView
调用的一个对象方法,那么这个方法中使用的url
和completedBlock
是不是也是imageView
这个对象持有呢?
如果是imageView
持有completedBlock
而completedBlock
中又直接访问imageView
,那么这样子看上去是存在循环引用的。
但实际并不是这样,我们来看一下源码用代码说话吧。
这个方法内部调用经过了基层封装,最后调用的是如下方法。
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary *)context {
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
[sself sd_removeActivityIndicator];
if (!sself) { return; }
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
// case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
// OR
// case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
if (shouldNotSetImage) {
dispatch_main_async_safe(callCompletedBlockClojure);
return;
}
UIImage *targetImage = nil;
NSData *targetData = nil;
if (image) {
// case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
targetImage = image;
targetData = data;
} else if (options & SDWebImageDelayPlaceholder) {
// case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
targetImage = placeholder;
targetData = nil;
}
BOOL shouldUseGlobalQueue = NO;
if (context && [context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey]) {
shouldUseGlobalQueue = [[context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey] boolValue];
}
dispatch_queue_t targetQueue = shouldUseGlobalQueue ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) : dispatch_get_main_queue();
dispatch_queue_async_safe(targetQueue, ^{
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
dispatch_main_async_safe(callCompletedBlockClojure);
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
因为这个方法是分类方法,不能直接给imageView
添加属性。所以这里用objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
的方式把url
添加为ImageView
的辅助对象。不过这个和咱们讨论的问题无关。
这里主要看completedBlock
是不是imageView
持有的
观察下面代码
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
[sself sd_removeActivityIndicator];
if (!sself) { return; }
BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
(!image && !(options & SDWebImageDelayPlaceholder)));
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
……
}];
其中SDWebImageManager.sharedManager
对self
(也就是imageView
)存在弱引用关系,便于图片下载完成后给imageView
赋值。
completedBlock
这里是copy
给SDWebImageManager.sharedManager
的如下方法当参数的。
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
再继续深入SDWebImage
内部的各种封装传递。completedBlock
最终的执行在如下方法中。
- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
completion:(nullable SDInternalCompletionBlock)completionBlock
image:(nullable UIImage *)image
data:(nullable NSData *)data
error:(nullable NSError *)error
cacheType:(SDImageCacheType)cacheType
finished:(BOOL)finished
url:(nullable NSURL *)url {
dispatch_main_async_safe(^{
if (operation && !operation.isCancelled && completionBlock) {
completionBlock(image, data, error, cacheType, finished, url);
}
});
}
这个方法正式SDWebImageManager.sharedManager
的对象方法。
所以实际上的持有关系如下
所以当
ViewController
释放之后。View
、MyView
都会顺序释放。而imageView
因为还在被completionBlock
持有所以不会释放。只有当completionBlock
执行完成后,completionBlock
对imageView
的引用也消失了,imageView
才会释放。
思考题*
如果在completionBlock
中没有对imageView
添加强引用,这时候的释放顺序是怎么样的?