发布于 

YYImage 源码梳理

图片相关的处理,在移动应用中属于比较重要的一个角色。本篇主要是对 YYImage 的源码实现做一个梳理,内容结构:

  • UIImage 相关的处理
  • YYImage 框架结构
  • YYImage
  • YYFrameImage
  • YYSpriteSheetImage
  • YYAnimatedImage
  • YYAnimatedImageView
  • YYImageCoder

UIImage 相关处理

一张图片从磁盘到显示到屏幕的大致过程为:

  • 从磁盘加载图片信息
  • 解码图片二进制数据为位图
  • 通过 CoreAnimation 框架处理最终绘制到屏幕上

这一过程中耗时较大的操作是图片解码的过程。

图片的加载和解压

日常开发中,我们常会通过 UIImage 的 imageNamed:imageWithData: 方法从内存中加载图片生成 UIImage 对象。在这一过程中,图片不会进行解压,当 RunLoop 准备处理图片显示的处理(CATransaction)时,才进行解压,而这个解压过程是在主线程中执行的,所以大图解压也是导致卡顿的一个重要因素。

imageNamed:

在我们使用 imageNamed: 加载图片信息生成 UIImage 对象的同时,图片的信息会被缓存起来。所以在使用该方法第一次加载某图片时,会消耗较多时间,而之后再加载该图会快很多。

到这里图片还未进行解压操作,当某图片要绘制到屏幕前,会进行解压操作,系统也会将解压信息缓存到内存中。这些缓存是全局的,也就是说,即使当前 UIImage 对象被释放也不会影响该图片的缓存,只有当应用收到内存警告会应用进入后台才会进行缓存处理。具体的缓存清理策略是由系统决定的。

imageWithData:

在我们使用 imageWithData: 加载图片生成 UIImage 对象时,从加载图片信息到解压图片进行屏幕上绘制的这一过程中。都不会将图片信息及解压信息以全局的形式缓存,在该 UIImage 对象释放时,相关的图片信息及解压信息都会被销毁。

对比

  • imageNamed: 会产生全局的内存占用,但第二次使用同一张图时,可以直接使用缓存数据,性能更好。
  • imageWIthData: 可以理解为即用即生成,即使是同一张图的重复使用,每次都需要走一遍加载和解压的过程。

对比来看,**imageNamed:** 方法适合小而高频次使用的图片;**imageWithData:** 方法适合大而低频次的图片。

基于前面的了解可以对图片的加载和解压过程做一些优化。

  • 加载的优化:可以在异步线程中通过 imageWithContentsOfFile: 方法加载图片。
  • 解压的优化:系统默认将解压的耗时操作放到了主线程中执行,比较通用的做法是在异步线程中通过 CGBitmapContextCreate 方法主动将二进制图片数据解压成位图数据,

大图处理

主流 iOS 设备最高支持 4096x4096 的纹理尺寸,若图片像素过大,在显示时会额外消耗资源来处理图片。且常会的图片加载过程会占用过多的内存。

大图加载的出发点是,这张图最终的展示效果是需要怎样的?如果大图展示能够满足展示窗口的展示效果,一定程度上我们可以进行大图的压缩。

在我们缩小图片是,会按取平均值的方式把多个像素点变为一个像素点,但超大图处理的过程中,老设备很可能会出现 OOM 的情况。原因是常规图片绘制的过程中,会先解码图片,再生成原始分辨率大小的 bitmap,这会很消耗内存。

解决办法是使用更底层的 ImageIO 接口,它可以直接读取图像大小和元数据信息,不带来额外的内存开销。

另一种解决办法是,WWDC2018 苹果工程师建议开发者使用 UIGraphicsImageRenderer 来代替 **UIGraphicsBeginImageContextWithOptions**。该方法从 iOS10 引入,在 iOS12 中自动选择最佳的图片格式,可以减少很多的内存。

YYImage 框架结构

前面铺垫了解了一些 UIImage 相关的处理过程。再来看下 YYImage 的框架结构。

YYImage 特性(来自 README):

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

YYImage 的目录结构:

  • YYImage
  • YYFrameImage
  • YYSpriteSheetImage
  • YYAnimatedImageView
  • YYImageCoder

主要分为三个层级,分别为:

  • 继承自 UIImage 的图像层
  • 继承自 UIImageView 的视图层
  • 编解码层

其中 YYImage、YYFrameImage、YYSpriteSheetImage 都继承自 UIImage。YYAnimatedImageView 继承自 UIImageView,用于处理框架自定义的图片类。YYImageCoder 负责编码和解码。

YYImage

YYImage 是 UIImage 的子类,支持 png、jpeg、jpg、gif、webp、apng 格式图片的解码,提供了类似 UIImage 的初始化方法。其中为了避免 imageName: 方法产生全局缓存,重载了该方法。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ (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];
}
  • 根据图片名取得拓展名,若未指定拓展名,遍历查询所有支持的类型
  • scales 为根据设备拿到的对应分辨率,@[@1,@2,@3]
  • 得到有效 path 后,break
  • 调用 initWithData:scale: 方法进行初始化

初始化方法最终都会调用 -initWithData:scale: 来进行初始化,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
if (data.length == 0) return nil;
if (scale <= 0) scale = [UIScreen mainScreen].scale;
_preloadedLock = dispatch_semaphore_create(1);
@autoreleasepool {
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];
YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];
UIImage *image = frame.image;
if (!image) return nil;
self = [self initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];
if (!self) return nil;
_animatedImageType = decoder.type;
if (decoder.frameCount > 1) {
_decoder = decoder;
_bytesPerFrame = CGImageGetBytesPerRow(image.CGImage) * CGImageGetHeight(image.CGImage);
_animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;
}
self.yy_isDecodedForDisplay = YES;
}
return self;
}

方法实现过程:

  • 初始化信号量 _preloadedLock
  • 初始化图像解码器 _decoder(YYImageDecoder )
  • 通过解压器获取第一帧解压后的图像
  • 通过 -initWithCGImage:scale:orientation: 初始化得到 YYImage 实例
  • 若帧数 > 1
    • 暂存解码器 _decoder
    • 暂存每帧内存占用大小 _bytesPerFrame
    • 暂存总内存占用大小 _animatedImageMemorySize
  • return 初始化完成的 YYImage

需要注意的几个点:

  1. _preloadedLockdispatch_semaphore_t 信号量锁,的目的是为了保证 _preloadedFrames 在内存中的读写安全。

  2. **_preloadedFrames**:对应 preloadAllAnimatedImageFrames 对外属性。若开启预加载所有帧到内存,_preloadedFrames 数组会保存所有帧的图像。

  3. 为什么锁选信号量:原因是 _preloadedFrames 的读写本身不会太耗时,不会有长时间的等待,使用信号量这样的自旋锁会比较合适。

YYFrameImage

YYFrameImage 是专门用来处理帧动画图片的类,可以配置每一帧的图片信息和显示时长,也是 UIImage 的子类,仅支持 png 和 jpeg 格式。

主要的初始化方法:

1
2
3
4
5
6
- (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;

方法实现过程:

  • 通过 path 拿到 NSData(或直接使用传入的 NSData)
  • 通过 yy_imageByDecoded 解压 data,得到 UIImage
  • 通过 UIImage,再带上帧动画相关的私有变量,封装得到 YYFrameImage
  • return YYFrameImage

YYSpriteSheetImage

YYSpriteSheetImage 是用于支持 SpriteSheet 动画显示的图像类,也是 UIImage 的子类。SpriteSheet 动画可以理解为在一张大图上分布有很多块小图,不同时刻显示不同的小图,已达到动画展示的目的。一张图的加载耗时相对多图会好很多,避免了一些非必要的资源浪费。

接口源码如下:

1
2
3
4
5
6
7
8
- (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;

方法实现过程:

  • 根据传入数据,初始化 SpriteSheet 动画播放过程中需要的参数
    • _contentRects
    • _frameDurations
    • _loopCount
  • 封装得到 YYSpriteSheetImage,并返回

YYAnimatedImage 协议

YYAnimatedImage 协议将 YYAnimatedImageView 和 YYImage、YYFrameImage、YYSpriteSheetImage 之间构成了联系。不论是这三种图像类或以后会有拓展的图像类,他们之间虽然存在区别,但最终动画展示的原理是不变的。可以将共性的模块通过 YYAnimatedImage 协议来实现。

对应协议源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@protocol YYAnimatedImage <NSObject>
@required
// 总帧数
- (NSUInteger)animatedImageFrameCount;
// 循环次数,0 表示无限循环
- (NSUInteger)animatedImageLoopCount;
// 每帧在内存中的占用大小
- (NSUInteger)animatedImageBytesPerFrame;

// 获取某一帧图像
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
// 获取某一帧的显示时间
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;

@optional
// 为 SpriteSheet 动画提供,获取某一动画的 contentsRect
- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
@end

共性抽离的思路在我们的日常开发中也常会用到,这里通过协议来统一接口的实现方式,使得逻辑看上去很清晰。

YYAnimatedImageView

YYAnimatedImageView 是用来展示 YYImage、YYFrameImage、YYSpriteSheetImage 的类。由于 YYImage、YYFrameImage、YYSpriteSheetImage 都实现了 YYAnimatedImage 的协议方法,YYAnimatedImageView 可以根据不同的类型来对应展示图片。具体看下内部的实现。

YYAnimatedImageView 可以理解为中间展示层,通过图像层解压的图像解码进行显示。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (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];
}

- (void)setAnimationImages:(NSArray *)animationImages {
if (self.animationImages == animationImages) return;
[self setImage:animationImages withType:YYAnimatedImageTypeImages];
}

- (void)setHighlightedAnimationImages:(NSArray *)highlightedAnimationImages {
if (self.highlightedAnimationImages == highlightedAnimationImages) return;
[self setImage:highlightedAnimationImages withType:YYAnimatedImageTypeHighlightedImages];
}

方法实现过程:

  • 在 YYAnimatedImageView 中可以看到四种 setImage 的方式
  • 先与已有 image 进行对比,如果不同则调用 -setImage:withType:
1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)setImage:(id)image withType:(YYAnimatedImageType)type {
[self stopAnimating];
if (_link) [self resetAnimated];
_curFrame = nil;
switch (type) {
case YYAnimatedImageTypeNone: break;
case YYAnimatedImageTypeImage: super.image = image; break;
case YYAnimatedImageTypeHighlightedImage: super.highlightedImage = image; break;
case YYAnimatedImageTypeImages: super.animationImages = image; break;
case YYAnimatedImageTypeHighlightedImages: super.highlightedAnimationImages = image; break;
}
[self imageChanged];
}

方法实现过程:

  • 根据不同的 type 重置对应 type 的 image 实例
  • 最终调用 **-imageChanged**。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
- (void)imageChanged {
// 获取当前 type 和 image 实例
YYAnimatedImageType newType = [self currentImageType];
id newVisibleImage = [self imageForType:newType];
NSUInteger newImageFrameCount = 0;
BOOL hasContentsRect = NO;
// 特殊处理 SpriteSheet 类型 Image
// 通过判断 protocol 是否有对应实现来判断,是否是 SpriteSheetImage
if ([newVisibleImage isKindOfClass:[UIImage class]] &&
[newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
if (newImageFrameCount > 1) {
hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
}
}
// 若之前展示过 SpriteSheetImage,这里进行 layer 复位
if (!hasContentsRect && _curImageHasContentsRect) {
// 复位 rect,且过程中取消隐式动画
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;
// 若是 SpriteSheetImage,取第一帧的 contentsRect
// 并定位到 image 中 contentsRect 对应的位置
if (hasContentsRect) {
CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
[self setContentsRect:rect forImage:newVisibleImage];
}
// 多帧图特殊处理
// 初始化属性 - totalLoop、_totalFrameCount 等
if (newImageFrameCount > 1) {
[self resetAnimated];
_curAnimatedImage = newVisibleImage;
_curFrame = newVisibleImage;
_totalLoop = _curAnimatedImage.animatedImageLoopCount;
_totalFrameCount = _curAnimatedImage.animatedImageFrameCount;
[self calcMaxBufferCount];
}
[self setNeedsDisplay];
[self didMoved];
}

方法实现过程:

  • 获取 imageType 和图像实例(image 或 images)
  • 通过 protocol 的实现情况,判断是否是 SpriteSheetImage
  • 若不是 SpriteSheetImage 且之前展示过 SpriteSheetImage,进行 layer 复位,且取消过程中的隐式动画
  • 若是 SpriteSheetImage,取第一帧的 contentsRect,并定位到对应 image 的位置
  • 多帧图的一些属性处理,调用 -resetAnimated 重置动画的配置项
  • 调用 didMoved 来执行动画

动画过程

1
2
3
4
5
6
7
8
9
- (void)didMoved {
if (self.autoPlayAnimatedImage) {
if(self.superview && self.window) {
[self startAnimating];
} else {
[self stopAnimating];
}
}
}

会判断是否有 superView 及 window,都满足的情况下,开启动画。

解压过程

前面 -resetAnimated 方法执行的过程中,会重置队列 **_requestQueue**。该队列的 maxConcurrentOperationCount 为 1,是一个串行队列。该队列作用是用来处理解压任务。

-resetAnimated 方法执行过程中会判断是否创建了定时器 **_link**,若未创建,则创建(关于定时器,放后面单独看):

1
2
3
4
5
6
_link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
// _runloopMode 为 NSRunLoopCommonModes
if (_runloopMode) {
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
}
_link.paused = YES;

看到会定时执行 -step: 方法,其内会判断当前 _requestQueue 是否无在执行的任务,如果没有加入新的 operation。该 operation 为 **_YYAnimatedImageViewFetchOperation**:

1
2
3
4
5
6
7
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 继承自 NSOperation,重写了 main 来自定义解压任务。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (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;
}
}
}
}

核心代码为:

1
2
UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
img = img.yy_imageByDecoded;

-animatedImageFrameAtIndex: 方法调用过程中会触发 yy_imageByDecoded,会进行解码操作。作者为了保证解码成功,又进行了第二次的解码(yy_imageByDecoded 方法内部会判断如果已经解码,不会再进行解码)。

解码完成后进行缓存。

缓存机制

我们可以看到 YYAnimatedImageView 中声明的关于缓存的私有变量:

1
2
3
4
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)

缓存的节点

在解码的过程中,会将解码的内容赋值给 _buffer

容量限制

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// dynamically adjust buffer size for current memory.
- (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); // BUFFER_SIZE = 10MB
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;
}

最大缓存容量是动态计算的:

  • 设备允许容量 = min(总内存x0.2, 总空闲内存x0.6)
  • 最大计算容量 = max(设备允许容量, BUFFER_SIZE) //BUFFER_SIZE=10MB
  • 最大容量 = min(最大计算容量, 自定义最大容量)

清理机制

在 resetAnimation 时会注册两个 Notification:内存警告、进入后台。

在应用进入后台时:

  • 先取消全部异步的解压操作
  • 计算下一帧的下标
  • 移除不是下一帧的所有缓存,保证进入前台时,能及时显示下一帧

_link 是基于 CADisplayLink 创建的定时器,帧率刷新的特性适合用来处理帧率相关的 UI 逻辑。单独拿出来梳理,是因为作者使用了 _YYImageWeakProxy 类进行消息转发来避免循环应用。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
A proxy used to hold a weak object.
It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink.
*/
@interface _YYImageWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation _YYImageWeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[_YYImageWeakProxy alloc] initWithTarget:target];
}
- (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)];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
return [_target respondsToSelector:aSelector];
}
- (BOOL)isEqual:(id)object {
return [_target isEqual:object];
}
- (NSUInteger)hash {
return [_target hash];
}
- (Class)superclass {
return [_target superclass];
}
- (Class)class {
return [_target class];
}
- (BOOL)isKindOfClass:(Class)aClass {
return [_target isKindOfClass:aClass];
}
- (BOOL)isMemberOfClass:(Class)aClass {
return [_target isMemberOfClass:aClass];
}
- (BOOL)conformsToProtocol:(Protocol *)aProtocol {
return [_target conformsToProtocol:aProtocol];
}
- (BOOL)isProxy {
return YES;
}
- (NSString *)description {
return [_target description];
}
- (NSString *)debugDescription {
return [_target debugDescription];
}
@end

消息转发过程:

  • _target 存在,向 _YYImageWeakProxy 实例发送消息,会正常转发给 target
  • _target 释放时,forwardingTargetForSelector: 重定向失败,会调用 methodSignatureForSelector: 来获取有效方法。
  • 若未获取到有效方法,抛出异常
  • 若获取到有效方法,调用 forwardInvocation: 进行消息转发。作者认为控制返回 null

YYImageCoder

该类主要包含:

  • YYImageFrame 类(图片帧信息)
  • YYImageDecoder 解码器
  • YYImageEncoder 编码器

解码过程描述:

  • 将 CGImageRef 转化为位图 bitmap
  • 先通过 CGBitmapContextCreate() 创建图片上下文
  • 再通过 CGContextDrawImage() 将图片绘制到上下文
  • 最后通过 CGBitmapContextCreateImage() 结合上下文生成位图

也通过 ImageIO 实现了渐进式解码。

多帧解码的过程中,会出现递归操作,作者选用了互斥锁 pthread_mutex_tpthread_mutex_t 本身不支持递归,可以通过设置打开递归选项。

总结

YYImage 可以理解为是基于 UIImage 的一个拓展。

功能上。一方面支持了更多的业务需求场景,如 webp 格式图片的展示。另一方面把图片的解码缓存,变为用户可控状态。我们可以根据自己的业务需求调整缓存的设定。

框架结构上。线程安全的处理、重叠模块的解耦都做的很棒。日常开发过程中,可以借鉴一些结构上的思路。

  • UIImage
    • 加载、解码 bitmap、CA 框架绘制
    • imageNamed:
    • imageWithData:
    • 大图
      • 4096x4096 最大纹理,老设备 OOM
      • ImageIO 取图像元数据
      • WWDC18 iOS10 引入 UIGraphicsImageRenderer
  • YYImage 框架
    • 图像层
    • 视图层
    • 编解码
  • YYImage
    • 重写 imageNamed
    • 初始化信号量 _preloadedLock,保证_preloadedFrames 读写安全
    • _preloadedFrames 保存所有帧图像
  • YYFrameImage
    • 帧动画
    • 帧图片 + 显示时长
    • png、jpeg
  • YYSpriteSheetImage
    • SpriteSheet 动画
    • rect
    • duration
    • loopCount
  • YYAnimatedImage 协议
    • 图像展示原理相同,展示通过该协议来实现
    • @required
    • @optional
  • YYAnimatedImageView
    • 展示上面的 Image 解码的图像
    • setImage/setHighlightedImage
    • setAnimationImages/setHighlightedAnimationImages
    • 根据 image type 拿到具体的 image 实例
    • 拿到实例后,imageChange
      • SpriteSheet 单独处理
      • 重置 rect,取消隐式动画
      • frameCount > 1,进行 resetAnimation
    • resetAnimation 做的几件事
      • 重置定时器(commonModes)
      • 使用了 NSProxy 避免循环引用
      • 添加 _YYAnimatedImageViewFetchOperation 进行预解码
      • 继承自 NSOperation,重写了 main 来自定义解压任务(二次解压保险)
      • 解码 image 时会进行 buffer 缓存
      • 缓存容量计算公式
        • 设备允许容量 = min(总内存x0.2, 总空闲内存x0.6)
        • 最大计算容量 = max(设备允许容量, BUFFER_SIZE) //BUFFER_SIZE=10MB
        • 最大容量 = min(最大计算容量, 自定义最大容量)
      • 缓存清理
        • resetAnimation 注册两通知(内存警告、进入后台)
        • 先取消全部异步解码操作
        • 计算下一帧的下标
        • 移除下一帧之外的所有缓存,以保证进入前台,可以立刻显示下一帧
  • YYImageCoder
    • YYImageFrame
    • YYImageDecoder
    • 解码过程
      • 将 CGImageRef 转为 bitmap
      • 通过 CGBitmapContextCreate() 创建上下文
    • 多帧解码过程中使用了 pthread_mutex_t,因为存在递归操作,这里使用了互斥锁

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @JonyFang 创建,使用 Stellar 作为主题,您可以在 GitHub 找到本站源码。