Merge pull request #3368 from dreampiggy/optimize_force_decode_solution
Added `SDImageCoder.defaultDecodeSolution` to control the force decode solution. Automatic by default
This commit is contained in:
commit
d0f3c39335
|
@ -55,6 +55,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
|
||||||
SDImageCacheMatchAnimatedImageClass = 1 << 7,
|
SDImageCacheMatchAnimatedImageClass = 1 << 7,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A token associated with each cache query. Can be used to cancel a cache query
|
||||||
|
*/
|
||||||
@interface SDImageCacheToken : NSObject <SDWebImageOperation>
|
@interface SDImageCacheToken : NSObject <SDWebImageOperation>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,6 +10,15 @@
|
||||||
#import "SDWebImageCompat.h"
|
#import "SDWebImageCompat.h"
|
||||||
#import "SDImageFrame.h"
|
#import "SDImageFrame.h"
|
||||||
|
|
||||||
|
typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) {
|
||||||
|
/// automatically choose the solution based on image format, hardware, OS version. This keep balance for compatibility and performance. Default after SDWebImage 5.13.0
|
||||||
|
SDImageCoderDecodeSolutionAutomatic,
|
||||||
|
/// always use CoreGraphics to draw on bitmap context and trigger decode. Best compatibility. Default before SDWebImage 5.13.0
|
||||||
|
SDImageCoderDecodeSolutionCoreGraphics,
|
||||||
|
/// available on iOS/tvOS 15+, use UIKit's new CGImageDecompressor/CMPhoto to decode. Best performance. If failed, will fallback to CoreGraphics as well
|
||||||
|
SDImageCoderDecodeSolutionUIKit
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Provide some common helper methods for building the image decoder/encoder.
|
Provide some common helper methods for building the image decoder/encoder.
|
||||||
*/
|
*/
|
||||||
|
@ -111,6 +120,12 @@
|
||||||
*/
|
*/
|
||||||
+ (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes;
|
+ (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
Control the default force decode solution. Available solutions in `SDImageCoderDecodeSolution`.
|
||||||
|
@note Defaults to `SDImageCoderDecodeSolutionAutomatic`, which prefers to use UIKit for JPEG/HEIF, and fallback on CoreGraphics. If you want control on your hand, set the other solution.
|
||||||
|
*/
|
||||||
|
@property (class, readwrite) SDImageCoderDecodeSolution defaultDecodeSolution;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Control the default limit bytes to scale down largest images.
|
Control the default limit bytes to scale down largest images.
|
||||||
This value must be larger than 4 Bytes (at least 1x1 pixel). Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS.
|
This value must be larger than 4 Bytes (at least 1x1 pixel). Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS.
|
||||||
|
|
|
@ -16,12 +16,64 @@
|
||||||
#import "UIImage+Metadata.h"
|
#import "UIImage+Metadata.h"
|
||||||
#import "SDInternalMacros.h"
|
#import "SDInternalMacros.h"
|
||||||
#import "SDGraphicsImageRenderer.h"
|
#import "SDGraphicsImageRenderer.h"
|
||||||
|
#import "SDInternalMacros.h"
|
||||||
#import <Accelerate/Accelerate.h>
|
#import <Accelerate/Accelerate.h>
|
||||||
|
|
||||||
static inline size_t SDByteAlign(size_t size, size_t alignment) {
|
static inline size_t SDByteAlign(size_t size, size_t alignment) {
|
||||||
return ((size + (alignment - 1)) / alignment) * alignment;
|
return ((size + (alignment - 1)) / alignment) * alignment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if SD_UIKIT
|
||||||
|
static inline UIImage *SDImageDecodeUIKit(UIImage *image) {
|
||||||
|
// See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay
|
||||||
|
// Need CGImage-based
|
||||||
|
if (@available(iOS 15, tvOS 15, *)) {
|
||||||
|
UIImage *decodedImage = [image imageByPreparingForDisplay];
|
||||||
|
if (decodedImage) {
|
||||||
|
SDImageCopyAssociatedObject(image, decodedImage);
|
||||||
|
decodedImage.sd_isDecoded = YES;
|
||||||
|
return decodedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline UIImage *SDImageDecodeAndScaleDownUIKit(UIImage *image, CGSize destResolution) {
|
||||||
|
// See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize
|
||||||
|
// Need CGImage-based
|
||||||
|
if (@available(iOS 15, tvOS 15, *)) {
|
||||||
|
// Calculate thumbnail point size
|
||||||
|
CGFloat scale = image.scale ?: 1;
|
||||||
|
CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale);
|
||||||
|
UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize];
|
||||||
|
if (decodedImage) {
|
||||||
|
SDImageCopyAssociatedObject(image, decodedImage);
|
||||||
|
decodedImage.sd_isDecoded = YES;
|
||||||
|
return decodedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline BOOL SDImageSupportsHardwareHEVCDecoder(void) {
|
||||||
|
static dispatch_once_t onceToken;
|
||||||
|
static BOOL supportsHardware = NO;
|
||||||
|
dispatch_once(&onceToken, ^{
|
||||||
|
SEL DeviceInfoSelector = SD_SEL_SPI(deviceInfoForKey:);
|
||||||
|
NSString *HEVCDecoder8bitSupported = @"N8lZxRgC7lfdRS3dRLn+Ag";
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||||
|
if ([UIDevice.currentDevice respondsToSelector:DeviceInfoSelector] && [UIDevice.currentDevice performSelector:DeviceInfoSelector withObject:HEVCDecoder8bitSupported]) {
|
||||||
|
supportsHardware = YES;
|
||||||
|
}
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
});
|
||||||
|
return supportsHardware;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionAutomatic;
|
||||||
|
|
||||||
static const size_t kBytesPerPixel = 4;
|
static const size_t kBytesPerPixel = 4;
|
||||||
static const size_t kBitsPerComponent = 8;
|
static const size_t kBitsPerComponent = 8;
|
||||||
|
|
||||||
|
@ -367,16 +419,23 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UIImage *decodedImage;
|
||||||
#if SD_UIKIT
|
#if SD_UIKIT
|
||||||
// See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay
|
SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
|
||||||
// Need CGImage-based
|
if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
|
||||||
if (@available(iOS 15, tvOS 15, *)) {
|
// See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
|
||||||
UIImage *decodedImage = [image imageByPreparingForDisplay];
|
SDImageFormat format = image.sd_imageFormat;
|
||||||
if (decodedImage) {
|
if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
|
||||||
SDImageCopyAssociatedObject(image, decodedImage);
|
decodedImage = SDImageDecodeUIKit(image);
|
||||||
decodedImage.sd_isDecoded = YES;
|
} else if (format == SDImageFormatJPEG) {
|
||||||
return decodedImage;
|
decodedImage = SDImageDecodeUIKit(image);
|
||||||
}
|
}
|
||||||
|
} else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
|
||||||
|
// Arbitrarily call CMPhoto
|
||||||
|
decodedImage = SDImageDecodeUIKit(image);
|
||||||
|
}
|
||||||
|
if (decodedImage) {
|
||||||
|
return decodedImage;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -392,7 +451,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
format.scale = image.scale;
|
format.scale = image.scale;
|
||||||
CGSize imageSize = image.size;
|
CGSize imageSize = image.size;
|
||||||
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
|
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
|
||||||
UIImage *decodedImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
|
decodedImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
|
||||||
[image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
|
[image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
|
||||||
}];
|
}];
|
||||||
SDImageCopyAssociatedObject(image, decodedImage);
|
SDImageCopyAssociatedObject(image, decodedImage);
|
||||||
|
@ -432,24 +491,26 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale));
|
destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale));
|
||||||
destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));
|
destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));
|
||||||
|
|
||||||
|
UIImage *decodedImage;
|
||||||
#if SD_UIKIT
|
#if SD_UIKIT
|
||||||
// See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize
|
SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
|
||||||
// Need CGImage-based
|
if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
|
||||||
if (@available(iOS 15, tvOS 15, *)) {
|
// See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
|
||||||
// Calculate thumbnail point size
|
SDImageFormat format = image.sd_imageFormat;
|
||||||
CGFloat scale = image.scale ?: 1;
|
if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
|
||||||
CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale);
|
decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
|
||||||
UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize];
|
} else if (format == SDImageFormatJPEG) {
|
||||||
if (decodedImage) {
|
decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
|
||||||
SDImageCopyAssociatedObject(image, decodedImage);
|
|
||||||
decodedImage.sd_isDecoded = YES;
|
|
||||||
return decodedImage;
|
|
||||||
}
|
}
|
||||||
|
} else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
|
||||||
|
// Arbitrarily call CMPhoto
|
||||||
|
decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
|
||||||
|
}
|
||||||
|
if (decodedImage) {
|
||||||
|
return decodedImage;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
CGContextRef destContext = NULL;
|
|
||||||
|
|
||||||
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
|
// 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];
|
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
|
||||||
@autoreleasepool {
|
@autoreleasepool {
|
||||||
|
@ -469,13 +530,13 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
// RGB888
|
// RGB888
|
||||||
bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
|
bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
|
||||||
}
|
}
|
||||||
destContext = CGBitmapContextCreate(NULL,
|
CGContextRef destContext = CGBitmapContextCreate(NULL,
|
||||||
destResolution.width,
|
destResolution.width,
|
||||||
destResolution.height,
|
destResolution.height,
|
||||||
kBitsPerComponent,
|
kBitsPerComponent,
|
||||||
0,
|
0,
|
||||||
colorspaceRef,
|
colorspaceRef,
|
||||||
bitmapInfo);
|
bitmapInfo);
|
||||||
|
|
||||||
if (destContext == NULL) {
|
if (destContext == NULL) {
|
||||||
return image;
|
return image;
|
||||||
|
@ -543,20 +604,25 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
#if SD_MAC
|
#if SD_MAC
|
||||||
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
|
decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
|
||||||
#else
|
#else
|
||||||
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
|
decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
|
||||||
#endif
|
#endif
|
||||||
CGImageRelease(destImageRef);
|
CGImageRelease(destImageRef);
|
||||||
if (destImage == nil) {
|
SDImageCopyAssociatedObject(image, decodedImage);
|
||||||
return image;
|
decodedImage.sd_isDecoded = YES;
|
||||||
}
|
return decodedImage;
|
||||||
SDImageCopyAssociatedObject(image, destImage);
|
|
||||||
destImage.sd_isDecoded = YES;
|
|
||||||
return destImage;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (SDImageCoderDecodeSolution)defaultDecodeSolution {
|
||||||
|
return kDefaultDecodeSolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)setDefaultDecodeSolution:(SDImageCoderDecodeSolution)defaultDecodeSolution {
|
||||||
|
kDefaultDecodeSolution = defaultDecodeSolution;
|
||||||
|
}
|
||||||
|
|
||||||
+ (NSUInteger)defaultScaleDownLimitBytes {
|
+ (NSUInteger)defaultScaleDownLimitBytes {
|
||||||
return kDestImageLimitBytes;
|
return kDestImageLimitBytes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,30 @@
|
||||||
#import "UIImage+ForceDecode.h"
|
#import "UIImage+ForceDecode.h"
|
||||||
#import "SDImageCoderHelper.h"
|
#import "SDImageCoderHelper.h"
|
||||||
#import "objc/runtime.h"
|
#import "objc/runtime.h"
|
||||||
|
#import "NSImage+Compatibility.h"
|
||||||
|
|
||||||
@implementation UIImage (ForceDecode)
|
@implementation UIImage (ForceDecode)
|
||||||
|
|
||||||
- (BOOL)sd_isDecoded {
|
- (BOOL)sd_isDecoded {
|
||||||
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded));
|
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded));
|
||||||
return value.boolValue;
|
if (value != nil) {
|
||||||
|
return value.boolValue;
|
||||||
|
} else {
|
||||||
|
// Assume only CGImage based can use lazy decoding
|
||||||
|
CGImageRef cgImage = self.CGImage;
|
||||||
|
if (cgImage) {
|
||||||
|
CFStringRef uttype = CGImageGetUTType(self.CGImage);
|
||||||
|
if (uttype) {
|
||||||
|
// Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier`
|
||||||
|
return NO;
|
||||||
|
} else {
|
||||||
|
// Thumbnail or CGBitmapContext drawn image
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Assume others as non-decoded
|
||||||
|
return NO;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)setSd_isDecoded:(BOOL)sd_isDecoded {
|
- (void)setSd_isDecoded:(BOOL)sd_isDecoded {
|
||||||
|
|
|
@ -166,10 +166,8 @@
|
||||||
return imageFormat;
|
return imageFormat;
|
||||||
}
|
}
|
||||||
// Check CGImage's UTType, may return nil for non-Image/IO based image
|
// Check CGImage's UTType, may return nil for non-Image/IO based image
|
||||||
if (@available(iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0, *)) {
|
CFStringRef uttype = CGImageGetUTType(self.CGImage);
|
||||||
CFStringRef uttype = CGImageGetUTType(self.CGImage);
|
imageFormat = [NSData sd_imageFormatFromUTType:uttype];
|
||||||
imageFormat = [NSData sd_imageFormatFromUTType:uttype];
|
|
||||||
}
|
|
||||||
return imageFormat;
|
return imageFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue