码迷,mamicode.com
首页 > Web开发 > 详细

SDWebImage源码解析(二)

时间:2016-04-13 17:08:56      阅读:379      评论:0      收藏:0      [点我收藏+]

标签:

源码来源: https://github.com/rs/SDWebImage

版本: 3.7

SDWebImage是一个开源的第三方库,它提供了UIImageView的一个分类,以支持从远程服务器下载并缓存图片的功能。它具有以下功能:

  1. 提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
  2. 一个异步的图片加载器
  3. 一个异步的内存+磁盘图片缓存
  4. 支持GIF图片
  5. 支持WebP图片
  6. 后台图片解压缩处理
  7. 确保同一个URL的图片不被下载多次
  8. 确保虚假的URL不会被反复加载
  9. 确保下载及缓存时,主线程不被阻塞

从github上对SDWebImage使用情况就可以看出,SDWebImage在图片下载及缓存的处理方面还是很被认可的。在本文中,我们主要从源码的角度来分析一下SDWebImage的实现机制。讨论的内容将主要集中在图片的下载及缓存,而不包含对GIF图片及WebP图片的支持操作。


上一篇:SDWebImage源码解析(一)


2、缓存

为了减少网络流量的消耗,我们都希望下载下来的图片缓存到本地,下次再去获取同一张图片时,可以直接从本地获取,而不再从远程服务器获取。这样做的另一个好处是提升了用户体验,用户第二次查看同一幅图片时,能快速从本地获取图片直接呈现给用户。

SDWebImage提供了对图片缓存的支持,而该功能是由SDImageCache类来完成的。该类负责处理内存缓存及一个可选的磁盘缓存。其中磁盘缓存的写操作是异步的,这样就不会对UI操作造成影响。


内存缓存及磁盘缓存

内存缓存的处理是使用NSCache对象来实现的。NSCache是一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。

磁盘缓存的处理则是使用NSFileManager对象来实现的。图片存储的位置是位于Cache文件夹。另外,SDImageCache还定义了一个串行队列,来异步存储图片。

内存缓存与磁盘缓存相关变量的声明及定义如下:

@interface SDImageCache ()
@property (strong, nonatomic) NSCache *memCache;
@property (strong, nonatomic) NSString *diskCachePath;
@property (strong, nonatomic) NSMutableArray *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;
@end
- (id)initWithNamespace:(NSString *)ns {
    if ((self = [super init])) {
  NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
  ...
  _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
  ...
  // Init the memory cache
  _memCache = [[NSCache alloc] init];
  _memCache.name = fullNamespace;
  // Init the disk cache
  NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
  _diskCachePath = [paths[0] stringByAppendingPathComponent:fullNamespace];
  dispatch_sync(_ioQueue, ^{
      _fileManager = [NSFileManager new];
  });
  ...
    }
    return self;
}

SDImageCache提供了大量方法来缓存、获取、移除及清空图片。而对于每个图片,为了方便地在内存或磁盘中对它进行这些操作,我们需要一个key值来索引它。在内存中,我们将其作为NSCache的key值,而在磁盘中,我们用这个key作为图片的文件名。对于一个远程服务器下载的图片,其url是作为这个key的最佳选择了。我们在后面会看到这个key值的重要性。


存储图片

我们先来看看图片的缓存操作,该操作会在内存中放置一份缓存,而如果确定需要缓存到磁盘,则将磁盘缓存操作作为一个task放到串行队列中处理。在iOS中,会先检测图片是PNG还是JPEG,并将其转换为相应的图片数据,最后将数据写入到磁盘中(文件名是对key值做MD5摘要后的串)。缓存操作的基础方法是-storeImage:recalculateFromImage:imageData:forKey:toDisk,它的具体实现如下:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
  ...
  // 1. 内存缓存,将其存入NSCache中,同时传入图片的消耗值
  [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];
  if (toDisk) {
    // 2. 如果确定需要磁盘缓存,则将缓存操作作为一个任务放入ioQueue中
    dispatch_async(self.ioQueue, ^{
      NSData *data = imageData;
      if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
        // 3. 需要确定图片是PNG还是JPEG。PNG图片容易检测,因为有一个唯一签名。PNG图像的前8个字节总是包含以下值:137 80 78 71 13 10 26 10
        // 在imageData为nil的情况下假定图像为PNG。我们将其当作PNG以避免丢失透明度。而当有图片数据时,我们检测其前缀,确定图片的类型
        BOOL imageIsPng = YES;
        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
      }
      // 4. 创建缓存文件并存储图片
      if (data) {
        if (![_fileManager fileExistsAtPath:_diskCachePath]) {
          [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
      }
    });
  }
}


查询图片

如果我们想在内存或磁盘中查询是否有key指定的图片,则可以分别使用以下方法:
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
而如果只是想查看本地是否在key指定的图片,则不管是在内存还是在磁盘上,则可以使用以下方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
  ...
  // 1. 首先查看内存缓存,如果查找到,则直接回调doneBlock并返回
  UIImage *image = [self imageFromDiskCacheForKey:key];
  if (image) {
    doneBlock(image, SDImageCacheTypeMemory);
    return nil;
  }
  // 2. 如果内存中没有,则在磁盘中查找。如果找到,则将其放到内存缓存,并调用doneBlock回调
  NSOperation *operation = [NSOperation new];
  dispatch_async(self.ioQueue, ^{
    if (operation.isCancelled) {
      return;
    }
    @autoreleasepool {
      UIImage *diskImage = [self diskImageForKey:key];
      if (diskImage) {
        CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale;
        [self.memCache setObject:diskImage forKey:key cost:cost];
      }
      dispatch_async(dispatch_get_main_queue(), ^{
        doneBlock(diskImage, SDImageCacheTypeDisk);
      });
    }
  });
  return operation;
}

移除图片

  图片的移除操作则可以使用以下方法:
- (void)removeImageForKey:(NSString *)key;
- (void)removeImageForKey:(NSString *)key withCompletion:(SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk;
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;

我们可以选择同时移除内存及磁盘上的图片。


清理图片

磁盘缓存图片的清理操作可以分为完全清空和部分清理。完全清空操作是直接把缓存的文件夹移除,清空操作有以下两个方法:

- (void)clearDisk;
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
而部分清理则是根据我们设定的一些参数值来移除一些文件,这里主要有两个指标:文件的缓存有效期及最大缓存空间大小。文件的缓存有效期可以通过maxCacheAge属性来设置,默认是1周的时间。如果文件的缓存时间超过这个时间值,则将其移除。而最大缓存空间大小是通过maxCacheSize属性来设置的,如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。清理的操作在-cleanDiskWithCompletionBlock:方法中,其实现如下:

- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
  dispatch_async(self.ioQueue, ^{
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
    NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    // 1. 该枚举器预先获取缓存文件的有用的属性
    NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                           includingPropertiesForKeys:resourceKeys
                                    options:NSDirectoryEnumerationSkipsHiddenFiles
                                 errorHandler:NULL];
    NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
    NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
    NSUInteger currentCacheSize = 0;
    // 2. 枚举缓存文件夹中所有文件,该迭代有两个目的:移除比过期日期更老的文件;存储文件属性以备后面执行基于缓存大小的清理操作
    NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
    for (NSURL *fileURL in fileEnumerator) {
      NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
      // 3. 跳过文件夹
      if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
        continue;
      }
      // 4. 移除早于有效期的老文件
      NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
      if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
        [urlsToDelete addObject:fileURL];
        continue;
      }
      // 5. 存储文件的引用并计算所有文件的总大小,以备后用
      NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
      currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
      [cacheFiles setObject:resourceValues forKey:fileURL];
    }
    for (NSURL *fileURL in urlsToDelete) {
      [_fileManager removeItemAtURL:fileURL error:nil];
    }
    // 6.如果磁盘缓存的大小大于我们配置的最大大小,则执行基于文件大小的清理,我们首先删除最老的文件
    if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
      // 7. 以设置的最大缓存大小的一半作为清理目标
      const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
      // 8. 按照最后修改时间来排序剩下的缓存文件
      NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                              usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                              }];
      // 9. 删除文件,直到缓存总大小降到我们期望的大小
      for (NSURL *fileURL in sortedFiles) {
        if ([_fileManager removeItemAtURL:fileURL error:nil]) {
          NSDictionary *resourceValues = cacheFiles[fileURL];
          NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
          currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
          if (currentCacheSize < desiredCacheSize) {
            break;
          }
        }
      }
    }
                if (completionBlock) {
      dispatch_async(dispatch_get_main_queue(), ^{
        completionBlock();
      });
    }
  });
}


小结

以上分析了图片缓存操作,当然,除了上面讲的几个操作,SDImageCache类还提供了一些辅助方法。如获取缓存大小、缓存中图片的数量、判断缓存中是否存在某个key指定的图片。另外,SDImageCache类提供了一个单例方法的实现,所以我们可以将其当作单例对象来处理。


SDWebImageManager

在实际的运用中,我们并不直接使用SDWebImageDownloader类及SDImageCache类来执行图片的下载及缓存。为了方便用户的使用,SDWebImage提供了SDWebImageManager对象来管理图片的下载与缓存。而且我们经常用到的诸如UIImageView+WebCache等控件的分类都是基于SDWebImageManager对象的。该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们,如下代码所示:

@interface SDWebImageManager : NSObject

@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;

@property (strong, nonatomic, readonly) SDImageCache *imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;

...

@end
从上面的代码中我们还可以看到有一个delegate属性,其是一个id<SDWebImageManagerDelegate>对象。SDWebImageManagerDelegate声明了两个可选实现的方法,如下所示:

// 控制当图片在缓存中没有找到时,应该下载哪个图片
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

// 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

这两个代理方法会在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中调用,而这个方法是SDWebImageManager类的核心所在。我们来看看它的具体实现:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                     options:(SDWebImageOptions)options
                    progress:(SDWebImageDownloaderProgressBlock)progressBlock
                     completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
  ...
  // 前面省略n行。主要作了如下处理:
  // 1. 判断url的合法性
  // 2. 创建SDWebImageCombinedOperation对象
  // 3. 查看url是否是之前下载失败过的
  // 4. 如果url为nil,或者在不可重试的情况下是一个下载失败过的url,则直接返回操作对象并调用完成回调
  operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    ...
    if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
      // 下载
      id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
        if (weakOperation.isCancelled) {
          // 操作被取消,则不做任务事情
        }
        else if (error) {
          // 如果出错,则调用完成回调,并将url放入下载挫败url数组中
          ...
        }
        else {
          BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
          if (options & SDWebImageRefreshCached && image && !downloadedImage) {
            // Image refresh hit the NSURLCache cache, do not call the completion block
          }
          else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
            // 在全局队列中并行处理图片的缓存
            // 首先对图片做个转换操作,该操作是代理对象实现的
            // 然后对图片做缓存处理
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
              UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
              if (transformedImage && finished) {
                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:data forKey:key toDisk:cacheOnDisk];
              }
              ...
            });
          }
          else {
            if (downloadedImage && finished) {
              [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
            }
            ...
          }
        }
        // 下载完成并缓存后,将操作从队列中移除
        if (finished) {
          @synchronized (self.runningOperations) {
            [self.runningOperations removeObject:operation];
          }
        }
      }];
      // 设置取消回调
      operation.cancelBlock = ^{
        [subOperation cancel];
        @synchronized (self.runningOperations) {
          [self.runningOperations removeObject:weakOperation];
        }
      };
    }
    else if (image) {
      ...
    }
    else {
      ...
    }
  }];
  return operation;
}
对于这个方法,我们没有做过多的解释。其主要就是下载图片并根据操作选项来缓存图片。上面这个下载方法中的操作选项参数是由枚举SDWebImageOptions来定义的,这个操作中的一些选项是与SDWebImageDownloaderOptions中的选项对应的。我们来看看这个SDWebImageOptions选项都有哪些:
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
  // 默认情况下,当URL下载失败时,URL会被列入黑名单,导致库不会再去重试,该标记用于禁用黑名单
  SDWebImageRetryFailed = 1 << 0,
  // 默认情况下,图片下载开始于UI交互,该标记禁用这一特性,这样下载延迟到UIScrollView减速时
  SDWebImageLowPriority = 1 << 1,
  // 该标记禁用磁盘缓存
  SDWebImageCacheMemoryOnly = 1 << 2,
  // 该标记启用渐进式下载,图片在下载过程中是渐渐显示的,如同浏览器一下。
  // 默认情况下,图像在下载完成后一次性显示
  SDWebImageProgressiveDownload = 1 << 3,
  // 即使图片缓存了,也期望HTTP响应cache control,并在需要的情况下从远程刷新图片。
  // 磁盘缓存将被NSURLCache处理而不是SDWebImage,因为SDWebImage会导致轻微的性能下载。
  // 该标记帮助处理在相同请求URL后面改变的图片。如果缓存图片被刷新,则完成block会使用缓存图片调用一次
  // 然后再用最终图片调用一次
  SDWebImageRefreshCached = 1 << 4,
  // 在iOS 4+系统中,当程序进入后台后继续下载图片。这将要求系统给予额外的时间让请求完成
  // 如果后台任务超时,则操作被取消
  SDWebImageContinueInBackground = 1 << 5,
  // 通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES;来处理存储在NSHTTPCookieStore中的cookie
  SDWebImageHandleCookies = 1 << 6,
  // 允许不受信任的SSL认证
  SDWebImageAllowInvalidSSLCertificates = 1 << 7,
  // 默认情况下,图片下载按入队的顺序来执行。该标记将其移到队列的前面,
  // 以便图片能立即下载而不是等到当前队列被加载
  SDWebImageHighPriority = 1 << 8,
  // 默认情况下,占位图片在加载图片的同时被加载。该标记延迟占位图片的加载直到图片已以被加载完成
  SDWebImageDelayPlaceholder = 1 << 9,
  // 通常我们不调用动画图片的transformDownloadedImage代理方法,因为大多数转换代码可以管理它。
  // 使用这个票房则不任何情况下都进行转换。
  SDWebImageTransformAnimatedImage = 1 << 10,
};

大家在看-downloadImageWithURL:options:progress:completed:,可以看到两个SDWebImageOptions与SDWebImageDownloaderOptions中的选项是如何对应起来的,在此不多做解释。


视图扩展

我在使用SDWebImage的时候,使用得最多的是UIImageView+WebCache中的针对UIImageView的扩展方法,这些扩展方法将UIImageView与WebCache集成在一起,来让UIImageView对象拥有异步下载和缓存远程图片的能力。其中最核心的方法是-sd_setImageWithURL:placeholderImage:options:progress:completed:,其使用SDWebImageManager单例对象下载并缓存图片,完成后将图片赋值给UIImageView对象的image属性,以使图片显示出来,其具体实现如下:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
  ...
  if (url) {
    __weak UIImageView *wself = self;
    // 使用SDWebImageManager单例对象来下载图片
    id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
      if (!wself) return;
      dispatch_main_sync_safe(^{
        if (!wself) return;
        // 图片下载完后显示图片
        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 {
    ...
  }
}

除了扩展UIImageView之外,SDWebImage还扩展了UIView、UIButton、MKAnnotationView等视图类,大家可以参考源码。

当然,如果不想使用这些扩展,则可以直接使用SDWebImageManager来下载图片,这也是很OK的。


技术点

SDWebImage的主要任务就是图片的下载和缓存。为了支持这些操作,它主要使用了以下知识点:

  1. dispatch_barrier_sync函数:该方法用于对操作设置屏幕,确保在执行完任务后才会执行后续操作。该方法常用于确保类的线程安全性操作。
  2. NSMutableURLRequest:用于创建一个网络请求对象,我们可以根据需要来配置请求报头等信息。
  3. NSOperation及NSOperationQueue:操作队列是Objective-C中一种高级的并发处理方法,现在它是基于GCD来实现的。相对于GCD来说,操作队列的优点是可以取消在任务处理队列中的任务,另外在管理操作间的依赖关系方面也容易一些。对SDWebImage中我们就看到了如何使用依赖将下载顺序设置成后进先出的顺序。
  4. NSURLConnection:用于网络请求及响应处理。在iOS7.0后,苹果推出了一套新的网络请求接口,即NSURLSession类。
  5. 开启一个后台任务。
  6. NSCache类:一个类似于集合的容器。它存储key-value对,这一点类似于NSDictionary类。我们通常用使用缓存来临时存储短时间使用但创建昂贵的对象。重用这些对象可以优化性能,因为它们的值不需要重新计算。另外一方面,这些对象对于程序来说不是紧要的,在内存紧张时会被丢弃。
  7. 清理缓存图片的策略:特别是最大缓存空间大小的设置。如果所有缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间。
  8. 对图片的解压缩操作:这一操作可以查看SDWebImageDecoder.m中+decodedImageWithImage方法的实现。
  9. 对GIF图片的处理
  10. 对WebP图片的处理


感兴趣的同学可以深入研究一下这些知识点。当然,这只是其中一部分,更多的知识还有待大家去发掘。


参考:
  1. http://southpeak.github.io/blog/2015/02/07/yuan-ma-pian-:sdwebimage/?utm_source=tuicool&utm_medium=referral
  2. http://blog.sina.com.cn/s/blog_8988732e0101af25.html
  3. http://www.jianshu.com/p/c07df06c60be

SDWebImage源码解析(二)

标签:

原文地址:http://blog.csdn.net/xubinlxb/article/details/51143682

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!