图像的基本元素。举个例子:将一张图片放到PS中尽可能的放大,那么我们可以看到一个个的小格子,其中每个小格子就是一个像素点,每个像素点有且仅有一个颜色。
像素由四种不同的向量组成,即我们熟悉的RGBA(red,green,blue,alpha)。
位图就是一个像素数组,数组中的每个像素都代表图片中的一个点。我们经常用到的JPEG和PNG图片就是位图。(压缩过的图片格式)。
帧缓冲区(显存):是由像素组成的二维数组,每一个存储单元对应屏幕上的一个像素,整个帧缓冲对应一帧图像即当前屏幕画面。我们知道iOS设备屏幕是一秒刷新60次,如果帧缓冲区的内容有改变,那么我们看到的屏幕显示内容就会改变。
从图片文件把 图片数据的像素拿出来(RGBA), 对像素进行操作, 进行一个转换(Bitmap (GPU))
修改完之后,还原(图片的属性 RGBA,RGBA (宽度,高度,色值空间,拿到宽度和高度,每一个画多少个像素,画多少行))
一张图片从磁盘中显示到屏幕上过程大致如下:从磁盘加载图片信息、解码二进制图片数据为位图、通过 CoreAnimation 框架处理最终绘制到屏幕上
对于加载过程,若文件过大或加载频繁影响了帧率(比如列表展示大图),可以使用异步方式加载图片,减少主线程的压力,代码大致如下:
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框架中包含6个头文件,其中完成主要功能的是前两个头文件中定义的方法:
1.CGImageSource.h:负责读取图片数据。
2.CGImageDestination.h:负责写入图片数据。
3.CGImageMetadata.h:图片文件元数据类。
4.CGImageProperties:定义了框架中使用的字符串常量和宏。
5.ImageIOBase.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);
//创建存储路径
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 源码可以按照其与 UIKit 的对应关系划分为三个层级:
层级: | UIKit | YYImage |
---|---|---|
图像层 | UIImage | YImage,YYFrameImage,YYSpriteSheetImage |
视图层 | UIImageView | YYAnimatedImageView |
编/解码层 | ImageIO.framework | YYImageCoder |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8we4MVk4-1572924259122)(evernotecid://DC8466E2-282C-4B26-91B9-7D010D5B0CAB/appyinxiangcom/6357986/ENResource/p873)]
该类对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]; //图片解压
该文件中主要包含了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()通过上下文生成图片。
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数据块
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 规范引入了三个新大块,分别是:acTL(动画控制块)、fcTL(帧控制块)、fdAT(帧数据块),下图是三个独立的 PNG 文件组成 APNG 的示意图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9s0RXo9u-1572924259132)(evernotecid://DC8466E2-282C-4B26-91B9-7D010D5B0CAB/appyinxiangcom/6357986/ENResource/p870)]
从图中可以发现第一帧与后面两帧不同,那是因为第一帧 APNG 文件存储的为一个正常的 PNG 数据块,对于不支持 APNG 的浏览器或软件,只会显示 APNG 文件的第一帧,忽略后面附加的动画块,这也是为什么 APNG 能向下兼容 PNG 的原因。
更多详细的请参考
http://web.jobbole.com/88847/
通过以下方法对图片数据进行解压获取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类通过YYImage、YYFrameImage、YYSpriteSheetImage实现的协议方法拿到帧图片数据和相关信息进行动画展示。
该类重写了一系列方法让它们都走自定义配置:
- (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];
}
- (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有个队列_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串行队列执行的,不会影响主线程。
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 <YYAnimatedImage> *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];
}
}
具体思路