Merge pull request #3524 from dreampiggy/feature/sdanimatedimage_frame_pool
Performance: 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:
commit
c27e18506d
|
@ -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 */,
|
||||
|
|
|
@ -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);
|
||||
NSRunLoopMode _runLoopMode;
|
||||
}
|
||||
|
||||
@property (nonatomic, 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) NSUInteger currentFrameBytes;
|
||||
@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,10 +47,7 @@
|
|||
self.totalLoopCount = provider.animatedImageLoopCount;
|
||||
self.animatedProvider = provider;
|
||||
self.playbackRate = 1.0;
|
||||
SD_LOCK_INIT(_lock);
|
||||
#if SD_UIKIT
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
#endif
|
||||
self.framePool = [SDImageFramePool registerProvider:provider];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -60,47 +57,12 @@
|
|||
return player;
|
||||
}
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
|
||||
- (void)dealloc {
|
||||
#if SD_UIKIT
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (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];
|
||||
// Dereference the frame pool, when zero the frame pool for provider will dealloc
|
||||
[SDImageFramePool unregisterProvider:self.animatedProvider];
|
||||
}
|
||||
|
||||
#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) {
|
||||
|
@ -153,11 +115,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;
|
||||
SD_LOCK(self->_lock);
|
||||
self.frameBuffer[@(self.currentFrameIndex)] = posterFrame;
|
||||
SD_UNLOCK(self->_lock);
|
||||
[self.framePool setFrame:posterFrame atIndex:self.currentFrameIndex];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,9 +136,7 @@
|
|||
}
|
||||
|
||||
- (void)clearFrameBuffer {
|
||||
SD_LOCK(_lock);
|
||||
[_frameBuffer removeAllObjects];
|
||||
SD_UNLOCK(_lock);
|
||||
[self.framePool removeAllFrames];
|
||||
}
|
||||
|
||||
#pragma mark - Animation Control
|
||||
|
@ -184,12 +144,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.
|
||||
|
@ -197,7 +154,6 @@
|
|||
}
|
||||
|
||||
- (void)pausePlaying {
|
||||
[_fetchQueue cancelAllOperations];
|
||||
[_displayLink stop];
|
||||
}
|
||||
|
||||
|
@ -259,26 +215,11 @@
|
|||
|
||||
|
||||
// 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];
|
||||
|
||||
// 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) {
|
||||
bufferFull = YES;
|
||||
}
|
||||
SD_UNLOCK(_lock);
|
||||
|
||||
// Update the current frame immediately
|
||||
self.currentFrame = currentFrame;
|
||||
[self handleFrameChange];
|
||||
|
@ -300,8 +241,7 @@
|
|||
if (self.currentTime < currentDuration) {
|
||||
// Current frame timestamp not reached, prefetch frame in advance.
|
||||
[self prefetchFrameAtIndex:currentFrameIndex
|
||||
nextIndex:nextFrameIndex
|
||||
bufferFull:bufferFull];
|
||||
nextIndex:nextFrameIndex];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -337,43 +277,29 @@
|
|||
}
|
||||
|
||||
[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;
|
||||
SD_LOCK(_lock);
|
||||
fetchFrame = self.frameBuffer[@(nextIndex)];
|
||||
SD_UNLOCK(_lock);
|
||||
fetchFrame = [self.framePool frameAtIndex:nextIndex];
|
||||
}
|
||||
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];
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -390,9 +316,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) {
|
||||
|
@ -410,7 +344,7 @@
|
|||
maxBufferCount = 1;
|
||||
}
|
||||
|
||||
self.maxBufferCount = maxBufferCount;
|
||||
self.framePool.maxBufferCount = maxBufferCount;
|
||||
}
|
||||
|
||||
+ (NSString *)defaultRunLoopMode {
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
// Lock to ensure atomic behavior
|
||||
SD_LOCK_DECLARE_STATIC(_providerFramePoolMapLock);
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
#pragma mark - Life Cycle
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_frameBuffer = [NSMutableDictionary dictionary];
|
||||
_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];
|
||||
}
|
||||
|
||||
+ (void)initialize {
|
||||
// Lock to ensure atomic behavior
|
||||
SD_LOCK_INIT(_providerFramePoolMapLock);
|
||||
}
|
||||
|
||||
+ (instancetype)registerProvider:(id<SDAnimatedImageProvider>)provider {
|
||||
// Lock to ensure atomic behavior
|
||||
SD_LOCK(_providerFramePoolMapLock);
|
||||
SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider];
|
||||
if (!framePool) {
|
||||
framePool = [[SDImageFramePool alloc] init];
|
||||
framePool.provider = provider;
|
||||
[self.providerFramePoolMap setObject:framePool forKey:provider];
|
||||
}
|
||||
framePool.registerCount += 1;
|
||||
SD_UNLOCK(_providerFramePoolMapLock);
|
||||
return framePool;
|
||||
}
|
||||
|
||||
+ (void)unregisterProvider:(id<SDAnimatedImageProvider>)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 {
|
||||
@synchronized (self) {
|
||||
NSUInteger frameCount = self.frameBuffer.count;
|
||||
if (frameCount > self.maxBufferCount) {
|
||||
// Remove the frame buffer if need
|
||||
// TODO, use LRU or better algorithm to detect which frames to clear
|
||||
self.frameBuffer[@(index - 1)] = nil;
|
||||
self.frameBuffer[@(index + 1)] = 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];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
#import "SDTestCase.h"
|
||||
#import "SDInternalMacros.h"
|
||||
#import "SDImageFramePool.h"
|
||||
#import <KVOController/KVOController.h>
|
||||
#import <SDWebImageWebPCoder/SDWebImageWebPCoder.h>
|
||||
|
||||
|
@ -59,7 +60,7 @@ static BOOL _isCalled;
|
|||
|
||||
@interface SDAnimatedImagePlayer ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIImage *> *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];
|
||||
|
|
Loading…
Reference in New Issue