YYImage 源码剖析:图片处理技巧

系列文章:
YYText 源码剖析:CoreText 与异步绘制
YYAsyncLayer 源码剖析:异步绘制
YYCache 源码剖析:一览亮点
YYModel 源码剖析:关注性能
YYImage 源码剖析:图片处理技巧
YYWebImage 源码剖析:线程处理与缓存策略

引言

首先问一个问题:你会用图片么?

图片是现代化 APP 界面设计里应用广泛的东西,精美的图片可以带来视觉上的享受,提高用户体验。由此给技术上带来了一些挑战,比如动图的处理、图片显示流畅程度的优化、图片包大小的优化、超大图片的处理等。

本文主要是结合 YYImage 源码对图片处理技巧进行讲解。而笔者不会逐字逐句的翻译源码,主要是提取源码中有思维价值的东西。所以最好是打开源码,本文作为思想引导。

源码基于 1.0.4 版本。

一、图片处理技巧

首先来谈一谈图片处理的一些注意事项和技巧,以下结论参考其他博文、官方文档、实际测试得出,欢迎指出错误😁。

一张图片从磁盘中显示到屏幕上过程大致如下:从磁盘加载图片信息、解码二进制图片数据为位图、通过 CoreAnimation 框架处理最终绘制到屏幕上。

实际上图片的绘制过程往往不是性能瓶颈,最耗时的操作是解码过程,若图片文件过大,从磁盘读取的过程也有可观的耗时。

1、加载和解压

一般使用imageNamed:或者imageWithData:从内存中加载图片生成UIImage的实例,此刻图片并不会解压,当 RunLoop 准备处理图片显示的事务(CATransaction)时,才进行解压,而这个解压过程是在主线程中的,这是导致卡顿的重要因素。

imageNamed: 方法

使用imageNamed:方法加载图片信息的同时(生成UIImage实例),还会将图片信息缓存起来,所以当使用该方法第一次加载某张图片时,会消耗较多的时间,而之后再次加载该图片速度就会非常快(注意此时该图片是未绘制到屏幕上的,也就是说还未解压)。

在绘制到屏幕之前,第一次解压成功后,系统会将解压信息缓存到内存。

值得注意的是,这些缓存都是全局的,并不会因为当前UIImage实例的释放而清除,在收到内存警告或者 APP 第一次进入后台才有可能会清除,而这个清除的时机和内容是系统决定的,我们无法干涉。

imageWithData: 方法

使用imageWithData:方式加载图片时,不管是加载过程还是解压过程,都不会像imageNamed:缓存到全局,当该UIImage实例释放时,相关的图片信息和解压信息就会销毁。

两种加载方式的区别

从上面的分析可知,imageNamed:使用时会产生全局的内存占用,但是第二次使用同一张图片时性能很好;imageWithData:不会有全局的内存占用,但对于同一张图片每次加载和解压都会“从头开始”。

由此可见,imageNamed:适合“小”且“使用频繁”的图片,imageWithData:适合“大”且“低频使用”的图片。

2、加载和解压的优化

这里说的优化并不是解压算法的优化,只是基于用户体验的优化。

加载优化

对于加载过程,若文件过大或加载频繁影响了帧率(比如列表展示大图),可以使用异步方式加载图片,减少主线程的压力,代码大致如下:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      UIImage *image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"testImage" ofType:@"jpeg"]];
      dispatch_async(dispatch_get_main_queue(), ^{
          //业务
      });
});
解压优化

解压是耗时的,而系统默认是在主线程执行,所以业界通常有一种做法是,异步强制解压,也就是在异步线程主动将二进制图片数据解压成位图数据,使用CGBitmapContextCreate(...)系列方法就能实现。

该处理方式在众多图片处理框架下都有体现。

3、超大图的处理

值得注意的是,可能业务中需要载入一张很大的图片。这时,若还使用常规的方式加载会占用过多的内存;况且,若图片的像素过大(目前主流 iOS 设备最高支持 4096 x 4096 纹理尺寸),在显示的时候 CPU 和 GPU 都会消耗额外的资源来处理图片。

所以,在处理超大图时,需要一些特别的手段。

比如想要显示完整的图片,就可以使用如下方法压缩到目标大小 (targetSize):

UIGraphicsBeginImageContext(targetSize);
[originalImage drawInRect:CGRectMake(0, 0, targetSize.width, targetSize.height)];
UIImage *targetImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

若想要显示超大图的局部,可以这么做:

CGImageRef tmpImage = CGImageCreateWithImageInRect(originalImage, rect);
UIImage *targetImage = [UIImage imageWithCGImage: tmpImage];
CGImageRelease(tmpImage);

或者直接使用CALayercontentsRect属性来达到相同的效果。

二、YYImage 框架整体概览

上文中谈了一下图片处理的一些原理和核心思想,做为背景知识,下面从一个宏观的角度观察一下 YYImage 框架的设计,目录结构如下:

YYImage.h (.m)  
YYFrameImage.h (.m)
YYSpriteSheetImage.h (.m)
YYAnimatedImageView.h (.m)
YYImageCoder.h (.m)

从命名大致就可以猜测出来它们的功能,YYImage、YYFrameImage、YYSpriteSheetImage都是继承自UIImage的图片类,YYAnimatedImageView继承自UIImageView用于处理框架自定义的图片类,YYImageCoder是编码和解码器。

以下是该框架 github 上 README 写的特性:

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

三、YYImage 类

该类对UIImage进行拓展,支持 WebP、APNG、GIF 格式的图片解码,为了避免产生全局缓存,重载了imageNamed:方法:

+ (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];
}
  • 可以看到,若未指定图片的拓展名,这里会遍历查询所有支持的类型。
  • scales为形为@[@1,@2,@3];的数组,不同屏幕 物理分辨率/逻辑分辨率 不同,查询的优先级也不同。
  • 找到第一个有效的path就会调用initWithData:scale:方法初始化。

这里虽然比以往使用UIImage更方便,除png外的图片类型也可以不写拓展名,但是为了极致的性能考虑,还是指定拓展名比较好。

众多初始化方法的落脚点都是initWithData:scale:,在该方法中初始化了信号量 (作为锁)、图片解码器 (YYImageDecoder),以及通过解码器获取第一帧解压过后的图像等。最终调用initWithCGImage:scale:orientation获取实例。

可以看到这样一个属性:@property (nonatomic) BOOL preloadAllAnimatedImageFrames;,它的作用是预加载,缓存解压过后的所有帧图片,是一个优化选项,但是需要注意内存的占用,看看它的setter方法实现:

- (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);
        }
    }
}

主要是在for循环中,拿到每一帧解压后的图片(笔者改动了一下代码,至于animatedImageFrameAtIndex后面解释)。由于是解压后的,所以该方法实际上会消耗一定的 CPU 资源,所以在实际使用中可以在异步线程调用。

值得一提的是,此处使用信号量dispatch_semaphore_t作为线程锁来用非常适合,因为该锁主要是保证_preloadedFrames的读写安全,耗时短,使用信号量性能很好。

四、YYFrameImage 类

该类是帧动画图片类,可以配置每一帧的图片信息和显示时长,图片支持 png 和 jpeg:

- (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;

主要是这两个初始化方法,很简单,然后配置好每一帧的图片后,通过YYAnimatedImageView载体操作和显示。

五、YYSpriteSheetImage 类

SpriteSheet 动画,原理可以理解为一张大图上分布有很多完整的小图,然后不同时刻显示不同位置的小图。

这么做的目的是将多张图片的加载、解压合并为一张大图的加载、解压,可以减少图片占用的内存,提高整体的解压缩性能。

其实该框架的做法很简单,YYSpriteSheetImage.h方法如下:

- (nullable instancetype)initWithSpriteSheetImage:(UIImage *)image
                                     contentRects:(NSArray<NSValue *> *)contentRects
                                   frameDurations:(NSArray<NSNumber *> *)frameDurations
                                        loopCount:(NSUInteger)loopCount;

@property (nonatomic, readonly) NSArray<NSValue *> *contentRects;
@property (nonatomic, readonly) NSArray<NSValue *> *frameDurations;
@property (nonatomic, readonly) NSUInteger loopCount;

初始化方法中,需要传入两个数组,一个是CGRect表示范围的数组,一个是对应时长的数组。

然后利用CALayercontentsRect属性,动态的读取这张大图某个范围的内容。当然,这个过程的逻辑同样在YYAnimatedImageView类中。

六、YYAnimatedImage 协议

YYAnimatedImage 协议是YYAnimatedImageViewYYImage、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都分别实现了该协议,具体操作可以看源码,没有难度。

其中,- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;是可选方法,是YYSpriteSheetImage做 SpriteSheet 动画需要的数据,这算是一个共有逻辑之外的特例。

利用协议来规范共有逻辑,是一个值得学习的技巧,它能让逻辑更清晰,代码更有条理。

七、YYAnimatedImageView 类

一句话理解:YYAnimatedImageView类通过YYImage、YYFrameImage、YYSpriteSheetImage实现的<YYAnimatedImage>协议方法拿到帧图片数据和相关信息进行动画展示。

它的原理就是如此,下面主要分析技术细节,含金量蛮高。

1、初始化流程

@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];
            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];
}

值得提出的是,1 中归位self.layer.contentsRectCGRectMake(0, 0, 1, 1)使用了CATransaction事务来取消隐式动画。(由于此处完全不需要那 0.25 秒的隐式动画)

2、动画启动和结束的时机

- (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];
}

didMoveToWindowdidMoveToSuperview周期方法中尝试启动或结束动画,不需要在组件内部特意的去调用就能实现自动的播放和停止。而didMoved方法中判断是否开启动画写了个self.superview && self.window,意味着YYAnimatedImageView光有父视图还不能开启动画,还需要展示在window上才行。

3、异步解压

YYAnimatedImageView有个队列变量NSOperationQueue *_requestQueue;

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

可以看出_requestQueue是一个串行的队列,用于处理解压任务。

_YYAnimatedImageViewFetchOperation继承自NSOperation,重写了main方法自定义解压任务。它是结合变量_requestQueue;来使用的:

- (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;
            }
        }
    }
}

关键代码中,animatedImageFrameAtIndex方法便会调用解码,后面yy_imageByDecoded属性是对解码成功的第二重保证,view->_buffer[@(idx)] = img是做缓存。

可以看到作者经常使用if ([self isCancelled]) break(return);判断返回,因为在执行NSOperation任务的过程中该任务可能会被取消。

for循环中使用@autoreleasepool避免同一 RunLoop 循环中堆积过多的局部变量。

由此,基本可以保证解压过程是在_requestQueue串行队列执行的,不会影响主线程。

4、缓存机制

YYAnimatedImageView有如下几个变量:

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)

_buffter就是缓存池,在_YYAnimatedImageViewFetchOperation私有类的main函数中有给_buffer赋值,作者还限制了最大缓存数量。

缓存限制计算

- (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;
}

该方法并不复杂,通过_YYDeviceMemoryTotal()拿到内存总数乘以 0.2,通过_YYDeviceMemoryFree()拿到剩余的内存乘以 0.6,然后取它们最小值;之后通过最小的缓存值BUFFER_SIZE和用户自定义的_maxBufferSize属性综合判断。

缓存清理时机

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
}

在进入后台时,清除所有的异步解压任务,然后计算下一帧的下标,最后移除不是下一帧的所有缓存,保证进入前台时下一帧的及时显示。

在收到内存警告时处理方式大同小异,不多赘述。

5、计时器

该类使用CADisplayLink做计时任务,显示系统每帧回调都会触发,所以默认大致是 60 次/秒。CADisplayLink的特性决定了它非常适合做和帧率相关的 UI 逻辑。

防止循环引用

_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];

这里使用了一个_YYImageWeakProxy私有类进行消息转发防止循环引用,看看_YYImageWeakProxy核心代码:

@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)];
}
,,,

target存在时,发送给_YYImageWeakProxy实例的方法能正常的转发给target

target释放时,forwardingTargetForSelector:重定向失败,会调用methodSignatureForSelector:尝试获取有效的方法,而若获取的方法无效,将会抛出异常,所以这里随便返回了一个init方法。

methodSignatureForSelector:获取到一个有效的方法过后,会调用forwardInvocation:方法开始消息转发。而这里作者给[invocation setReturnValue:&null];一个空的返回值,让最外层的方法调用者不会得到不可控的返回值。虽然这里不调用方法默认会返回 null ,但是为了保险起见,能尽量人为控制默认值就不要用系统控制。

计时任务

计时器回调方法- (void)step:(CADisplayLink *)link {...}就是调用动画的核心代码,实际上代码比较容易看懂,主要是显示当前帧图像、发起下一帧的解压任务等。

八、YYImageCoder 编解码

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

注意,本文对 WebP / APNG 等的图片解压缩算法不会讨论,主要是说明一些基于 ImageIO 的使用。

1、解码核心代码

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;
    ...
}

解码核心代码不难找到,实际上就是将CGImageRef数据转化为位图数据:

  • 使用CGBitmapContextCreate()创建图片上下文。
  • 使用CGContextDrawImage()将图片绘制到上下文中。
  • 使用CGBitmapContextCreateImage()通过上下文生成图片。

2、渐进式解码

_updateSourceImageIO私有方法中可以看到渐进式的解压逻辑,由于代码过多不贴出来,主要逻辑大致如下:

  • 使用CGImageSourceCreateIncremental(NULL)创建空图片源。
  • 使用CGImageSourceUpdateData()更新图片源
  • 使用CGImageSourceCreateImageAtIndex()创建图片

渐进式解压可以在下载图片的过程中进行解压、显示,达到网页上显示图片的效果,体验不错。

3、YYImageDecoder 类使用的锁

确实笔者疲于继续查看 ImageIO 或 CoreGraphics 下晦涩的 C 代码,个人认为这些东西了解一些就好,如果业务有需要在深入探究,想要一次性吃透确实过于困难😂。

有意思的是,在YYImageDecoder中使用了两个锁。

一个是dispatch_semaphore_t _framesLock;信号量,从它的命名就可以看出,_framesLock锁是用来保护NSArray *_frames; ///< Array<GGImageDecoderFrame>, without image变量的线程安全,由于受保护的代码块执行速度快,可以体现信号量的性能优势。

另一个是pthread_mutex_t _lock; // recursive lock互斥锁,当笔者看到作者的注释// recursive lock时,赶紧去查看了一下使用过程:

pthread_mutexattr_t attr;
pthread_mutexattr_init (&attr);
pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init (&_lock, &attr);
pthread_mutexattr_destroy (&attr);

果不其然,互斥锁pthread_mutex_t还支持递归锁,确实学了一手,完全可以替代性能更差的NSRecursiveLock

那么,这里为什么要使用递归锁呢?

互斥锁有个特性,当同一个线程多次获取锁时(锁还未解开),会导致死锁,而递归锁允许同一线程多次获取锁,或者说“递归”获取锁。也就是说,对于同一线程,递归锁是可重入的,对于多线程仍然和互斥锁无异。

但是,笔者查看了一下源码,貌似也没发现重入锁的情况发生,估计也是作者长远的考虑,降低编码死锁的可能性。

后语

对于这种比较大一点的开源库,切勿陷入逐字逐句看明白的误区,因为一个成熟的项目是经过很多次维护的,重要的是看明白作者的思路,理解一些核心的东西,本文抛砖引玉,不喜勿喷。

那么现在,读者朋友可以说自己会用图片了么?

参考文献:
iOS 处理图片的一些小 Tip
移动端图片格式调研

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 20170529丹丹丹评万维钢 凡不能毁灭我的,必使我更强大 这节课是万老师对《B选项》这本书解读后的复习总结。内...
    猫书的思考泡泡阅读 148评论 0 1
  • 作者:咩咩 光陈粒-如也 朋友给我讲过她同事Cathy的故事,她和Cathy在办公室里关系最好,所以有啥事Cath...
    陈小猫阅读 695评论 0 0
  • 严格来说,我爸一点都不合格,他很像个小孩子,和我一起慢慢在妈妈的引导下一步一步长大。 他是个很贪玩的人,在我的朋友...
    傻得都方了阅读 653评论 4 9
  • 连续320 【耶9:25】耶和华说:“看哪,日子将到,我要刑罚一切受过割礼,心却未受割礼的, 《感动》耶和华刑罚错...
    报佳音阅读 79评论 0 0