From ecedea2e06054e74107f6b4355d5512d3b2fc43c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 2 Aug 2024 17:25:02 +0800 Subject: [PATCH 1/2] Use the better way to detect lazy/non-lazy CGImage. Only do force decoding for lazy image This effect the thumbnail decoding (which produce non-lazy CGImage, but accidentally been force decoded) --- SDWebImage/Core/SDImageCoderHelper.h | 6 +++ SDWebImage/Core/SDImageCoderHelper.m | 48 ++++++++++++++++++++++-- SDWebImage/Core/SDImageIOAnimatedCoder.m | 11 +++--- Tests/Tests/SDImageCoderTests.m | 8 ++++ 4 files changed, 63 insertions(+), 10 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index d0b51152..fe51c4f5 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -107,6 +107,12 @@ typedef struct SDImagePixelFormat { */ + (BOOL)CGImageContainsAlpha:(_Nonnull CGImageRef)cgImage; +/** + Detect whether the CGImage is lazy and not-yet decoded. (lazy means, only when the caller access the underlying bitmap buffer via provider like `CGDataProviderCopyData` or `CGDataProviderRetainBytePtr`, the decoder will allocate memory, it's a lazy allocation) + The implementation use the Core Graphics internal to check whether the CGImage is `CGImageProvider` based, or `CGDataProvider` based. The `CGDataProvider` based is treated as non-lazy. + */ ++ (BOOL)CGImageIsLazy:(_Nonnull CGImageRef)cgImage; + /** Create a decoded CGImage by the provided CGImage. This follows The Create Rule and you are response to call release after usage. It will detect whether image contains alpha channel, then create a new bitmap context with the same size of image, and draw it. This can ensure that the image do not need extra decoding after been set to the imageView. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 0808685b..422e0f98 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -381,6 +381,45 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return hasAlpha; } ++ (BOOL)CGImageIsLazy:(CGImageRef)cgImage { + if (!cgImage) { + return NO; + } + // CoreGraphics use CGImage's C struct filed (offset 0xd8 on iOS 17.0) + // But since the description of `CGImageRef` always contains the `[DP]` (DataProvider) and `[IP]` (ImageProvider), we can use this as a hint + NSString *description = (__bridge_transfer NSString *)CFCopyDescription(cgImage); + if (description) { + // Solution 1: Parse the description to get provider + // (IP) -> YES + // (DP) -> NO + NSArray *lines = [description componentsSeparatedByString:@"\n"]; + if (lines.count > 0) { + NSString *firstLine = lines[0]; + NSRange startRange = [firstLine rangeOfString:@"("]; + NSRange endRange = [firstLine rangeOfString:@")"]; + if (startRange.location != NSNotFound && endRange.location != NSNotFound) { + NSRange resultRange = NSMakeRange(startRange.location + 1, endRange.location - startRange.location - 1); + NSString *providerString = [firstLine substringWithRange:resultRange]; + if ([providerString isEqualToString:@"IP"]) { + return YES; + } else if ([providerString isEqualToString:@"DP"]) { + return NO; + } else { + // New cases ? fallback + } + } + } + } + // Solution 2: Use UTI metadata + CFStringRef uttype = CGImageGetUTType(cgImage); + if (uttype) { + // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` metadata for lazy decoded CGImage + return YES; + } else { + return NO; + } +} + + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage { return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp]; } @@ -930,12 +969,13 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over // Check policy (automatic) CGImageRef cgImage = image.CGImage; if (cgImage) { - CFStringRef uttype = CGImageGetUTType(cgImage); - if (uttype) { - // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` + // Check if it's lazy CGImage wrapper or not + BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:cgImage]; + if (isLazy) { + // Lazy CGImage should trigger force decode before rendering return YES; } else { - // Now, let's check if the CGImage is hardware supported (not byte-aligned will cause extra copy) + // Now, let's check if this non-lazy CGImage is hardware supported (not byte-aligned will cause extra copy) BOOL isSupported = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage]; return !isSupported; } diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index d30da12f..27665b46 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -479,7 +479,6 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { if (!imageRef) { return nil; } - BOOL isDecoded = NO; // Thumbnail image post-process if (!createFullImage) { if (preserveAspectRatio) { @@ -491,19 +490,19 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { if (scaledImageRef) { CGImageRelease(imageRef); imageRef = scaledImageRef; - isDecoded = YES; } } } // Check whether output CGImage is decoded + BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:imageRef]; if (!lazyDecode) { - if (!isDecoded) { - // Use CoreGraphics to trigger immediately decode + if (isLazy) { + // Use CoreGraphics to trigger immediately decode to drop lazy CGImage CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; if (decodedImageRef) { CGImageRelease(imageRef); imageRef = decodedImageRef; - isDecoded = YES; + isLazy = NO; } } } else if (animatedImage) { @@ -545,7 +544,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; #endif CGImageRelease(imageRef); - image.sd_isDecoded = isDecoded; + image.sd_isDecoded = !isLazy; return image; } diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 5545f8b6..32866943 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -353,6 +353,10 @@ CGSize imageSize = image.size; expect(imageSize.width).equal(400); expect(imageSize.height).equal(263); + // `CGImageSourceCreateThumbnailAtIndex` should always produce non-lazy CGImage + CGImageRef cgImage = image.CGImage; + expect([SDImageCoderHelper CGImageIsLazy:cgImage]).beFalsy(); + expect(image.sd_isDecoded).beTruthy(); } - (void)test23ThatThumbnailEncodeCalculation { @@ -360,6 +364,10 @@ NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath]; UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:nil]; expect(image.size).equal(CGSizeMake(5250, 3450)); + // `CGImageSourceCreateImageAtIndex` should always produce lazy CGImage + CGImageRef cgImage = image.CGImage; + expect([SDImageCoderHelper CGImageIsLazy:cgImage]).beTruthy(); + expect(image.sd_isDecoded).beFalsy(); CGSize thumbnailSize = CGSizeMake(4000, 4000); // 3450 < 4000 < 5250 NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEG options:@{ SDImageCoderEncodeMaxPixelSize: @(thumbnailSize) From 0df663ca958e387fb223403deac98c66d0b56837 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 7 Aug 2024 14:32:50 +0800 Subject: [PATCH 2/2] Update the test case --- Tests/Tests/SDAnimatedImageTest.m | 10 ++++++++-- Tests/Tests/SDUtilsTests.m | 4 ---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Tests/Tests/SDAnimatedImageTest.m b/Tests/Tests/SDAnimatedImageTest.m index 6b9a8b82..450f611e 100644 --- a/Tests/Tests/SDAnimatedImageTest.m +++ b/Tests/Tests/SDAnimatedImageTest.m @@ -352,13 +352,19 @@ static BOOL _isCalled; - (void)test24AnimatedImageViewCategoryDiskCache { XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category disk cache"]; SDAnimatedImageView *imageView = [SDAnimatedImageView new]; - NSURL *testURL = [NSURL URLWithString:kTestGIFURL]; - [SDImageCache.sharedImageCache removeImageFromMemoryForKey:testURL.absoluteString]; + NSURL *testURL = [NSURL URLWithString:@"https://foobar.non-exists.org/bizbuz.gif"]; + NSString *testKey = testURL.absoluteString; + [SDImageCache.sharedImageCache removeImageFromMemoryForKey:testKey]; + [SDImageCache.sharedImageCache removeImageFromDiskForKey:testKey]; + NSData *imageData = [self testGIFData]; + [SDImageCache.sharedImageCache storeImageDataToDisk:imageData forKey:testKey]; [imageView sd_setImageWithURL:testURL placeholderImage:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { expect(error).to.beNil(); expect(image).notTo.beNil(); expect(cacheType).equal(SDImageCacheTypeDisk); expect([image isKindOfClass:[SDAnimatedImage class]]).beTruthy(); + [SDImageCache.sharedImageCache removeImageFromMemoryForKey:testKey]; + [SDImageCache.sharedImageCache removeImageFromDiskForKey:testKey]; [expectation fulfill]; }]; [self waitForExpectationsWithCommonTimeout]; diff --git a/Tests/Tests/SDUtilsTests.m b/Tests/Tests/SDUtilsTests.m index 4d61e6e7..0d4e628c 100644 --- a/Tests/Tests/SDUtilsTests.m +++ b/Tests/Tests/SDUtilsTests.m @@ -123,11 +123,7 @@ #endif expect(format.scale).equal(screenScale); expect(format.opaque).beFalsy(); -#if SD_UIKIT expect(format.preferredRange).equal(SDGraphicsImageRendererFormatRangeAutomatic); -#elif SD_MAC - expect(format.preferredRange).equal(SDGraphicsImageRendererFormatRangeStandard); -#endif CGSize size = CGSizeMake(100, 100); SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; #if SD_MAC