SDWebImage 时序图 通过UML了解sd的整体架构及流程,对象关系等等(另一个文件中已收集) @autoreleasepool 临时计算或者使用的对象,用完即可释放掉,以免占用内存,比如for循环处理,释放 局部变量 for (int i = 0; i < 1000000; i++) { @autoreleasepool { ..... //其他处理 } } 图片的解压缩IO SDWebImageImageIOCoder.m @autoreleasepool{ //部分代码 CGImageRef imageRef = image.CGImage; CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:imageRef]; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); .... } 显示图片 JPG/PNG 直接使用sd_setImageWithURL:placeholderImage: completed:系列方法 GIF 方法1: UIImage+GIF.h sd_animatedGIFWithData v > 4.2 方法2 :依赖框架FLAnimatedImageView 替换UIImageView 判断图片格式 NSData+ImageContentType.h 将数据data转为十六进制数据,取第一个字节数据进行判断: uint8_t c; [data getBytes:&c length:1]; switch (c) { case 0xFF: return SDImageFormatJPEG; case 0x89: return SDImageFormatPNG; case 0x47: return SDImageFormatGIF; case 0x49: case 0x4D: return SDImageFormatTIFF; case 0x52: if (....) { return SDImageFormatWebP; } } case 0x52: // R as RIFF for WEBP if (data.length < 12) { return SDImageFormatUndefined; } NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { return SDImageFormatWebP; } 文件命名 Url绝对路径取MD5值+文件后缀(如果获取成功) cd3f8d1414719727286eda32fda4b96d.png cd3f8d1414719727286eda32fda4b96d.jpg cd3f8d1414719727286eda32fda4b96d.gif cd3f8d1414719727286eda32fda4b96d (无后缀) 默认 默认最大并发数=6 _downloadQueue.maxConcurrentOperationCount = 6; 默认超时时长=15s _downloadTimeout = 15.0; 默认缓存有效期 一个星期 存储 Memory缓存 类 : AutoPurgeCache 继承NSCache 删除缓存时机 当系统内存紧张时,NSCache 会自动删除一些缓存对象 进入后台时 手动删除 即将退出 Memory缓存 ,你把cache 值设成0,不存储到缓存 进入后台处理任务 后台删除需要申请延长挂起时间, long-running background task 开启后台 UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)]; // 后台任务标识--注册一个后台任务 __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ // Clean up any unfinished task business by marking where you // stopped or ending the task outright. [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; 关闭后台 [self deleteOldFilesWithCompletionBlock:^{ //结束后台任务 [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; 存取值方式 key:value: 线程安全 NSCache是线程安全的,在多线程操作增删改查中,不需要对Cache进行加锁, NSCache的key只是对对象的强引用,对象不需要实现NSCopying协议, NSCache也不会像NSDictionary一样复制对象 读取缓存的时候是在主线程进行,线程安全 对比NSMutableDictionary key:value:形式 多线程不安全,修改须加锁 磁盘/沙盒存储-Disk 默认有效时长1天 默认存储路径 ~/Library/Caches/default/com.hackemist.SDWebImageCache.default 缓存空间超过限定值(默认无限制) 1 清理已过期的缓存 2 按早晚顺序继续清理指定限制值以内 ioQueue sd创建了一个名为 ioQueue 的串行队列,大部分Disk操作都在此队列中,逐个 执行 @property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue; // Create IO serial queue 创建串行队列 _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); 读写/计算 磁盘写入等操作都是在这个ioQueue队列上进行的,以保证线程安全 计算大小、获取文件总数等操作。则是在主线程进行 主线程上计算会导致堵塞吗? 如果直接在主队列(系统UI等操作的队列)上执行则会导致堵塞 但是在主线程上,不会直接在主队列上, 会在其他队列上或者空闲时处理 如何判断是相同队列 GCD 创建一个 queue 的时候会指定 queue_label,可以理解为队列名,而这个 queueLabel 是唯一的,然后通过 strcmp 函数进行比较,如果为0说明在同一个 队列中 如何保证执行是在ioQueue队列中? - (void)checkIfQueueIsIOQueue { const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL); const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue); if (strcmp(currentQueueLabel, ioQueueLabel) != 0) { //当前不在ioQueue队列中 } } 同理,如何判断是在主队列中 ? //取得当前队列的队列名 dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) //取得主队列的队列名 dispatch_queue_get_label(dispatch_get_main_queue()) if (strcmp(currentQueueLabel, ioQueueLabel) == 0) { //当前在主队列中 } 主线程 新版SDWebImage 从判断是否在主线程执行改为判断是否由主队列上调度,为什 么? 主线程 在主线程执行的任务不一定是由主队列调度的,可能是其他队列 主队列 主队列(系统UI等操作的队列)是一个串行队列,无论任务是异步同步都不会开辟 新线程 在主队列调度的任务肯定在主线程执行 问题 如果某个库依赖于在主队列上检查执行,那么从主线程上执行的非主队列调用API 将导致问题。也就是说,如果在主线程执行非主队列调度的API,而这个API需要 检查是否由主队列上调度,那么将会出现问题 当我们操作UI时需要回到主线程进行操作, 但是我们是通过回到主队列中执行的, 因为主队列一定在主线程上(其他方法参考多线程篇) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_ DEFAULT, 0), ^{ // 需要在主队列(主线程)执行的代码 }); 枚举 NS_OPTIONS 位移枚举 同时能使用多种状态,以”|”,”&”与或 间隔 SDWebImageLowPriority | SDWebImageCacheMemoryOnly typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { SDWebImageRetryFailed = 1 << 0, // 值为2的0次方 SDWebImageLowPriority = 1 << 1, // 值为2的1次方 ... }; NS_ENUM 通用枚举 同时只能使用一种状态: doneBlock(diskImage, diskData, SDImageCacheTypeDisk); typedef NS_ENUM(NSInteger, SDImageCacheType) { SDImageCacheTypeNone, // 默认从0开始 SDImageCacheTypeDisk, // 值为1 SDImageCacheTypeMemory // 值为2 }; 图片加载 1、通过UIImageView+WebCache 的 sd_setImageWithURL方法(等)作为入口 来加载图片 2、通过UIView+WebCache的'sd_internalSetImageWithURL'对 UIImageView、UIButton 、MKAnnotationView中图片的下载请求进行汇总 3、开始加载图片 通过SDWebImageManager的loadImageWithURL对图片进行加载 4、查找本地 通过SDImageCache的queryCacheOperationForKey查找缓存中是否存在图 片。如果不存在再通过diskImageDataBySearchingAllPathsForKey进行磁盘搜 5、返回本地图片给SDWebImageManager 6、下载图片 如果本地查询不到对应图片、则通过SDImageDownloader的downloadImage 进行图片下载 7、下载完毕返回图片给SDWebImageManager 8、由UIView+WebCache通过storeImage将下载图片缓存,保存本地 9、返回图片给UIView+WebCache 10、设置图片 主线程 图片 解压缩 显示图片为什么需要解压缩过程? 解压 假设您有一个UIScrollView,它为应用程序的各个页面显示UIImageViews列 表。 只要下一页的一个像素滚动出现在屏幕上,就会实例化(或重用) UIImageView并将其弹出到滚动视图的内容区域。 这在Simulator中效果很好, 但是当您在设备上进行测试时,发现每次尝试翻页至下一页时,都会出现明显的 延迟。 这种延迟是由于需要将图像从其文件形式解压缩以在屏幕上呈现而导致 的。 不幸的是,UIImage在即将要显示它的时候才进行这种解压缩。 简化加载流程: 1.从网络上请求图片(已经压缩过的JPEG,PNG...) 2.使用这个压缩过的图片对UIImageView初始化 3.开始渲染图片前,对图片数据进行解压缩,然后开始显示 在我们使用 UIImage 的时候,创建的图片通常不会直接加载到内存,而是在渲染 的时候再进行解压并加载到内存。这就会导致 UIImage 在渲染的时候效率上不是 那么高效。为了提高效率通过 decodedImageWithImage方法把图片提前解压加 载到内存,这样这张新图片就不再需要重复解压了,提高了渲染效率。这是一种 空间换时间的做法。 显示图片: [UIImage imageNamed:@"logo"]; 显示是CPU与GPU协同合作完成一次渲染 位图 iOS展示图是基于位图的,非矢量图 图片解码功能的实现依赖于Quartz 2D的图像处理库 位图对象类 : CGImageRef 列如 : CGImageRef imageRef = image.CGImage; 颜色模型 CMYK Gray Spaces 灰度空间 黑-灰-白 RGB (iOS 常用) RGB分量都用8位表示,取值范围为0-255 就是256 ^3种 RGB24 24位表示一个像素 RGB32 32位, 就是多了一个8位的 alpha 通道来表示透明度 RGB-32 SDWebImage解码就是采用32位, 其alpha写死了8位(iOS最高支持8位alpha通 道,下载的图片源可能是16位的,所以直接写死8位) 每通道8位 static const size_t kBitsPerComponent = 8; 每个像素由四通道 static const size_t kBytesPerPixel = 4; 图片叠加 alpha为yes的位图叠加显示需要更大的渲染开销, 而离屏渲染就是一个例子, 所以 能不使用尽量不使用alpha通道 解码 图片解码的类 : SDWebImageDecoder SDWebImage解决思路 : 提前解压缩 当图片从网络中获取到的时候就进行解压缩 当图片从磁盘缓存中获取到的时候立即解压缩 但是, 当图片解压后大小超过了限制最大值时, 就要对图片进行压缩大小处理 sd解码源码 以下解压源码decodedImageWithImage:image步骤: CGImageRef imageRef = image.CGImage;//1.获取传入的UIImage对应的 CGImageRef(位图) CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];//获取彩色空间 size_t width = CGImageGetWidth(imageRef);//获取高和宽 size_t height = CGImageGetHeight(imageRef); size_t bytesPerRow = kBytesPerPixel * width;// 每个像素占4个字节大小 共 32位 (RGBA) //2 初始化bitmap graphics context 上下文 CGContextRef context = CGBitmapContextCreate(NULL,width,height,kBitsPerComponent,bytesPer Row, colorspaceRef,kCGBitmapByteOrderDefault| kCGImageAlphaNoneSkipLast); if (context == NULL) {//上下文获取失败 return image; } //3 将CGImageRef对象画到上面生成的上下文中,且将alpha通道移除 CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); //4 使用上下文创建位图 CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); //5 从位图创建UIImage对象 UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation]; CGContextRelease(context);//释放CG对象 CGImageRelease(imageRefWithoutAlpha); //6 返回新建的图片 return imageWithoutAlpha; sd解码源码-主要步骤 : 1.获取image的位图,及宽高等数据 2.创建上下文 3.根据步骤1的数据在上下文中创建新的位图 4.从新位图创建新的UIImage对象 5.结束:返回新的image 图片的压缩 当上面解压后的图片数据超过限定值时, 需要对图片大小进行压缩,直到满足(太大 内存会暴增), 方法 : decodedAndScaledDownImageWithImage: 时机 : 在创建新的image之前先判断大小是否超出范围, 超出则开始压缩 压缩 通过调整图片尺寸来压缩图片 可参阅NSHisper的一篇博客 位图 : 是由像素组成的矩阵 缩放尺寸 通过系数缩放 let size = CGSizeApplyAffineTransform(image.size, CGAffineTransformMakeScale(0.5, 0.5)) 通过长宽比缩放 import AVFoundation let rect = AVMakeRectWithAspectRatioInsideRect(image.size, imageView.bounds) 缩放图片 UIKIt框架 创建一个临时渲染上下文,在这上面绘制原始图片。然后缩放图片的尺寸: UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale) image.drawInRect(CGRect(origin: CGPointZero, size: size)) Core Graphic/Quartz 2D 通过CGContextSetInterpolationQuality设置上下文在各个保真度(清晰等级 low-hight)插入像素。 设置为高清 : CGContextSetInterpolationQuality(context, kCGInterpolationHigh) CGContextDrawImage(...) 在制定的尺寸和位置上画图,允许在特定边缘或者适应一组图片特征比如faces, 裁剪图片 最后 ,通过CGBitmapContextCreateImage从上下文中创建CGImage Image I/O 通过方法CGImageSourceCreateThumbnailAtIndex,设置不同参数来调整图片 通过一个常量系数来缩放图片,同时保持原始的长宽比,自动缓存缩放的结果 Core Image CILanczosScaleTransform滤镜内置的Lanczos 采样filter函数 let filter = CIFilter(name: "CILanczosScaleTransform") filter.setValue(image, forKey: "inputImage") let outputImage = filter.valueForKey("outputImage") as CIImage self.context.createCGImage(outputImage, fromRect: outputImage.extent())) PNG缩放对比 框架 压缩时间 压缩大小 UIKIt 0.001 25% Core Graphic 0.005 12% Image I/O 0.001 82% Core Image 0.234 43% UIKit,Core Graphic 和Image I/O 对大多数图片缩放操作表现良好 图片下载 两个重要的类——SDWebImageDownloader和 SDWebImageDownloaderOperation 方法dloadImageWithURL()下载 下载方法 //使用更新的downloaderOptions开启下载图片任务 SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed ... 1. SDWebImageDownloader 单例 下载核心代码 用于管理NSURLRequest对象请求头的封装、缓存、cookie的设置,加载选项的 处理等功能。管理Operation之间的依赖关系。 SDWebImageDownloaderOperation是一个自定义的并行Operation子类。这 个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、 Operation生命周期管理等。 初始化 _operationClass = [SDWebImageDownloaderOperation class]; _shouldDecompressImages = YES;//默认需要对图片进行解压 _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;//默认的 任务执行方式为FIFO队列 _downloadQueue = [NSOperationQueue new]; _downloadQueue.maxConcurrentOperationCount = 6;//默认最大并发任务 的数目为6个 _downloadQueue.name = @"com.hackemist.SDWebImageDownloader"; _URLOperations = [NSMutableDictionary new]; //设置默认的HTTP请求头 #ifdef SD_WEBP _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy]; #else _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy]; #endif _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQ ueue", DISPATCH_QUEUE_CONCURRENT); _downloadTimeout = 15.0;//设置默认的请求超时 sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout; //初始化session,delegateQueue设为nil因此session会创建一个串行任务队 列来处理代理方法和请求回调。 self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration ... 核心 //1. 设置超时 15s NSTimeInterval timeoutInterval = sself.downloadTimeout; //2. 关闭NSURLCache,防止重复缓存图片请求 NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; //如果options中设置了使用NSURLCache则开启NSURLCache,默认关闭 if (options & SDWebImageDownloaderIgnoreCachedResponse) { cachePolicy = NSURLRequestReturnCacheDataDontLoad; } else { cachePolicy = NSURLRequestUseProtocolCachePolicy; } //3. 初始化URLRequest NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval]; request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES; //4. 添加请求头 if (sself.headersFilter) { request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]); } else { request.allHTTPHeaderFields = sself.HTTPHeaders; } //5. 初始化operation 对象 SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options]; //是否要对图片进行解压 operation.shouldDecompressImages = sself.shouldDecompressImages; //6. 指定验证方式 if (sself.urlCredential) { //SSL验证 operation.credential = sself.urlCredential; } else if (sself.username && sself.password) { //Basic验证 operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession]; } if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } //7. 将当前operation添加到下载队列 [sself.downloadQueue addOperation:operation]; if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { //为operation添加依赖关系 //模拟栈的数据结构 先进后出 [sself.lastAddedOperation addDependency:operation]; sself.lastAddedOperation = operation; } //返回 return operation; 2. SDWebImageDownloaderOperation 2 继承自 NSOperation 对NSOperation的几个方法进行重载, start , reset, cancel start重写 //1. 进行后台任务的处理,先判断是否开启和需要后台下载 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:^{ __strong __typeof (wself) sself = wself; if (sself) { [sself cancel]; //结束后台任务 [app endBackgroundTask:sself.backgroundTaskId]; sself.backgroundTaskId = UIBackgroundTaskInvalid; } }]; } //2. 初始化session NSURLSession *session = self.unownedSession; if (!self.unownedSession) { //如果Downloader没有传入session(self 对 unownedSession弱引 用,因为默认该变量为downloader强引用) //使用defaultSessionConfiguration初始化session NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForRequest = 15;//超时 self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; session = self.ownedSession; } //使用request初始化dataTask self.dataTask = [session dataTaskWithRequest:self.request]; self.executing = YES; //开始执行网络请求 [self.dataTask resume]; if (self.dataTask) { //对callbacks中的每个progressBlock进行调用,并传入进度参数 //#define NSURLResponseUnknownLength ((long long)-1) for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { progressBlock(0, NSURLResponseUnknownLength, self.request.URL); } //主队列通知SDWebImageDownloadStartNotification dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; }); } else { //连接不成功 //执行回调输出错误信息 [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]]; } cancel重写 //重写NSOperation的cancel方法 - (void)cancel { @synchronized (self) { [self cancelInternal]; } } //内部方法cancel : 如果已结束直接退出 - (void)cancelInternal { if (self.isFinished) return; [super cancel]; if (self.dataTask) {//如果未完成,取消,并通知后,结束下载, [self.dataTask cancel]; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self]; }); if (self.isExecuting) self.executing = NO; if (!self.isFinished) self.finished = YES; } [self reset];//最后重置 } reset重写 - (void)reset {//使用栅栏函数保证任务删除后执行后面异步任务 dispatch_barrier_async(self.barrierQueue, ^{ [self.callbackBlocks removeAllObjects]; }); self.dataTask = nil; self.imageData = nil; if (self.ownedSession) { [self.ownedSession invalidateAndCancel]; self.ownedSession = nil; } }