iOS图片解析与YYImage源码学习

基础知识

像素

图像的基本元素。举个例子:将一张图片放到PS中尽可能的放大,那么我们可以看到一个个的小格子,其中每个小格子就是一个像素点,每个像素点有且仅有一个颜色。
像素由四种不同的向量组成,即我们熟悉的RGBA(red,green,blue,alpha)。

位图

位图就是一个像素数组,数组中的每个像素都代表图片中的一个点。我们经常用到的JPEG和PNG图片就是位图。(压缩过的图片格式)。

帧缓冲区

帧缓冲区(显存):是由像素组成的二维数组,每一个存储单元对应屏幕上的一个像素,整个帧缓冲对应一帧图像即当前屏幕画面。我们知道iOS设备屏幕是一秒刷新60次,如果帧缓冲区的内容有改变,那么我们看到的屏幕显示内容就会改变。

图片处理的过程知识

从图片文件把 图片数据的像素拿出来(RGBA), 对像素进行操作, 进行一个转换(Bitmap (GPU))
修改完之后,还原(图片的属性 RGBA,RGBA (宽度,高度,色值空间,拿到宽度和高度,每一个画多少个像素,画多少行))

iOS图片显示的流程

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

  1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  2. 然后将生成的 UIImage 赋值给 UIImageView ;
  3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  4. 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
  5. 分配内存缓冲区用于管理文件 IO 和解压缩操作;
  6. 将文件数据从磁盘读到内存中;
  7. 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
  8. 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。
    在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。
加载优化

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

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(), ^{
//业务
});
});

ImageIO 核心

ImageIO框架提供了读取与写入图片数据的基本方法,使用它可以直接获取到图片文件的内容数据,ImageIO框架中包含6个头文件,其中完成主要功能的是前两个头文件中定义的方法:

1.CGImageSource.h:负责读取图片数据。

2.CGImageDestination.h:负责写入图片数据。

3.CGImageMetadata.h:图片文件元数据类。

4.CGImageProperties:定义了框架中使用的字符串常量和宏。

5.ImageIOBase.h:预处理逻辑,无需关心。

  1. CGImageSource.h:负责读取图片数据。

CGImageSource类的主要作用是用来读取图片数据,在平时开发中,关于图片我们使用的最多的可能是UIImage类,UIImage是iOS系统UI系统中用于构建图像对象的类,但是其中只有图像数据,实际上一个图片文件中存储的除了图片数据外,还有一些地理位置、设备类型、时间等信息,除此之外,一个图片文件中可能存储的也不只一张图像(例如gif文件)。CGImageSource就是这样的一个抽象图片数据示例,从其中可以获取到我们所关心的所有数据。
读取图片文件数据,并将其展示在视图的简单代码示例如下:

//获取图片文件路径
NSString * path = [[NSBundle mainBundle]pathForResource:@"timg" ofType:@"jpeg"];
NSURL * url = [NSURL fileURLWithPath:path];
CGImageRef myImage = NULL;
CGImageSourceRef myImageSource;
//通过文件路径创建CGImageSource对象
myImageSource = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
//获取第一张图片
myImage = CGImageSourceCreateImageAtIndex(myImageSource,
0,
NULL);
CFRelease(myImageSource);
UIImageView * image = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 200, 200)];
image.image = [UIImage imageWithCGImage:myImage];
[self.view addSubview:image];

上面的示例代码采用的是本地的一个素材文件,当然通过网络图片链接也是可以创建CGImageSource独享的。除了通过URL链接的方式创建对象,ImageIO框架中还提供了两种方法,解析如下:

//通过数据提供器创建CGImageSource对象
/*
CGDataProviderRef是CoreGraphics框架中的一个数据读取类,其也可以通过Data数据,URL和文件名来创建
*/
CGImageSourceRef __nullable CGImageSourceCreateWithDataProvider(CGDataProviderRef __nonnull provider, CFDictionaryRef __nullable options);
//通过Data数据创建CGImageSource对象
CGImageSourceRef __nullable CGImageSourceCreateWithData(CFDataRef __nonnull data, CFDictionaryRef __nullable options);
  1. CGImageDestination.h:负责写入图片数据
    CGImageSource是图片文件数据的抽象对象,而CGImageDestination的作用则是将抽象的图片数据写入指定的目标中。将图片写成文件示例如下:
//创建存储路径
NSArray *paths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
NSString *newPath = [paths.firstObject stringByAppendingPathComponent:[NSString stringWithFormat:@"image.png"]];
CFURLRef URL =  CFURLCreateWithFileSystemPath (
kCFAllocatorDefault,
(CFStringRef)newPath,
kCFURLPOSIXPathStyle, 
false);
//创建CGImageDestination对象
CGImageDestinationRef myImageDest = CGImageDestinationCreateWithURL(URL,CFSTR("public.png"), 1, NULL);
UIImage * image = [UIImage imageNamed:@"timg.jpeg"];
//写入图片
CGImageDestinationAddImage(myImageDest, image.CGImage, NULL);
CGImageDestinationFinalize(myImageDest);
CFRelease(myImageDest);

更多详情查看ImageIO更多的详情

YYImage 结构

通过 YYImage 源码可以按照其与 UIKit 的对应关系划分为三个层级:

层级: UIKit YYImage
图像层 UIImage YImage,YYFrameImage,YYSpriteSheetImage
视图层 UIImageView YYAnimatedImageView
编/解码层 ImageIO.framework YYImageCoder
  • 图像层,把不同类型的图像信息封装成类并提供初始化和其他便捷接口。
  • 视图层,负责图像层内容的显示(包含动态图像的动画播放)工作。
  • 编/解码层,提供图像底层支持,使整个框架得以支持市场主流的图片格式。
YYImage 结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8we4MVk4-1572924259122)(evernotecid://DC8466E2-282C-4B26-91B9-7D010D5B0CAB/appyinxiangcom/6357986/ENResource/p873)]

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

initWithData 核心代码

YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];  //用来获取图片组里每个图片的属性:每张图片停留的时间等其他属性、循环次数、图片方向、图片宽、高,并保存到frames这么个数组里

YYImageFrame *frame = [decoder frameAtIndex:0 decodeForDisplay:YES];  //图片解压
YYImageCoder 编解码

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

1、解码核心代码

GImageRef 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()通过上下文生成图片。

APNG的处理

PNG的文件结构

PNG文件结构很简单,主要有数据块(Chunk Block)组成,最少包含4个数据块。PNG标识符 PNG数据块(IHDR) PNG数据块(其他类型数据块) … PNG结尾数据块(IEND)

PNG标识符,其文件头位置总是由位固定的字节来描述的:
十进制数
137 80 78 71 13 10 26 10
十六进制数
89 50 4E 47 0D 0A 1A 0A

一个标准的PNG文件结构应该如下:

内容 内容 内容 内容
PNG文件标志 PNG数据块 …… PNG数据块

PNG数据块 …… PNG数据块

PNG文件格式中的数据块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r09lBk40-1572924259124)(evernotecid://DC8466E2-282C-4B26-91B9-7D010D5B0CAB/appyinxiangcom/6357986/ENResource/p871)]

PNG 由 4 部分组成,首先以 PNG Signature(PNG签名块)开头,紧接着一个 IHDR(图像头部块),然后是一个或多个的 IDAT(图像数据块),最终以 IEND(图像结束块)结尾。

数据块结构

PNG文件中,每个数据块由4个部分组成,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tiUFHiic-1572924259131)(evernotecid://DC8466E2-282C-4B26-91B9-7D010D5B0CAB/appyinxiangcom/6357986/ENResource/p872)]

APNG 的组成

APNG 规范引入了三个新大块,分别是:acTL(动画控制块)、fcTL(帧控制块)、fdAT(帧数据块),下图是三个独立的 PNG 文件组成 APNG 的示意图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9s0RXo9u-1572924259132)(evernotecid://DC8466E2-282C-4B26-91B9-7D010D5B0CAB/appyinxiangcom/6357986/ENResource/p870)]

  • acTL 块必须在第一个 IDAT 块之前,用于告诉解析器这是一个动画 PNG,包含动画帧总数和循环次数的信息
  • fcTL 块是每一帧都必须的,出现在 IDAT 或 fdAT 之前,包含顺序号、宽高、帧位置、延时等信息
  • fdAT 块与 IDAT 块有着相同的结构,除了 fcTL 中的顺序号

从图中可以发现第一帧与后面两帧不同,那是因为第一帧 APNG 文件存储的为一个正常的 PNG 数据块,对于不支持 APNG 的浏览器或软件,只会显示 APNG 文件的第一帧,忽略后面附加的动画块,这也是为什么 APNG 能向下兼容 PNG 的原因。

APNG

更多详细的请参考
http://web.jobbole.com/88847/

APNG 代码逻辑

通过以下方法对图片数据进行解压获取apng的信息
yy_png_info *apng = yy_png_info_create(_data.bytes, (uint32_t)_data.length); //data 图片压缩的数据
首先读取apng的信息

// parse png chunks
uint32_t offset = 8;
uint32_t chunk_num = 0;  //数据块数量
uint32_t chunk_capacity = chunk_realloc_num;  //内存区域容量
uint32_t apng_loop_num = 0;   //循环次数
int32_t apng_sequence_index = -1;   //序号
int32_t apng_frame_index = 0;   //frame的编号
int32_t apng_frame_number = -1;   //frame的数量

然后遍历所有的数据块,只针对IDAT、fcTL、acTL、FdAT数据块进行处理,最终这个for 循环得出了info->apng_frames(这么一个指针),它指向所有的frame数据.

for (int32_t i = 0; i < info->chunk_num; i++) {  
yy_png_chunk_info *chunk = info->chunks + i;
switch (chunk->fourcc) {
case YY_FOUR_CC('I', 'D', 'A', 'T'): {
if (info->apng_shared_insert_index == 0) {
info->apng_shared_insert_index = i;
}
if (first_frame_is_cover) {
yy_png_frame_info *frame = info->apng_frames + frame_index;
frame->chunk_num++;
frame->chunk_size += chunk->length + 12;
}
} break;
case YY_FOUR_CC('a', 'c', 'T', 'L'): {
} break;
case YY_FOUR_CC('f', 'c', 'T', 'L'): {
frame_index++;
yy_png_frame_info *frame = info->apng_frames + frame_index;
frame->chunk_index = i + 1;
yy_png_chunk_fcTL_read(&frame->frame_control, data + chunk->offset + 8);
} break;
case YY_FOUR_CC('f', 'd', 'A', 'T'): {
yy_png_frame_info *frame = info->apng_frames + frame_index;
frame->chunk_num++;
frame->chunk_size += chunk->length + 12;
} break;
default: {
*shared_chunk_index = i;
shared_chunk_index++;
info->apng_shared_chunk_size += chunk->length + 12;
info->apng_shared_chunk_num++;
} break;
}
}

通过YYImageDecoder 来读取图片组里每个图片的属性:每张图片停留的时间等其他属性、循环次数、图片方向、图片宽、高,并保存到frames这么个数组里 通过CGImageSourceRef 解压出图片数据生成UIImage

YYAnimatedImageView

YYAnimatedImageView类通过YYImage、YYFrameImage、YYSpriteSheetImage实现的协议方法拿到帧图片数据和相关信息进行动画展示。

1.初始化流程

该类重写了一系列方法让它们都走自定义配置:

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

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

3.异步解压

YYAnimatedImageView有个队列_requestQueue = [[NSOperationQueue alloc] init];
_requestQueue.maxConcurrentOperationCount = 1;变量NSOperationQueue *_requestQueue;

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

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

YAnimatedImageViewFetchOperation继承自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属性综合判断。

动画的核心方法

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

- (void)step:(CADisplayLink *)link {

UIImage  *image = _curAnimatedImage;
NSMutableDictionary *buffer = _buffer;
UIImage *bufferedImage = nil;
NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
BOOL bufferIsFull = NO;

if (!image) return;
if (_loopEnd) { // view will keep in last frame
[self stopAnimating];
return;
}

NSTimeInterval delay = 0;
if (!_bufferMiss) {  //下一张图片缺失,那么此时_bufferMiss=YES
_time += link.duration;
delay = [image animatedImageDurationAtIndex:_curIndex];  //第一张图片的停留时间

if (_time < delay) return;  //如果累积时间小于停留时间 啥也不做,返回

_time -= delay;  //累积时间大于停留时间了,那么换下一张图片;

if (nextIndex == 0) {
_curLoop++;
if (_curLoop >= _totalLoop && _totalLoop != 0) {
_loopEnd = YES;
[self stopAnimating];
[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
return; // stop at last frame
}
}
delay = [image animatedImageDurationAtIndex:nextIndex];  //获取到下一帧的图片停留时间

if (_time > delay) _time = delay; // do not jump over frame  下一帧图片的停留时间小于累积时间
}
LOCK(

//获取到下一张图片
bufferedImage = buffer[@(nextIndex)];
if (bufferedImage) {
if ((int)_incrBufferCount < _totalFrameCount) {
[buffer removeObjectForKey:@(nextIndex)];
}
[self willChangeValueForKey:@"currentAnimatedImageIndex"];
_curIndex = nextIndex;
[self didChangeValueForKey:@"currentAnimatedImageIndex"];
_curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
if (_curImageHasContentsRect) {
_curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex]; //sprite sheet image里的contentsRect数组里的第_curIndex个数据
[self setContentsRect:_curContentsRect forImage:_curFrame];
}
nextIndex = (_curIndex + 1) % _totalFrameCount;
_bufferMiss = NO;
if (buffer.count == _totalFrameCount) {
bufferIsFull = YES;
}
} else {
_bufferMiss = YES;  //下一张图片缺少了
}
)//LOCK

//若图片存在
if (!_bufferMiss) {
[self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
}

//此时线程池里没有线程开启
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];
}
}

具体思路

  1. 创建一个CADisplayLink 定时器,并添加到RunloopCommonMode下, 跟屏幕刷新相关 调用时间:1/60S = 16.7ms,到了时间点就调用一个方法:step方法
  2. 看CADisplayLink时间的累积,是不是达到临界值,达到了我就可以更新下一张图片(开辟一个线程,获取到下一张解压缩的图片)
  3. 更新图片的时候(获取到下一张图片,图片不存在,开启线程来获取,直到获取到,然后展示),只要把2步骤的解压缩图片赋值到界面上,就能展示出来了

你可能感兴趣的:(IOS)