YYKit源码分析(1)——YYImage图片处理

YYKit源码分析(1)——YYImage图片处理_第1张图片

YYImage是由@ibireme开发的一款功能强大的 iOS 图像框架(该项目是 YYKit 组件之一),它支持当前市场主流的静/动态图像编/解码与动态图像的动画播放显示,其主要功能如下:

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

{\large\text{作者:奚山遇白 链接:https://www.jianshu.com/p/10b7430380f2 来源: 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。}}

  • 基础

在分析YYImage框架之前,先了解一下平常图片加载的过程:

  • 先获取图片原始数据(data buffer),然后解码成图像元素信息(image buffer),显示到屏幕时变成(frame buffer)。

用代码说明就是:

UIImage *image = [[UIImage alloc]initWithName: @""];//data buffer
_imageView.image = image;//解码成image buffer,再变成frame buffer

然后就会有下面的过程:

YYKit源码分析(1)——YYImage图片处理_第2张图片

优化图片有两点:一是内存优化,可以通过对图片适当尺寸采样显示进行优化;二是cpu优化,充分利用cpu对图片提前解码,所以也就有了YYImage。首先看看框架使用:

YYImage *image = [YYImage imageNamed:@"name"];
YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc]initWithImage:image];
[self.view addSubview:imageView];
(一)YYImage

YYImage继承UIImage,通过重写初始化方法来实现处理:

@interface YYImage : UIImage 

+ (nullable YYImage *)imageNamed:(NSString *)name; // no cache!
+ (nullable YYImage *)imageWithContentsOfFile:(NSString *)path;
+ (nullable YYImage *)imageWithData:(NSData *)data;
+ (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale;
  1. 直接来看看源码,内部自己实现根据名字查找图片:
@implementation YYImage

+ (YYImage *)imageNamed:(NSString *)name {
    if (name.length == 0) return nil;
    if ([name hasSuffix:@"/"]) return nil;
    
    NSString *res = name.stringByDeletingPathExtension;
    NSString *ext = name.pathExtension;
    NSString *path = nil;
    CGFloat scale = 1;//分辨率
    
    // If no extension, guess by system supported (same as UIImage).
    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;
    }
    if (path.length == 0) return nil;
    
    NSData *data = [NSData dataWithContentsOfFile:path];//获取
    if (data.length == 0) return nil;
    
    return [[self alloc] initWithData:data scale:scale];
}
  1. 然后初始化解码器
@implementation YYImage
...
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
    ...
    @autoreleasepool {
        YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];//解码器
        YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];//解码
        ...
    }
    return self;
}
@implementation YYImageDecoder {
...
+ (instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale {
    if (!data) return nil;
    YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:scale];
    [decoder updateData:data final:YES];
    if (decoder.frameCount == 0) return nil;
    return decoder;
}
...
- (BOOL)updateData:(NSData *)data final:(BOOL)final {
    BOOL result = NO;
    pthread_mutex_lock(&_lock);//递归锁;渐进式解码;可能被同一个线程反复访问
    result = [self _updateData:data final:final];
    pthread_mutex_unlock(&_lock);
    return result;
}
...
- (BOOL)_updateData:(NSData *)data final:(BOOL)final {
    ...
    YYImageType type = YYImageDetectType((__bridge CFDataRef)data);//判断图片类型
    if (_sourceTypeDetected) {
        if (_type != type) {
            return NO;
        } else {
            [self _updateSource];
        }
    } else { ... }
    return YES;
}
...
- (void)_updateSource {
    switch (_type) {
        case YYImageTypeWebP: ...
        case YYImageTypePNG: ...
        default: {
            [self _updateSourceImageIO];
        } break;
    }
}
...
- (void)_updateSourceImageIO {
    ...
    if (!_source) {
        if (_finalized) {
            _source = CGImageSourceCreateWithData((__bridge CFDataRef)_data, NULL);//普通输入源
        } else {
            _source = CGImageSourceCreateIncremental(NULL);//渐进式数据输入源
            if (_source) CGImageSourceUpdateData(_source, (__bridge CFDataRef)_data, false);
        }
    } else { ... }
    ...
    /*
     ICO, GIF, APNG may contains multi-frame.
     */
    NSMutableArray *frames = [NSMutableArray new];
    for (NSUInteger i = 0; i < _frameCount; i++) {
        _YYImageDecoderFrame *frame = [_YYImageDecoderFrame new];
        frame.index = i;
        frame.blendFromIndex = i;
        frame.hasAlpha = YES;
        frame.isFullSize = YES;
        [frames addObject:frame];
        ...
    }
    ...
}

最后图像每一帧的信息会被保存在_YYImageDecoderFrame中:

@interface _YYImageDecoderFrame : YYImageFrame
@property (nonatomic, assign) BOOL hasAlpha;                ///< Whether frame has alpha.
@property (nonatomic, assign) BOOL isFullSize;              ///< Whether frame fill the canvas.
@property (nonatomic, assign) NSUInteger blendFromIndex;    ///< Blend from frame index to current frame.
@end
  1. 回到第2步,进行解码:
@implementation YYImageDecoder
...
- (YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay {
    YYImageFrame *result = nil;
    pthread_mutex_lock(&_lock);//渐进式解码时,会反复调用,所以加锁
    result = [self _frameAtIndex:index decodeForDisplay:decodeForDisplay];//解压缩当前的图片
    pthread_mutex_unlock(&_lock);
    return result;
}
...
- (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay {
    if (index >= _frames.count) return 0;
    _YYImageDecoderFrame *frame = [(_YYImageDecoderFrame *)_frames[index] copy];
    ...
    if (!_needBlend) {
        CGImageRef imageRef = [self _newUnblendedImageAtIndex:index extendToCanvas:extendToCanvas decoded:&decoded];//
        ...
        UIImage *image = [UIImage imageWithCGImage:imageRef scale:_scale orientation:_orientation];
        ...
        frame.image = image;//已经解码成image buffer的image
        return frame;
    }
    ...
}
...
- (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index
                         extendToCanvas:(BOOL)extendToCanvas
                                decoded:(BOOL *)decoded CF_RETURNS_RETAINED {
    ...
    if (_source) {
        CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_source, index, (CFDictionaryRef)@{(id)kCGImageSourceShouldCache:@(YES)});//会立即解码
        if (imageRef && extendToCanvas) {
            size_t width = CGImageGetWidth(imageRef);
            size_t height = CGImageGetHeight(imageRef);
            if (width == _width && height == _height) {//判断是否一致
                CGImageRef imageRefExtended = YYCGImageCreateDecodedCopy(imageRef, YES);//核心
                ...
            } else { ... }
        }
        return imageRef;
    }
}
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    ...
    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;
        
    } else { ... }
}
  • YYCGImageCreateDecodedCopy是解压缩的核心,也就是渲染图片性能显著的原因。它接受一个原始的位图参数imageRef,最终返回一个新的解压缩后的位图newImage
YYKit源码分析(1)——YYImage图片处理_第3张图片
解码的操作
(二)YYAnimatedImageView
YYKit源码分析(1)——YYImage图片处理_第4张图片
YYAnimatedImageView工作原理
  1. 直接从初始化函数探索:
@implementation YYAnimatedImageView
...
- (instancetype)initWithImage:(UIImage *)image {
    self = [super init];
    _runloopMode = NSRunLoopCommonModes;
    
    _autoPlayAnimatedImage = YES;
    self.frame = (CGRect) {CGPointZero, image.size };
    self.image = image;//重写setter
    return self;
}
...
- (void)setImage:(UIImage *)image {
    if (self.image == image) return;
    [self setImage:image withType:YYAnimatedImageTypeImage];
}
...
- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
    [self stopAnimating];
    if (_link) [self resetAnimated];//定时器播放
    ...
    [self imageChanged];//
}
  1. 接着重置动画:
@implementation YYAnimatedImageView
...
- (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;//默认暂停
        ...
    }
    ...
}
  • 其实YYAnimatedImageView就是通过获取动态图的每一帧,利用CADisplayLink进行播放显示。
  1. 然后回到第2步,开启动画:
@implementation YYAnimatedImageView
...
- (void)imageChanged {
    ...
    [self setNeedsDisplay];
    [self didMoved];
}
...
- (void)didMoved {
    if (self.autoPlayAnimatedImage) {
        if(self.superview && self.window) {
            [self startAnimating];
        } else {
            [self stopAnimating];
        }
    }
}
@implementation YYAnimatedImageView
...
- (void)startAnimating {
    
    YYAnimatedImageType type = [self currentImageType];
    if (type == YYAnimatedImageTypeImages || type == YYAnimatedImageTypeHighlightedImages) {
        ...
    } else {
        if (_curAnimatedImage && _link.paused) {
            _curLoop = 0;
            _loopEnd = NO;
            _link.paused = NO;//启动
            self.currentIsPlayingAnimation = YES;
        }
    }
}
  1. CADisplayLink开启后便执行step :
@implementation YYAnimatedImageView
...
- (void)step:(CADisplayLink *)link {
    ...
    if (!_bufferMiss) {//核心
        _time += link.duration;
        delay = [image animatedImageDurationAtIndex:_curIndex];//获取这一帧的播放时间
        if (_time < delay) return; //小于
        _time -= delay; //大于
        ...
        delay = [image animatedImageDurationAtIndex:nextIndex];
        if (_time > delay) _time = delay; // do not jump over frame //避免跳过下一帧
    }
    ...
    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];//子线程执行
    }
}

最后会启动_YYAnimatedImageViewFetchOperation

@implementation _YYAnimatedImageViewFetchOperation
- (void)main {
    ...
    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) {
                // 读取丢失的缓存 根据index拿出_YYImageDecoderFrame 进行帧图片解码
                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
                // 还是在异步线程再次调用解码  如果没有,就解码,有的话就返回self
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                // 将解码的图片存储到buffer
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]);
                view = nil;
            }
        }
    }
}
@end
@implementation YYImage
...
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
    ...
    return [_decoder frameAtIndex:index decodeForDisplay:YES].image;//解压缩图片
}
  • 总结
YYKit源码分析(1)——YYImage图片处理_第5张图片
逻辑流程
YYKit源码分析(1)——YYImage图片处理_第6张图片
层级
  • 补充

resetAnimated函数中其实有一个普通而厉害的操作:

- (void)resetAnimated {
    ...
    LOCK(
         if (_buffer.count) {//因为图片数据比较大,所以用子线程异步释放对象
             NSMutableDictionary *holder = _buffer;//释放压力转移到holder,由holder去持有然后子线程释放
             _buffer = [NSMutableDictionary new];//清空_buffer;因为释放的不是_buffer,而是_buffer指向的内存空间
             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];//就是随便发一条消息以便在该子线程执行一下,然后走向释放,把内容释放;如果在子线程:_buffer=nil,_buffer指向的内存空间还是会在主线程释放
             });
         }
    );
    ...
}

你可能感兴趣的:(YYKit源码分析(1)——YYImage图片处理)