diff --git a/SDWebImage/Core/SDAnimatedImageView.h b/SDWebImage/Core/SDAnimatedImageView.h index ec5bda13..abf4e5d7 100644 --- a/SDWebImage/Core/SDAnimatedImageView.h +++ b/SDWebImage/Core/SDAnimatedImageView.h @@ -58,6 +58,20 @@ */ @property (nonatomic, assign) BOOL shouldIncrementalLoad; +/** + Whether or not to clear the frame buffer cache when animation stopped. See `maxBufferSize` + This is useful when you want to limit the memory usage during frequently visibility changes (such as image view inside a list view, then push and pop) + Default is NO. + */ +@property (nonatomic, assign) BOOL clearBufferWhenStopped; + +/** + Whether or not to reset the current frame index when animation stopped. + For some of use case, you may want to reset the frame index to 0 when stop, but some other want to keep the current frame index. + Default is NO. + */ +@property (nonatomic, assign) BOOL resetFrameIndexWhenStopped; + #if SD_UIKIT /** You can specify a runloop mode to let it rendering. diff --git a/SDWebImage/Core/SDAnimatedImageView.m b/SDWebImage/Core/SDAnimatedImageView.m index 9c4dddd0..0adcfd61 100644 --- a/SDWebImage/Core/SDAnimatedImageView.m +++ b/SDWebImage/Core/SDAnimatedImageView.m @@ -149,21 +149,15 @@ static NSUInteger SDDeviceFreeMemory() { self.animatedImage = nil; self.totalFrameCount = 0; self.totalLoopCount = 0; - self.currentFrame = nil; - self.currentFrameIndex = 0; - self.currentLoopCount = 0; - self.currentTime = 0; - self.bufferMiss = NO; + // reset current state + [self resetCurrentFrameIndex]; self.shouldAnimate = NO; self.isProgressive = NO; self.maxBufferCount = 0; self.animatedImageScale = 1; [_fetchQueue cancelAllOperations]; - _fetchQueue = nil; - SD_LOCK(self.lock); - [_frameBuffer removeAllObjects]; - _frameBuffer = nil; - SD_UNLOCK(self.lock); + // clear buffer cache + [self clearFrameBuffer]; } - (void)resetProgressiveImage @@ -179,6 +173,22 @@ static NSUInteger SDDeviceFreeMemory() { // preserve buffer cache } +- (void)resetCurrentFrameIndex +{ + self.currentFrame = nil; + self.currentFrameIndex = 0; + self.currentLoopCount = 0; + self.currentTime = 0; + self.bufferMiss = NO; +} + +- (void)clearFrameBuffer +{ + SD_LOCK(self.lock); + [_frameBuffer removeAllObjects]; + SD_UNLOCK(self.lock); +} + #pragma mark - Accessors #pragma mark Public @@ -466,6 +476,12 @@ static NSUInteger SDDeviceFreeMemory() { #else _displayLink.paused = YES; #endif + if (self.resetFrameIndexWhenStopped) { + [self resetCurrentFrameIndex]; + } + if (self.clearBufferWhenStopped) { + [self clearFrameBuffer]; + } } else { #if SD_UIKIT [super stopAnimating]; diff --git a/Tests/Tests/SDAnimatedImageTest.m b/Tests/Tests/SDAnimatedImageTest.m index 573bd5b3..cc04cd0d 100644 --- a/Tests/Tests/SDAnimatedImageTest.m +++ b/Tests/Tests/SDAnimatedImageTest.m @@ -16,6 +16,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun @interface SDAnimatedImageView () @property (nonatomic, assign) BOOL isProgressive; +@property (nonatomic, strong) NSMutableDictionary *frameBuffer; @end @@ -206,6 +207,7 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun #else imageView.animates = NO; #endif + [imageView removeFromSuperview]; [expectation fulfill]; } }]; @@ -298,6 +300,80 @@ static const NSUInteger kTestGIFFrameCount = 5; // local TestImage.gif loop coun [self waitForExpectationsWithCommonTimeout]; } +- (void)test25AnimatedImageStopAnimatingNormal { + XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView stopAnimating normal behavior"]; + + SDAnimatedImageView *imageView = [SDAnimatedImageView new]; + +#if SD_UIKIT + [self.window addSubview:imageView]; +#else + [self.window.contentView addSubview:imageView]; +#endif + // This APNG duration is 2s + SDAnimatedImage *image = [SDAnimatedImage imageWithData:[self testAPNGPData]]; + imageView.image = image; + + 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.frameBuffer.count).beGreaterThan(0); + expect(imageView.currentFrameIndex).beGreaterThan(0); + }); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ +#if SD_UIKIT + [imageView stopAnimating]; +#else + imageView.animates = NO; +#endif + expect(imageView.frameBuffer.count).beGreaterThan(0); + expect(imageView.currentFrameIndex).beGreaterThan(0); + + [imageView removeFromSuperview]; + [expectation fulfill]; + }); + + [self waitForExpectationsWithCommonTimeout]; +} + +- (void)test25AnimatedImageStopAnimatingClearBuffer { + XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView stopAnimating clear buffer when stopped"]; + + SDAnimatedImageView *imageView = [SDAnimatedImageView new]; + imageView.clearBufferWhenStopped = YES; + imageView.resetFrameIndexWhenStopped = YES; + +#if SD_UIKIT + [self.window addSubview:imageView]; +#else + [self.window.contentView addSubview:imageView]; +#endif + // This APNG duration is 2s + SDAnimatedImage *image = [SDAnimatedImage imageWithData:[self testAPNGPData]]; + imageView.image = image; + + 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.frameBuffer.count).beGreaterThan(0); + expect(imageView.currentFrameIndex).beGreaterThan(0); + }); + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ +#if SD_UIKIT + [imageView stopAnimating]; +#else + imageView.animates = NO; +#endif + expect(imageView.frameBuffer.count).equal(0); + expect(imageView.currentFrameIndex).equal(0); + + [imageView removeFromSuperview]; + [expectation fulfill]; + }); + + [self waitForExpectationsWithCommonTimeout]; +} + #pragma mark - Helper - (UIWindow *)window { if (!_window) {