Added `SDImageCoderDecodeUseLazyDecoding` to control whether to use lazy-decoding for ImageIO or not
Defaults to NO for animated image coder but YES for static image coder to match current behavior This also use another way to solve iOS 15+'s CGImageGetImageSource issue
This commit is contained in:
parent
ddcf347c7d
commit
abc06f020a
|
@ -61,6 +61,19 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFileExtens
|
|||
*/
|
||||
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeTypeIdentifierHint;
|
||||
|
||||
/**
|
||||
A BOOL value indicating whether to use lazy-decoding. Defaults to NO on animated image coder, but defaults to YES on static image coder.
|
||||
CGImageRef, this image object typically support lazy-decoding, via the `CGDataProviderCreateDirectAccess` or `CGDataProviderCreateSequential`
|
||||
Which allows you to provide a lazy-called callback to access bitmap buffer, so that you can achieve lazy-decoding when consumer actually need bitmap buffer
|
||||
UIKit on iOS use heavy on this and ImageIO codec prefers to lazy-decoding for common Hardware-Accelerate format like JPEG/PNG/HEIC
|
||||
But however, the consumer may access bitmap buffer when running on main queue, like CoreAnimation layer render image. So this is a trade-off
|
||||
You can force us to disable the lazy-decoding and always allocate bitmap buffer on RAM, but this may have higher ratio of OOM (out of memory)
|
||||
@note The default value is NO for animated image coder (means `animatedImageFrameAtIndex:`)
|
||||
@note The default value is YES for static image coder (means `decodedImageWithData:`)
|
||||
@note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`.
|
||||
*/
|
||||
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDecoding;
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -14,6 +14,7 @@ SDImageCoderOption const SDImageCoderDecodePreserveAspectRatio = @"decodePreserv
|
|||
SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnailPixelSize";
|
||||
SDImageCoderOption const SDImageCoderDecodeFileExtensionHint = @"decodeFileExtensionHint";
|
||||
SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint";
|
||||
SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding";
|
||||
|
||||
SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
|
||||
SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";
|
||||
|
|
|
@ -23,6 +23,25 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati
|
|||
// Specify File Size for lossy format encoding, like JPEG
|
||||
static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
|
||||
|
||||
// This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+
|
||||
// However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case
|
||||
static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
||||
if (!image) return nil;
|
||||
size_t width = CGImageGetWidth(image);
|
||||
size_t height = CGImageGetHeight(image);
|
||||
size_t bitsPerComponent = CGImageGetBitsPerComponent(image);
|
||||
size_t bitsPerPixel = CGImageGetBitsPerPixel(image);
|
||||
size_t bytesPerRow = CGImageGetBytesPerRow(image);
|
||||
CGColorSpaceRef space = CGImageGetColorSpace(image);
|
||||
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
|
||||
CGDataProviderRef provider = CGImageGetDataProvider(image);
|
||||
const CGFloat *decode = CGImageGetDecode(image);
|
||||
bool shouldInterpolate = CGImageGetShouldInterpolate(image);
|
||||
CGColorRenderingIntent intent = CGImageGetRenderingIntent(image);
|
||||
CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, decode, shouldInterpolate, intent);
|
||||
return newImage;
|
||||
}
|
||||
|
||||
@interface SDImageIOCoderFrame : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
|
||||
|
@ -46,6 +65,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
BOOL _finished;
|
||||
BOOL _preserveAspectRatio;
|
||||
CGSize _thumbnailSize;
|
||||
BOOL _lazyDecode;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
|
@ -193,7 +213,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
return frameDuration;
|
||||
}
|
||||
|
||||
+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize forceDecode:(BOOL)forceDecode options:(NSDictionary *)options {
|
||||
+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode options:(NSDictionary *)options {
|
||||
// Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
|
||||
// Parse the image properties
|
||||
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
|
||||
|
@ -250,7 +270,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
}
|
||||
}
|
||||
// Check whether output CGImage is decoded
|
||||
if (forceDecode) {
|
||||
if (!lazyDecode) {
|
||||
if (!isDecoded) {
|
||||
// Use CoreGraphics to trigger immediately decode
|
||||
CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
|
||||
|
@ -258,13 +278,24 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
imageRef = decodedImageRef;
|
||||
isDecoded = YES;
|
||||
}
|
||||
} else {
|
||||
// iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273
|
||||
if (@available(iOS 15, tvOS 15, *)) {
|
||||
// User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef
|
||||
if (imageRef) {
|
||||
// CGImageRef newImageRef = CGImageCreateCopy(imageRef);
|
||||
CGImageRef newImageRef = SDCGImageCreateCopy(imageRef);
|
||||
CGImageRelease(imageRef);
|
||||
imageRef = newImageRef;
|
||||
}
|
||||
}
|
||||
}
|
||||
#if SD_CHECK_CGIMAGE_RETAIN_SOURCE
|
||||
// Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+)
|
||||
// If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check
|
||||
extern CGImageSourceRef CGImageGetImageSource(CGImageRef);
|
||||
NSCAssert(!CGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if SD_UIKIT || SD_WATCH
|
||||
UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
|
||||
|
@ -307,6 +338,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
preserveAspectRatio = preserveAspectRatioValue.boolValue;
|
||||
}
|
||||
|
||||
BOOL lazyDecode = YES; // Defaults YES for static image coder
|
||||
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
|
||||
if (lazyDecodeValue != nil) {
|
||||
lazyDecode = lazyDecodeValue.boolValue;
|
||||
}
|
||||
|
||||
#if SD_MAC
|
||||
// If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
|
||||
// Which decode frames in time and reduce memory usage
|
||||
|
@ -353,12 +390,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
|
||||
BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
|
||||
if (decodeFirstFrame || count <= 1) {
|
||||
animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize forceDecode:NO options:nil];
|
||||
animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil];
|
||||
} else {
|
||||
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:count];
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize forceDecode:NO options:nil];
|
||||
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil];
|
||||
if (!image) {
|
||||
continue;
|
||||
}
|
||||
|
@ -414,6 +451,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
preserveAspectRatio = preserveAspectRatioValue.boolValue;
|
||||
}
|
||||
_preserveAspectRatio = preserveAspectRatio;
|
||||
BOOL lazyDecode = YES; // Defaults YES for static image coder
|
||||
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
|
||||
if (lazyDecodeValue != nil) {
|
||||
lazyDecode = lazyDecodeValue.boolValue;
|
||||
}
|
||||
_lazyDecode = lazyDecode;
|
||||
SD_LOCK_INIT(_lock);
|
||||
#if SD_UIKIT
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
|
@ -468,7 +511,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
if (scaleFactor != nil) {
|
||||
scale = MAX([scaleFactor doubleValue], 1);
|
||||
}
|
||||
image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize forceDecode:NO options:nil];
|
||||
image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode options:nil];
|
||||
if (image) {
|
||||
image.sd_imageFormat = self.class.imageFormat;
|
||||
}
|
||||
|
@ -715,28 +758,27 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
|
||||
- (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
|
||||
NSDictionary *options;
|
||||
BOOL forceDecode = NO;
|
||||
if (@available(iOS 15, tvOS 15, *)) {
|
||||
// iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273
|
||||
forceDecode = YES;
|
||||
BOOL lazyDecode = NO; // Defaults NO for animated image coder
|
||||
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
|
||||
if (lazyDecodeValue != nil) {
|
||||
lazyDecode = lazyDecodeValue.boolValue;
|
||||
}
|
||||
if (!lazyDecode) {
|
||||
options = @{
|
||||
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO),
|
||||
(__bridge NSString *)kCGImageSourceShouldCache : @(NO)
|
||||
};
|
||||
} else {
|
||||
// Animated Image should not use the CGContext solution to force decode on lower firmware. Prefers to use Image/IO built in method, which is safer and memory friendly, see https://github.com/SDWebImage/SDWebImage/issues/2961
|
||||
forceDecode = NO;
|
||||
options = @{
|
||||
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
|
||||
(__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
|
||||
};
|
||||
}
|
||||
UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize forceDecode:forceDecode options:options];
|
||||
UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:lazyDecode options:options];
|
||||
if (!image) {
|
||||
return nil;
|
||||
}
|
||||
image.sd_imageFormat = self.class.imageFormat;
|
||||
image.sd_isDecoded = YES;
|
||||
return image;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
BOOL _finished;
|
||||
BOOL _preserveAspectRatio;
|
||||
CGSize _thumbnailSize;
|
||||
BOOL _lazyDecode;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
|
@ -112,6 +113,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
preserveAspectRatio = preserveAspectRatioValue.boolValue;
|
||||
}
|
||||
|
||||
BOOL lazyDecode = YES; // Defaults YES for static image coder
|
||||
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
|
||||
if (lazyDecodeValue != nil) {
|
||||
lazyDecode = lazyDecodeValue.boolValue;
|
||||
}
|
||||
|
||||
NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
|
||||
if (!typeIdentifierHint) {
|
||||
// Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension
|
||||
|
@ -163,7 +170,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
thumbnailSize = CGSizeZero;
|
||||
}
|
||||
|
||||
UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize forceDecode:NO options:decodingOptions];
|
||||
UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:decodingOptions];
|
||||
CFRelease(source);
|
||||
if (!image) {
|
||||
return nil;
|
||||
|
@ -205,6 +212,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
preserveAspectRatio = preserveAspectRatioValue.boolValue;
|
||||
}
|
||||
_preserveAspectRatio = preserveAspectRatio;
|
||||
BOOL lazyDecode = YES; // Defaults YES for static image coder
|
||||
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
|
||||
if (lazyDecodeValue != nil) {
|
||||
lazyDecode = lazyDecodeValue.boolValue;
|
||||
}
|
||||
_lazyDecode = lazyDecode;
|
||||
#if SD_UIKIT
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
#endif
|
||||
|
@ -255,7 +268,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
|
|||
if (scaleFactor != nil) {
|
||||
scale = MAX([scaleFactor doubleValue], 1);
|
||||
}
|
||||
image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize forceDecode:NO options:nil];
|
||||
image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode options:nil];
|
||||
if (image) {
|
||||
CFStringRef uttype = CGImageSourceGetType(_imageSource);
|
||||
image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];
|
||||
|
|
|
@ -602,7 +602,7 @@ didReceiveResponse:(NSURLResponse *)response
|
|||
}
|
||||
}];
|
||||
}
|
||||
if (@available(iOS 13.0, *)) {
|
||||
if (@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, *)) {
|
||||
[self.coderQueue addBarrierBlock:^{
|
||||
@strongify(self);
|
||||
if (!self) {
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
+ (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source;
|
||||
+ (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source;
|
||||
+ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize forceDecode:(BOOL)forceDecode options:(nullable NSDictionary *)options;
|
||||
+ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode options:(nullable NSDictionary *)options;
|
||||
+ (BOOL)canEncodeToFormat:(SDImageFormat)format;
|
||||
+ (BOOL)canDecodeFromFormat:(SDImageFormat)format;
|
||||
|
||||
|
|
Loading…
Reference in New Issue