1 概述
SDWebImage
使用了很多工具类来对图片的处理。比如获取图片类型、图片放大缩小、GIF图片处理、图片解压缩处理等。接下来我就要分析下面这几个工具类的实现。
2 NSData+ImageContentType分析
这个类提供了一个类方法sd_imageFormatForImageData
。通过这个方法传入图片的NSData数据,然后返回图片类型。图片类型通过SDImageFormat
来定义。
/**
不同图片类型的枚举
- SDImageFormatUndefined: 未知
- SDImageFormatJPEG: JPG
- SDImageFormatPNG: PNG
- SDImageFormatGIF: GIF
- SDImageFormatTIFF: TIFF
- SDImageFormatWebP: WEBP
*/
typedef NS_ENUM(NSInteger, SDImageFormat) {
SDImageFormatUndefined = -1,
SDImageFormatJPEG = 0,
SDImageFormatPNG,
SDImageFormatGIF,
SDImageFormatTIFF,
SDImageFormatWebP
};
/**
根据图片NSData获取图片的类型
@param data NSData数据
@return 图片数据类型
*/
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
if (!data) {
return SDImageFormatUndefined;
}
uint8_t c;
//获取图片数据的第一个字节数据
[data getBytes:&c length:1];
//根据字母的ASC码比较
switch (c) {
case 0xFF:
return SDImageFormatJPEG;
case 0x89:
return SDImageFormatPNG;
case 0x47:
return SDImageFormatGIF;
case 0x49:
case 0x4D:
return SDImageFormatTIFF;
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;
}
}
return SDImageFormatUndefined;
}
3 SDWebImageCompat分析
SDWebImageCompat
就提供一个全局方法SDScaledImageForKey
。这个方法根据原始图片绘制一张放大或者缩小的图片。
/**
给定一张图片,通过scale属性返回一个放大的图片。
@param key 图片名称
@param image 资源图片
@return 处理以后的图片
*/
inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
//异常处理
if (!image) {
return nil;
}
#if SD_MAC
return image;
#elif SD_UIKIT || SD_WATCH
//如果是动态图片,比如GIF图片,则迭代处理
if ((image.images).count > 0) {
NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];
//迭代处理每一张图片
for (UIImage *tempImage in image.images) {
[scaledImages addObject:SDScaledImageForKey(key, tempImage)];
}
//把处理结束的图片再合成一张动态图片
return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
}
else {//非动态图片
#if SD_WATCH
if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {
#elif SD_UIKIT
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
#endif
CGFloat scale = 1;
// “@2x.png”的长度为7,所以此处添加了这个判断,很巧妙
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
//返回对应分辨率下面的图片
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;
}
return image;
}
#endif
}
4 UIImage+MultiFormat分类分析
UIImage+MultiFormat
分类实现了NSData与UIImage对象之间的相互转换。并且是根据图片类型做转换。比如GIF的UIImage转换为GIF格式的NSData。
并且还有UIImage的Orientation和alpha的处理。
/**
根据image的data数据。生成对应的image对象
@param data 图片的数据
@return image对象
*/
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
if (!data) {
return nil;
}
UIImage *image;
//获取data的图片类型,png,gif,jpg
SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
if (imageFormat == SDImageFormatGIF) {
//gif处理:返回一张只包含数据第一张image 的gif图片
image = [UIImage sd_animatedGIFWithData:data];
}
#ifdef SD_WEBP
else if (imageFormat == SDImageFormatWebP)
{
image = [UIImage sd_imageWithWebPData:data];
}
#endif
else {
image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
//获取方向
UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
//如果不是向上的,还需要再次生成图片
if (orientation != UIImageOrientationUp) {
image = [UIImage imageWithCGImage:image.CGImage
scale:image.scale
orientation:orientation];
}
#endif
}
return image;
}
#if SD_UIKIT || SD_WATCH
/**
根据图片数据获取图片的方向
@param imageData 图片数据
@return 方向
*/
+(UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData {
//默认是向上的
UIImageOrientation result = UIImageOrientationUp;
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
if (imageSource) {
//获取图片的属性列表
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
CFTypeRef val;
int exifOrientation;
//获取图片方向
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) {
CFNumberGetValue(val, kCFNumberIntType, &exifOrientation);
result = [self sd_exifOrientationToiOSOrientation:exifOrientation];
} // else - if it's not set it remains at up
CFRelease((CFTypeRef) properties);
} else {
//NSLog(@"NO PROPERTIES, FAIL");
}
CFRelease(imageSource);
}
return result;
}
/**
根据不同的值返回不同的图片方向
@param exifOrientation 输入值
@return 图片的方向
*/
+ (UIImageOrientation) sd_exifOrientationToiOSOrientation:(int)exifOrientation {
UIImageOrientation orientation = UIImageOrientationUp;
switch (exifOrientation) {
case 1:
orientation = UIImageOrientationUp;
break;
case 3:
orientation = UIImageOrientationDown;
break;
case 8:
orientation = UIImageOrientationLeft;
break;
case 6:
orientation = UIImageOrientationRight;
break;
case 2:
orientation = UIImageOrientationUpMirrored;
break;
case 4:
orientation = UIImageOrientationDownMirrored;
break;
case 5:
orientation = UIImageOrientationLeftMirrored;
break;
case 7:
orientation = UIImageOrientationRightMirrored;
break;
default:
break;
}
return orientation;
}
#endif
- (nullable NSData *)sd_imageData {
return [self sd_imageDataAsFormat:SDImageFormatUndefined];
}
/**
根据指定的图片类型,把image对象转换为对应格式的data
@param imageFormat 指定的image格式
@return 返回data对象
*/
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
NSData *imageData = nil;
if (self) {
#if SD_UIKIT || SD_WATCH
int alphaInfo = CGImageGetAlphaInfo(self.CGImage);
//是否有透明度
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
//只有png图片有alpha属性
BOOL usePNG = hasAlpha;
// the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel
//是否是PNG类型的图片
if (imageFormat != SDImageFormatUndefined) {
usePNG = (imageFormat == SDImageFormatPNG);
}
//根据不同的图片类型获取到对应的图片data
if (usePNG) {
imageData = UIImagePNGRepresentation(self);
} else {
imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0);
}
#else
NSBitmapImageFileType imageFileType = NSJPEGFileType;
if (imageFormat == SDImageFormatGIF) {
imageFileType = NSGIFFileType;
} else if (imageFormat == SDImageFormatPNG) {
imageFileType = NSPNGFileType;
}
imageData = [NSBitmapImageRep representationOfImageRepsInArray:self.representations
usingType:imageFileType
properties:@{}];
#endif
}
return imageData;
}
5 UIImage+GIF分类分析
UIImage+GIF
实现了对GIF图片的NSData的处理。并且处理方法就是取出GIF图片的第一张UIImage来显示。如果真的要显示动态图片的话,我们需要使用FLAnimatedImageView
来显示。
/**
根据gif图片的data生成对应的gif的UIImage对象。而且只会取GIF图片的第一张UIImage。
@param data gif图片的data对象
@return 生成的image对象。这里只获取gif图片的第一张图像,如果要实现gif完整图像,使用FLAnimatedImageView
*/
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
//获取GIF图片包含的UIImage数量
size_t count = CGImageSourceGetCount(source);
UIImage *staticImage;
//如果只有一张UIImage
if (count <= 1) {
staticImage = [[UIImage alloc] initWithData:data];
} else {
#if SD_WATCH
CGFloat scale = 1;
scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
CGFloat scale = 1;
scale = [UIScreen mainScreen].scale;
#endif
//获取第一张UIImage对象
CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
//获取gif图片的第一张图片
UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
//用第一张图片生成一个新的gif图片
staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#elif SD_MAC
staticImage = [[UIImage alloc] initWithCGImage:CGImage size:NSZeroSize];
#endif
CGImageRelease(CGImage);
}
CFRelease(source);
return staticImage;
}
/**
判断一张图片是不是GIF图片
@return bool值
*/
- (BOOL)isGIF {
return (self.images != nil);
}
6 SDWebImageDecoder分析
通过这个类实现图片的解压缩操作。对于太大的图片,先按照一定比例缩小图片然后再解压缩。
#if SD_UIKIT || SD_WATCH
//每个像素占用的字节数
static const size_t kBytesPerPixel = 4;
//色彩空间占用的字节数
static const size_t kBitsPerComponent = 8;
/**
解压缩图片
@param image UIImage对象
@return 返回解压缩以后的图片
*/
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
//图片是否能够加压缩
if (![UIImage shouldDecodeImage:image]) {
return image;
}
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
/*
*解压缩操作放入一个自动释放池里面。一遍自动释放所有的变量。
*/
@autoreleasepool{
CGImageRef imageRef = image.CGImage;
//获取图片的色彩空间
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
//宽度和高度
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
//图片占用的字节数
size_t bytesPerRow = kBytesPerPixel * width;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
//创建一个绘制图片的上下文
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
//绘制一个和图片大小一样的图片
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
//创建一个么有alpha通道的图片
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
//得到解压缩以后的图片
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
}
}
/*
*定义一张图片可以占用的最大空间
*/
static const CGFloat kDestImageSizeMB = 60.0f;
static const CGFloat kSourceImageTileSizeMB = 20.0f;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
//1MB可以存储多少像素
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
//如果像素小于这个值,则不解压缩
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
/**
如果原始图片占用的空间太大。则按照一定的比例解压缩。从而不让解压缩以后的图片占用的空间太大。
@param image UIImage对象
@return 返回处理结束的UIImage对象
*/
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
//图片是否支持解压缩
if (![UIImage shouldDecodeImage:image]) {
return image;
}
//图片不需要处理。直接解压缩
if (![UIImage shouldScaleDownImage:image]) {
return [UIImage decodedImageWithImage:image];
}
CGContextRef destContext;
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool {
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
//获取原始图片的宽度和高度
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
//获取原始图片的总像素
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
//根据一定的比例设置目标图片的宽度和高度
float imageScale = kDestTotalPixels / sourceTotalPixels;
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width*imageScale);
destResolution.height = (int)(sourceResolution.height*imageScale);
// current color space
//获取原始图片的像素空间。默认是RGB
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
//每一行像素占用的内存空间大小
size_t bytesPerRow = kBytesPerPixel * destResolution.width;
// Allocate enough pixel data to hold the output image.
//目标图片占用的总内存空间大小。一行占用内存空间大小*高度
void* destBitmapData = malloc( bytesPerRow * destResolution.height );
if (destBitmapData == NULL) {
return image;
}
//根据各种设置创建一个上下文环境
destContext = CGBitmapContextCreate(destBitmapData,
destResolution.width,
destResolution.height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (destContext == NULL) {
free(destBitmapData);
return image;
}
//设置目标图片的质量
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
//生成处理结束以后的图片
UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
return destImage;
}
}
/**
imge是否能够加压缩
@param image 图片
@return 能否解压缩
*/
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// do not decode animated images
//如果是动态图片不处理
if (image.images != nil) {
return NO;
}
CGImageRef imageRef = image.CGImage;
//获取image的alpha通道。通过通道获取图片数据
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// do not decode images with alpha
//如果有alpha通道值,则不处理
if (anyAlpha) {
return NO;
}
return YES;
}
/**
是否需要减少原始图片的大小
@param image UIImage对象
@return 是否支持scale
*/
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
BOOL shouldScaleDown = YES;
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
//图片总共像素
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
//如果图片的总像素大于一定比例,则需要做简化处理
float imageScale = kDestTotalPixels / sourceTotalPixels;
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
/**
获取图片的色彩空间
@param imageRef 图片
@return 色彩空间
*/
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace) {
colorspaceRef = CGColorSpaceCreateDeviceRGB();
CFAutorelease(colorspaceRef);
}
return colorspaceRef;
}
#elif SD_MAC
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
return image;
}
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
return image;
}
7 总结
下面是几个分类工具的使用。
/**
根据图片数据获取图片类型
*/
- (IBAction)getImageType:(id)sender {
NSData *imageData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"rock.gif" ofType:nil]];
SDImageFormat formate = [NSData sd_imageFormatForImageData:imageData];
NSString *message = [NSString stringWithFormat:@"%d",formate];
showMessage(message,self);
}
/**
获取一张图片对应的两倍或者三倍屏幕对应的图片
*/
- (IBAction)getScaleImage:(id)sender {
UIImage *sourceImage = [UIImage imageNamed:@"2.png"];
UIImage *dis2ScaleImage = SDScaledImageForKey(@"dist@2x.png", sourceImage);
UIImage *dis3ScaleImage = SDScaledImageForKey(@"dist@3x.png", sourceImage);
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
//NSLog(@"document:%@",documentPath);
NSString *path1 = [documentPath stringByAppendingPathComponent:@"dist.png"];
[UIImagePNGRepresentation(sourceImage) writeToFile:path1 atomically:YES];
NSString *path2 = [documentPath stringByAppendingPathComponent:@"dist@2x.png"];
[UIImagePNGRepresentation(dis2ScaleImage) writeToFile:path2 atomically:YES];
NSString *path3 = [documentPath stringByAppendingPathComponent:@"dist@3x.png"];
[UIImagePNGRepresentation(dis3ScaleImage) writeToFile:path3 atomically:YES];
}
/**
解压缩图片
@param sender 解压缩图片
*/
- (IBAction)unZipImage:(id)sender {
UIImage *sourceImage = [UIImage imageNamed:@"2.png"];
UIImage *distImage = [UIImage decodedAndScaledDownImageWithImage:sourceImage];
NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path1 = [documentPath stringByAppendingPathComponent:@"distImage.png"];
[UIImagePNGRepresentation(distImage) writeToFile:path1 atomically:YES];
NSString *path2 = [documentPath stringByAppendingPathComponent:@"sourceImage.png"];
[UIImagePNGRepresentation(sourceImage) writeToFile:path2 atomically:YES];
}