FLAnimatedImage源码剖析

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源码剖析)