阅读版本 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;
}
参考
许多的博主,感谢!