// // YYAnimatedImageView.m // YYKit // // Created by ibireme on 14/10/19. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "YYAnimatedImageView.h" #import "YYWeakProxy.h" #import "UIDevice+YYAdd.h" #import "YYImageCoder.h" #import "YYKitMacro.h" #define BUFFER_SIZE (10 * 1024 * 1024) // 10MB (minimum memory buffer size) #define LOCK(...) dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER); \ __VA_ARGS__; \ dispatch_semaphore_signal(self->_lock); #define LOCK_VIEW(...) dispatch_semaphore_wait(view->_lock, DISPATCH_TIME_FOREVER); \ __VA_ARGS__; \ dispatch_semaphore_signal(view->_lock); typedef NS_ENUM(NSUInteger, YYAnimatedImageType) { YYAnimatedImageTypeNone = 0, YYAnimatedImageTypeImage, YYAnimatedImageTypeHighlightedImage, YYAnimatedImageTypeImages, YYAnimatedImageTypeHighlightedImages, }; @interface YYAnimatedImageView() { @package UIImage *_curAnimatedImage; dispatch_semaphore_t _lock; ///< lock for _buffer NSOperationQueue *_requestQueue; ///< image request queue, serial CADisplayLink *_link; ///< ticker for change frame NSTimeInterval _time; ///< time after last frame UIImage *_curFrame; ///< current frame to display NSUInteger _curIndex; ///< current frame index (from 0) NSUInteger _totalFrameCount; ///< total frame count BOOL _loopEnd; ///< whether the loop is end. NSUInteger _curLoop; ///< current loop count (from 0) NSUInteger _totalLoop; ///< total loop count, 0 means infinity NSMutableDictionary *_buffer; ///< frame buffer BOOL _bufferMiss; ///< whether miss frame on last opportunity NSUInteger _maxBufferCount; ///< maximum buffer count NSInteger _incrBufferCount; ///< current allowed buffer count (will increase by step) CGRect _curContentsRect; BOOL _curImageHasContentsRect; ///< image has implementated "animatedImageContentsRectAtIndex:" } @property (nonatomic, readwrite) BOOL currentIsPlayingAnimation; - (void)calcMaxBufferCount; @end /// An operation for image fetch @interface _YYAnimatedImageViewFetchOperation : NSOperation @property (nonatomic, weak) YYAnimatedImageView *view; @property (nonatomic, assign) NSUInteger nextIndex; @property (nonatomic, strong) UIImage *curImage; @end @implementation _YYAnimatedImageViewFetchOperation - (void)main { __strong YYAnimatedImageView *view = _view; if (!view) return; if ([self isCancelled]) return; view->_incrBufferCount++; if (view->_incrBufferCount == 0) [view calcMaxBufferCount]; if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) { view->_incrBufferCount = view->_maxBufferCount; } NSUInteger idx = _nextIndex; NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount; NSUInteger total = view->_totalFrameCount; view = nil; for (int i = 0; i < max; i++, idx++) { @autoreleasepool { if (idx >= total) idx = 0; if ([self isCancelled]) break; __strong YYAnimatedImageView *view = _view; if (!view) break; LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil)); if (miss) { UIImage *img = [_curImage animatedImageFrameAtIndex:idx]; img = img.imageByDecoded; if ([self isCancelled]) break; LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); view = nil; } } } } @end @implementation YYAnimatedImageView - (instancetype)init { self = [super init]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; return self; } - (instancetype)initWithImage:(UIImage *)image { self = [super init]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; self.frame = (CGRect) {CGPointZero, image.size }; self.image = image; return self; } - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage { self = [super init]; _runloopMode = NSRunLoopCommonModes; _autoPlayAnimatedImage = YES; CGSize size = image ? image.size : highlightedImage.size; self.frame = (CGRect) {CGPointZero, size }; self.image = image; self.highlightedImage = highlightedImage; return self; } // init the animated params. - (void)resetAnimated { if (!_link) { _lock = dispatch_semaphore_create(1); _buffer = [NSMutableDictionary new]; _requestQueue = [[NSOperationQueue alloc] init]; _requestQueue.maxConcurrentOperationCount = 1; _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(step:)]; if (_runloopMode) { [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode]; } _link.paused = YES; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; } [_requestQueue cancelAllOperations]; LOCK( if (_buffer.count) { NSMutableDictionary *holder = _buffer; _buffer = [NSMutableDictionary new]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ // Capture the dictionary to global queue, // release these images in background to avoid blocking UI thread. [holder class]; }); } ); _link.paused = YES; _time = 0; if (_curIndex != 0) { [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = 0; [self didChangeValueForKey:@"currentAnimatedImageIndex"]; } _curAnimatedImage = nil; _curFrame = nil; _curLoop = 0; _totalLoop = 0; _totalFrameCount = 1; _loopEnd = NO; _bufferMiss = NO; _incrBufferCount = 0; } - (void)setImage:(UIImage *)image { if (self.image == image) return; [self setImage:image withType:YYAnimatedImageTypeImage]; } - (void)setHighlightedImage:(UIImage *)highlightedImage { if (self.highlightedImage == highlightedImage) return; [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage]; } - (void)setAnimationImages:(NSArray *)animationImages { if (self.animationImages == animationImages) return; [self setImage:animationImages withType:YYAnimatedImageTypeImages]; } - (void)setHighlightedAnimationImages:(NSArray *)highlightedAnimationImages { if (self.highlightedAnimationImages == highlightedAnimationImages) return; [self setImage:highlightedAnimationImages withType:YYAnimatedImageTypeHighlightedImages]; } - (void)setHighlighted:(BOOL)highlighted { [super setHighlighted:highlighted]; if (_link) [self resetAnimated]; [self imageChanged]; } - (id)imageForType:(YYAnimatedImageType)type { switch (type) { case YYAnimatedImageTypeNone: return nil; case YYAnimatedImageTypeImage: return self.image; case YYAnimatedImageTypeHighlightedImage: return self.highlightedImage; case YYAnimatedImageTypeImages: return self.animationImages; case YYAnimatedImageTypeHighlightedImages: return self.highlightedAnimationImages; } return nil; } - (YYAnimatedImageType)currentImageType { YYAnimatedImageType curType = YYAnimatedImageTypeNone; if (self.highlighted) { if (self.highlightedAnimationImages.count) curType = YYAnimatedImageTypeHighlightedImages; else if (self.highlightedImage) curType = YYAnimatedImageTypeHighlightedImage; } if (curType == YYAnimatedImageTypeNone) { if (self.animationImages.count) curType = YYAnimatedImageTypeImages; else if (self.image) curType = YYAnimatedImageTypeImage; } return curType; } - (void)setImage:(id)image withType:(YYAnimatedImageType)type { [self stopAnimating]; if (_link) [self resetAnimated]; _curFrame = nil; switch (type) { case YYAnimatedImageTypeNone: break; case YYAnimatedImageTypeImage: super.image = image; break; case YYAnimatedImageTypeHighlightedImage: super.highlightedImage = image; break; case YYAnimatedImageTypeImages: super.animationImages = image; break; case YYAnimatedImageTypeHighlightedImages: super.highlightedAnimationImages = image; break; } [self imageChanged]; } - (void)imageChanged { YYAnimatedImageType newType = [self currentImageType]; id newVisibleImage = [self imageForType:newType]; NSUInteger newImageFrameCount = 0; BOOL hasContentsRect = NO; if ([newVisibleImage isKindOfClass:[UIImage class]] && [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) { newImageFrameCount = ((UIImage *) newVisibleImage).animatedImageFrameCount; if (newImageFrameCount > 1) { hasContentsRect = [((UIImage *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)]; } } if (!hasContentsRect && _curImageHasContentsRect) { if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) { [CATransaction begin]; [CATransaction setDisableActions:YES]; self.layer.contentsRect = CGRectMake(0, 0, 1, 1); [CATransaction commit]; } } _curImageHasContentsRect = hasContentsRect; if (hasContentsRect) { CGRect rect = [((UIImage *) newVisibleImage) animatedImageContentsRectAtIndex:0]; [self setContentsRect:rect forImage:newVisibleImage]; } if (newImageFrameCount > 1) { [self resetAnimated]; _curAnimatedImage = newVisibleImage; _curFrame = newVisibleImage; _totalLoop = _curAnimatedImage.animatedImageLoopCount; _totalFrameCount = _curAnimatedImage.animatedImageFrameCount; [self calcMaxBufferCount]; } [self setNeedsDisplay]; [self didMoved]; } // dynamically adjust buffer size for current memory. - (void)calcMaxBufferCount { int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; if (bytes == 0) bytes = 1024; int64_t total = [UIDevice currentDevice].memoryTotal; int64_t free = [UIDevice currentDevice].memoryFree; int64_t max = MIN(total * 0.2, free * 0.6); max = MAX(max, BUFFER_SIZE); if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; double maxBufferCount = (double)max / (double)bytes; maxBufferCount = YY_CLAMP(maxBufferCount, 1, 512); _maxBufferCount = maxBufferCount; } - (void)dealloc { [_requestQueue cancelAllOperations]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidEnterBackgroundNotification object:nil]; [_link invalidate]; } - (BOOL)isAnimating { return self.currentIsPlayingAnimation; } - (void)stopAnimating { [super stopAnimating]; [_requestQueue cancelAllOperations]; _link.paused = YES; self.currentIsPlayingAnimation = NO; } - (void)startAnimating { YYAnimatedImageType type = [self currentImageType]; if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) { NSArray *images = [self imageForType:type]; if (images.count > 0) { [super startAnimating]; self.currentIsPlayingAnimation = YES; } } else { if (_curAnimatedImage && _link.paused) { _curLoop = 0; _loopEnd = NO; _link.paused = NO; self.currentIsPlayingAnimation = YES; } } } - (void)didReceiveMemoryWarning:(NSNotification *)notification { [_requestQueue cancelAllOperations]; [_requestQueue addOperationWithBlock: ^{ _incrBufferCount = -60 - (int)(arc4random() % 120); // about 1~3 seconds to grow back.. NSNumber *next = @((_curIndex + 1) % _totalFrameCount); LOCK( NSArray * keys = _buffer.allKeys; for (NSNumber * key in keys) { if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation [_buffer removeObjectForKey:key]; } } )//LOCK }]; } - (void)didEnterBackground:(NSNotification *)notification { [_requestQueue cancelAllOperations]; NSNumber *next = @((_curIndex + 1) % _totalFrameCount); LOCK( NSArray * keys = _buffer.allKeys; for (NSNumber * key in keys) { if (![key isEqualToNumber:next]) { // keep the next frame for smoothly animation [_buffer removeObjectForKey:key]; } } )//LOCK } - (void)step:(CADisplayLink *)link { UIImage *image = _curAnimatedImage; NSMutableDictionary *buffer = _buffer; UIImage *bufferedImage = nil; NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount; BOOL bufferIsFull = NO; if (!image) return; if (_loopEnd) { // view will keep in last frame [self stopAnimating]; return; } NSTimeInterval delay = 0; if (!_bufferMiss) { _time += link.duration; delay = [image animatedImageDurationAtIndex:_curIndex]; if (_time < delay) return; _time -= delay; if (nextIndex == 0) { _curLoop++; if (_curLoop >= _totalLoop && _totalLoop != 0) { _loopEnd = YES; [self stopAnimating]; [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep return; // stop at last frame } } delay = [image animatedImageDurationAtIndex:nextIndex]; if (_time > delay) _time = delay; // do not jump over frame } LOCK( bufferedImage = buffer[@(nextIndex)]; if (bufferedImage) { if ((int)_incrBufferCount < _totalFrameCount) { [buffer removeObjectForKey:@(nextIndex)]; } [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = nextIndex; [self didChangeValueForKey:@"currentAnimatedImageIndex"]; _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage; if (_curImageHasContentsRect) { _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex]; [self setContentsRect:_curContentsRect forImage:_curFrame]; } nextIndex = (_curIndex + 1) % _totalFrameCount; _bufferMiss = NO; if (buffer.count == _totalFrameCount) { bufferIsFull = YES; } } else { _bufferMiss = YES; } )//LOCK if (!_bufferMiss) { [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep } if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new]; operation.view = self; operation.nextIndex = nextIndex; operation.curImage = image; [_requestQueue addOperation:operation]; } } - (void)displayLayer:(CALayer *)layer { if (_curFrame) { layer.contents = (__bridge id)_curFrame.CGImage; } } - (void)setContentsRect:(CGRect)rect forImage:(UIImage *)image{ CGRect layerRect = CGRectMake(0, 0, 1, 1); if (image) { CGSize imageSize = image.size; if (imageSize.width > 0.01 && imageSize.height > 0.01) { layerRect.origin.x = rect.origin.x / imageSize.width; layerRect.origin.y = rect.origin.y / imageSize.height; layerRect.size.width = rect.size.width / imageSize.width; layerRect.size.height = rect.size.height / imageSize.height; layerRect = CGRectIntersection(layerRect, CGRectMake(0, 0, 1, 1)); if (CGRectIsNull(layerRect) || CGRectIsEmpty(layerRect)) { layerRect = CGRectMake(0, 0, 1, 1); } } } [CATransaction begin]; [CATransaction setDisableActions:YES]; self.layer.contentsRect = layerRect; [CATransaction commit]; } - (void)didMoved { if (self.autoPlayAnimatedImage) { if(self.superview && self.window) { [self startAnimating]; } else { [self stopAnimating]; } } } - (void)didMoveToWindow { [super didMoveToWindow]; [self didMoved]; } - (void)didMoveToSuperview { [super didMoveToSuperview]; [self didMoved]; } - (void)setCurrentAnimatedImageIndex:(NSUInteger)currentAnimatedImageIndex { if (!_curAnimatedImage) return; if (currentAnimatedImageIndex >= _curAnimatedImage.animatedImageFrameCount) return; if (_curIndex == currentAnimatedImageIndex) return; dispatch_async_on_main_queue(^{ LOCK( [_requestQueue cancelAllOperations]; [_buffer removeAllObjects]; [self willChangeValueForKey:@"currentAnimatedImageIndex"]; _curIndex = currentAnimatedImageIndex; [self didChangeValueForKey:@"currentAnimatedImageIndex"]; _curFrame = [_curAnimatedImage animatedImageFrameAtIndex:_curIndex]; if (_curImageHasContentsRect) { _curContentsRect = [_curAnimatedImage animatedImageContentsRectAtIndex:_curIndex]; } _time = 0; _loopEnd = NO; _bufferMiss = NO; [self.layer setNeedsDisplay]; )//LOCK }); } - (NSUInteger)currentAnimatedImageIndex { return _curIndex; } - (void)setRunloopMode:(NSString *)runloopMode { if ([_runloopMode isEqual:runloopMode]) return; if (_link) { if (_runloopMode) { [_link removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode]; } if (runloopMode.length) { [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:runloopMode]; } } _runloopMode = runloopMode.copy; } #pragma mark - Overrice NSObject(NSKeyValueObservingCustomization) + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"currentAnimatedImageIndex"]) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; } #pragma mark - NSCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; _runloopMode = [aDecoder decodeObjectForKey:@"runloopMode"]; if (_runloopMode.length == 0) _runloopMode = NSRunLoopCommonModes; if ([aDecoder containsValueForKey:@"autoPlayAnimatedImage"]) { _autoPlayAnimatedImage = [aDecoder decodeBoolForKey:@"autoPlayAnimatedImage"]; } else { _autoPlayAnimatedImage = YES; } UIImage *image = [aDecoder decodeObjectForKey:@"YYAnimatedImage"]; UIImage *highlightedImage = [aDecoder decodeObjectForKey:@"YYHighlightedAnimatedImage"]; if (image) { self.image = image; [self setImage:image withType:YYAnimatedImageTypeImage]; } if (highlightedImage) { self.highlightedImage = highlightedImage; [self setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:_runloopMode forKey:@"runloopMode"]; [aCoder encodeBool:_autoPlayAnimatedImage forKey:@"autoPlayAnimatedImage"]; BOOL ani, multi; ani = [self.image conformsToProtocol:@protocol(YYAnimatedImage)]; multi = (ani && ((UIImage *)self.image).animatedImageFrameCount > 1); if (multi) [aCoder encodeObject:self.image forKey:@"YYAnimatedImage"]; ani = [self.highlightedImage conformsToProtocol:@protocol(YYAnimatedImage)]; multi = (ani && ((UIImage *)self.highlightedImage).animatedImageFrameCount > 1); if (multi) [aCoder encodeObject:self.highlightedImage forKey:@"YYHighlightedAnimatedImage"]; } @end