YYImage 播放动图原理分析

#一、通常使用动图有以下几种方式

  • GIF

GIF是一种位图。位图的大致原理是:图片由许多的像素组成,每一个像素都被指定了一种颜色,这些像素综合起来就构成了图片。GIF采用的是Lempel-Zev-Welch(LZW)压缩算法,最高支持256种颜色。由于这种特性,GIF比较适用于色彩较少的图片,比如卡通造型、公司标志等等。如果碰到需要用真彩色的场合,那么GIF的表现力就有限了。GIF通常会自带一个调色板,里面存放需要用到的各种颜色。在Web运用中,图像的文件量的大小将会明显地影响到下载的速度,因此我们可以根据GIF带调色板的特性来优化调色板,减少图像使用的颜色数(有些图像用不到的颜色可以舍去),而不影响到图片的质量(摘自百度百科)
我通常把它理解为:
gif 其实是一个帧动画序列,即将一个图片的集合压缩到gif文件格式中,比如下面这张:

  • 帧动画
  • SVG动画

二、gif播放原理

  • 获取gif文件中每一帧的图像数据然后按照一定的时间间隔依次按顺序呈现它

三、YYImage播放原理

  • 解析gif文件中的所有图片帧数据并将它缓存一个字典中,其中以图片数据在gif中的索引作为key,Image数据作为值建立一个字典,每次更新图片帧数据时从这个字典中获取。代码如下:
///  根据UIImage先创建一个可视的l图像的矩形,w使当前的UIImageViewl  Layer图像呈现层与其尺寸一致
- (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];
}

重点落在 [self resetAnimated]; 这个函数

- (void)resetAnimated {
    if (!_link) {
        _lock = dispatch_semaphore_create(1);
        _buffer = [NSMutableDictionary new];
        _requestQueue = [[NSOperationQueue alloc] init];
        _requestQueue.maxConcurrentOperationCount = 1;
        _link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy 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;
}

在这个函数接口中,做了这么几件事:

  • 创建了一个NSOperation,并且设置最大并发数为1。结果很明显,作者想让它串行执行
    _requestQueue = [[NSOperationQueue alloc] init];
    _requestQueue.maxConcurrentOperationCount = 1;
  • 创建一个CADisplayLink 对象并将它添加到了runloop中,关于CADisplayLink与NSTimer的关系及用处可参考这编文章:
    https://www.cnblogs.com/oc-bowen/p/8665465.html
    在动态呈现图片帧时作者没有采用NSTimer方案,而是采用了NSDisplayLink,目的为了让gif中的图片帧数据保持一个较高的帧率,显示效果更流畅,如果这里对gif效果要求不高的话换成NSTimer也是可以的。仔细观察CADisplayLink的初始可以知道在设置target时使用了一个弱引用对象_YYImageWeakProxy。
 _link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
        if (_runloopMode) {
            [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
        }

分析它的引用链得知:
self持有 _link(_link添加到了主runLoop中)的引用,同时的runloop的selector回调中step中引用了self._curAnimatedImage等成员变量,从而造成了循环引用。为防止内存泄露此处使用弱引用。

#三、图片帧的驱动方式

  • 通过更新索引_curIndex的值,从_buffer获取gif中的图片帧数据,然后更新UIImageView当前显示的内容,代码如下:
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

使用缓存的目的是为了避免获取下一帧图片数据时再从gif文件中进行解析,因解析gif中涉及到IO操作。
缓存到字典的代码为:

- (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.yy_imageByDecoded;
                if ([self isCancelled]) break;
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
                view = nil;
            }
        }
    }
}

这个缓存操作是开了一线程进行的,主要是因为在进行gif解析时,缓存的是UIImage解码之后的数据:

 UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
  img = img.yy_imageByDecoded;
  if ([self isCancelled]) break;
  LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
 view = nil;

图片解码是一个比较耗时的操作,所以选择在子线程中进行,对于解码的知识可阅读这编文章:
https://www.jianshu.com/p/f9ef5dba9ba3?_dt_push=1

四、总结

  • YYImage 播放动图用到了这么几个知识点:
  • 使用CADisplayLink+RunLoop 驱动图片帧的更换
  • 在子线程中缓存图片的解码工作,提升性能
  • 在设置CADisplay的回调接口中使用了弱引用防止内存泄露

你可能感兴趣的:(IOS,YYImage原理分析,IOS,播放GIF原理分析)