SDImageCoderHelper 源码分析
前提知识补充,看我这篇文章
动图拆分为每一帧,和将所有帧组装成动图
// 构建动画图片,根据传进来的帧组进行构建
+ (UIImage *)animatedImageWithFrames:(NSArray *)frames {
NSUInteger frameCount = frames.count;
if (frameCount == 0) {
return nil;
}
UIImage *animatedImage;
#if SD_UIKIT || SD_WATCH
// 创建一个 NSUInteger 数组,不能为负数,里面存放着每一帧的播放时间
NSUInteger durations[frameCount];
for (size_t i = 0; i < frameCount; i++) {
// 将每一帧的播放时间变成ms放到数组中去
durations[i] = frames[i].duration * 1000;
}
// 求出最大公约数
NSUInteger const gcd = gcdArray(frameCount, durations);
// 总播放时间
__block NSUInteger totalDuration = 0;
NSMutableArray *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
[frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
// 取出每一帧的详细信息
UIImage *image = frame.image;
NSUInteger duration = frame.duration * 1000;
totalDuration += duration;
NSUInteger repeatCount;
if (gcd) {
// 如果最大公约数大于0,那么重复次数就是时间/最大公约数
repeatCount = duration / gcd;
} else {
// 否则播放一次
repeatCount = 1;
}
// 根据重复次数将图片放入数组中
for (size_t i = 0; i < repeatCount; ++i) {
[animatedImages addObject:image];
}
}];
// 创建动图
animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];
#else
NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:SDImageFormatGIF];
// Create an image destination. GIF does not support EXIF image orientation
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL);
if (!imageDestination) {
// Handle failure.
return nil;
}
for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
SDImageFrame *frame = frames[i];
NSTimeInterval frameDuration = frame.duration;
CGImageRef frameImageRef = frame.image.CGImage;
NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
}
}
// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {
// Handle failure.
CFRelease(imageDestination);
return nil;
}
CFRelease(imageDestination);
CGFloat scale = MAX(frames.firstObject.image.scale, 1);
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
imageRep.size = size;
animatedImage = [[NSImage alloc] initWithSize:size];
[animatedImage addRepresentation:imageRep];
#endif
return animatedImage;
}
// 从动图中拆分出每一帧
+ (NSArray *)framesFromAnimatedImage:(UIImage *)animatedImage {
if (!animatedImage) {
return nil;
}
NSMutableArray *frames = [NSMutableArray array];
NSUInteger frameCount = 0;
#if SD_UIKIT || SD_WATCH
// 取出动图中所有帧
NSArray *animatedImages = animatedImage.images;
frameCount = animatedImages.count;
if (frameCount == 0) {
return nil;
}
// 平均时间
NSTimeInterval avgDuration = animatedImage.duration / frameCount;
// 如果没有时间,默认 100ms
if (avgDuration == 0) {
avgDuration = 0.1; // if it's a animated image but no duration, set it to default 100ms (this do not have that 10ms limit like GIF or WebP to allow custom coder provide the limit)
}
// 记录不同的帧额数量
__block NSUInteger index = 0;
// 每一帧的重复次数
__block NSUInteger repeatCount = 1;
// 记录前一张图片,每次和前一张图片比较
__block UIImage *previousImage = animatedImages.firstObject;
[animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
// ignore first
// 忽略第一张
if (idx == 0) {
return;
}
// 如果当前和前一张是一张图片
if ([image isEqual:previousImage]) {
// 那么这张图片额重复次数就+1
repeatCount++;
} else {
// 如果不一样,那么就直接构建 SDImageFrame 对象
// 将前一帧图片放进去,然后更新重复次数
SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
[frames addObject:frame];
repeatCount = 1;
index++;
}
// 然后更改 pre 的值
previousImage = image;
// last one
// 如果是最后一张,直接加进去
// 最后一张和之前有可能是重复也有可能没有重复,但一定没有加进去过
if (idx == frameCount - 1) {
SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
[frames addObject:frame];
}
}];
#else
NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
NSBitmapImageRep *bitmapImageRep;
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
bitmapImageRep = (NSBitmapImageRep *)imageRep;
}
if (!bitmapImageRep) {
return nil;
}
frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue];
if (frameCount == 0) {
return nil;
}
CGFloat scale = animatedImage.scale;
for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
// NSBitmapImageRep need to manually change frame. "Good taste" API
[bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
NSTimeInterval frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] doubleValue];
NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
[frames addObject:frame];
}
}
#endif
// 返回帧组
return frames;
}
获取颜色空间和透明度
// 获取颜色空间
// 通过案例,提高性能,因为获取一次就够了
+ (CGColorSpaceRef)colorSpaceGetDeviceRGB {
#if SD_MAC
CGColorSpaceRef screenColorSpace = NSScreen.mainScreen.colorSpace.CGColorSpace;
if (screenColorSpace) {
return screenColorSpace;
}
#endif
static CGColorSpaceRef colorSpace;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
#if SD_UIKIT
if (@available(iOS 9.0, tvOS 9.0, *)) {
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
} else {
colorSpace = CGColorSpaceCreateDeviceRGB();
}
#else
colorSpace = CGColorSpaceCreateDeviceRGB();
#endif
});
return colorSpace;
}
// 获取透明度
+ (BOOL)CGImageContainsAlpha:(CGImageRef)cgImage {
if (!cgImage) {
return NO;
}
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
return hasAlpha;
}
解压缩图片
// 解码图片
+ (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);
// 将新图片的属性 copy 一份
SDImageCopyAssociatedObject(image, decodedImage);
// 标记为已解码
decodedImage.sd_isDecoded = YES;
return decodedImage;
}
// 解压缩图片
// https://code.imerc.cc/2017/07/09/ios-image-decode/
// https://www.jianshu.com/p/cc670b4ec97e
+ (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);
// 通过draw先将图片画出来
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;
}
调整图片大小
// 调整原始图片的大小
+ (CGImageRef)CGImageCreateScaled:(CGImageRef)cgImage size:(CGSize)size {
if (!cgImage) {
return NULL;
}
// 原图片的宽高
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
// 如果原来的宽高和要调节的宽高一样,就直接返回
if (width == size.width && height == size.height) {
CGImageRetain(cgImage);
return cgImage;
}
// 创建 input output buffer
__block vImage_Buffer input_buffer = {}, output_buffer = {};
// 此处是个骚操作
@onExit {
if (input_buffer.data) free(input_buffer.data);
if (output_buffer.data) free(output_buffer.data);
};
// 获取透明度
BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
// iOS display alpha info (BGRA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
vImage_CGImageFormat format = (vImage_CGImageFormat) {
.bitsPerComponent = 8,
.bitsPerPixel = 32,
.colorSpace = NULL,
.bitmapInfo = bitmapInfo,
.version = 0,
.decode = NULL,
.renderingIntent = kCGRenderingIntentDefault,
};
// 先根据原始数据生成原始inputbuffer
vImage_Error a_ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags);
if (a_ret != kvImageNoError) return NULL;
// 创建 output buffer
output_buffer.width = MAX(size.width, 0);
output_buffer.height = MAX(size.height, 0);
// 这里是什么意思呢?
output_buffer.rowBytes = SDByteAlign(output_buffer.width * 4, 64);
output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height);
if (!output_buffer.data) return NULL;
vImage_Error ret = vImageScale_ARGB8888(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling);
if (ret != kvImageNoError) return NULL;
// 根据output buffer 创建image
CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &format, NULL, NULL, kvImageNoFlags, &ret);
if (ret != kvImageNoError) {
CGImageRelease(outputImage);
return NULL;
}
return outputImage;
}
其他工具方法
// 默认缩放大小限制
+ (NSUInteger)defaultScaleDownLimitBytes {
return kDestImageLimitBytes;
}
// 设置缩放的最大限制
+ (void)setDefaultScaleDownLimitBytes:(NSUInteger)defaultScaleDownLimitBytes {
if (defaultScaleDownLimitBytes < kBytesPerMB) {
return;
}
kDestImageLimitBytes = defaultScaleDownLimitBytes;
}
#if SD_UIKIT || SD_WATCH
// Convert an EXIF image orientation to an iOS one.
/**
将EXIF图像方向转换为iOS版本的方向
*/
+ (UIImageOrientation)imageOrientationFromEXIFOrientation:(CGImagePropertyOrientation)exifOrientation {
UIImageOrientation imageOrientation = UIImageOrientationUp;
switch (exifOrientation) {
case kCGImagePropertyOrientationUp:
imageOrientation = UIImageOrientationUp;
break;
case kCGImagePropertyOrientationDown:
imageOrientation = UIImageOrientationDown;
break;
case kCGImagePropertyOrientationLeft:
imageOrientation = UIImageOrientationLeft;
break;
case kCGImagePropertyOrientationRight:
imageOrientation = UIImageOrientationRight;
break;
case kCGImagePropertyOrientationUpMirrored:
imageOrientation = UIImageOrientationUpMirrored;
break;
case kCGImagePropertyOrientationDownMirrored:
imageOrientation = UIImageOrientationDownMirrored;
break;
case kCGImagePropertyOrientationLeftMirrored:
imageOrientation = UIImageOrientationLeftMirrored;
break;
case kCGImagePropertyOrientationRightMirrored:
imageOrientation = UIImageOrientationRightMirrored;
break;
default:
break;
}
return imageOrientation;
}
// Convert an iOS orientation to an EXIF image orientation.
/**
将iOS版本的方向转换为EXIF图像方向
*/
+ (CGImagePropertyOrientation)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation {
CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
switch (imageOrientation) {
case UIImageOrientationUp:
exifOrientation = kCGImagePropertyOrientationUp;
break;
case UIImageOrientationDown:
exifOrientation = kCGImagePropertyOrientationDown;
break;
case UIImageOrientationLeft:
exifOrientation = kCGImagePropertyOrientationLeft;
break;
case UIImageOrientationRight:
exifOrientation = kCGImagePropertyOrientationRight;
break;
case UIImageOrientationUpMirrored:
exifOrientation = kCGImagePropertyOrientationUpMirrored;
break;
case UIImageOrientationDownMirrored:
exifOrientation = kCGImagePropertyOrientationDownMirrored;
break;
case UIImageOrientationLeftMirrored:
exifOrientation = kCGImagePropertyOrientationLeftMirrored;
break;
case UIImageOrientationRightMirrored:
exifOrientation = kCGImagePropertyOrientationRightMirrored;
break;
default:
break;
}
return exifOrientation;
}
#endif
#pragma mark - Helper Function
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// Avoid extra decode
// 已经被解码
if (image.sd_isDecoded) {
return NO;
}
// do not decode animated images
// 动图
if (image.sd_isAnimated) {
return NO;
}
// do not decode vector images
// 矢量图
if (image.sd_isVector) {
return NO;
}
return YES;
}
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
BOOL shouldScaleDown = YES;
// 原始分辨率大小
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
if (sourceTotalPixels <= 0) {
return NO;
}
// 目标总像素
CGFloat destTotalPixels;
// 如果设置最大限制为0,那么就用默认的60M
if (bytes == 0) {
bytes = kDestImageLimitBytes;
}
// 目标期望大小,总的像素数
destTotalPixels = bytes / kBytesPerPixel;
// 1M 里面的像素总数都比目标大小大或者等于,就代表设置的目标大小太小了,没法缩放
if (destTotalPixels <= kPixelsPerMB) {
// Too small to scale down
return NO;
}
// 目标大小和原来总像素大小做一个比例
float imageScale = destTotalPixels / sourceTotalPixels;
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
static inline CGAffineTransform SDCGContextTransformFromOrientation(CGImagePropertyOrientation orientation, CGSize size) {
// Inspiration from @libfeihu
// We need to calculate the proper transformation to make the image upright.
// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
CGAffineTransform transform = CGAffineTransformIdentity;
switch (orientation) {
case kCGImagePropertyOrientationDown:
case kCGImagePropertyOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, size.width, size.height);
transform = CGAffineTransformRotate(transform, M_PI);
break;
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationLeftMirrored:
transform = CGAffineTransformTranslate(transform, size.width, 0);
transform = CGAffineTransformRotate(transform, M_PI_2);
break;
case kCGImagePropertyOrientationRight:
case kCGImagePropertyOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, 0, size.height);
transform = CGAffineTransformRotate(transform, -M_PI_2);
break;
case kCGImagePropertyOrientationUp:
case kCGImagePropertyOrientationUpMirrored:
break;
}
switch (orientation) {
case kCGImagePropertyOrientationUpMirrored:
case kCGImagePropertyOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, size.width, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
case kCGImagePropertyOrientationLeftMirrored:
case kCGImagePropertyOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, size.height, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
case kCGImagePropertyOrientationUp:
case kCGImagePropertyOrientationDown:
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationRight:
break;
}
return transform;
}
#if SD_UIKIT || SD_WATCH
// 求最大公约数
static NSUInteger gcd(NSUInteger a, NSUInteger b) {
NSUInteger c;
while (a != 0) {
c = a;
a = b % a;
b = c;
}
return b;
}
static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) {
if (count == 0) {
return 0;
}
NSUInteger result = values[0];
for (size_t i = 1; i < count; ++i) {
result = gcd(values[i], result);
}
return result;
}
#endif