美文网首页
SDWebImage源码分析 3

SDWebImage源码分析 3

作者: 白熊 | 来源:发表于2016-07-08 10:02 被阅读74次

    上章节中遗漏了storeImage这个方法,这章节补完,先来SDWebImageCache.h看它的注释:

    /**
     * 根据给的key将图片存储到内存以及磁盘(可选)缓存中
     *
     * @param image       需要存储的图片
     * @param recalculate 图片数据能否在UIImage中被构建出来
     * @param imageData   从服务器返回的图片数据
     * @param key         唯一的图片缓存key, 常使用图片的绝对URL
     * @param toDisk      设为YES时将图片存储进磁盘中
     */
    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
    

    通过注释得知该方法是将图片存储进缓存中用的,看下里面的代码:

    if (!image || !key) {
        return;
    }
    // 如果开启内存缓存
    if (self.shouldCacheImagesInMemory) { //< 默认是开启的
        NSUInteger cost = SDCacheCostForImage(image); //< 计算cost
        [self.memCache setObject:image forKey:key cost:cost]; //< 利用内建的NSCache进行存储
    }
    //如果开启写入磁盘
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{ //< 存入磁盘也是在异步中进行的
            NSData *data = imageData;
    
            if (image && (recalculate || !data)) {
    #if TARGET_OS_IPHONE
                // 这里主要的功能就是探测图片是PNG还是JPEG
                // We need to determine if the image is a PNG or a JPEG
                // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
                // The first eight bytes of a PNG file always contain the following (decimal) values:
                // 137 80 78 71 13 10 26 10
    
                // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
                // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha;
    
                // 如果有图片数据,查看前8个字节,判断是不是png
                if ([imageData length] >= [kPNGSignatureData length]) {
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }
    
                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
    #else
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
    #endif
            }
    
            [self storeImageDataToDisk:data forKey:key];
        });
    }
    

    通过代码可以知道,图片默认是通过url作为key被存储进NSCache中的。

    if (!imageData) {
        return;
    }
    //如果没有对应目录就生成一个
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // 为图片key获取缓存路径
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // 转换成NSURL
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    
    // 关闭iCloud备份
    if (self.shouldDisableiCloud) {
        [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
    

    如果需要将图片缓存进磁盘中,就开一个子线程,在里面探测图片是属于哪种类型的,再通过storeImageDataToDisk方法存入磁盘,默认情况下不允许iCloud备份,又学到一招。

    defaultCachePathForKey这个方法是用来生成缓存路径的,跟踪进去发现调用的是下面这个方法:

    - (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
        NSString *filename = [self cachedFileNameForKey:key];
        return [path stringByAppendingPathComponent:filename];
    }
    

    这段代码将key丢入cachedFileNameForKey方法中生成文件名,然后再将其追加到path后返回缓存路径的。

    我们再跟到cachedFileNameForKey中一探究竟:

    - (NSString *)cachedFileNameForKey:(NSString *)key {
        const char *str = [key UTF8String];
        if (str == NULL) {
            str = "";
        }
        unsigned char r[CC_MD5_DIGEST_LENGTH];
        CC_MD5(str, (CC_LONG)strlen(str), r);
        NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                              r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                              r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
    
        return filename;
    }
    

    该方法将url转为了MD5命名,目的应该是防止缓存进磁盘的文件命名冲突

    回到SDWebImageDownloader.m中,上期还没讲的downloadImageWithURL 方法:

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
      __block SDWebImageDownloaderOperation *operation;
      __weak __typeof(self)wself = self;
      [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url     createCallback:^{
        ...
    

    这里第一行就调用了addProgressCallback这个方法,跟踪进去:

    - (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
        //URL用作回调字典的键所以不允许为nil,如果为nil立即调用completed block传入空的image,data,error
        if (url == nil) {
            if (completedBlock != nil) {
                completedBlock(nil, nil, nil, NO);
            }
            return;
        }
        
        dispatch_barrier_sync(self.barrierQueue, ^{
            BOOL first = NO;
            if (!self.URLCallbacks[url]) {
                self.URLCallbacks[url] = [NSMutableArray new]; //< URLCallbacks中没有对应的url就创建一个NSMutableArray
                first = YES;
            }
    
            //同一个URL的情况下只允许一个下载请求
            NSMutableArray *callbacksForURL = self.URLCallbacks[url];
            NSMutableDictionary *callbacks = [NSMutableDictionary new];
            if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
            if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
            
            [callbacksForURL addObject:callbacks];
            
            self.URLCallbacks[url] = callbacksForURL;
            
            if (first) {
                createCallback();
            }
        });
    }
    

    这段代码中用到了dispatch_barrier_sync,当写入线程与其他写入线程或读取线程并行时候会产生问题,比如:

    //摘自《Objective-C高级编程 iOS与OS X多线程和内存管理》
    dispatch_queue_t queue = dispatch_create("barrier",DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue,blk0_for_reading);
    dispatch_async(queue,blk1_for_reading);
    dispatch_async(queue,blk2_for_reading);
    dispatch_async(queue,blk_for_writing);//可能造成读取的数据与预期不符,如果追加多个写入则可能发生更多问题,比如数据竞争。
    dispatch_async(queue,blk3_for_reading);
    dispatch_async(queue,blk4_for_reading);
    dispatch_async(queue,blk5_for_reading);
    

    将写入任务换成dispatch_barrier_async可以解决这个问题,它会等待这个blk02并发执行完毕后,开始执行。执行完毕之后才轮到blk35并发执行

    另外,关注一下self.URLCallbacks[url]里存放的数据格。以url作为key,将completed和progress存放进去,如下所示:

    {
     "http://www.xxx.com/xxx.jpg" = (
         {
             completed = "<__NSMallocBlock__: 0x7faa6b426620>";
             progress = "<__NSGlobalBlock__: 0x10a508b90>";
         }
     );
    }
    

    接着看addProgressCallback方法回调部分:

    NSTimeInterval timeoutInterval = wself.downloadTimeout;
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0; //< 下载超时的时间,默认是15秒。
    }
    

    在没设置downloadTimeout的情况下,默认超时时间为15秒

    //开启SDWebImageDownloaderUseNSURLCache选项则使用NSURL的Cache,否则忽略
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
    request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); //< 简单地说就是请求时将cookie一起带上
    request.HTTPShouldUsePipelining = YES; //< 不等待上次请求响应,直接发送数据
    if (wself.headersFilter) { //< 自定义http 头部
        request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
    }
    else { //< 默认http 头部
        request.allHTTPHeaderFields = wself.HTTPHeaders;
    }
    

    这里用于构建NSMutableURLRequest以及设置http头部信息

    //wself.operationClass是Class类型,由init的时候或set时指定
    //_operationClass = [SDWebImageDownloaderOperation class];
    operation = [[wself.operationClass alloc] initWithRequest:request
      options:options
      progress:^(NSInteger receivedSize, NSInteger expectedSize) {
        ...
      }
      completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
        ...
      }
      cancelled:^{
        ...
      }];
    

    这里将上面构造好的request放入SDWebImageDownloaderOperation中被初始化。三段回调里的代码比较长,这里拆开来说

    progress:

    progress:^(NSInteger receivedSize, NSInteger expectedSize) {
     SDWebImageDownloader *sself = wself;
     if (!sself) return;
     __block NSArray *callbacksForURL;
     //此处并未使用dispatch_barrier_sync,个人感觉是因为此处并未对sself.URLCallbacks进行修改,所以不会对后面的任务产生数据竞争(竞态条件),使用dispatch_sync更恰当
     dispatch_sync(sself.barrierQueue, ^{
         callbacksForURL = [sself.URLCallbacks[url] copy];
     });
     for (NSDictionary *callbacks in callbacksForURL) {
         dispatch_async(dispatch_get_main_queue(), ^{ //< 回主线程
             SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey]; //< process
             // receivedSize:下载了多少,expectedSize:预期下载量多大
             // 只要有新数据块到达,这个block就会不停地被调用
             if (callback) callback(receivedSize, expectedSize);
         });
     }
    }
    

    completed:

    completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
      SDWebImageDownloader *sself = wself;
      if (!sself) return;
      __block NSArray *callbacksForURL;
      dispatch_barrier_sync(sself.barrierQueue, ^{
          callbacksForURL = [sself.URLCallbacks[url] copy];
          if (finished) {
              [sself.URLCallbacks removeObjectForKey:url]; //< 任务完成后删除对应url的回调
          }
      });
      for (NSDictionary *callbacks in callbacksForURL) {
          SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey]; //< completed
          if (callback) callback(image, data, error, finished);
      }
    }
    

    cancelled:

    cancelled:^{
        SDWebImageDownloader *sself = wself;
        if (!sself) return;
        dispatch_barrier_async(sself.barrierQueue, ^{
            [sself.URLCallbacks removeObjectForKey:url];//< 任务取消后删除对应url的回调
        });
    }
    
    //图片在下载后以及在缓存中进行解压可以提高性能但会消耗大量内存,默认YES
    operation.shouldDecompressImages = wself.shouldDecompressImages;
    //鉴权相关
    if (wself.urlCredential) {
        operation.credential = wself.urlCredential;
    } else if (wself.username && wself.password) {
        operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
    }
    //优先级相关
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    //将operation加入到downloadQueue
    [wself.downloadQueue addOperation:operation];
    
    if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { //< 如果执行顺序为LIFO后进先出
        // 将新添加的operations作为最后一个operation的依赖,执行顺序就是新添加的operation先执行,后进先出的过程,类似栈结构
        // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
        [wself.lastAddedOperation addDependency:operation];
        wself.lastAddedOperation = operation;
    }
    

    至此本章将SDWebImageDownloader的主要流程给介绍完了。对于网络请求鉴权什么的我不打算深究,留着以后看完AFNetWorking再说。

    P.S:顺便吐槽下简书的代码显示太窄,影响阅读效果。

    相关文章

      网友评论

          本文标题:SDWebImage源码分析 3

          本文链接:https://www.haomeiwen.com/subject/lsazdttx.html