iOS之深入剖析YYImage的图片处理原理

眉间皱痕 提交于 2020-08-05 13:23:16

YYImage的使用

特性

  • 支持WebP、APNG、GIF类型动画图像的播放/编码/解码
  • 支持WebP、PNG、GIF、JPEG、JP2、TIFF、BMP、ICO、ICNS类型静态图像的显示/编码/解码
  • 支持PNG、GIF、JPEG、BMP类型图片的渐进式/逐行扫描/隔行扫描解码
  • 支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet 动画
  • 高效的动态内存缓存管理,以保证高性能低内存的动画播放;
  • 完全兼容 UIImage 和 UIImageView,使用方便;
  • 保留可扩展的接口,以支持自定义动画;
  • 每个类和方法都有完善的文档注释。

基本用法

  • 显示动画类型图片
    UIImage *image         = [YYImage imageNamed:@"animation.gif"];
    UIImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];
  • 播放帧动画
    // frame1.png, frame2.png, frame3.png三张图片
    NSArray *paths         = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
    NSArray *times         = @[@0.1, @0.2, @0.1];
    UIImage *image         = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
    UIImageView *imageView = [YYAnimatedImageView alloc] initWithImage:image];
    [self.view addSubview:imageView];
  • 播放 sprite sheet 动画
    // 8 * 12 sprites in a single sheet image
    UIImage *spriteSheet = [UIImage imageNamed:@"sprite-sheet"];
    NSMutableArray *contentRects = [NSMutableArray new];
    NSMutableArray *durations = [NSMutableArray new];
    for (int j = 0; j < 12; j++) {
       for (int i = 0; i < 8; i++) {
           CGRect rect;
           rect.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
           rect.origin.x = img.size.width / 8 * i;
           rect.origin.y = img.size.height / 12 * j;
           [contentRects addObject:[NSValue valueWithCGRect:rect]];
           [durations addObject:@(1 / 60.0)];
       }
    }
    YYSpriteSheetImage *sprite;
    sprite = [[YYSpriteSheetImage alloc] initWithSpriteSheetImage:img
                                                    contentRects:contentRects
                                                  frameDurations:durations
                                                       loopCount:0];
    YYAnimatedImageView *imageView = [YYAnimatedImageView new];
    imageView.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
    imageView.image = sprite;
    [self.view addSubview:imageView];
  • 动画播放控制
    // 1.初始化YYAnimatedImageView
    YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] init];
    imageView.backgroundColor = [UIColor whiteColor];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    [self.view addSubview:imageView];
     
    // 2.加载网络GIF图片
    [imageView yy_setImageWithURL:[NSURL URLWithString:@"gif图url链接"] placeholder:[UIImage imageNamed:@"default"]];
     
    // 3.通过RAC或者自己写观察者,观察currentAnimatedImageIndex播放到什么位置,如果播放到最后一张图,则停止播放
    [RACObserve(imageView, currentAnimatedImageIndex) subscribeNext:^(id _Nullable x) {
        if ([x integerValue] == imageView.animationImages.count) {
            [_imageView stopAnimating];
        }
    }];
  • 图片解码
    // 解码单帧图片
    NSData *data  = [NSData dataWithContentsOfFile:@"/tmp/image.webp"];
    YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
        
    // 渐进式图片解码 (可用于图片下载显示)
    NSMutableData *data     = [NSMutableData new];
    YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
    while(newDataArrived) {
       [data appendData:newData];
       [decoder updateData:data final:NO];
       if (decoder.frameCount > 0) {
           UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
           // progressive display...
       }
    }
    [decoder updateData:data final:YES];
    UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
  • 图片编码
    // 编码静态图 (支持各种常见图片格式):
    YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
    jpegEncoder.quality         = 0.9;
    [jpegEncoder addImage:image duration:0];
    NSData jpegData             = [jpegEncoder encode];
     
    // 编码动态图 (支持 GIF/APNG/WebP):
    YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
    webpEncoder.loopCount       = 5;
    [webpEncoder addImage:image0 duration:0.1];
    [webpEncoder addImage:image1 duration:0.15];
    [webpEncoder addImage:image2 duration:0.2];
    NSData webpData             = [webpEncoder encode];
  • 图片类型 判断
    // 获取图片类型
    YYImageType type = YYImageDetectType(data);
    if (type == YYImageTypePNG) ... 

YYImage 框架整体概览

  • 目录结构如下:
    YYImage.h (.m)
    YYFrameImage.h (.m)
    YYSpriteSheetImage.h (.m)
    YYAnimatedImageView.h (.m)
    YYImageCoder.h (.m)
  • YYImage、YYFrameImage、YYSpriteSheetImage都是继承自UIImage的图片类,YYAnimatedImageView继承自UIImageView,用于处理框架自定义的图片类,YYImageCoder是编码和解码器。

YYImage的原理解析

YYImage 类

  • 该类对UIImage进行拓展,支持 WebP、APNG、GIF 格式的图片解码,可以看到,为了避免产生全局缓存,YYImage 重载了imageNamed:方法:
    ① 若未指定图片的拓展名,这里会遍历查询所有支持的类型
    ② scales为形为@[@1,@2,@3]的数组,不同屏幕的物理分辨率/逻辑分辨率不同,查询的优先级也不同;
    ③ 找到第一个有效的path就会调用initWithData:scale:方法初始化;
    ④ 虽然比以往使用UIImage更方便,除png外的图片类型也可以不写拓展名,但是为了极致的性能考虑,还是指定拓展名比较好;



 - (YYImage *)imageNamed:(NSString *)name {
    ...
    NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
    NSArray *scales = _NSBundlePreferredScales();
    for (int s = 0; s < scales.count; s++) {
        scale = ((NSNumber *)scales[s]).floatValue;
        NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
        for (NSString *e in exts) {
            path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
            if (path) break;
        }
        if (path) break;
    }
    ...
    return [[self alloc] initWithData:data scale:scale];
}
  • 众多初始化方法的基础都是initWithData:scale:,在该方法中初始化了信号量 (作为锁)图片解码器 (YYImageDecoder),以及通过解码器获取第一帧解压过后的图像等,最终调用initWithCGImage:scale:orientation获取实例。
  • 不难发现,有这么一个属性property (nonatomic) BOOL preloadAllAnimatedImageFrames;,它的作用是预加载,缓存解压过后的所有帧图片,是一个优化选项,但是需要注意内存的占用,看看它的setter方法实现:
    ① 主要是在for循环中,拿到每一帧解压后的图片;
    ② 由于是解压后的,所以该方法实际上会消耗一定的 CPU 资源,所以在实际使用中可以在异步线程调用。

- (void)setPreloadAllAnimatedImageFrames:(BOOL)preloadAllAnimatedImageFrames {
    if (_preloadAllAnimatedImageFrames != preloadAllAnimatedImageFrames) {
        if (preloadAllAnimatedImageFrames && _decoder.frameCount > 0) {
            NSMutableArray *frames = [NSMutableArray new];
            //拿到所有帧的图片
            for (NSUInteger i = 0, max = _decoder.frameCount; i < max; i++) {
                UIImage *img = [self animatedImageFrameAtIndex:i];
                [frames addObject:img ?: [NSNull null]];
            }
            dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
            _preloadedFrames = frames;
            dispatch_semaphore_signal(_preloadedLock);
        } else {
            dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
            _preloadedFrames = nil;
            dispatch_semaphore_signal(_preloadedLock);
        }
    }
}

YYFrameImage 类

  • 该类是帧动画图片类,可以配置每一帧的图片信息和显示时长,图片支持 png 和 jpeg;主要是两个初始化方法,然后配置好每一帧的图片后,通过YYAnimatedImageView载体操作和显示:
- (nullable instancetype)initWithImagePaths:(NSArray<NSString *> *)paths
                             frameDurations:(NSArray<NSNumber *> *)frameDurations
                                  loopCount:(NSUInteger)loopCount;
- (nullable instancetype)initWithImageDataArray:(NSArray<NSData *> *)dataArray
                                 frameDurations:(NSArray *)frameDurations
                                      loopCount:(NSUInteger)loopCount;

YYSpriteSheetImage 类

  • SpriteSheet 动画:可以理解为一张大图上分布有很多完整的小图,然后不同时刻显示不同位置的小图。其目的是将多张图片的加载、解压合并为一张大图的加载、解压,可以减少图片占用的内存,提高整体的解压缩性能。YYSpriteSheetImage.h的处理方法
    如下:
@property (nonatomic, readonly) NSArray<NSValue *> *contentRects;
@property (nonatomic, readonly) NSArray<NSValue *> *frameDurations;
@property (nonatomic, readonly) NSUInteger loopCount;


- (nullable instancetype)initWithSpriteSheetImage:(UIImage *)image
                                     contentRects:(NSArray<NSValue *> *)contentRects
                                   frameDurations:(NSArray<NSNumber *> *)frameDurations
                                        loopCount:(NSUInteger)loopCount;
  • 初始化方法中,需要传入两个数组,一个是CGRect表示范围的数组,一个是对应时长的数组;然后利用CALayer的contentsRect属性,动态的读取这张大图某个范围的内容。

YYAnimatedImage 协议

  • YYAnimatedImage 协议是YYAnimatedImageView和YYImage、YYFrameImage、YYSpriteSheetImage交互的中介;
@protocol YYAnimatedImage <NSObject>
@required
//帧数量
- (NSUInteger)animatedImageFrameCount;
//动画循环次数
- (NSUInteger)animatedImageLoopCount;
//每帧在内存中的大小
- (NSUInteger)animatedImageBytesPerFrame;
//index 下标的帧图片
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
//index 下标帧图片持续时间
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@optional
//index 下标帧图片的范围(CGRect)
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
@end
  • 不管是.gif还是帧图片数组还是 SpriteSheet,当需要利用动画来显示它们的时候,实际上并不关心它们是何种来源,该协议是一个共有逻辑提取。任何类型的UIImage子类的动画图片的数据都能通过这个协议体现,YYImage、YYFrameImage、YYSpriteSheetImage都分别实现了该协议,具体操作可以看源码。

YYAnimatedImageView 类

YYAnimatedImageView类通过YYImage、YYFrameImage、YYSpriteSheetImage实现的协议方法拿到帧图片数据和相关信息进行动画展示。原理如下:

  • @property (nonatomic, copy) NSString *runloopMode;属性默认为NSRunLoopCommonModes保证在拖动滚动视图时动画还能继续,重写了部分方法,让其执行其自定义的配置;
 - (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];
}
...
  • setImage:withType:方法就是将这些图片数据赋值给super.image等,该方法最后会走imageChanged方法:
 - (void)imageChanged {
    YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;
    ... //省略判断是否是 SpriteSheet 类型来源

    /*1、若上一次是 SpriteSheet 类型而当前显示的图片不是,则归位 self.layer.contentsRect */
    if (!hasContentsRect && _curImageHasContentsRect) {
        if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            // 使用CATransaction事件来取消隐式动画
            self.layer.contentsRect = CGRectMake(0, 0, 1, 1);
            [CATransaction commit];
        }
    }
    _curImageHasContentsRect = hasContentsRect;

    /*2、SpriteSheet 类型时,通过`setContentsRect:forImage:`方法 配置self.layer.contentsRect */
    if (hasContentsRect) {
        CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
        [self setContentsRect:rect forImage:newVisibleImage];
    }
    
    /*3、若是多帧的图片,通过`resetAnimated`方法初始化显示多帧动画需要的配置;然后拿到第一帧图片调用`setNeedsDisplay `绘制出来 */
    if (newImageFrameCount > 1) {
        [self resetAnimated];
        _curAnimatedImage = newVisibleImage;
        _curFrame = newVisibleImage;
        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
        [self calcMaxBufferCount];
    }
    [self setNeedsDisplay];
    [self didMoved];
}
  • 动画启动和结束的时机:在didMoveToWindow和didMoveToSuperview周期方法中尝试启动或结束动画,不需要在组件内部特意的去调用就能实现自动的播放和停止。而didMoved方法中判断是否开启动画写了个self.superview && self.window,意味着YYAnimatedImageView光有父视图还不能开启动画,还需要展示在window上才行;
- (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];
}

  • 异步解压
    ① YYAnimatedImageView有个队列变量NSOperationQueue *_requestQueue,不难发现,_requestQueue是一个串行的队列,用于处理解压任务;
    ② _YYAnimatedImageViewFetchOperation继承自NSOperation,重写了main方法自定义解压任务。它是结合变量_requestQueue来使用的;
    ③ animatedImageFrameAtIndex方法便会调用解码,后面yy_imageByDecoded属性是对解码成功的第二重保证,view->_buffer[@(idx)] = img是做缓存;
    ④ 使用if ([self isCancelled]) break(return);判断返回,因为在执行NSOperation任务的过程中该任务可能会被取消;
    ⑤ for循环中使用@autoreleasepool避免同一 RunLoop 循环中堆积过多的局部变量。由此,基本可以保证解压过程是在_requestQueue串行队列执行的,不会影响主线程。




_requestQueue = [[NSOperationQueue alloc] init];
_requestQueue.maxConcurrentOperationCount = 1;

 - (void)main {
    ...
    for (int i = 0; i < max; i++, idx++) {
        @autoreleasepool {
            ...
            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;
            }
        }
    }
}
  • 缓存机制:YYAnimatedImageView有如下几个变量:_buffter就是缓存池,在_YYAnimatedImageViewFetchOperation私有类的main函数中有给_buffer赋值,作者还限制了最大缓存数量;
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)
  • 缓存限制计算:通过_YYDeviceMemoryTotal()拿到内存总数乘以 0.2,通过_YYDeviceMemoryFree()拿到剩余的内存乘以 0.6,然后取它们最小值;之后通过最小的缓存值BUFFER_SIZE和用户自定义的_maxBufferSize属性综合判断。
- (void)calcMaxBufferCount {
    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame;
    if (bytes == 0) bytes = 1024;
    
    int64_t total = _YYDeviceMemoryTotal();
    int64_t free = _YYDeviceMemoryFree();
    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;
    if (maxBufferCount < 1) maxBufferCount = 1;
    else if (maxBufferCount > 512) maxBufferCount = 512;
    _maxBufferCount = maxBufferCount;
}
  • 缓存清理时机:在resetAnimated方法中注册了两个监听,在收到内存警告或者 APP 进入后台时,修改缓存:在进入后台时,清除所有的异步解压任务,然后计算下一帧的下标,最后移除不是下一帧的所有缓存,保证进入前台时下一帧的及时显示;
// resetAnimated中的监听
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];

// 内存警告或者APP进入后台时,修改缓存
- (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
}

  • 使用了一个_YYImageWeakProxy私有类进行消息转发防止循环引用:
    ① 当target存在时,发送给_YYImageWeakProxy实例的方法能正常的转发给target。
    ② 当target释放时,forwardingTargetForSelector:重定向失败,会调用methodSignatureForSelector:尝试获取有效的方法,而若获取的方法无效,将会抛出异常,所以这里随便返回了一个init方法。
    ③ 当methodSignatureForSelector:获取到一个有效的方法过后,会调用forwardInvocation:方法开始消息转发。而这里作者给[invocation setReturnValue:&null];一个空的返回值,让最外层的方法调用者不会得到不可控的返回值。虽然这里不调用方法默认会返回 null ,但是为了保险起见,能尽量人为控制默认值就不要用系统控制。


@interface _YYImageWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
...
@end
...


- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}
,,,

YYImageCoder 编解码

该文件中主要包含了YYImageFrame图片帧信息的类、YYImageDecoder解码器、YYImageEncoder编码器。

  • 解码核心代码:将CGImageRef数据转化为位图数据;
    ① 使用CGBitmapContextCreate()创建图片上下文;
    ② 使用CGContextDrawImage()将图片绘制到上下文中;
    ③ 使用CGBitmapContextCreateImage()通过上下文生成图片。


CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    ...
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
    BOOL hasAlpha = NO;
    if (alphaInfo == kCGImageAlphaPremultipliedLast ||
        alphaInfo == kCGImageAlphaPremultipliedFirst ||
        alphaInfo == kCGImageAlphaLast ||
        alphaInfo == kCGImageAlphaFirst) {
        hasAlpha = YES;
    }
    // BGRA8888 (premultiplied) or BGRX8888
    // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
    if (!context) return NULL;
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
    CGImageRef newImage = CGBitmapContextCreateImage(context);
    CFRelease(context);
    return newImage;
    ...
}
  • 渐进式解码:在_updateSourceImageIO私有方法中可以看到渐进式的解压逻辑,由于代码过多不贴出来,主要逻辑大致如下:
    ① 使用CGImageSourceCreateIncremental(NULL)创建空图片源;
    ② 使用CGImageSourceUpdateData()更新图片源;
    ③ 使用CGImageSourceCreateImageAtIndex()创建图片;
    渐进式解压可以在下载图片的过程中进行解压、显示,达到网页上显示图片的效果,体验不错。



  • YYImageDecoder 类使用的锁
    ① 一个是dispatch_semaphore_t _framesLock;信号量,从它的命名就可以看出,_framesLock锁是用来保护NSArray *_frames; < Array, without image变量的线程安全,由于受保护的代码块执行速度快,可以体现信号量的性能优势。
    ② 另外一个是pthread_mutex_t _lock; recursive lock互斥锁,支持递归锁(互斥锁有个特性,当同一个线程多次获取锁时(锁还未解开),会导致死锁,而递归锁允许同一线程多次获取锁,或者说“递归”获取锁。也就是说,对于同一线程,递归锁是可重入的,对于多线程仍然和互斥锁无异);

YYImage主要类调用逻辑

渲染GIF/WebP/PNG(APNG)方法调用顺序

  • YYImage *image = [YYImage imageNamed:name]; //传入图片名创建YYImage对象;
  • [[self alloc] initWithData:data scale:scale];//用重写的方法初始化图像数据;
  • YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];//创建解码类 YYImageDecoder 对象,紧接着更新数据;
  • result = [self _updateData:data final:final];//根据图像的data算出图片的type以及其他信息,再根据不同type 的图像去分别更新数据;
  • [self _updateSourceImageIO];// 计算出PNG、GIF等图片信息(图片的每一帧的属性,包括宽、高、方向、动画重复次数(gif类型)、持续时间(gif类型));
  • YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image]; //把图片添加到 UIImageView 的子类;
  • [self setImage:image withType:YYAnimatedImageTypeImage];// 设置图片,类似Setter方法;
  • [self imageChanged];//判断当前图片类型以及帧数,由CATransaction支持的显示事务去更新图层的 contentsRect,以及重置动画的参数;
  • [self resetAnimated];//重置动画多种参数;[self calcMaxBufferCount]; // 动态调整当前内存的缓冲区大小;
  • [self didMoved];// 窗口对象或者父视图对象改变,则开始控制动画的启动(停止),这是动画得以显示的关键。

渲染帧动画方法调用顺序

  • UIImage *image = [[YYFrameImage alloc] initWithImagePaths:paths oneFrameDuration:0.1 loopCount:0]; //传入图片组的路径、每一个帧(每一个图片)的时间以及循环多少次,计算出总的durations;
  • [self initWithImagePaths:paths frameDurations:durations loopCount:loopCount];// 把第一张图片解码后返回,并求出第一帧的大小,作为每一帧的大小;
  • YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
  • 后面步骤跟渲染GIF/WebP/PNG(APNG)方法调用顺序一样,从[self setImage:image withType:YYAnimatedImageTypeImage];开始类似。

核心处理代码

  • YYCGImageCreateDecodedCopy 是解压缩的核心,将CGImageRef数据转化为位图数据,也就是渲染图片性能显著的原因。该方法首先求出图片的宽高,注意这里的图片是指编码前的图片的每一帧图片。
// 它接受一个原始的位图参数 imageRef ,最终返回一个新的解压缩后的位图 newImage
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    if (!imageRef) return NULL;
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    if (width == 0 || height == 0) return NULL;
    
    // 重新绘制解码(可能会失去一些精度)
    if (decodeForDisplay) { //decode with redraw (may lose some precision)
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        // BGRA8888 (premultiplied) or BGRX8888
        // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
        if (!context) return NULL;
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);
        return newImage; // 返回一个新的解压缩后的位图 newImage
        
    } else {
    
    }
}
  • 设置图片setImage:withType:方法就是将这些图片数据赋值给super.image等,该方法最后会走imageChanged方法
 - (void)imageChanged {
    YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;
    ... //省略判断是否是 SpriteSheet 类型来源

    /*1、若上一次是 SpriteSheet 类型而当前显示的图片不是,则归位 self.layer.contentsRect */
    if (!hasContentsRect && _curImageHasContentsRect) {
        if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            // 使用CATransaction事件来取消隐式动画
            self.layer.contentsRect = CGRectMake(0, 0, 1, 1);
            [CATransaction commit];
        }
    }
    _curImageHasContentsRect = hasContentsRect;

    /*2、SpriteSheet 类型时,通过`setContentsRect:forImage:`方法 配置self.layer.contentsRect */
    if (hasContentsRect) {
        CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
        [self setContentsRect:rect forImage:newVisibleImage];
    }
    
    /*3、若是多帧的图片,通过`resetAnimated`方法初始化显示多帧动画需要的配置;然后拿到第一帧图片调用`setNeedsDisplay `绘制出来 */
    if (newImageFrameCount > 1) {
        [self resetAnimated];
        _curAnimatedImage = newVisibleImage;
        _curFrame = newVisibleImage;
        _totalLoop = _curAnimatedImage.animatedImageLoopCount;
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
        [self calcMaxBufferCount];
    }
    [self setNeedsDisplay];
    [self didMoved];
}
  • 图片改变的处理核心
    ① 初始化动画参数 resetAniamted;
    ② 初始化或者重置后求出动画播放循环次数、当前帧、总帧数;
    ③ 调用动态调整缓冲区方法 calcMaxBufferCount 、调用控制动画方法 didMoved。


// init the animated params.
 - (void)resetAnimated {
    if (!_link) {
        _lock = dispatch_semaphore_create(1);
        _buffer = [NSMutableDictionary new];
        
        // 添加到这种队列中的操作,就会自动放到子线程中执行。
        _requestQueue = [[NSOperationQueue alloc] init];
        /* maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
        为1时,队列为串行队列。只能串行执行。大于1时,队列为并发队列 */
        _requestQueue.maxConcurrentOperationCount = 1;
        /* 初始化一个新的 CADisplayLink 对象,在屏幕更新时调用。为了使显示循环与显示同步,应用程序使用addToRunLoop:forMode:方法将其添加到运行循环中
            一个计时器对象,允许应用程序将其绘图同步到显示的刷新率。
         */
        _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];  // 捕获字典到全局队列,在后台释放这些图像以避免阻塞UI线程。
                 
             });
         }
    );
    _link.paused = YES;
    _time = 0;
    if (_curIndex != 0) {
        [self willChangeValueForKey:@"currentAnimatedImageIndex"];
        _curIndex = 0; // 把索引值重置为0
        [self didChangeValueForKey:@"currentAnimatedImageIndex"];
    }
    _curAnimatedImage = nil; // 当前图像为空
    _curFrame = nil; // 当前帧
    _curLoop = 0; //当前循环次数
    _totalLoop = 0; // 总循环次数
    _totalFrameCount = 1; // 总帧数
    _loopEnd = NO; // 是否循环结尾
    _bufferMiss = NO; // 是否丢帧
    _incrBufferCount = 0; // 当前允许的缓存
}
  • 重置图片的参数和内存警告时释放内存,初始化一个新的 CADisplayLink 对象,在屏幕更新时调用:
// 只有屏幕刷新累加时间不小于当前帧的动画播放时间才显示图片,播放下一帧。
// 播放 GIF 的关键
- (void)step:(CADisplayLink *)link {
    UIImage <YYAnimatedImage> *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; // link.duration 屏幕刷新的时间,默认1/60 s
        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; // 用KVO改变 当前索引值
             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
             _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
             
             // 实现YYSpriteSheetImage 的协议方法,才会进入该 if 语句
             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
    }
    
    /* _YYAnimatedImageViewFetchOperation 为 NSOperation 的子类
        还未获取完所有图像,交给它获取下一张图像 */
    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]; //
    }
}

  • 动画播放,是 CADisplayLink对象的方法,每 1/60s 也就是屏幕刷新一次就调用一次:
 - (void)calcMaxBufferCount {
    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; // 求出每一帧的字节数
    if (bytes == 0) bytes = 1024; // 如果为0,则给定1024
    
    int64_t total = _YYDeviceMemoryTotal(); // 获取设备的CPU物理内存
    int64_t free = _YYDeviceMemoryFree(); // 获取设备的容量
    int64_t max = MIN(total * 0.2, free * 0.6); // 比较内存的0.2倍以及容量的0.6倍最小值
    max = MAX(max, BUFFER_SIZE); // 如果不够 10 M,则以 10 M 作为最大缓冲区大小
    
    /** _maxBufferSize 内部帧缓冲区大小
     * 当设备有足够的空闲内存时,这个视图将请求并解码一些或所有未来的帧图像进入一个内部缓冲区。
     * 默认值为0 如果这个属性的值是0,那么最大缓冲区大小将根据当前的状态进行动态调整设备释放内存。否则,缓冲区大小将受到此值的限制。
     * 当收到内存警告或应用程序进入后台时,缓冲区将被立即释放
     */
    if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; //得出缓冲区的最大值
    
    double maxBufferCount = (double)max / (double)bytes;
    if (maxBufferCount < 1) maxBufferCount = 1;
    else if (maxBufferCount > 512) maxBufferCount = 512;
    _maxBufferCount = maxBufferCount; // 最大缓冲数
}
  • 动态计算最大缓冲数:
/* 从自定义的 start 方法中调用 main 方法
 调用[self didMoved]; 从而调用此方法
*/
- (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; // 获取 Operation 中传过来的 下一个索引值
    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];
                NSLog(@"当前线程---%@", [NSThread currentThread]); // 打印当前线程,每次打印都是 name = (null),说明在异步线程
                // 在异步线程再次调用解码图片,如果无法解码或已经解码就返回self
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); // 每次添加一张图片到 _buffer 数组
                view = nil;
            }
        }
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!