From a206229905dab02c6dfa35fad6a0b4a17ce97e0d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 25 Apr 2023 18:35:12 +0800 Subject: [PATCH 1/5] Introduce frame pool for SDAnimatedImage playback. Solve when multiple image view references the same URL image cause un-wanted decode which waste RAM/CPU --- SDWebImage.xcodeproj/project.pbxproj | 10 ++ SDWebImage/Core/SDAnimatedImagePlayer.m | 150 ++++++++++++------------ SDWebImage/Private/SDImageFramePool.h | 40 +++++++ SDWebImage/Private/SDImageFramePool.m | 140 ++++++++++++++++++++++ 4 files changed, 263 insertions(+), 77 deletions(-) create mode 100644 SDWebImage/Private/SDImageFramePool.h create mode 100644 SDWebImage/Private/SDImageFramePool.m diff --git a/SDWebImage.xcodeproj/project.pbxproj b/SDWebImage.xcodeproj/project.pbxproj index f48659c7..8283cc4a 100644 --- a/SDWebImage.xcodeproj/project.pbxproj +++ b/SDWebImage.xcodeproj/project.pbxproj @@ -45,6 +45,9 @@ 321E60C01F38E91700405457 /* UIImage+ForceDecode.h in Headers */ = {isa = PBXBuildFile; fileRef = 321E60BC1F38E91700405457 /* UIImage+ForceDecode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 321E60C41F38E91700405457 /* UIImage+ForceDecode.m in Sources */ = {isa = PBXBuildFile; fileRef = 321E60BD1F38E91700405457 /* UIImage+ForceDecode.m */; }; 321E60C61F38E91700405457 /* UIImage+ForceDecode.m in Sources */ = {isa = PBXBuildFile; fileRef = 321E60BD1F38E91700405457 /* UIImage+ForceDecode.m */; }; + 3237321429F8D0D600D1DA41 /* SDImageFramePool.h in Headers */ = {isa = PBXBuildFile; fileRef = 3237321229F8D0D600D1DA41 /* SDImageFramePool.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 3237321529F8D0D600D1DA41 /* SDImageFramePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 3237321329F8D0D600D1DA41 /* SDImageFramePool.m */; }; + 3237321629F8D0E200D1DA41 /* SDImageFramePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 3237321329F8D0D600D1DA41 /* SDImageFramePool.m */; }; 3237F9E820161AE000A88143 /* NSImage+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 4397D2F51D0DE2DF00BB2784 /* NSImage+Compatibility.m */; }; 3237F9EB20161AE000A88143 /* NSImage+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 4397D2F51D0DE2DF00BB2784 /* NSImage+Compatibility.m */; }; 3240BB6523968FA1003BA07D /* SDFileAttributeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 325F7CC523893B2E00AEDFCC /* SDFileAttributeHelper.m */; }; @@ -407,6 +410,8 @@ 321E60A11F38E8F600405457 /* SDImageGIFCoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SDImageGIFCoder.m; path = Core/SDImageGIFCoder.m; sourceTree = ""; }; 321E60BC1F38E91700405457 /* UIImage+ForceDecode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImage+ForceDecode.h"; path = "Core/UIImage+ForceDecode.h"; sourceTree = ""; }; 321E60BD1F38E91700405457 /* UIImage+ForceDecode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ForceDecode.m"; path = "Core/UIImage+ForceDecode.m"; sourceTree = ""; }; + 3237321229F8D0D600D1DA41 /* SDImageFramePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDImageFramePool.h; sourceTree = ""; }; + 3237321329F8D0D600D1DA41 /* SDImageFramePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDImageFramePool.m; sourceTree = ""; }; 3240BB6623968FE6003BA07D /* SDAssociatedObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDAssociatedObject.h; sourceTree = ""; }; 3240BB6723968FE6003BA07D /* SDAssociatedObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDAssociatedObject.m; sourceTree = ""; }; 324406292296C5F400A36084 /* SDWebImageOptionsProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDWebImageOptionsProcessor.h; path = Core/SDWebImageOptionsProcessor.h; sourceTree = ""; }; @@ -691,6 +696,8 @@ 325C460122339330004CAE11 /* SDImageAssetManager.m */, 325C460C223394D8004CAE11 /* SDImageCachesManagerOperation.h */, 325C460D223394D8004CAE11 /* SDImageCachesManagerOperation.m */, + 3237321229F8D0D600D1DA41 /* SDImageFramePool.h */, + 3237321329F8D0D600D1DA41 /* SDImageFramePool.m */, 32C78E39233371AD00C6B7F8 /* SDImageIOAnimatedCoderInternal.h */, 3253F235244982D3006C2BE8 /* SDWebImageTransitionInternal.h */, 325C461E2233A02E004CAE11 /* UIColor+SDHexString.h */, @@ -909,6 +916,7 @@ 32D3CDD121DDE87300C4DB49 /* UIImage+MemoryCacheCost.h in Headers */, 328BB6AC2081FEE500760D6C /* SDWebImageCacheSerializer.h in Headers */, 325F7CCA238942AB00AEDFCC /* UIImage+ExtendedCacheData.h in Headers */, + 3237321429F8D0D600D1DA41 /* SDImageFramePool.h in Headers */, 325C46272233A0A8004CAE11 /* NSBezierPath+SDRoundedCorners.h in Headers */, 3253F236244982D3006C2BE8 /* SDWebImageTransitionInternal.h in Headers */, 321B378F2083290E00C0EA77 /* SDImageLoadersManager.h in Headers */, @@ -1188,6 +1196,7 @@ 32F21B5920788D8C0036B1D5 /* SDWebImageDownloaderRequestModifier.m in Sources */, 321B37952083290E00C0EA77 /* SDImageLoadersManager.m in Sources */, 4A2CAE361AB4BB7500B6BC39 /* UIImageView+WebCache.m in Sources */, + 3237321529F8D0D600D1DA41 /* SDImageFramePool.m in Sources */, 4A2CAE1E1AB4BB6800B6BC39 /* SDWebImageDownloaderOperation.m in Sources */, 3298655E2337230C0071958B /* SDImageHEICCoder.m in Sources */, 32F7C0802030719600873181 /* UIImage+Transform.m in Sources */, @@ -1262,6 +1271,7 @@ 32C0FDE72013426C001B8F2D /* SDWebImageIndicator.m in Sources */, 32B5CC63222F8B70005EB74E /* SDAsyncBlockOperation.m in Sources */, 32F21B5720788D8C0036B1D5 /* SDWebImageDownloaderRequestModifier.m in Sources */, + 3237321629F8D0E200D1DA41 /* SDImageFramePool.m in Sources */, 5376130B155AD0D5005750A4 /* SDWebImageDownloader.m in Sources */, 321B37932083290E00C0EA77 /* SDImageLoadersManager.m in Sources */, 32F7C07E2030719600873181 /* UIImage+Transform.m in Sources */, diff --git a/SDWebImage/Core/SDAnimatedImagePlayer.m b/SDWebImage/Core/SDAnimatedImagePlayer.m index 88d8e4de..701c3310 100644 --- a/SDWebImage/Core/SDAnimatedImagePlayer.m +++ b/SDWebImage/Core/SDAnimatedImagePlayer.m @@ -10,24 +10,24 @@ #import "NSImage+Compatibility.h" #import "SDDisplayLink.h" #import "SDDeviceHelper.h" +#import "SDImageFramePool.h" #import "SDInternalMacros.h" @interface SDAnimatedImagePlayer () { - SD_LOCK_DECLARE(_lock); +// SD_LOCK_DECLARE(_lock); NSRunLoopMode _runLoopMode; } +@property (atomic, strong) SDImageFramePool *framePool; + @property (nonatomic, strong, readwrite) UIImage *currentFrame; @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount; @property (nonatomic, strong) id animatedProvider; -@property (nonatomic, strong) NSMutableDictionary *frameBuffer; @property (nonatomic, assign) NSTimeInterval currentTime; @property (nonatomic, assign) BOOL bufferMiss; @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable; @property (nonatomic, assign) BOOL shouldReverse; -@property (nonatomic, assign) NSUInteger maxBufferCount; -@property (nonatomic, strong) NSOperationQueue *fetchQueue; @property (nonatomic, strong) SDDisplayLink *displayLink; @end @@ -47,7 +47,8 @@ self.totalLoopCount = provider.animatedImageLoopCount; self.animatedProvider = provider; self.playbackRate = 1.0; - SD_LOCK_INIT(_lock); + self.framePool = [SDImageFramePool registerProvider:provider]; +// SD_LOCK_INIT(_lock); #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif @@ -69,38 +70,24 @@ } - (void)didReceiveMemoryWarning:(NSNotification *)notification { - [_fetchQueue cancelAllOperations]; - NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ - NSNumber *currentFrameIndex = @(self.currentFrameIndex); - SD_LOCK(self->_lock); - NSArray *keys = self.frameBuffer.allKeys; - // only keep the next frame for later rendering - for (NSNumber * key in keys) { - if (![key isEqualToNumber:currentFrameIndex]) { - [self.frameBuffer removeObjectForKey:key]; - } - } - SD_UNLOCK(self->_lock); - }]; - [_fetchQueue addOperation:operation]; + [self clearFrameBuffer]; +// [_fetchQueue cancelAllOperations]; +// NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ +// NSNumber *currentFrameIndex = @(self.currentFrameIndex); +// SD_LOCK(self->_lock); +// NSArray *keys = self.frameBuffer.allKeys; +// // only keep the next frame for later rendering +// for (NSNumber * key in keys) { +// if (![key isEqualToNumber:currentFrameIndex]) { +// [self.frameBuffer removeObjectForKey:key]; +// } +// } +// SD_UNLOCK(self->_lock); +// }]; +// [_fetchQueue addOperation:operation]; } #pragma mark - Private -- (NSOperationQueue *)fetchQueue { - if (!_fetchQueue) { - _fetchQueue = [[NSOperationQueue alloc] init]; - _fetchQueue.maxConcurrentOperationCount = 1; - _fetchQueue.name = @"com.hackemist.SDAnimatedImagePlayer.fetchQueue"; - } - return _fetchQueue; -} - -- (NSMutableDictionary *)frameBuffer { - if (!_frameBuffer) { - _frameBuffer = [NSMutableDictionary dictionary]; - } - return _frameBuffer; -} - (SDDisplayLink *)displayLink { if (!_displayLink) { @@ -155,9 +142,10 @@ if (posterFrame) { // HACK: The first frame should not check duration and immediately display self.needsDisplayWhenImageBecomesAvailable = YES; - SD_LOCK(self->_lock); - self.frameBuffer[@(self.currentFrameIndex)] = posterFrame; - SD_UNLOCK(self->_lock); + [self.framePool setFrame:posterFrame atIndex:self.currentFrameIndex]; +// SD_LOCK(self->_lock); +// self.frameBuffer[@(self.currentFrameIndex)] = posterFrame; +// SD_UNLOCK(self->_lock); } } @@ -174,9 +162,10 @@ } - (void)clearFrameBuffer { - SD_LOCK(_lock); - [_frameBuffer removeAllObjects]; - SD_UNLOCK(_lock); + [self.framePool removeAllFrames]; +// SD_LOCK(_lock); +// [_frameBuffer removeAllObjects]; +// SD_UNLOCK(_lock); } #pragma mark - Animation Control @@ -189,7 +178,7 @@ } - (void)stopPlaying { - [_fetchQueue cancelAllOperations]; +// [_fetchQueue cancelAllOperations]; // Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method. [_displayLink stop]; // We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct. @@ -197,7 +186,7 @@ } - (void)pausePlaying { - [_fetchQueue cancelAllOperations]; +// [_fetchQueue cancelAllOperations]; [_displayLink stop]; } @@ -261,23 +250,26 @@ // Check if we need to display new frame firstly BOOL bufferFull = NO; if (self.needsDisplayWhenImageBecomesAvailable) { - UIImage *currentFrame; - SD_LOCK(_lock); - currentFrame = self.frameBuffer[@(currentFrameIndex)]; - SD_UNLOCK(_lock); + UIImage *currentFrame = [self.framePool frameAtIndex:currentFrameIndex]; +// SD_LOCK(_lock); +// currentFrame = self.frameBuffer[@(currentFrameIndex)]; +// SD_UNLOCK(_lock); // Update the current frame if (currentFrame) { - SD_LOCK(_lock); - // Remove the frame buffer if need - if (self.frameBuffer.count > self.maxBufferCount) { - self.frameBuffer[@(currentFrameIndex)] = nil; - } - // Check whether we can stop fetch - if (self.frameBuffer.count == totalFrameCount) { + if (self.framePool.currentFrameCount == totalFrameCount) { bufferFull = YES; } - SD_UNLOCK(_lock); +// SD_LOCK(_lock); +// // Remove the frame buffer if need +// if (self.frameBuffer.count > self.maxBufferCount) { +// self.frameBuffer[@(currentFrameIndex)] = nil; +// } +// // Check whether we can stop fetch +// if (self.frameBuffer.count == totalFrameCount) { +// bufferFull = YES; +// } +// SD_UNLOCK(_lock); // Update the current frame immediately self.currentFrame = currentFrame; @@ -351,31 +343,35 @@ UIImage *fetchFrame = nil; if (!self.bufferMiss) { fetchFrameIndex = nextIndex; - SD_LOCK(_lock); - fetchFrame = self.frameBuffer[@(nextIndex)]; - SD_UNLOCK(_lock); + fetchFrame = [self.framePool frameAtIndex:nextIndex]; + // SD_LOCK(_lock); + // fetchFrame = self.frameBuffer[@(nextIndex)]; + // SD_UNLOCK(_lock); } - if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { - // Prefetch next frame in background queue - id animatedProvider = self.animatedProvider; - @weakify(self); - NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ - @strongify(self); - if (!self) { - return; - } - UIImage *frame = [animatedProvider animatedImageFrameAtIndex:fetchFrameIndex]; - - BOOL isAnimating = self.displayLink.isRunning; - if (isAnimating) { - SD_LOCK(self->_lock); - self.frameBuffer[@(fetchFrameIndex)] = frame; - SD_UNLOCK(self->_lock); - } - }]; - [self.fetchQueue addOperation:operation]; + if (!fetchFrame && !bufferFull) { + [self.framePool prefetchFrameAtIndex:fetchFrameIndex]; } } +// if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { +// // Prefetch next frame in background queue +// id animatedProvider = self.animatedProvider; +// @weakify(self); +// NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ +// @strongify(self); +// if (!self) { +// return; +// } +// UIImage *frame = [animatedProvider animatedImageFrameAtIndex:fetchFrameIndex]; +// +// BOOL isAnimating = self.displayLink.isRunning; +// if (isAnimating) { +// SD_LOCK(self->_lock); +// self.frameBuffer[@(fetchFrameIndex)] = frame; +// SD_UNLOCK(self->_lock); +// } +// }]; +// [self.fetchQueue addOperation:operation]; +// } - (void)handleFrameChange { if (self.animationFrameHandler) { @@ -410,7 +406,7 @@ maxBufferCount = 1; } - self.maxBufferCount = maxBufferCount; + self.framePool.maxBufferCount = maxBufferCount; } + (NSString *)defaultRunLoopMode { diff --git a/SDWebImage/Private/SDImageFramePool.h b/SDWebImage/Private/SDImageFramePool.h new file mode 100644 index 00000000..6fedc83f --- /dev/null +++ b/SDWebImage/Private/SDImageFramePool.h @@ -0,0 +1,40 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import +#import "SDWebImageCompat.h" +#import "SDImageCoder.h" + +NS_ASSUME_NONNULL_BEGIN + +/// A per-provider (provider means, AnimatedImage object) based frame pool, each player who use the same provider share the same frame buffer +@interface SDImageFramePool : NSObject + +/// Register and return back a frame pool, also increase reference count ++ (instancetype)registerProvider:(id)provider; +/// Unregister a frame pool, also decrease reference count, if zero dealloc the frame pool ++ (void)unregisterProvider:(id)provider; + +/// Prefetch the current frame, query using `frameAtIndex:` by caller to check whether finished. +- (void)prefetchFrameAtIndex:(NSUInteger)index; + +/// Control the max buffer count for current frame pool, used for RAM/CPU balance, default unlimited +@property (nonatomic, assign) NSUInteger maxBufferCount; +/// Control the max concurrent fetch queue operation count, used for CPU balance, default 1 +@property (nonatomic, assign) NSUInteger maxConcurrentCount; + +// Frame Operations +@property (nonatomic, readonly) NSUInteger currentFrameCount; +- (nullable UIImage *)frameAtIndex:(NSUInteger)index; +- (void)setFrame:(nullable UIImage *)frame atIndex:(NSUInteger)index; +- (void)removeFrameAtIndex:(NSUInteger)index; +- (void)removeAllFrames; + +NS_ASSUME_NONNULL_END + +@end diff --git a/SDWebImage/Private/SDImageFramePool.m b/SDWebImage/Private/SDImageFramePool.m new file mode 100644 index 00000000..9148e727 --- /dev/null +++ b/SDWebImage/Private/SDImageFramePool.m @@ -0,0 +1,140 @@ +/* +* This file is part of the SDWebImage package. +* (c) Olivier Poitrey +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +#import "SDImageFramePool.h" +#import "SDInternalMacros.h" +#import "objc/runtime.h" + +@interface SDImageFramePool () + +@property (class, readonly) NSMapTable *providerFramePoolMap; + +@property (weak) id provider; +@property (atomic) NSUInteger registerCount; + +@property (nonatomic, strong) NSMutableDictionary *frameBuffer; +@property (nonatomic, strong) NSOperationQueue *fetchQueue; + +@end + +@implementation SDImageFramePool + ++ (NSMapTable *)providerFramePoolMap { + static NSMapTable *providerFramePoolMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + providerFramePoolMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; + }); + return providerFramePoolMap; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _frameBuffer = [NSMutableDictionary dictionary]; + _fetchQueue = [[NSOperationQueue alloc] init]; + _fetchQueue.maxConcurrentOperationCount = 1; + _fetchQueue.name = @"com.hackemist.SDImageFramePool.fetchQueue"; + } + return self; +} + ++ (instancetype)registerProvider:(id)provider { + SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider]; + if (!framePool) { + framePool = [[SDImageFramePool alloc] init]; + framePool.provider = provider; + [self.providerFramePoolMap setObject:framePool forKey:provider]; + } + framePool.registerCount += 1; + return framePool; +} + ++ (void)unregisterProvider:(id)provider { + SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider]; + if (!framePool) { + return; + } + framePool.registerCount -= 1; + if (framePool.registerCount == 0) { + [self.providerFramePoolMap removeObjectForKey:provider]; + } +} + +- (void)prefetchFrameAtIndex:(NSUInteger)index { + @synchronized (self) { + NSUInteger frameCount = self.frameBuffer.count; + if (frameCount > self.maxBufferCount) { + // Remove the frame buffer if need + self.frameBuffer[@(index)] = nil; + } + } + + if (self.fetchQueue.operationCount == 0) { + // Prefetch next frame in background queue + id animatedProvider = self.provider; + @weakify(self); + NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ + @strongify(self); + if (!self) { + return; + } + UIImage *frame = [animatedProvider animatedImageFrameAtIndex:index]; + + [self setFrame:frame atIndex:index]; + }]; + [self.fetchQueue addOperation:operation]; + } +} + +- (void)setMaxConcurrentCount:(NSUInteger)maxConcurrentCount { + self.fetchQueue.maxConcurrentOperationCount = maxConcurrentCount; +} + +- (NSUInteger)currentFrameCount { + NSUInteger frameCount = 0; + @synchronized (self) { + frameCount = self.frameBuffer.count; + } + return frameCount; +} + +- (void)setFrame:(UIImage *)frame atIndex:(NSUInteger)index { + @synchronized (self) { + self.frameBuffer[@(index)] = frame; + } +} + +- (UIImage *)frameAtIndex:(NSUInteger)index { + UIImage *frame; + @synchronized (self) { + frame = self.frameBuffer[@(index)]; + } + return frame; +} + +- (void)removeFrameAtIndex:(NSUInteger)index { + @synchronized (self) { + self.frameBuffer[@(index)] = nil; + } +} + +- (void)removeAllFrames { + @synchronized (self) { + [self.frameBuffer removeAllObjects]; + } +} + +- (NSMutableDictionary *)frameBuffer { + if (!_frameBuffer) { + _frameBuffer = [NSMutableDictionary dictionary]; + } + return _frameBuffer; +} + +@end From 858b64aef75a330b10c984cdaa4f7efc21bce8be Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 26 Apr 2023 11:57:52 +0800 Subject: [PATCH 2/5] Try to optimize the time when calculate the maxBufferCount, it's cheap to get free memory so we check each time before prefetch Also, move the memory warning handler into frame pool class (100 player may use one frame pool) --- SDWebImage/Core/SDAnimatedImagePlayer.m | 116 +++++------------------- SDWebImage/Private/SDImageFramePool.m | 18 +++- 2 files changed, 42 insertions(+), 92 deletions(-) diff --git a/SDWebImage/Core/SDAnimatedImagePlayer.m b/SDWebImage/Core/SDAnimatedImagePlayer.m index 701c3310..d9abb2b4 100644 --- a/SDWebImage/Core/SDAnimatedImagePlayer.m +++ b/SDWebImage/Core/SDAnimatedImagePlayer.m @@ -14,7 +14,6 @@ #import "SDInternalMacros.h" @interface SDAnimatedImagePlayer () { -// SD_LOCK_DECLARE(_lock); NSRunLoopMode _runLoopMode; } @@ -24,6 +23,7 @@ @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount; @property (nonatomic, strong) id animatedProvider; +@property (nonatomic, assign) NSUInteger currentFrameBytes; @property (nonatomic, assign) NSTimeInterval currentTime; @property (nonatomic, assign) BOOL bufferMiss; @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable; @@ -48,10 +48,6 @@ self.animatedProvider = provider; self.playbackRate = 1.0; self.framePool = [SDImageFramePool registerProvider:provider]; -// SD_LOCK_INIT(_lock); -#if SD_UIKIT - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; -#endif } return self; } @@ -61,32 +57,6 @@ return player; } -#pragma mark - Life Cycle - -- (void)dealloc { -#if SD_UIKIT - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; -#endif -} - -- (void)didReceiveMemoryWarning:(NSNotification *)notification { - [self clearFrameBuffer]; -// [_fetchQueue cancelAllOperations]; -// NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ -// NSNumber *currentFrameIndex = @(self.currentFrameIndex); -// SD_LOCK(self->_lock); -// NSArray *keys = self.frameBuffer.allKeys; -// // only keep the next frame for later rendering -// for (NSNumber * key in keys) { -// if (![key isEqualToNumber:currentFrameIndex]) { -// [self.frameBuffer removeObjectForKey:key]; -// } -// } -// SD_UNLOCK(self->_lock); -// }]; -// [_fetchQueue addOperation:operation]; -} - #pragma mark - Private - (SDDisplayLink *)displayLink { @@ -140,12 +110,11 @@ UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation]; #endif if (posterFrame) { + // Calculate max buffer size + [self calculateMaxBufferCountWithFrame:posterFrame]; // HACK: The first frame should not check duration and immediately display self.needsDisplayWhenImageBecomesAvailable = YES; [self.framePool setFrame:posterFrame atIndex:self.currentFrameIndex]; -// SD_LOCK(self->_lock); -// self.frameBuffer[@(self.currentFrameIndex)] = posterFrame; -// SD_UNLOCK(self->_lock); } } @@ -163,9 +132,6 @@ - (void)clearFrameBuffer { [self.framePool removeAllFrames]; -// SD_LOCK(_lock); -// [_frameBuffer removeAllObjects]; -// SD_UNLOCK(_lock); } #pragma mark - Animation Control @@ -173,12 +139,9 @@ [self.displayLink start]; // Setup frame [self setupCurrentFrame]; - // Calculate max buffer size - [self calculateMaxBufferCount]; } - (void)stopPlaying { -// [_fetchQueue cancelAllOperations]; // Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method. [_displayLink stop]; // We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct. @@ -186,7 +149,6 @@ } - (void)pausePlaying { -// [_fetchQueue cancelAllOperations]; [_displayLink stop]; } @@ -248,29 +210,11 @@ // Check if we need to display new frame firstly - BOOL bufferFull = NO; if (self.needsDisplayWhenImageBecomesAvailable) { UIImage *currentFrame = [self.framePool frameAtIndex:currentFrameIndex]; -// SD_LOCK(_lock); -// currentFrame = self.frameBuffer[@(currentFrameIndex)]; -// SD_UNLOCK(_lock); // Update the current frame if (currentFrame) { - if (self.framePool.currentFrameCount == totalFrameCount) { - bufferFull = YES; - } -// SD_LOCK(_lock); -// // Remove the frame buffer if need -// if (self.frameBuffer.count > self.maxBufferCount) { -// self.frameBuffer[@(currentFrameIndex)] = nil; -// } -// // Check whether we can stop fetch -// if (self.frameBuffer.count == totalFrameCount) { -// bufferFull = YES; -// } -// SD_UNLOCK(_lock); - // Update the current frame immediately self.currentFrame = currentFrame; [self handleFrameChange]; @@ -292,8 +236,7 @@ if (self.currentTime < currentDuration) { // Current frame timestamp not reached, prefetch frame in advance. [self prefetchFrameAtIndex:currentFrameIndex - nextIndex:nextFrameIndex - bufferFull:bufferFull]; + nextIndex:nextFrameIndex]; return; } @@ -329,49 +272,31 @@ } [self prefetchFrameAtIndex:currentFrameIndex - nextIndex:nextFrameIndex - bufferFull:bufferFull]; + nextIndex:nextFrameIndex]; } // Check if we should prefetch next frame or current frame // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame // Or, most cases, the decode speed is faster than render speed, we fetch next frame - (void)prefetchFrameAtIndex:(NSUInteger)currentIndex - nextIndex:(NSUInteger)nextIndex - bufferFull:(BOOL)bufferFull { + nextIndex:(NSUInteger)nextIndex { NSUInteger fetchFrameIndex = currentIndex; UIImage *fetchFrame = nil; if (!self.bufferMiss) { fetchFrameIndex = nextIndex; fetchFrame = [self.framePool frameAtIndex:nextIndex]; - // SD_LOCK(_lock); - // fetchFrame = self.frameBuffer[@(nextIndex)]; - // SD_UNLOCK(_lock); + } + BOOL bufferFull = NO; + if (self.framePool.currentFrameCount == self.totalFrameCount) { + bufferFull = YES; } if (!fetchFrame && !bufferFull) { + // Calculate max buffer size + [self calculateMaxBufferCountWithFrame:self.currentFrame]; + // Prefetch next frame [self.framePool prefetchFrameAtIndex:fetchFrameIndex]; } } -// if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) { -// // Prefetch next frame in background queue -// id animatedProvider = self.animatedProvider; -// @weakify(self); -// NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ -// @strongify(self); -// if (!self) { -// return; -// } -// UIImage *frame = [animatedProvider animatedImageFrameAtIndex:fetchFrameIndex]; -// -// BOOL isAnimating = self.displayLink.isRunning; -// if (isAnimating) { -// SD_LOCK(self->_lock); -// self.frameBuffer[@(fetchFrameIndex)] = frame; -// SD_UNLOCK(self->_lock); -// } -// }]; -// [self.fetchQueue addOperation:operation]; -// } - (void)handleFrameChange { if (self.animationFrameHandler) { @@ -386,9 +311,17 @@ } #pragma mark - Util -- (void)calculateMaxBufferCount { - NSUInteger bytes = CGImageGetBytesPerRow(self.currentFrame.CGImage) * CGImageGetHeight(self.currentFrame.CGImage); - if (bytes == 0) bytes = 1024; +- (void)calculateMaxBufferCountWithFrame:(nonnull UIImage *)frame { + NSUInteger bytes = self.currentFrameBytes; + if (bytes == 0) { + bytes = CGImageGetBytesPerRow(frame.CGImage) * CGImageGetHeight(frame.CGImage); + if (bytes == 0) { + bytes = 1024; + } else { + // Cache since most animated image each frame bytes is the same + self.currentFrameBytes = bytes; + } + } NSUInteger max = 0; if (self.maxBufferSize > 0) { @@ -406,6 +339,7 @@ maxBufferCount = 1; } + printf("current maxBufferCount: %lu\n", (unsigned long)maxBufferCount); self.framePool.maxBufferCount = maxBufferCount; } diff --git a/SDWebImage/Private/SDImageFramePool.m b/SDWebImage/Private/SDImageFramePool.m index 9148e727..0b6cfa0a 100644 --- a/SDWebImage/Private/SDImageFramePool.m +++ b/SDWebImage/Private/SDImageFramePool.m @@ -33,6 +33,7 @@ return providerFramePoolMap; } +#pragma mark - Life Cycle - (instancetype)init { self = [super init]; if (self) { @@ -40,10 +41,23 @@ _fetchQueue = [[NSOperationQueue alloc] init]; _fetchQueue.maxConcurrentOperationCount = 1; _fetchQueue.name = @"com.hackemist.SDImageFramePool.fetchQueue"; +#if SD_UIKIT + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif } return self; } +- (void)dealloc { +#if SD_UIKIT + [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif +} + +- (void)didReceiveMemoryWarning:(NSNotification *)notification { + [self removeAllFrames]; +} + + (instancetype)registerProvider:(id)provider { SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider]; if (!framePool) { @@ -71,7 +85,9 @@ NSUInteger frameCount = self.frameBuffer.count; if (frameCount > self.maxBufferCount) { // Remove the frame buffer if need - self.frameBuffer[@(index)] = nil; + // TODO, use LRU or better algorithm to detect which frames to clear + self.frameBuffer[@(index - 1)] = nil; + self.frameBuffer[@(index + 1)] = nil; } } From ef69460dc7a742278d8e0864900ef4b6216edd64 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 26 Apr 2023 14:39:48 +0800 Subject: [PATCH 3/5] Fix the test case test26AnimatedImageStopAnimatingClearBuffer --- SDWebImage/Core/SDAnimatedImagePlayer.m | 3 +-- SDWebImage/Private/SDImageFramePool.m | 7 ------- Tests/Tests/SDAnimatedImageTest.m | 11 ++++++----- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/SDWebImage/Core/SDAnimatedImagePlayer.m b/SDWebImage/Core/SDAnimatedImagePlayer.m index d9abb2b4..1facb794 100644 --- a/SDWebImage/Core/SDAnimatedImagePlayer.m +++ b/SDWebImage/Core/SDAnimatedImagePlayer.m @@ -17,7 +17,7 @@ NSRunLoopMode _runLoopMode; } -@property (atomic, strong) SDImageFramePool *framePool; +@property (nonatomic, strong) SDImageFramePool *framePool; @property (nonatomic, strong, readwrite) UIImage *currentFrame; @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; @@ -339,7 +339,6 @@ maxBufferCount = 1; } - printf("current maxBufferCount: %lu\n", (unsigned long)maxBufferCount); self.framePool.maxBufferCount = maxBufferCount; } diff --git a/SDWebImage/Private/SDImageFramePool.m b/SDWebImage/Private/SDImageFramePool.m index 0b6cfa0a..7cde316a 100644 --- a/SDWebImage/Private/SDImageFramePool.m +++ b/SDWebImage/Private/SDImageFramePool.m @@ -146,11 +146,4 @@ } } -- (NSMutableDictionary *)frameBuffer { - if (!_frameBuffer) { - _frameBuffer = [NSMutableDictionary dictionary]; - } - return _frameBuffer; -} - @end diff --git a/Tests/Tests/SDAnimatedImageTest.m b/Tests/Tests/SDAnimatedImageTest.m index 9d47b1b8..f1a61cfb 100644 --- a/Tests/Tests/SDAnimatedImageTest.m +++ b/Tests/Tests/SDAnimatedImageTest.m @@ -9,6 +9,7 @@ #import "SDTestCase.h" #import "SDInternalMacros.h" +#import "SDImageFramePool.h" #import #import @@ -59,7 +60,7 @@ static BOOL _isCalled; @interface SDAnimatedImagePlayer () -@property (nonatomic, strong) NSMutableDictionary *frameBuffer; +@property (nonatomic, strong) SDImageFramePool *framePool; @end @@ -382,7 +383,7 @@ static BOOL _isCalled; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 0.5s is not finished, frame index should not be 0 - expect(imageView.player.frameBuffer.count).beGreaterThan(0); + expect(imageView.player.framePool.currentFrameCount).beGreaterThan(0); expect(imageView.currentFrameIndex).beGreaterThan(0); }); @@ -392,7 +393,7 @@ static BOOL _isCalled; #else imageView.animates = NO; #endif - expect(imageView.player.frameBuffer.count).beGreaterThan(0); + expect(imageView.player.framePool.currentFrameCount).beGreaterThan(0); expect(imageView.currentFrameIndex).beGreaterThan(0); [imageView removeFromSuperview]; @@ -420,7 +421,7 @@ static BOOL _isCalled; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 0.5s is not finished, frame index should not be 0 - expect(imageView.player.frameBuffer.count).beGreaterThan(0); + expect(imageView.player.framePool.currentFrameCount).beGreaterThan(0); expect(imageView.currentFrameIndex).beGreaterThan(0); }); @@ -430,7 +431,7 @@ static BOOL _isCalled; #else imageView.animates = NO; #endif - expect(imageView.player.frameBuffer.count).equal(0); + expect(imageView.player.framePool.currentFrameCount).equal(0); [imageView removeFromSuperview]; [expectation fulfill]; From 181f8d21836a730ff1753593747d760aa2b89a1d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Wed, 26 Apr 2023 14:58:19 +0800 Subject: [PATCH 4/5] Fix the issue that per-provider frame pool does not get dealloced --- SDWebImage/Core/SDAnimatedImagePlayer.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SDWebImage/Core/SDAnimatedImagePlayer.m b/SDWebImage/Core/SDAnimatedImagePlayer.m index 1facb794..499be675 100644 --- a/SDWebImage/Core/SDAnimatedImagePlayer.m +++ b/SDWebImage/Core/SDAnimatedImagePlayer.m @@ -57,6 +57,11 @@ return player; } +- (void)dealloc { + // Dereference the frame pool, when zero the frame pool for provider will dealloc + [SDImageFramePool unregisterProvider:self.animatedProvider]; +} + #pragma mark - Private - (SDDisplayLink *)displayLink { From 507f1b6b45d80b0ed6de9454593112505bab5ea1 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 9 May 2023 16:40:59 +0800 Subject: [PATCH 5/5] Using lock to ensure SDImageFramePool register/unregister logic in multi-thread environment --- SDWebImage/Private/SDImageFramePool.m | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/SDWebImage/Private/SDImageFramePool.m b/SDWebImage/Private/SDImageFramePool.m index 7cde316a..1eb2cdb3 100644 --- a/SDWebImage/Private/SDImageFramePool.m +++ b/SDWebImage/Private/SDImageFramePool.m @@ -22,6 +22,9 @@ @end +// Lock to ensure atomic behavior +SD_LOCK_DECLARE_STATIC(_providerFramePoolMapLock); + @implementation SDImageFramePool + (NSMapTable *)providerFramePoolMap { @@ -58,7 +61,14 @@ [self removeAllFrames]; } ++ (void)initialize { + // Lock to ensure atomic behavior + SD_LOCK_INIT(_providerFramePoolMapLock); +} + + (instancetype)registerProvider:(id)provider { + // Lock to ensure atomic behavior + SD_LOCK(_providerFramePoolMapLock); SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider]; if (!framePool) { framePool = [[SDImageFramePool alloc] init]; @@ -66,18 +76,23 @@ [self.providerFramePoolMap setObject:framePool forKey:provider]; } framePool.registerCount += 1; + SD_UNLOCK(_providerFramePoolMapLock); return framePool; } + (void)unregisterProvider:(id)provider { + // Lock to ensure atomic behavior + SD_LOCK(_providerFramePoolMapLock); SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider]; if (!framePool) { + SD_UNLOCK(_providerFramePoolMapLock); return; } framePool.registerCount -= 1; if (framePool.registerCount == 0) { [self.providerFramePoolMap removeObjectForKey:provider]; } + SD_UNLOCK(_providerFramePoolMapLock); } - (void)prefetchFrameAtIndex:(NSUInteger)index {