Introduce frame pool for SDAnimatedImage playback. Solve when multiple image view references the same URL image cause un-wanted decode which waste RAM/CPU

This commit is contained in:
DreamPiggy 2023-04-25 18:35:12 +08:00
parent 3289629ef6
commit a206229905
4 changed files with 263 additions and 77 deletions

View File

@ -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 = "<group>"; };
321E60BC1F38E91700405457 /* UIImage+ForceDecode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "UIImage+ForceDecode.h"; path = "Core/UIImage+ForceDecode.h"; sourceTree = "<group>"; };
321E60BD1F38E91700405457 /* UIImage+ForceDecode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ForceDecode.m"; path = "Core/UIImage+ForceDecode.m"; sourceTree = "<group>"; };
3237321229F8D0D600D1DA41 /* SDImageFramePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDImageFramePool.h; sourceTree = "<group>"; };
3237321329F8D0D600D1DA41 /* SDImageFramePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDImageFramePool.m; sourceTree = "<group>"; };
3240BB6623968FE6003BA07D /* SDAssociatedObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDAssociatedObject.h; sourceTree = "<group>"; };
3240BB6723968FE6003BA07D /* SDAssociatedObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDAssociatedObject.m; sourceTree = "<group>"; };
324406292296C5F400A36084 /* SDWebImageOptionsProcessor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SDWebImageOptionsProcessor.h; path = Core/SDWebImageOptionsProcessor.h; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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<SDAnimatedImageProvider> animatedProvider;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *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<NSNumber *,UIImage *> *)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<SDAnimatedImageProvider> 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<SDAnimatedImageProvider> 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 {

View File

@ -0,0 +1,40 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import <Foundation/Foundation.h>
#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<SDAnimatedImageProvider>)provider;
/// Unregister a frame pool, also decrease reference count, if zero dealloc the frame pool
+ (void)unregisterProvider:(id<SDAnimatedImageProvider>)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

View File

@ -0,0 +1,140 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* 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<SDAnimatedImageProvider> provider;
@property (atomic) NSUInteger registerCount;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *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<SDAnimatedImageProvider>)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<SDAnimatedImageProvider>)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<SDAnimatedImageProvider> 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<NSNumber *,UIImage *> *)frameBuffer {
if (!_frameBuffer) {
_frameBuffer = [NSMutableDictionary dictionary];
}
return _frameBuffer;
}
@end