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:
DreamPiggy 2022-07-16 19:33:46 +08:00 committed by GitHub
commit d0f3c39335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 142 additions and 42 deletions

View File

@ -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>
/** /**

View File

@ -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.

View File

@ -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;
} }

View File

@ -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 {

View File

@ -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;
} }