图片缓存框架绕不开的一个问题就是编码和解码,SDWebImage 也不例外。在系统的了解 SDWebImage 是如何编码和解码图片以前,要先理解什么是编码和解码。html
咱们从互联网下载的数据,本质上都是二进制数据。二进制数据跟图片之间是没有必然联系的,要想把二进制数据以图片的形式表现出来,这个过程就叫解码。git
当咱们想把数据存到本地的时候,直接保存图片是不现实的。因而咱们须要把图片转化为二进制数据,这个过程就叫编码。github
虽然咱们可使用 UIImage 的类方法直接把二进制数据转化为图片,可是这个过程是在主线程执行的,这样会形成线程阻塞。因而 SDWebImage 实现了本身的 Coder 来进行图片的编解码,而接下来咱们就来了解一下它是怎么实现的。数组
SDWebImage 中包含不少种不一样类型的 coder,靠 SDImageCoder 协议统一接口。声明以下缓存
@protocol SDImageCoder <NSObject>
@required
#pragma mark - Decoding
/** Returns YES if this coder can decode some data. Otherwise, the data should be passed to another coder. @param data The image data so we can look at it @return YES if this coder can decode the data, NO otherwise */
- (BOOL)canDecodeFromData:(nullable NSData *)data;
/** Decode the image data to image. @note This protocol may supports decode animated image frames. You can use `+[SDImageCoderHelper animatedImageWithFrames:]` to produce an animated image with frames. @param data The image data to be decoded @param options A dictionary containing any decoding options. Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for image. Pass @{SDImageCoderDecodeFirstFrameOnly: @(YES)} to decode the first frame only. @return The decoded image from data */
- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data
options:(nullable SDImageCoderOptions *)options;
#pragma mark - Encoding
/** Returns YES if this coder can encode some image. Otherwise, it should be passed to another coder. For custom coder which introduce new image format, you'd better define a new `SDImageFormat` using like this. If you're creating public coder plugin for new image format, also update `https://github.com/rs/SDWebImage/wiki/Coder-Plugin-List` to avoid same value been defined twice. * @code static const SDImageFormat SDImageFormatHEIF = 10; * @endcode @param format The image format @return YES if this coder can encode the image, NO otherwise */
- (BOOL)canEncodeToFormat:(SDImageFormat)format NS_SWIFT_NAME(canEncode(to:));
/** Encode the image to image data. @note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames. @param image The image to be encoded @param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible @param options A dictionary containing any encoding options. Pass @{SDImageCoderEncodeCompressionQuality: @(1)} to specify compression quality. @return The encoded image data */
- (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image
format:(SDImageFormat)format
options:(nullable SDImageCoderOptions *)options;
@end
复制代码
全部的 Coder 类都要遵照这个协议,经过实现的不一样来实现不一样的解码能力。app
SDProgressiveImageCoder 是提供渐进式图片编解码的协议,声明以下:框架
/** This is the image coder protocol to provide custom progressive image decoding. These methods are all required to implement. @note Pay attention that these methods are not called from main queue. */
@protocol SDProgressiveImageCoder <SDImageCoder>
@required
/** Returns YES if this coder can incremental decode some data. Otherwise, it should be passed to another coder. @param data The image data so we can look at it @return YES if this coder can decode the data, NO otherwise */
- (BOOL)canIncrementalDecodeFromData:(nullable NSData *)data;
/** Because incremental decoding need to keep the decoded context, we will alloc a new instance with the same class for each download operation to avoid conflicts This init method should not return nil @param options A dictionary containing any progressive decoding options (instance-level). Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for progressive animated image (each frames should use the same scale). @return A new instance to do incremental decoding for the specify image format */
- (nonnull instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options;
/** Update the incremental decoding when new image data available @param data The image data has been downloaded so far @param finished Whether the download has finished */
- (void)updateIncrementalData:(nullable NSData *)data finished:(BOOL)finished;
/** Incremental decode the current image data to image. @note Due to the performance issue for progressive decoding and the integration for image view. This method may only return the first frame image even if the image data is animated image. If you want progressive animated image decoding, conform to `SDAnimatedImageCoder` protocol as well and use `animatedImageFrameAtIndex:` instead. @param options A dictionary containing any progressive decoding options. Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for progressive image @return The decoded image from current data */
- (nullable UIImage *)incrementalDecodedImageWithOptions:(nullable SDImageCoderOptions *)options;
@end
复制代码
若是但愿 Coder 支持这样的能力,须要遵照 SDProgressiveImageCoder 协议,并实现其所有方法。less
SDAnimatedImageProvider 是一个专门为了动图提供的协议类,包含了一系列方法来控制动图的渲染。dom
/** This is the animated image protocol to provide the basic function for animated image rendering. It's adopted by `SDAnimatedImage` and `SDAnimatedImageCoder` */
@protocol SDAnimatedImageProvider <NSObject>
@required
/** The original animated image data for current image. If current image is not an animated format, return nil. We may use this method to grab back the original image data if need, such as NSCoding or compare. @return The animated image data */
@property (nonatomic, copy, readonly, nullable) NSData *animatedImageData;
/** Total animated frame count. If the frame count is less than 1, then the methods below will be ignored. @return Total animated frame count. */
@property (nonatomic, assign, readonly) NSUInteger animatedImageFrameCount;
/** Animation loop count, 0 means infinite looping. @return Animation loop count */
@property (nonatomic, assign, readonly) NSUInteger animatedImageLoopCount;
/** Returns the frame image from a specified index. @note The index maybe randomly if one image was set to different imageViews, keep it re-entrant. (It's not recommend to store the images into array because it's memory consuming) @param index Frame index (zero based). @return Frame's image */
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
/** Returns the frames's duration from a specified index. @note The index maybe randomly if one image was set to different imageViews, keep it re-entrant. (It's recommend to store the durations into array because it's not memory-consuming) @param index Frame index (zero based). @return Frame's duration */
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@end
复制代码
SDAnimatedImageCoder 是一个组合协议,它组合了 SDImageCoder 和 SDAnimatedImageProvider 两个协议,同时提供了初始化方法用于生成自定义图片类。ide
/** This is the animated image coder protocol for custom animated image class like `SDAnimatedImage`. Through it inherit from `SDImageCoder`. We currentlly only use the method `canDecodeFromData:` to detect the proper coder for specify animated image format. */
@protocol SDAnimatedImageCoder <SDImageCoder, SDAnimatedImageProvider>
@required
/** Because animated image coder should keep the original data, we will alloc a new instance with the same class for the specify animated image data The init method should return nil if it can't decode the specify animated image data to produce any frame. After the instance created, we may call methods in `SDAnimatedImageProvider` to produce animated image frame. @param data The animated image data to be decode @param options A dictionary containing any animated decoding options (instance-level). Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for animated image (each frames should use the same scale). @return A new instance to do animated decoding for specify image data */
- (nullable instancetype)initWithAnimatedImageData:(nullable NSData *)data options:(nullable SDImageCoderOptions *)options;
复制代码
SDImageCoderOption 是一系列的编译选项,用来供使用者决定编解码的选项:
// These options are for image decoding
/** A Boolean value indicating whether to decode the first frame only for animated image during decoding. (NSNumber). If not provide, decode animated image if need. @note works for `SDImageCoder`. */
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFirstFrameOnly;
/** A CGFloat value which is greater than or equal to 1.0. This value specify the image scale factor for decoding. If not provide, use 1.0. (NSNumber) @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleFactor;
// These options are for image encoding
/** A Boolean value indicating whether to encode the first frame only for animated image during encoding. (NSNumber). If not provide, encode animated image if need. @note works for `SDImageCoder`. */
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeFirstFrameOnly;
/** A double value between 0.0-1.0 indicating the encode compression quality to produce the image data. 1.0 resulting in no compression and 0.0 resulting in the maximum compression possible. If not provide, use 1.0. (NSNumber) @note works for `SDImageCoder` */
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressionQuality;
/** A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext) This option is ignored for all built-in coders and take no effect. But this may be useful for some custom coders, because some business logic may dependent on things other than image or image data inforamtion only. See `SDWebImageContext` for more detailed information. */
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext;
复制代码
SDImageCodersManager 是 Coder 的管理类,声明以下:
@interface SDImageCodersManager : NSObject <SDImageCoder>
/** Returns the global shared coders manager instance. */
@property (nonatomic, class, readonly, nonnull) SDImageCodersManager *sharedManager;
/** All coders in coders manager. The coders array is a priority queue, which means the later added coder will have the highest priority */
@property (nonatomic, copy, nullable) NSArray<id<SDImageCoder>> *coders;
/** Add a new coder to the end of coders array. Which has the highest priority. @param coder coder */
- (void)addCoder:(nonnull id<SDImageCoder>)coder;
/** Remove a coder in the coders array. @param coder coder */
- (void)removeCoder:(nonnull id<SDImageCoder>)coder;
@end
复制代码
它遵照了 SDImageCoder 协议,同时提供了对 coder 的管理功能,并经过单例的方式来使用这个类。好比当你想解码一个图片时,可使用以下方法:
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
if (!data) {
return nil;
}
UIImage *image;
NSArray<id<SDImageCoder>> *coders = self.coders;
for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canDecodeFromData:data]) {
image = [coder decodedImageWithData:data options:options];
break;
}
}
return image;
}
复制代码
就是很简单的从数组中依次取出 coder,而后看是否可以解码图片,可以就解码。
SDImageCoderHelper 是一个工具类,用于提供一些方便方法供外界使用:
SDImageFrame 是一个模型类,用于将动图的每一帧保存起来,声明以下:
/** This class is used for creating animated images via `animatedImageWithFrames` in `SDImageCoderHelper`. @note If you need to specify animated images loop count, use `sd_imageLoopCount` property in `UIImage+Metadata.h`. */
@interface SDImageFrame : NSObject
/** The image of current frame. You should not set an animated image. */
@property (nonatomic, strong, readonly, nonnull) UIImage *image;
/** The duration of current frame to be displayed. The number is seconds but not milliseconds. You should not set this to zero. */
@property (nonatomic, readonly, assign) NSTimeInterval duration;
/** Create a frame instance with specify image and duration @param image current frame's image @param duration current frame's duration @return frame instance */
+ (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration;
@end
复制代码
+ (NSArray<SDImageFrame *> *)framesFromAnimatedImage:(UIImage *)animatedImage {
if (!animatedImage) {
return nil;
}
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
NSUInteger frameCount = 0;
#if SD_UIKIT || SD_WATCH
NSArray<UIImage *> *animatedImages = animatedImage.images; // 获取动图全部图片帧
frameCount = animatedImages.count; // 获取帧数
if (frameCount == 0) {
return nil;
}
NSTimeInterval avgDuration = animatedImage.duration / frameCount; // 总时间长度除以帧数,看每一帧的时间是多少
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) { // 枚举全部帧,并计算时间新建 SDImageFrame 模型
// ignore first
if (idx == 0) {
return;
}
if ([image isEqual:previousImage]) {
repeatCount++;
} else {
SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
[frames addObject:frame];
repeatCount = 1;
index++;
}
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)];
float frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
[frames addObject:frame];
}
}
#endif
return frames;
}
复制代码
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
NSUInteger frameCount = frames.count;
if (frameCount == 0) {
return nil;
}
UIImage *animatedImage;
#if SD_UIKIT || SD_WATCH
NSUInteger durations[frameCount]; // 建立 druation 数组
for (size_t i = 0; i < frameCount; i++) { // 存储每一帧的时间
durations[i] = frames[i].duration * 1000;
}
NSUInteger const gcd = gcdArray(frameCount, durations); // 获取帧数和 durations 获取最小公约数
__block NSUInteger totalDuration = 0;
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
[frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) { // 遍历每个 frame 并组装数据
UIImage *image = frame.image;
NSUInteger duration = frame.duration * 1000;
totalDuration += duration;
NSUInteger repeatCount; // 获取同一张图片的重复次数
if (gcd) {
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];
float 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;
}
复制代码
展转相除法取公约数:
#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
复制代码
获取设备颜色空间,在 iOS 中经过 GCD 只建立一次。
+ (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;
}
复制代码
这两个方法的目的是对 CGImage 作一次解码,以免图片在被设置给 imageView 的时候再次进行解码
+ (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); // 建立 context
if (!context) {
return NULL;
}
// Apply transform
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight)); // 根据方向和宽高建立 transform
CGContextConcatCTM(context, transform); // 调整 CTM
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height // 绘制
CGImageRef newImageRef = CGBitmapContextCreateImage(context); // 获取 CGImage
CGContextRelease(context); // 释放上下文
return newImageRef;
}
复制代码
处理 transform 的方法以下所示:
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;
}
复制代码
与上面的方法大同小异:
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
#if SD_MAC
return image;
#else
if (![self shouldDecodeImage:image]) {
return image;
}
CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
if (!imageRef) {
return image;
}
UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(imageRef);
decodedImage.sd_isDecoded = YES;
decodedImage.sd_imageFormat = image.sd_imageFormat;
return decodedImage;
#endif
}
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Avoid extra decode
if (image.sd_isDecoded) {
return NO;
}
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// do not decode animated images
if (image.images != nil) {
return NO;
}
return YES;
}
复制代码
+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
#if SD_MAC
return image;
#else
if (![self shouldDecodeImage:image]) {
return image;
}
if (![self shouldScaleDownImage:image limitBytes:bytes]) {
return [self decodedImageWithImage:image];
}
CGFloat destTotalPixels;
CGFloat tileTotalPixels;
if (bytes > 0) {
destTotalPixels = bytes / kBytesPerPixel;
tileTotalPixels = destTotalPixels / 3;
} else {
destTotalPixels = kDestTotalPixels;
tileTotalPixels = kTileTotalPixels;
}
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); // 获取图片高度
CGFloat 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.
CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels); // 计算降采样的 scale
CGSize destResolution = CGSizeZero; // 生成对应的目标宽高
destResolution.width = (int)(sourceResolution.width * imageScale);
destResolution.height = (int)(sourceResolution.height * imageScale);
// device color space
CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB]; // 获取设备颜色空间
BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef]; // 查看是否具备颜色通道
// iOS display alpha info (BGRA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipFirst
// to create bitmap graphics contexts without alpha info.
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (destContext == NULL) {
return image;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh); // 设置质量
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
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); // 取出 CGImage
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
destImage.sd_isDecoded = YES;
destImage.sd_imageFormat = image.sd_imageFormat;
return destImage;
}
#endif
}
+ (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; // 计算目标像素数
if (bytes > 0) {
destTotalPixels = bytes / kBytesPerPixel;
} else {
destTotalPixels = kDestTotalPixels;
}
if (destTotalPixels <= kPixelsPerMB) { // 若是目标像素数过小,返回 NO
// Too small to scale down
return NO;
}
float imageScale = destTotalPixels / sourceTotalPixels; // 计算下降的 scale
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
复制代码
#if SD_UIKIT || SD_WATCH
// Convert an EXIF image orientation to an iOS one.
+ (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.
+ (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
复制代码
SDImageGraphics 提供了跨平台快捷获取图形上下文的函数:
CGContextRef SDGraphicsGetCurrentContext(void) {
#if SD_UIKIT || SD_WATCH
return UIGraphicsGetCurrentContext();
#else
return NSGraphicsContext.currentContext.CGContext;
#endif
}
复制代码
void SDGraphicsBeginImageContext(CGSize size) {
#if SD_UIKIT || SD_WATCH
UIGraphicsBeginImageContext(size);
#else
SDGraphicsBeginImageContextWithOptions(size, NO, 1.0);
#endif
}
void SDGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) {
#if SD_UIKIT || SD_WATCH
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
#else
CGContextRef context = SDCGContextCreateBitmapContext(size, opaque, scale);
if (!context) {
return;
}
NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:context flipped:NO];
objc_setAssociatedObject(graphicsContext, &kNSGraphicsContextScaleFactorKey, @(scale), OBJC_ASSOCIATION_RETAIN);
CGContextRelease(context);
[NSGraphicsContext saveGraphicsState];
NSGraphicsContext.currentContext = graphicsContext;
#endif
}
#if SD_MAC
static void *kNSGraphicsContextScaleFactorKey;
static CGContextRef SDCGContextCreateBitmapContext(CGSize size, BOOL opaque, CGFloat scale) {
if (scale == 0) {
// Match `UIGraphicsBeginImageContextWithOptions`, reset to the scale factor of the device’s main screen if scale is 0.
scale = [NSScreen mainScreen].backingScaleFactor;
}
size_t width = ceil(size.width * scale);
size_t height = ceil(size.height * scale);
if (width < 1 || height < 1) return NULL;
//pre-multiplied BGRA for non-opaque, BGRX for opaque, 8-bits per component, as Apple's doc
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
CGImageAlphaInfo alphaInfo = kCGBitmapByteOrder32Host | (opaque ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaPremultipliedFirst);
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, space, kCGBitmapByteOrderDefault | alphaInfo);
CGColorSpaceRelease(space);
if (!context) {
return NULL;
}
CGContextScaleCTM(context, scale, scale);
return context;
}
#endif
复制代码
void SDGraphicsEndImageContext(void) {
#if SD_UIKIT || SD_WATCH
UIGraphicsEndImageContext();
#else
[NSGraphicsContext restoreGraphicsState];
#endif
}
复制代码
UIImage * SDGraphicsGetImageFromCurrentImageContext(void) {
#if SD_UIKIT || SD_WATCH
return UIGraphicsGetImageFromCurrentImageContext();
#else
NSGraphicsContext *context = NSGraphicsContext.currentContext;
CGContextRef contextRef = context.CGContext;
if (!contextRef) {
return nil;
}
CGImageRef imageRef = CGBitmapContextCreateImage(contextRef);
if (!imageRef) {
return nil;
}
CGFloat scale = 0;
NSNumber *scaleFactor = objc_getAssociatedObject(context, &kNSGraphicsContextScaleFactorKey);
if ([scaleFactor isKindOfClass:[NSNumber class]]) {
scale = scaleFactor.doubleValue;
}
if (!scale) {
// reset to the scale factor of the device’s main screen if scale is 0.
scale = [NSScreen mainScreen].backingScaleFactor;
}
NSImage *image = [[NSImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
CGImageRelease(imageRef);
return image;
#endif
}
复制代码