Merge pull request #3736 from dreampiggy/performance/better_lazy_cgimage_detection

Use the better way to detect lazy/non-lazy CGImage. Only do force decoding for lazy image (avoid thumbnail image been decoded twice)
This commit is contained in:
DreamPiggy 2024-08-07 15:44:01 +08:00 committed by GitHub
commit 87b7a9a45c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 71 additions and 16 deletions

View File

@ -107,6 +107,12 @@ typedef struct SDImagePixelFormat {
*/ */
+ (BOOL)CGImageContainsAlpha:(_Nonnull CGImageRef)cgImage; + (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. 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. 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.

View File

@ -381,6 +381,45 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
return hasAlpha; 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
// <CGImage 0x10740ffe0> (IP) -> YES
// <CGImage 0x10740ffe0> (DP) -> NO
NSArray<NSString *> *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 { + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp]; 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) // Check policy (automatic)
CGImageRef cgImage = image.CGImage; CGImageRef cgImage = image.CGImage;
if (cgImage) { if (cgImage) {
CFStringRef uttype = CGImageGetUTType(cgImage); // Check if it's lazy CGImage wrapper or not
if (uttype) { BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:cgImage];
// Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` if (isLazy) {
// Lazy CGImage should trigger force decode before rendering
return YES; return YES;
} else { } 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]; BOOL isSupported = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
return !isSupported; return !isSupported;
} }

View File

@ -479,7 +479,6 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
if (!imageRef) { if (!imageRef) {
return nil; return nil;
} }
BOOL isDecoded = NO;
// Thumbnail image post-process // Thumbnail image post-process
if (!createFullImage) { if (!createFullImage) {
if (preserveAspectRatio) { if (preserveAspectRatio) {
@ -491,19 +490,19 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
if (scaledImageRef) { if (scaledImageRef) {
CGImageRelease(imageRef); CGImageRelease(imageRef);
imageRef = scaledImageRef; imageRef = scaledImageRef;
isDecoded = YES;
} }
} }
} }
// Check whether output CGImage is decoded // Check whether output CGImage is decoded
BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:imageRef];
if (!lazyDecode) { if (!lazyDecode) {
if (!isDecoded) { if (isLazy) {
// Use CoreGraphics to trigger immediately decode // Use CoreGraphics to trigger immediately decode to drop lazy CGImage
CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
if (decodedImageRef) { if (decodedImageRef) {
CGImageRelease(imageRef); CGImageRelease(imageRef);
imageRef = decodedImageRef; imageRef = decodedImageRef;
isDecoded = YES; isLazy = NO;
} }
} }
} else if (animatedImage) { } else if (animatedImage) {
@ -545,7 +544,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
#endif #endif
CGImageRelease(imageRef); CGImageRelease(imageRef);
image.sd_isDecoded = isDecoded; image.sd_isDecoded = !isLazy;
return image; return image;
} }

View File

@ -352,13 +352,19 @@ static BOOL _isCalled;
- (void)test24AnimatedImageViewCategoryDiskCache { - (void)test24AnimatedImageViewCategoryDiskCache {
XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category disk cache"]; XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category disk cache"];
SDAnimatedImageView *imageView = [SDAnimatedImageView new]; SDAnimatedImageView *imageView = [SDAnimatedImageView new];
NSURL *testURL = [NSURL URLWithString:kTestGIFURL]; NSURL *testURL = [NSURL URLWithString:@"https://foobar.non-exists.org/bizbuz.gif"];
[SDImageCache.sharedImageCache removeImageFromMemoryForKey:testURL.absoluteString]; 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) { [imageView sd_setImageWithURL:testURL placeholderImage:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
expect(error).to.beNil(); expect(error).to.beNil();
expect(image).notTo.beNil(); expect(image).notTo.beNil();
expect(cacheType).equal(SDImageCacheTypeDisk); expect(cacheType).equal(SDImageCacheTypeDisk);
expect([image isKindOfClass:[SDAnimatedImage class]]).beTruthy(); expect([image isKindOfClass:[SDAnimatedImage class]]).beTruthy();
[SDImageCache.sharedImageCache removeImageFromMemoryForKey:testKey];
[SDImageCache.sharedImageCache removeImageFromDiskForKey:testKey];
[expectation fulfill]; [expectation fulfill];
}]; }];
[self waitForExpectationsWithCommonTimeout]; [self waitForExpectationsWithCommonTimeout];

View File

@ -353,6 +353,10 @@
CGSize imageSize = image.size; CGSize imageSize = image.size;
expect(imageSize.width).equal(400); expect(imageSize.width).equal(400);
expect(imageSize.height).equal(263); 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 { - (void)test23ThatThumbnailEncodeCalculation {
@ -360,6 +364,10 @@
NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath]; NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:nil]; UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:nil];
expect(image.size).equal(CGSizeMake(5250, 3450)); 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 CGSize thumbnailSize = CGSizeMake(4000, 4000); // 3450 < 4000 < 5250
NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEG options:@{ NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEG options:@{
SDImageCoderEncodeMaxPixelSize: @(thumbnailSize) SDImageCoderEncodeMaxPixelSize: @(thumbnailSize)

View File

@ -123,11 +123,7 @@
#endif #endif
expect(format.scale).equal(screenScale); expect(format.scale).equal(screenScale);
expect(format.opaque).beFalsy(); expect(format.opaque).beFalsy();
#if SD_UIKIT
expect(format.preferredRange).equal(SDGraphicsImageRendererFormatRangeAutomatic); expect(format.preferredRange).equal(SDGraphicsImageRendererFormatRangeAutomatic);
#elif SD_MAC
expect(format.preferredRange).equal(SDGraphicsImageRendererFormatRangeStandard);
#endif
CGSize size = CGSizeMake(100, 100); CGSize size = CGSizeMake(100, 100);
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format];
#if SD_MAC #if SD_MAC