美文网首页
YYKit源码分析(1)——YYImage图片处理

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

作者: 无悔zero | 来源:发表于2021-01-15 21:01 被阅读0次

    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
    

    然后就会有下面的过程:

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

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

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

    @interface YYImage : UIImage <YYAnimatedImage>
    
    + (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
    解码的操作
    (二)YYAnimatedImageView
    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;//解压缩图片
    }
    
    • 总结
    逻辑流程 层级
    • 补充

    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图片处理

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