美文网首页
FLAnimatedImage源码剖析

FLAnimatedImage源码剖析

作者: Ericgogo | 来源:发表于2019-07-10 13:16 被阅读0次

    FLAnimatedImage iOS平台上播放GIF动画的一个优秀解决方案,支持可变帧间延时、内存内存表现良好、播放流畅等特点。

    FLAnimatedImage有两个类:

    • FLAnimatedImage 用来解析、封装GIF图像信息 (GIF帧数、GIF size、播放循环次数、posterImage、帧间延时)
    • FLAnimatedImageView 用来控制GIF的播放

    FLAnimatedImage

    GIF图像信息的解析,关键代码:
    关键是获取循环次数、帧间延时delayTimesForIndexesMutable, 用到了底层的CGImageSourceRef

    _imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data,
                                                       (__bridge CFDictionaryRef)@{(NSString *)kCGImageSourceShouldCache: @NO});
            // Early return on failure!
            if (!_imageSource) {
                FLLog(FLLogLevelError, @"Failed to `CGImageSourceCreateWithData` for animated GIF data %@", data);
                return nil;
            }
            
            // Early return if not GIF!
            CFStringRef imageSourceContainerType = CGImageSourceGetType(_imageSource);
            BOOL isGIFData = UTTypeConformsTo(imageSourceContainerType, kUTTypeGIF);
            if (!isGIFData) {
                FLLog(FLLogLevelError, @"Supplied data is of type %@ and doesn't seem to be GIF data %@", imageSourceContainerType, data);
                return nil;
            }
            
            NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(_imageSource, NULL);
            _loopCount = [[[imageProperties objectForKey:(id)kCGImagePropertyGIFDictionary] objectForKey:(id)kCGImagePropertyGIFLoopCount] unsignedIntegerValue];
            
            // Iterate through frame images
            size_t imageCount = CGImageSourceGetCount(_imageSource);
            NSUInteger skippedFrameCount = 0;
            NSMutableDictionary *delayTimesForIndexesMutable = [NSMutableDictionary dictionaryWithCapacity:imageCount];
            for (size_t i = 0; i < imageCount; i++) {
                @autoreleasepool {
                    CGImageRef frameImageRef = CGImageSourceCreateImageAtIndex(_imageSource, i, NULL);
                    if (frameImageRef) {
                        UIImage *frameImage = [UIImage imageWithCGImage:frameImageRef];
                        // Check for valid `frameImage` before parsing its properties as frames can be corrupted (and `frameImage` even `nil` when `frameImageRef` was valid).
                        if (frameImage) {
                            // Set poster image
                            if (!self.posterImage) {
                                _posterImage = frameImage;
                                // Set its size to proxy our size.
                                _size = _posterImage.size;
                                // Remember index of poster image so we never purge it; also add it to the cache.
                                _posterImageFrameIndex = i;
                                [self.cachedFramesForIndexes setObject:self.posterImage forKey:@(self.posterImageFrameIndex)];
                                [self.cachedFrameIndexes addIndex:self.posterImageFrameIndex];
                            }
                            
                            NSDictionary *frameProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(_imageSource, i, NULL);
                            NSDictionary *framePropertiesGIF = [frameProperties objectForKey:(id)kCGImagePropertyGIFDictionary];
                            
                            // Try to use the unclamped delay time; fall back to the normal delay time.
                            NSNumber *delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFUnclampedDelayTime];
                            if (!delayTime) {
                                delayTime = [framePropertiesGIF objectForKey:(id)kCGImagePropertyGIFDelayTime];
                            }
                            
                            delayTimesForIndexesMutable[@(i)] = delayTime;
                        } else {
                            skippedFrameCount++;
                        }
                        CFRelease(frameImageRef);
                    } else {
                        skippedFrameCount++;
                    }
                }
            }
    

    FLAnimatedImage有一个关键接口imageLazilyCachedAtIndex 用于获取某一帧对应的Image。
    关键思想是:内存管理、内存警告处理、缓存帧管理、子线程异步加载
    imageLazilyCachedAtIndex 获取某一帧的时候,会进行前面几帧的预加载,如果获取的一帧还没加载完成,那么会返回 nil 值,避免卡顿的情况。

    FLAnimatedImageView

    FLAnimatedImageView的职责是绘制GIF动画。
    那么如何绘制动画?如何驱动动画的绘制?怎么绘制?

    驱动的关键是CADisplayLink

    - (void)startAnimating
    {
        if (self.animatedImage) {
            // Lazily create the display link.
            if (!self.displayLink) {
                // It is important to note the use of a weak proxy here to avoid a retain cycle. `-displayLinkWithTarget:selector:`
                // will retain its target until it is invalidated. We use a weak proxy so that the image view will get deallocated
                // independent of the display link's lifetime. Upon image view deallocation, we invalidate the display
                // link which will lead to the deallocation of both the display link and the weak proxy.
                FLWeakProxy *weakProxy = [FLWeakProxy weakProxyForObject:self];
                self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
                
                [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];
            }
    
            // Note: The display link's `.frameInterval` value of 1 (default) means getting callbacks at the refresh rate of the display (~60Hz).
            // Setting it to 2 divides the frame rate by 2 and hence calls back at every other display refresh.
            const NSTimeInterval kDisplayRefreshRate = 60.0; // 60Hz
            self.displayLink.frameInterval = MAX([self frameDelayGreatestCommonDivisor] * kDisplayRefreshRate, 1);
    
            self.displayLink.paused = NO;
        } else {
            [super startAnimating];
        }
    }
    

    注意:其中NSRunLoop的mode设置, NSDefaultRunLoopMode 时,滑动scrollview时,GIF会暂停播放,NSRunLoopCommonModes模式是不会暂停。

    其中,有一个问题:使用CADisplayLink如何避免循环引用?
    CADisplayLink的target是retain这个target, 而displayLink会add到主线程的Runloop中,就会形成 Runloop -> CADisplayLink -> self 的引用关系。
    解决办法是使用FLWeakProxy 弱引用self, 这样引用关系变成了 Runloop -> CADisplayLink -> WeakProxy, WeakProxy再弱引用self。当self释放时移除CADisplayLink,这样就避免了循环引用。

    - (void)dealloc
    {
        [_displayLink invalidate];
    }
    
    
    绘制

    有了驱动,如何绘制?
    CADisplayLink的回调中:
    - (void)displayDidRefresh:(CADisplayLink *)displayLink
    实现了loopCount控制、帧Index计数、延时管理(不能播放太快,也不能太慢!)
    看源码:

    - (void)displayDidRefresh:(CADisplayLink *)displayLink
    {   
        NSNumber *delayTimeNumber = [self.animatedImage.delayTimesForIndexes objectForKey:@(self.currentFrameIndex)];
        // If we don't have a frame delay (e.g. corrupt frame), don't update the view but skip the playhead to the next frame (in else-block).
        if (delayTimeNumber) {
            NSTimeInterval delayTime = [delayTimeNumber floatValue];
            // If we have a nil image (e.g. waiting for frame), don't update the view nor playhead.
            UIImage *image = [self.animatedImage imageLazilyCachedAtIndex:self.currentFrameIndex];
            if (image) {
                FLLog(FLLogLevelVerbose, @"Showing frame %lu for animated image: %@", (unsigned long)self.currentFrameIndex, self.animatedImage);
                self.currentFrame = image;          //更新当前currentFrame,在绘制的时候使用
                if (self.needsDisplayWhenImageBecomesAvailable) {
                    [self.layer setNeedsDisplay];
                    self.needsDisplayWhenImageBecomesAvailable = NO;
                }
                
                self.accumulator += displayLink.duration * displayLink.frameInterval;
                
                // While-loop first inspired by & good Karma to: https://github.com/ondalabs/OLImageView/blob/master/OLImageView.m
                while (self.accumulator >= delayTime) { 
                    self.accumulator -= delayTime;
                    self.currentFrameIndex++;
                    if (self.currentFrameIndex >= self.animatedImage.frameCount) {
                        // If we've looped the number of times that this animated image describes, stop looping.
                        self.loopCountdown--;
                        if (self.loopCompletionBlock) {
                            self.loopCompletionBlock(self.loopCountdown);
                        }
                        
                        if (self.loopCountdown == 0) {
                            [self stopAnimating];
                            return;
                        }
                        self.currentFrameIndex = 0;
                    }
                    // Calling `-setNeedsDisplay` will just paint the current frame, not the new frame that we may have moved to.
                    // Instead, set `needsDisplayWhenImageBecomesAvailable` to `YES` -- this will paint the new image once loaded.
                    self.needsDisplayWhenImageBecomesAvailable = YES;
                }
            } else {
            }
        } else {
            self.currentFrameIndex++;
        }
    

    特别注意的是其中while的设计,是为了在本次DisplayLink中拿到正确的currentFrameIndex

    绘制

    非常简单,拿到GIF帧的图片后,直接显示:

    - (void)displayLayer:(CALayer *)layer
    {
        layer.contents = (__bridge id)self.image.CGImage;
    }
    
    - (UIImage *)image
    {
        UIImage *image = nil;
        if (self.animatedImage) {
            // Initially set to the poster image.
            image = self.currentFrame;
        } else {
            image = super.image;
        }
        return image;
    }
    

    相关文章

      网友评论

          本文标题:FLAnimatedImage源码剖析

          本文链接:https://www.haomeiwen.com/subject/zvgckctx.html