From ff6b3b9bb59613d1acde21258d45bfe83bd44fab Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 11 Jul 2022 15:50:55 +0800 Subject: [PATCH 1/3] Change only ImageIO decoded CGImage should enter the `Force Decode` logic Others coder, like WebP, should not use this approach --- SDWebImage/Core/UIImage+ForceDecode.m | 19 ++++++++++++++++++- SDWebImage/Core/UIImage+Metadata.m | 6 ++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/SDWebImage/Core/UIImage+ForceDecode.m b/SDWebImage/Core/UIImage+ForceDecode.m index 1b20bbd7..599036b7 100644 --- a/SDWebImage/Core/UIImage+ForceDecode.m +++ b/SDWebImage/Core/UIImage+ForceDecode.m @@ -14,7 +14,24 @@ - (BOOL)sd_isDecoded { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded)); - return value.boolValue; + if (value) { + 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 { diff --git a/SDWebImage/Core/UIImage+Metadata.m b/SDWebImage/Core/UIImage+Metadata.m index 5a8002c4..a526f9b9 100644 --- a/SDWebImage/Core/UIImage+Metadata.m +++ b/SDWebImage/Core/UIImage+Metadata.m @@ -166,10 +166,8 @@ return imageFormat; } // 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); - imageFormat = [NSData sd_imageFormatFromUTType:uttype]; - } + CFStringRef uttype = CGImageGetUTType(self.CGImage); + imageFormat = [NSData sd_imageFormatFromUTType:uttype]; return imageFormat; } From 213a8b8def68d594338d34c067d9d47e221f14ef Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Mon, 11 Jul 2022 16:28:10 +0800 Subject: [PATCH 2/3] Added `SDImageCoder.defaultDecodeSolution` to control the force decode solution, defaults to CoreGraphics (the same as 5.12) For user who want new UIKit solution, you can opt-in to change the `defaultDecodeSolution` case --- SDWebImage/Core/SDImageCache.h | 3 ++ SDWebImage/Core/SDImageCoderHelper.h | 13 ++++++++ SDWebImage/Core/SDImageCoderHelper.m | 44 ++++++++++++++++++--------- SDWebImage/Core/UIImage+ForceDecode.m | 2 +- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index 96b54e72..9a75580c 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -55,6 +55,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { SDImageCacheMatchAnimatedImageClass = 1 << 7, }; +/** + * A token associated with each cache query. Can be used to cancel a cache query + */ @interface SDImageCacheToken : NSObject /** diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index a7be63e4..b808f63b 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -10,6 +10,13 @@ #import "SDWebImageCompat.h" #import "SDImageFrame.h" +typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) { + /// always use the CoreGraphics to redraw on bitmap context, best compatibility + SDImageCoderDecodeSolutionCoreGraphics, + /// available on iOS/tvOS 15+, use UIKit's new CGImageDecompressor, best performance. If failed, will fallback to CoreGraphics as well + SDImageCoderDecodeSolutionUIKit +}; + /** Provide some common helper methods for building the image decoder/encoder. */ @@ -111,6 +118,12 @@ */ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes; +/** Control the default force decode solution. Available solutions in `SDImageCoderDecodeSolution`. + @note Defaults to `SDImageCoderDecodeSolutionCoreGraphics` + @note You can opt-in to use `SDImageCoderDecodeSolutionUIKit`, which will prefer to use UIKit on iOS 15, and fallback to CoreGraphics instead. + */ +@property (class, readwrite) SDImageCoderDecodeSolution defaultDecodeSolution; + /** 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. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 763a9a49..0a17adca 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -22,6 +22,8 @@ static inline size_t SDByteAlign(size_t size, size_t alignment) { return ((size + (alignment - 1)) / alignment) * alignment; } +static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionCoreGraphics; + static const size_t kBytesPerPixel = 4; static const size_t kBitsPerComponent = 8; @@ -370,12 +372,14 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over #if SD_UIKIT // 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; + if (self.defaultDecodeSolution == SDImageCoderDecodeSolutionUIKit) { + if (@available(iOS 15, tvOS 15, *)) { + UIImage *decodedImage = [image imageByPreparingForDisplay]; + if (decodedImage) { + SDImageCopyAssociatedObject(image, decodedImage); + decodedImage.sd_isDecoded = YES; + return decodedImage; + } } } #endif @@ -435,15 +439,17 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over #if SD_UIKIT // 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; + if (self.defaultDecodeSolution == SDImageCoderDecodeSolutionUIKit) { + 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; + } } } #endif @@ -557,6 +563,14 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over } } ++ (SDImageCoderDecodeSolution)defaultDecodeSolution { + return kDefaultDecodeSolution; +} + ++ (void)setDefaultDecodeSolution:(SDImageCoderDecodeSolution)defaultDecodeSolution { + kDefaultDecodeSolution = defaultDecodeSolution; +} + + (NSUInteger)defaultScaleDownLimitBytes { return kDestImageLimitBytes; } diff --git a/SDWebImage/Core/UIImage+ForceDecode.m b/SDWebImage/Core/UIImage+ForceDecode.m index 599036b7..d4fbff66 100644 --- a/SDWebImage/Core/UIImage+ForceDecode.m +++ b/SDWebImage/Core/UIImage+ForceDecode.m @@ -14,7 +14,7 @@ - (BOOL)sd_isDecoded { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded)); - if (value) { + if (value != nil) { return value.boolValue; } else { // Assume only CGImage based can use lazy decoding From a88e6694229b627ef32c2cfdee956a8999eb1159 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 16 Jul 2022 16:27:30 +0800 Subject: [PATCH 3/3] Added SDImageCoderDecodeSolutionAutomatic, which check image format as well This avoid the unwanted CMPhoto log --- SDWebImage/Core/SDImageCoderHelper.h | 12 ++- SDWebImage/Core/SDImageCoderHelper.m | 136 ++++++++++++++++++-------- SDWebImage/Core/UIImage+ForceDecode.m | 1 + 3 files changed, 102 insertions(+), 47 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index b808f63b..28e12401 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -11,9 +11,11 @@ #import "SDImageFrame.h" typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) { - /// always use the CoreGraphics to redraw on bitmap context, best compatibility + /// 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, best performance. If failed, will fallback to CoreGraphics as well + /// available on iOS/tvOS 15+, use UIKit's new CGImageDecompressor/CMPhoto to decode. Best performance. If failed, will fallback to CoreGraphics as well SDImageCoderDecodeSolutionUIKit }; @@ -118,9 +120,9 @@ typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) { */ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes; -/** Control the default force decode solution. Available solutions in `SDImageCoderDecodeSolution`. - @note Defaults to `SDImageCoderDecodeSolutionCoreGraphics` - @note You can opt-in to use `SDImageCoderDecodeSolutionUIKit`, which will prefer to use UIKit on iOS 15, and fallback to CoreGraphics instead. +/** + 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; diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 0a17adca..da30b65a 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -16,13 +16,63 @@ #import "UIImage+Metadata.h" #import "SDInternalMacros.h" #import "SDGraphicsImageRenderer.h" +#import "SDInternalMacros.h" #import static inline size_t SDByteAlign(size_t size, size_t alignment) { return ((size + (alignment - 1)) / alignment) * alignment; } -static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionCoreGraphics; +#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 kBitsPerComponent = 8; @@ -369,18 +419,23 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return image; } + UIImage *decodedImage; #if SD_UIKIT - // See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay - // Need CGImage-based - if (self.defaultDecodeSolution == SDImageCoderDecodeSolutionUIKit) { - if (@available(iOS 15, tvOS 15, *)) { - UIImage *decodedImage = [image imageByPreparingForDisplay]; - if (decodedImage) { - SDImageCopyAssociatedObject(image, decodedImage); - decodedImage.sd_isDecoded = YES; - return decodedImage; - } + SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution; + if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) { + // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :( + SDImageFormat format = image.sd_imageFormat; + if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) { + decodedImage = SDImageDecodeUIKit(image); + } else if (format == SDImageFormatJPEG) { + decodedImage = SDImageDecodeUIKit(image); } + } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) { + // Arbitrarily call CMPhoto + decodedImage = SDImageDecodeUIKit(image); + } + if (decodedImage) { + return decodedImage; } #endif @@ -396,7 +451,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over format.scale = image.scale; CGSize imageSize = image.size; 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)]; }]; SDImageCopyAssociatedObject(image, decodedImage); @@ -436,26 +491,26 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale)); destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale)); + UIImage *decodedImage; #if SD_UIKIT - // See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize - // Need CGImage-based - if (self.defaultDecodeSolution == SDImageCoderDecodeSolutionUIKit) { - 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; - } + SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution; + if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) { + // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :( + SDImageFormat format = image.sd_imageFormat; + if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) { + decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); + } else if (format == SDImageFormatJPEG) { + decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); } + } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) { + // Arbitrarily call CMPhoto + decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); + } + if (decodedImage) { + return decodedImage; } #endif - CGContextRef destContext = NULL; - // 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 { @@ -475,13 +530,13 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over // RGB888 bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; } - destContext = CGBitmapContextCreate(NULL, - destResolution.width, - destResolution.height, - kBitsPerComponent, - 0, - colorspaceRef, - bitmapInfo); + CGContextRef destContext = CGBitmapContextCreate(NULL, + destResolution.width, + destResolution.height, + kBitsPerComponent, + 0, + colorspaceRef, + bitmapInfo); if (destContext == NULL) { return image; @@ -549,17 +604,14 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return image; } #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 - UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; + decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; #endif CGImageRelease(destImageRef); - if (destImage == nil) { - return image; - } - SDImageCopyAssociatedObject(image, destImage); - destImage.sd_isDecoded = YES; - return destImage; + SDImageCopyAssociatedObject(image, decodedImage); + decodedImage.sd_isDecoded = YES; + return decodedImage; } } diff --git a/SDWebImage/Core/UIImage+ForceDecode.m b/SDWebImage/Core/UIImage+ForceDecode.m index d4fbff66..9fc72588 100644 --- a/SDWebImage/Core/UIImage+ForceDecode.m +++ b/SDWebImage/Core/UIImage+ForceDecode.m @@ -9,6 +9,7 @@ #import "UIImage+ForceDecode.h" #import "SDImageCoderHelper.h" #import "objc/runtime.h" +#import "NSImage+Compatibility.h" @implementation UIImage (ForceDecode)