zoukankan      html  css  js  c++  java
  • SDWebImage源码阅读-第一篇

    一 题外话

      之前写过一篇最新版SDWebImage的使用,也简单的介绍了一下原理。这两天正梳理自己的知识网络,觉得有必要再阅读一下源码,一是看具体实现,二是学习一下优秀开源代码的代码风格,比如接口设计,设计模式,变量命名等等。

      既然是第一篇,就要制定一个阅读源码的计划,以什么顺序阅读完全部代码。我们从最常见的入口切入sd_setImageWithURL,一路下去,最后再阅读没有设计到的部分。

      在开始之前强烈建议先去读我之前的文章:最新版SDWebImage的使用。心里有个大概再去探讨细节,效果更佳。

    二 入口

      我们为什么使用SDWebImage,是因为他帮我们实现了图片的二级缓存,使我们加载图片更流畅。当然你也可以使用SDWebImage中几个很棒的工具类,比如SDWebImageDownloader,用来下载图片。或者SDImageCache用来缓存图片或者NSData。我们先来看看UIImageView+WebCache中的基本方法:

      在UIImageView+WebCache类的最上面,很贴心的贴了一个使用例子,这也是我们很常见的tableViewCell加载图片的场景

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        static NSString *MyIdentifier = @"MyIdentifier";
     
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
     
        if (cell == nil) {
            cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier]
                     autorelease];
        }
     
        // Here we use the provided sd_setImageWithURL: method to load the web image
        // Ensure you use a placeholder image otherwise cells will be initialized with no image
        [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://example.com/image.jpg"]
                          placeholderImage:[UIImage imageNamed:@"placeholder"]];
     
        cell.textLabel.text = @"My Text";
        return cell;
    }

    sd_setImageWithURL: placeholderImage:,也是我们最常使用的方法,我们看看除了这个外的其他方法:

    //最基本方法
    - (void)sd_setImageWithURL:(NSURL *)url;
    
    //带placeholder,优先显示placeholder,下载完成后显示原图
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
    
    //多了缓存策略options 
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
    
    //多了下载完成后的block回调
    - (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock;
    
    //多了下载完成后的block回调
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock;
    
    //多了下载完成后的block回调
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
    
    //多了下载过程progressBlock回调
    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
    
    //多了下载过程progressBlock回调
    - (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

    这个接口设计很经典,也是看了这个接口设计,回头重构了自己项目中社交分享模块的接口。而下面这个方法,也可以看做是外观模式的具体体现。

    - (void)sd_setImageWithURL:(NSURL *)url;

    废话少说,进去看看实现。可以看到,所有方法都指向

    - (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

    具体实现如下

    - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
        [self sd_cancelCurrentImageLoad];
        objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                self.image = placeholder;
            });
        }
        
        if (url) {
    
            // check if activityView is enabled or not
            if ([self showActivityIndicatorView]) {
                [self addActivityIndicator];
            }
    
            __weak __typeof(self)wself = self;
            id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                [wself removeActivityIndicator];
                if (!wself) return;
                dispatch_main_sync_safe(^{
                    if (!wself) return;
                    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                    {
                        completedBlock(image, error, cacheType, url);
                        return;
                    }
                    else if (image) {
                        wself.image = image;
                        [wself setNeedsLayout];
                    } else {
                        if ((options & SDWebImageDelayPlaceholder)) {
                            wself.image = placeholder;
                            [wself setNeedsLayout];
                        }
                    }
                    if (completedBlock && finished) {
                        completedBlock(image, error, cacheType, url);
                    }
                });
            }];
            [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
        } else {
            dispatch_main_async_safe(^{
                [self removeActivityIndicator];
                if (completedBlock) {
                    NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                    completedBlock(nil, error, SDImageCacheTypeNone, url);
                }
            });
        }
    }

    可以看到第一步是取消当前下载,本着不放过任何一个细节的精神,我们看看是怎么取消当前下载的

    - (void)sd_cancelCurrentImageLoad {
        [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
    }
    - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
        // Cancel in progress downloader from queue
        NSMutableDictionary *operationDictionary = [self operationDictionary];
        id operations = [operationDictionary objectForKey:key];
        if (operations) {
            if ([operations isKindOfClass:[NSArray class]]) {
                for (id <SDWebImageOperation> operation in operations) {
                    if (operation) {
                        [operation cancel];
                    }
                }
            } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
                [(id<SDWebImageOperation>) operations cancel];
            }
            [operationDictionary removeObjectForKey:key];
        }
    }

      我们看到,operationDictionary就是多个线程的集合。在SDWebImageManager的downloadImageWithURL方法中创建operation并返回,保存在operationDictionary中。然后我们从operationDictionary中通过key "UIImageViewImageLoad" 取出负责下载的所有operation,然后cancel掉。并将下载的所有operation从operationDictionary中移除。特别值得注意的是,当前类是UIImageView的category,我们知道,category不能增加属性,只能增加方法,那么operationDictionary是哪里来的呢。答案是:objc_setAssociatedObject,对象关联,动态的给UIImageView添加新属性。在SDWebImage中有很多这种用法,看到你就要知道,这就是动态增加了属性。

      继续看如何cancel,SDWebImageOperation是一个协议,而SDWebImageDownloaderOperation实现了这个协议。我们上面说的从operationDictionary中通过key "UIImageViewImageLoad" 取出负责下载的所有queue,其实就是SDWebImageDownloaderOperation的实例的集合。熟悉NSOperation的都知道,SDWebImageDownloaderOperation继承于NSOperation,每一个SDWebImageDownloaderOperation的实例都是一个新线程。NSOperation其实自己也抽象了cancel方法。- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key方法中,[operation cancel];这里的operation其实是SDWebImageManager的

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
    
                                             options:(SDWebImageOptions)options
    
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
    
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

    这个方法中创建的SDWebImageCombinedOperation实例,

     __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;

    再看看SDWebImageCombinedOperation的定义和实现

    @interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
    
    @property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
    @property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
    @property (strong, nonatomic) NSOperation *cacheOperation;
    
    @end
    
    @implementation SDWebImageCombinedOperation
    
    - (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
        // check if the operation is already cancelled, then we just call the cancelBlock
        if (self.isCancelled) {
            if (cancelBlock) {
                cancelBlock();
            }
            _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
        } else {
            _cancelBlock = [cancelBlock copy];
        }
    }
    
    - (void)cancel {
        self.cancelled = YES;
        if (self.cacheOperation) {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        if (self.cancelBlock) {
            self.cancelBlock();
            
            // TODO: this is a temporary fix to #809.
            // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
    //        self.cancelBlock = nil;
            _cancelBlock = nil;
        }
    }
    
    @end

    我们看到,调用该operation的cancel方法,其实是执行cancelBlock,我们就看看,他的cancelBlock传入的是什么东西。在SDWebImageManager的downloadImageWithURL方法中,我们找到了赋值的地方

    operation.cancelBlock = ^{
                    [subOperation cancel];
                    
                    @synchronized (self.runningOperations) {
                        __strong __typeof(weakOperation) strongOperation = weakOperation;
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                };

    我们看到block里面调用了[subOperation cancel];而这个subOperation,是SDWebImageDownloader的downloadImageWithURL方法创建并返回的SDWebImageDownloaderOperation对象,它是NSOperation的子类。终于到了我们熟悉的对象。究其原因,cancle的时候其实就是SDWebImageDownloaderOperation的实例cancel,具体实现如下:

    - (void)cancel {
        @synchronized (self) {
            if (self.thread) {
                [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
            }
            else {
                [self cancelInternal];
            }
        }
    }
    - (void)cancelInternalAndStop {
        if (self.isFinished) return;
        [self cancelInternal];
    }

    - (void)cancelInternal { if (self.isFinished) return; [super cancel]; if (self.cancelBlock) self.cancelBlock(); if (self.dataTask) { [self.dataTask cancel]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); // As we cancelled the connection, its callback won't be called and thus won't // maintain the isFinished and isExecuting flags. if (self.isExecuting) self.executing = NO; if (!self.isFinished) self.finished = YES; } [self reset]; }

    最重要的就是这句:[self.dataTask cancel]。self.dataTask是NSURLSessionTask的实例,这里就是取消网络请求。说这么多,仅仅是取消了下载图片的网络请求。

    已经写了不少了,把大头戏放到下一篇。下一篇我们主要分析二级缓存的实现和SDWebImageDownloader的异步加载。

  • 相关阅读:
    2020最新JavaScript开发必须知道的41个技巧,你会几个?
    Vue项目上线要做哪些优化?面试必学
    javascript 关于dom节点操作的增删改查
    uniapp 上拉加载下拉刷新
    移动WEB适配布局
    微信小程序中封装网络请求方法
    react兄弟组件传值
    【python】Mutilindex多层索引的取值
    【python】通过Mutilindex生成二维表数据
    【python】python vs Excel ( 如何通过loc与iloc函数处理Excel数据)
  • 原文地址:https://www.cnblogs.com/6duxz/p/6964032.html
Copyright © 2011-2022 走看看