SDWebImage源码 知识点

1,106 阅读6分钟

阅读版本 version 5.6.1

知识点

图片解码

由NSData 二进制数据转换成uiimage后,在显示到屏幕之前,系统默认会在主线程立即进行图片的解码工作,这一过程就是把图片解码成可供控件直接使用的位图。当在主线程调用了大量的imageNamed:方法后,就会产生卡顿。为了解决这个问题需要提前解码,SD通过定义SDWebImageAvoidDecodeImage可以避免提前解码,解码会造成内存增加,需要使用时根据是内存峰值和主线程任务量进行选择

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (![self shouldDecodeImage:image]) {
        return image;
    }
    
    CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
    if (!imageRef) {
        return image;
    }
#if SD_MAC
    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
#endif
    CGImageRelease(imageRef);
    SDImageCopyAssociatedObject(image, decodedImage);
    decodedImage.sd_isDecoded = YES;
    return decodedImage;
}
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
    return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
}

+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
    if (!cgImage) {
        return NULL;
    }
    size_t width = CGImageGetWidth(cgImage);
    size_t height = CGImageGetHeight(cgImage);
    if (width == 0 || height == 0) return NULL;
    size_t newWidth;
    size_t newHeight;
    switch (orientation) {
        case kCGImagePropertyOrientationLeft:
        case kCGImagePropertyOrientationLeftMirrored:
        case kCGImagePropertyOrientationRight:
        case kCGImagePropertyOrientationRightMirrored: {
            // These orientation should swap width & height
            newWidth = height;
            newHeight = width;
        }
            break;
        default: {
            newWidth = width;
            newHeight = height;
        }
            break;
    }
    
    BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
    // iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
    // Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
    // But since our build-in coders use this bitmapInfo, this can have a little performance benefit
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
    CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
    if (!context) {
        return NULL;
    }
    
    // Apply transform
    CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
    CGContextConcatCTM(context, transform);
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
    CGImageRef newImageRef = CGBitmapContextCreateImage(context);
    CGContextRelease(context);
    
    return newImageRef;
}

调用的核心api

CGContextRef CGBitmapContextCreate(void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, uint32_t bitmapInfo);
void CGContextDrawImage(CGContextRef c, CGRect rect, CGImageRef image);
CGBitmapContextCreateImage(context);

传入宽、高、aplha信息返回一个位图上下文,然后利用这个上下文绘图。 创建的Image是context的一个拷贝,因此注意在下次绘制的时候释放先前生成的图像CGImageRelease(ref)

自定义NSOperation, SDWebImageDownloaderOperation类是继承自NSOperation类。它没有简单的实现main方法,而是采用更加灵活的start方法,以便自己管理下载的状态。下面是个模板,

  • 合成executing\finished,因为在NSOperation中这两个属性是只读的
  • 重写start方法,先判断是否被取消了。取消将finished设置为YES 返回,否则finished 设置为NO,executing 设置为YES。
  • 重写cancel,先判断isFinished。[super cancel]会设置isCancelled,设置executing 为NO,finished为YES 。
  • 设置是否允许并发
  • 重写setFinished:\setExecuting:以添加发送kvo通知,像NSOperationQueue会用到这个通知。
@implementation SDWebImageTestDownloadOperation

@synthesize executing = _executing;
@synthesize finished = _finished;

- (void)start {
	if (self.isCancelled) {
	self.finished = YES;

	return;
}
    self.finished = NO;
    self.executing = YES;
    // Do nothing but keep running
}

/**
不会强制任务停止,设置内部标记。标记cancel会使得处于操作队列但没执行的operation更快从队列移除
*/
- (void)cancel {
    if (self.isFinished) return;
    [super cancel];
    
    if (self.isExecuting) self.executing = NO;
    if (!self.isFinished) self.finished = YES;
}

- (BOOL)isConcurrent {
    return YES;
}

- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (instancetype)initWithRequest:(NSURLRequest *)request inSession:(NSURLSession *)session options:(SDWebImageDownloaderOptions)options {
    return [self initWithRequest:request inSession:session options:options context:nil];
}

- (instancetype)initWithRequest:(NSURLRequest *)request inSession:(NSURLSession *)session options:(SDWebImageDownloaderOptions)options context:(SDWebImageContext *)context {
    self = [super init];
    if (self) {
        self.request = request;
        self.completedBlocks = [NSMutableArray array];
    }
    return self;
}

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    if (completedBlock) {
        [self.completedBlocks addObject:completedBlock];
    }
    return NSStringFromClass([self class]);
}

- (BOOL)cancel:(id)token {
    [self cancel];
    return YES;
}

- (void)done {
-  self.finished = YES; 
-  self.executing = NO;
- }

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { 
	if (self.isFinished) return;
	[self done]
}

SDWebImageContinueInBackground 在程序切换到后台申请额外的时间

  • 经测试这个时间大概是30秒
if ([self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
                ...
                }
            }];
        }

图像格式的判断

从NSData中取出第1字节,比起读取文件后缀来更准确。

+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    // File signatures table: http://www.garykessler.net/library/file_sigs.html
    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 (data.length >= 12) {
                //RIFF....WEBP
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
                if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                    return SDImageFormatWebP;
                }
            }
            break;
        }
        case 0x00: {
            if (data.length >= 12) {
                //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) {
                    return SDImageFormatHEIC;
                }
                //....ftypmif1 ....ftypmsf1
                if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
                    return SDImageFormatHEIF;
                }
            }
            break;
        }
        case 0x25: {
            if (data.length >= 4) {
                //%PDF
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 3)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"PDF"]) {
                    return SDImageFormatPDF;
                }
            }
        }
        case 0x3C: {
            if (data.length > 100) {
                // Check end with SVG tag
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(data.length - 100, 100)] encoding:NSASCIIStringEncoding];
                if ([testString containsString:kSVGTagEnd]) {
                    return SDImageFormatSVG;
                }
            }
        }
    }
    return SDImageFormatUndefined;
}

将磁盘图片的i/o放在子线程

@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;
_ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
   dispatch_async(self.ioQueue, ^{
        [self.diskCache removeExpiredData];//耗时任务
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });

解压gif的data为图片数组

//获取data资源器,这个可以直接操作图片data
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
//获取图片数量 size_t 类似于无符号int
    size_t count = CGImageSourceGetCount(source);
for (size_t i = 0; i < count; i++) {
            CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
            duration += [self imageDurationAtIndex:i source:source];
            //scale:图片缩放因子 默认1  orientation:图片绘制方向 默认网上
            [imageArray addObject:[UIImage imageWithCGImage:imageRef scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];
            CGImageRelease(imageRef);
        }
animationImage = [UIImage animatedImageWithImages:imageArray duration:duration];

枚举文件目录 获取文件目录、移除文件、获取目录下文件大小

NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
count = fileEnumerator.allObjects.count;
[self.fileManager removeItemAtURL:fileURL error:nil];

NSUInteger size = 0;
    NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtPath:self.diskCachePath];
    for (NSString *fileName in fileEnumerator) {
        NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
        NSDictionary<NSString *, id> *attrs = [self.fileManager attributesOfItemAtPath:filePath error:nil];
        size += [attrs fileSize];//NSFileSize
    }
    return size;

写二进制文件到磁盘

- (void)setData:(nullable NSData *)data forKey:(nonnull NSString *)key {
    NSParameterAssert(data);
    NSParameterAssert(key);
    if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    NSString *filename = SDDiskCacheFileNameForKey(key);
    NSString *cachePathForKey = [self.diskCachePath stringByAppendingPathComponent:filename];
    
    // transform to NSUrl
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    [data writeToURL:fileURL options:NSDataWritingAtomic error:nil];
}

监测文件是否存在

    NSString *filePath = [self cachePathForKey:key];
    BOOL exists = [self.fileManager fileExistsAtPath:filePath];

利用关联对象在分类中添加属性

objc_setAssociatedObject(self, @selector(sd_memoryCost), @(sd_memoryCost), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_memoryCost));

判断是否主队列,而不是主线程,在ios中主队列和主线程的关系是,是主队列一定是主线程,反之不成立。另外有一些特殊情况会依赖主队列。

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

反例 同步执行block会优先选择在当前线程执行,因此在main thread 通过自定义串行队列执行block还是会在主线程执行。

UIImage 添加 extendData

存储额外信息,例如图片缩放比例、url rich link

对于大量临时操作,将其放入autoreleasepool以保证内存能及时释放。

获取gif图帧间隔

+ (float)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
    float frameDuration = 0.1f;
    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source,index,nil);
    NSDictionary *frameProperties = (__bridge NSDictionary*)cfFrameProperties;
    NSDictionary *gifProperties = frameProperties[(NSString*)kCGImagePropertyGIFDictionary];

    NSNumber *delayTimeUnclampedProp = gifProperties[(NSString*)kCGImagePropertyGIFUnclampedDelayTime];
    if(delayTimeUnclampedProp) {
        frameDuration = [delayTimeUnclampedProp floatValue];
    } else {

        NSNumber *delayTimeProp = gifProperties[(NSString*)kCGImagePropertyGIFDelayTime];
        if(delayTimeProp) {
            frameDuration = [delayTimeProp floatValue];
        }
    }

    // Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
    // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
    // a duration of <= 10 ms. See <rdar://problem/7689300> and <http://webkit.org/b/36082>
    // for more information.

    if (frameDuration < 0.011f)
        frameDuration = 0.100f;

    CFRelease(cfFrameProperties);
    return frameDuration;
}

创建一个base64data 测试数据

 NSData *PNGData = [NSData dataWithContentsOfFile:[self testPNGPath]];
    NSData *base64PNGData = [PNGData base64EncodedDataWithOptions:0];
    expect(base64PNGData).notTo.beNil();
    NSURL *base64FileURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"TestBase64.png"]];
    [base64PNGData writeToURL:base64FileURL atomically:YES];

内置解压缩zip

#import <compression.h>
if (@available(iOS 13, macOS 10.15, tvOS 13, *)) {
            return [data decompressedDataUsingAlgorithm:NSDataCompressionAlgorithmZlib error:nil];
        } else if (@available (iOS 9, macOS 10.11, tvOS 9, *)) {
            NSMutableData *decodedData = [NSMutableData dataWithLength:10 * data.length];
            compression_decode_buffer((uint8_t *)decodedData.bytes, decodedData.length, data.bytes, data.length, nil, COMPRESSION_ZLIB);
            return [decodedData copy];
        } else {
            // iOS 8 does not have built-in Zlib support, just mock the data
            return base64PNGData;
        }

参考

许多的博主,感谢!