YYKit 系列源码剖析文章:ios
首先问一个问题:你会用图片么?git
图片是现代化 APP 界面设计里应用普遍的东西,精美的图片能够带来视觉上的享受,提升用户体验。由此给技术上带来了一些挑战,好比动图的处理、图片显示流畅程度的优化、图片包大小的优化、超大图片的处理等。github
本文主要是结合 YYImage 源码对图片处理技巧进行讲解。而笔者不会逐字逐句的翻译源码,主要是提取源码中有思惟价值的东西。因此最好是打开源码,本文做为思想引导。web
源码基于 1.0.4 版本。算法
首先来谈一谈图片处理的一些注意事项和技巧,如下结论参考其余博文、官方文档、实际测试得出,欢迎指出错误😁。数组
一张图片从磁盘中显示到屏幕上过程大体以下:从磁盘加载图片信息、解码二进制图片数据为位图、经过 CoreAnimation 框架处理最终绘制到屏幕上。浏览器
实际上图片的绘制过程每每不是性能瓶颈,最耗时的操做是解码过程,若图片文件过大,从磁盘读取的过程也有可观的耗时。缓存
通常使用imageNamed:
或者imageWithData:
从内存中加载图片生成UIImage
的实例,此刻图片并不会解压,当 RunLoop 准备处理图片显示的事务(CATransaction)时,才进行解压,而这个解压过程是在主线程中的,这是致使卡顿的重要因素。安全
使用imageNamed:
方法加载图片信息的同时(生成UIImage
实例),还会将图片信息缓存起来,因此当使用该方法第一次加载某张图片时,会消耗较多的时间,而以后再次加载该图片速度就会很是快(注意此时该图片是未绘制到屏幕上的,也就是说还未解压)。bash
在绘制到屏幕以前,第一次解压成功后,系统会将解压信息缓存到内存。
值得注意的是,这些缓存都是全局的,并不会由于当前UIImage
实例的释放而清除,在收到内存警告或者 APP 第一次进入后台才有可能会清除,而这个清除的时机和内容是系统决定的,咱们没法干涉。
使用imageWithData:
方式加载图片时,无论是加载过程仍是解压过程,都不会像imageNamed:
缓存到全局,当该UIImage
实例释放时,相关的图片信息和解压信息就会销毁。
从上面的分析可知,imageNamed:
使用时会产生全局的内存占用,可是第二次使用同一张图片时性能很好;imageWithData:
不会有全局的内存占用,但对于同一张图片每次加载和解压都会“从头开始”。
因而可知,imageNamed:
适合“小”且“使用频繁”的图片,imageWithData:
适合“大”且“低频使用”的图片。
这里说的优化并非解压算法的优化,只是基于用户体验的优化。
对于加载过程,若文件过大或加载频繁影响了帧率(好比列表展现大图),可使用异步方式加载图片,减小主线程的压力,代码大体以下:
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(...)
系列方法就能实现。
该处理方式在众多图片处理框架下都有体现。
值得注意的是,可能业务中须要载入一张很大的图片。这时,若还使用常规的方式加载会占用过多的内存;何况,若图片的像素过大(目前主流 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);
复制代码
或者直接使用CALayer
的contentsRect
属性来达到相同的效果。
笔者有写过一个小东西,里面就使用了异步压缩和异步裁剪来处理超大图片:打造开源第一 iOS 图片浏览器 (支持视频)
上文中谈了一下图片处理的一些原理和核心思想,作为背景知识,下面从一个宏观的角度观察一下 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 写的特性:
该类对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
的读写安全,耗时短,使用信号量性能很好。
该类是帧动画图片类,能够配置每一帧的图片信息和显示时长,图片支持 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
载体操做和显示。
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
表示范围的数组,一个是对应时长的数组。
而后利用CALayer
的contentsRect
属性,动态的读取这张大图某个范围的内容。固然,这个过程的逻辑一样在YYAnimatedImageView
类中。
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
都分别实现了该协议,具体操做能够看源码,没有难度。
其中,- (CGRect)animatedImageContentsRectAtIndex:(NSUInteger)index;
是可选方法,是YYSpriteSheetImage
作 SpriteSheet 动画须要的数据,这算是一个共有逻辑以外的特例。
利用协议来规范共有逻辑,是一个值得学习的技巧,它能让逻辑更清晰,代码更有条理。
一句话理解:YYAnimatedImageView
类经过YYImage、YYFrameImage、YYSpriteSheetImage
实现的<YYAnimatedImage>
协议方法拿到帧图片数据和相关信息进行动画展现。
它的原理就是如此,下面主要分析技术细节,含金量蛮高。
@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 类型来源
/*一、若上一次是 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;
/*二、SpriteSheet 类型时,经过`setContentsRect:forImage:`方法
配置self.layer.contentsRect */
if (hasContentsRect) {
CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
[self setContentsRect:rect forImage:newVisibleImage];
}
/*三、如果多帧的图片,经过`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.contentsRect
为CGRectMake(0, 0, 1, 1)
使用了CATransaction
事务来取消隐式动画。(因为此处彻底不须要那 0.25 秒的隐式动画)
- (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];
}
复制代码
在didMoveToWindow
和didMoveToSuperview
周期方法中尝试启动或结束动画,不须要在组件内部特地的去调用就能实现自动的播放和中止。而didMoved
方法中判断是否开启动画写了个self.superview && self.window
,意味着YYAnimatedImageView
光有父视图还不能开启动画,还须要展现在window
上才行。
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
串行队列执行的,不会影响主线程。
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
}
复制代码
在进入后台时,清除全部的异步解压任务,而后计算下一帧的下标,最后移除不是下一帧的全部缓存,保证进入前台时下一帧的及时显示。
在收到内存警告时处理方式大同小异,很少赘述。
该类使用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 {...}
就是调用动画的核心代码,实际上代码比较容易看懂,主要是显示当前帧图像、发起下一帧的解压任务等。
该文件中主要包含了YYImageFrame
图片帧信息的类、YYImageDecoder
解码器、YYImageEncoder
编码器。
注意,本文对 WebP / APNG 等的图片解压缩算法不会讨论,主要是说明一些基于 ImageIO 的使用。
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()
经过上下文生成图片。在_updateSourceImageIO
私有方法中能够看到渐进式的解压逻辑,因为代码过多不贴出来,主要逻辑大体以下:
CGImageSourceCreateIncremental(NULL)
建立空图片源。CGImageSourceUpdateData()
更新图片源CGImageSourceCreateImageAtIndex()
建立图片渐进式解压能够在下载图片的过程当中进行解压、显示,达到网页上显示图片的效果,体验不错。
确实笔者疲于继续查看 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 移动端图片格式调研