YYImageCoder
An image decoder to decode image data.
YYImageCoder
支持解码像webP, APNG, GIF这样的动图和PNG, JPG, JP2, BMP, TIFF, PIC, ICNS, ICO这样的静图;它可以一次性完成所有图片数据的解码,也可以在下载图片过程中逐步解码图片数据;这个类是线程安全的;
建议大家在阅读先看下这些文章,了解下一些图片的格式,对于YY在解码过程的代码会有一个更好的理解,不然会非常的费劲。PNG格式详解,APNG格式,WebP API Documentation
需要了解的一些知识
互斥锁 pthread_mutex_t
在 YYImageDecoder 中用到了 pthread_mutex_t
进行加锁操作以保证数据的安全,应该还是有部分同学不是很了解这些 C 的API的,现在我们就先学习下这部分的知识。
在线程实际运行过程中,我们经常需要多个线程保持同步。这时可以用互斥锁来完成任务(互斥锁用来保证一段时间内只有一个线程在执行一段代码);互斥锁的使用过程中,主要有pthread_mutex_init
,pthread_mutex_destory
,pthread_mutex_lock
,pthread_mutex_unlock
这几个函数以完成锁的初始化,锁的销毁,上锁和释放锁操作。
-
锁的创建
- 宏创建:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 通过锁的属性动态创建:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
- 宏创建:
-
锁的属性
pthread_mutexattr_t
-
互斥锁属性可以由
pthread_mutexattr_init(pthread_mutexattr_t *mattr)
来初始化,然后可以调用其他的属性设置方法来设置其属性; -
互斥锁的范围:可以指定是该进程与其他进程的同步还是同一进程内不同的线程之间的同步。可以设置为
PTHREAD_PROCESS_SHARE
和PTHREAD_PROCESS_PRIVATE
。默认是后者,表示进程内使用锁 -
可以使用
int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr, int pshared)
或者int pthread_mutexattr_getshared(pthread_mutexattr_t *mattr,int *pshared)
来设置与获取锁的范围; -
互斥锁的类型:
-
PTHREAD_MUTEX_NORMAL
: 这种类型的互斥锁不会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会引起这个线程的死锁。如果试图解锁一个由别的线程锁定的互斥锁会引发不可预料的结果。如果一个线程试图解锁已经被解锁的互斥锁也会引发不可预料的结果; -
PTHREAD_MUTEX_ERRORCHECK
: 这种类型的互斥锁会自动检测死锁。如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码; -
PTHREAD_MUTEX_RECURSIVE
: 如果一个线程对这种类型的互斥锁重复上锁,不会引起死锁,一个线程对这类互斥锁的多次重复上锁必须由这个线程来重复相同数量的解锁,这样才能解开这个互斥锁,别的线程才能得到这个互斥锁。如果试图解锁一个由别的线程锁定的互斥锁将会返回一个错误代码。如果一个线程试图解锁已经被解锁的互斥锁也将会返回一个错误代码。这种类型的互斥锁只能是进程私有的(作用域属性为PTHREAD_PROCESS_PRIVATE); -
PTHREAD_MUTEX_DEFAULT
:同PTHREAD_MUTEX_NORMAL
;
-
-
可以用
pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type)
和pthread_mutexattr_gettype(pthread_mutexattr_t *attr , int *type)
获取或设置锁的类型;
-
-
锁的释放
- 调用
pthread_mutex_destory
之后,可以释放锁占用的资源,但这有一个前提上锁当前是没有被锁的状态;
- 调用
-
锁操作
-
pthread_mutex_lock()
: 锁住指定的mutex 对象,正常操作返回0,否则返回一个错误的提示码。如果mutex已经被锁住,调用这个函数的线程阻塞直到 mutex 可用为止。这跟函数返回的时候参数 mutex 对象变成锁住状态, 同时该函数的调用线程成为该 mutex 对象的拥有者; -
pthread_mutex_unlock()
: 函数释放 mutex ,正常操作返回0,否则返回一个错误的提示码;如果有多个线程为了获得该 mutex 锁阻塞,系统会根据一定的调度策略将被用来决定哪个线程可以获得该 mutex 锁(在 mutex 类型为PTHREAD_MUTEX_RECURSIVE
的情况下,只有当 lock count 减为0并且调用线程在该 mutex 上已经没有锁的时候)。如果一个线程在等待一个 mutex 锁得时候收到了一个 signal ,那么在从 signal handler 返回的时候,该线程继续等待该 mutex 锁,就像这个线程没有被中断一样(==signal 这点不是很懂==); -
pthread_mutex_trylock()
:当参数 mutex 处于被锁住的状态时立即返回,除此之外,它跟pthread_mutex_lock()
功能完全一样;在成功获得了一个 mutex 的锁后返回0,否则返回一个错误提示码错误;
-
大端小端
在很多时候,服务器返回的二进制数据是以大端模式存储的,而机器是小端模式,必须进行转换,否则使用时会出问题。
- 大端模式(Big Endian):数据的高字节,保存在内存的低地址中;数据的低字节,保存在内存的高地址中。
- 小端模式(Little Endian):数据的高字节,保存在内存的高地址中;数据的低字节,保存在内存的低地址中。
举例如下:
1. 16位宽的数0x1234,在不同的模式下,存储方式为:
模式 | 0x4000 | 0x4001 |
---|---|---|
小端模式 | 0x34 | 0x12 |
大端模式 | 0x12 | 0x34 |
2. 32位宽的数0x12345678,在不同的模式下,存储方式为:
模式 | 0x4000 | 0x4001 | 0x4002 | 0x4003 |
---|---|---|---|---|
小端模式 | 0x78 | 0x56 | 0x34 | 0x12 |
大端模式 | 0x12 | 0x34 | 0x56 | 0x78 |
对32位的数,即4个字节,大端转换成小端:
static inline uint32_t yy_swap_endian_uint32(uint32_t value) {
return
(uint32_t)((value & 0x000000FFU) << 24) |
(uint32_t)((value & 0x0000FF00U) << 8) |
(uint32_t)((value & 0x00FF0000U) >> 8) |
(uint32_t)((value & 0xFF000000U) >> 24) ;
}
即将低8位(08位)左移24位,变成了高8位(2432位),816位左移8位变成了(1624位)。将原高8位和高16位右移,变成了新的低8位和低16位;对于64位的同理,可以先将低32位调用上述函数得到 high_uint64
,再将 value
移位到高32位之后再次调用此方法得到 low_uint64
,最后返回 (high_uint64 << 32) + low_uint64
即可。
PNG格式
对于一个 PNG 文件来说,它是由文件头数据块和另外三个以上的数据块(chunk)组成:
数据块符号 | 数据块名称 | 位置 |
---|---|---|
IHDR | 文件头数据块 | 第一块 |
PLTE | 调色板数据块 | 在IDAT之前 |
IDAT | 图像数据块 | 与其他IDAT连续 |
IEND | 图像结束数据 | 最后一个数据块 |
PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是标准的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。关键数据块定义了4个标准数据块,每个PNG文件都必须包含它们,PNG读写软件也都必须要支持这些数据块。虽然PNG文件规范没有要求PNG编译码器对可选数据块进行编码和译码,但规范提倡支持可选数据块。
IHDR
它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。
这块包含了图片的如下信息:画布的长宽,色深,颜色类型,压缩方式,滤波方法和隔行扫描方法;
PLTE
调色板数据块 PLTE (palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。
PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成:Red
、 Green
、 Blue
对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。
真彩色图像和带α通道数据的真彩色图像也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。
IDAT
图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。
IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。
IEND
图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。
APNG 作为 PNG 的扩展在此基础上添加了 acTL
、 fcTL
、 fdAT
三个字段,作用分别如下:
-
acTL
: 动画控制块,包括了帧数和循环动画次数; -
fcTL
: 每一帧的控制模块,包含了如下信息:-
sequence_number
: 动画块的个数,从0开始; -
width
: 帧的宽度; -
height
: 帧的高度; -
x_offset
: 渲染在此帧上的图片的原点 X 坐标; -
y_offset
: 渲染在此帧上的图片的原点 Y 坐标; -
delay_num
: 帧延迟的分子; -
delay_den
: 帧延迟的分母;(这两个属性主要用来生成一个浮点数来表示动图显示时长) -
dispose_op
: 渲染模式; -
blend_op
: 混合模式;
-
-
fdAT
: 每一帧的数据块,包括动画块的个数,从0开始和此帧的二进制数据
YY用到的枚举
- YYImageType: 解码的类型
YYImageType | 类型 |
---|---|
YYImageTypeUnknown | 未知格式 |
YYImageTypeJPEG | jpeg, jpg |
YYImageTypeJPEG2000 | jp2 |
YYImageTypeTIFF | tiff, tif |
YYImageTypeBMP | bmp |
YYImageTypeICO | ico |
YYImageTypeICNS | icns |
YYImageTypeGIF | gif |
YYImageTypePNG | png |
YYImageTypeWebP | webp |
YYImageTypeOther | 其他图片格式 |
这里需要说明一下,以后缀名为文件格式的基准是不准确的,实际处理的时候我们应该以图片二进制中的数据来判断图片的类型;每个图片都在其文件头标识中表明其类型,因此拿到data之后我们必须分享头标识,然后准确的给文件一个类型,YY就是这么做的。具体可以看下这两篇博客 通过文件头标识判断图片格式 和 通过文件头判断图片类型
- YYImageDisposeMethod: 在画布上渲染下一帧之前,如何处理当前帧使用的区域;
- YYImageDisposeNone: 把当前帧增量绘制到画布上,不清空画布;
- YYImageDisposeBackground:绘制当前帧之前,先把画布清空为默认背景色;
- YYImageDisposePrevious:绘制下一帧前,把先把画布恢复为当前帧的前一帧;
- YYImageBlendOperation: 混合操作指定当前帧的透明像素如何与先前画布的像素混合。
- YYImageBlendNone: 绘制时,全部通道(包含Alpha通道)都会覆盖到画布,相当于绘制前先清空画布的指定区域;
- YYImageBlendOver:绘制时,Alpha 通道会被合成到画布,即通常情况下两张图片重叠的效果;
YYImageFrame
An image frame object.
一个实现了NSCopying
协议,用来表示图片中帧的类,如果是动图,会包含所有帧数据;
Property
-
index
: 帧的索引 -
width
: 帧的宽度 -
height
: 帧的高度 -
offsetX
: 帧在画布上的X方向上的偏移量(左下角为原点) -
offsetY
: 帧在画布上的Y方向上的偏移量 -
duration
: 帧经过的秒数; -
dispose
: YYImageDisposeMethod; -
blend
: YYImageBlendOperation; -
image
: the UIImage;
Public func
-
+ (instancetype)frameWithImage:(UIImage *)image;
直接调用new
来初始化,并将image复制给image属性; -
- (id)copyWithZone:(NSZone *)zone
实现NSCopying
协议,new
一个YYImageFrame
实例,赋值后返回;
_YYImageDecoderFrame
YY 在内部实现的时候还给了 YYImageFrame
一个子类,来更好的表示每一帧数据;并在 YYImageFrame
的基础上添加了如下属性:
-
hasAlpha
: 图片是否有alhpa
通道; -
isFullSize
: 图片是否填充了整个画布; -
blendFromIndex
: 图片从第几帧到当前帧开始混合;
YYImageDecoder
Public Property
-
data
: 图片的二进制数据; -
type
: 图片的类型(YYImageType); -
scale
: 图片的缩放比例; -
frameCount
: 图片的帧数; -
loopCount
: 动图时,循环显示的次数;如果为0则是无限循环; -
width
: 图片画布的宽; -
height
: 图片画布的高; -
finalized
: 当边下边显示图片时,用来表示是否为图片的最后一次数据;
Private Property
-
_lock
: 一个pthread_mutex_t
类型的递归锁; -
_sourceTypeDetected
: -
_source
: 指向在操作的当前帧数据资源,CGImageSourceRef 指针; -
_apngSource
: APNG 的信息;yy_png_info
结构体,用来表示 APNG 的所有信息,因为 iOS 是在8.0只有 ImageIO 才支持APNG的图片,为了支持老版本,作者自己做了解码工作,所以才自己添加了表示 APNG 的信息模块; -
_webpSource
: WebPDemuxer 类型;如果 webP 要支持动图,首先在编译 libwebp 时需要加上 demux 模块,解码 WebP 时需要先用 WebPDemuxer 尝试拆包,之后再把拆出来的单帧用 WebPDecode 解码; -
_orientation
: 图片的方向; -
_framesLock
: 操作每一帧数据时的锁;dispatch_semaphore_t
类型; -
_frames
: 原始帧数据;< Array<GGImageDecoderFrame>
-
_needBlend
: 是否需要混合; -
_blendFrameIndex
: 当前混合的帧的索引; -
_blendCanvas
: 混合到画布的上下文;CGContextRef 类型;
Fuctions
由于实现的代码有2000多行,所以我们直接看对外的接口,看到了用到的私有方法,再去分析每个私有方法具体实现的内容,看这样效果如何
Private Funcs
还是先看下一些私有的功能方法到底是实现了什么东西,怎么实现的,不然直接去看对外的接口,还真的是跳来跳去的;
-
- (void)_updateSource
这个方法是YYImageCoder
里一个非常重要的方法,这个方法里面根据用户提供的二进制数据来增量更新图片数据;后面我们会跟大家分析下不同类型的更新图片的方法;- 如果
_type
则调用- (void)_updateSourceWebP
方法; - 如果
_type
则调用- (void)_updateSourceAPNG
方法; - 为其他类型则滴啊用
- (void)_updateSourceImageIO
方法;
- 如果
-
- (void)_updateSourceWebP
如果图片是webP
类型的,那么会进行下面的操作;-
首先判断了宏
YYIMAGE_WEBP_ENABLED
因为如果webP要支持动图,首先在编译 libwebp 时需要加上 demux 模块,解码 WebP 时需要先用 WebPDemuxer 尝试拆包,之后再把拆出来的单帧用 WebPDecode 解码,YY 已经将这些类打包成了静态库,当需要的时候会import
进来做相应的解码操作; -
做相应数据的重置工作,如
_width
,_height
,_loopCount
初始化为 0;将_webpSource
重置为NULL;在_framesLock
信号量锁中将_frames
设置为nil
;做这些操作实际是因为 WebP 动图实际上是把多个单帧 WebP 数据简单打包到一个文件内,而并不是由单帧 WebP 扩展而来,以至于动图格式并不能向上兼容静态图。因此每次来了数据都需要重置之前的数据; -
作者给了 webp 的开发者网站链接:(WebP API Documentation 请自带梯子),并写了一些注释:我们可以使用
WebPIDecoder
去逐步解码 webp 数据;但是在解码后只能返回一个空图片,并不能像 jpeg 图那样显示一部分图片出来,因此我们不用逐步解码;如果我们直接使用WebPDecode()
去解码多帧的 webp 图片,我们会收到一个VP8_STATUS_UNSUPPORTED_FEATURE
的错误,因此我们需要先用WebPDemuxer
去解包;这些注释有便于我们接下来去看作者的处理逻辑;关于 demux 到底有什么什么含义,大家可以看下这篇文章 关于音视频的一些知识(demux、filter) -
初始化
WebPData
结构体的实例webPData
为 {0};并用此_data
去初始化webPData
的一些成员变量bytes
,size
;使用WebPDemuxer
给webPData
数据进行解码; -
使用
WEBP_EXTERN(uint32_t) WebPDemuxGetI(const WebPDemuxer* dmux, WebPFormatFeature feature)
函数来找出demux
中数据的各个特征;这些特征包括:-
WEBP_FF_FORMAT_FLAGS
:扩展标志量; -
WEBP_FF_CANVAS_WIDTH
:画布的宽; -
WEBP_FF_CANVAS_HEIGHT
:画布的高; -
WEBP_FF_LOOP_COUNT
:图片循环展示次数; -
WEBP_FF_BACKGROUND_COLOR
:图片的背景色; -
WEBP_FF_FRAME_COUNT
:当前demux
对象中的帧数;
-
-
如果
webpFrameCount
为0或者是宽高小于1,则直接认为这是一个废弃的数据直接WebPDemuxDelete(demuxer)
后返回; -
接下来我们需要了解另外一个结构体
WebPIterator
,它是用来在我们迭代每一帧时,每一帧数据显示的内容,其中包括帧数,图片的宽高,在画布(canvas)上的偏移量,动画持续时间(毫秒),混合方式,渲染方式,帧数据,是否包含alpha通道等; -
使用
WebPDemuxGetFrame
和WebPDemuxNextFrame
两个函数来迭代每一帧数据;-
WEBP_EXTERN(int) WebPDemuxGetFrame(const WebPDemuxer* dmux, int frame_number, WebPIterator* iter)
:检索第frame_number
帧的数据;如果frame_number
为0则返回最后一帧的图片;如dmux
为空或者frame_number
不合法则直接返回false
;当在展示iter
数据时,dmux
不可被释放;当迭代完成之后要使用WebPDemuxReleaseIterator()
方法来释放iter
资源; -
WebPDemuxNextFrame
: 找到当前iter
的下一帧数据;与之对应的还有一个函数WebPDemuxPrevFrame
即找到前一帧的数据;
-
-
接下里是每一帧数据的处理过程,使用了一个
do-while
循环:-
初始化一个
_YYImageDecoderFrame
对象frame
,并将其加入数组frames
中; -
使用
iter
来初始化frame
的所有成员变量(其中毫秒换为了秒,offset
改为了以左下角为原点); -
判断图片是否需要混合并且从第几帧开始混合(这段代码看的并不是很懂,对webp格式还是不太理解,如果有知道的同学可以告知下,谢谢);
-
找到下一帧,继续循环;循环结束后,释放
iter
资源; -
如果
frames.count
不等于之前获取demuxer
得到的webpFrameCount
那么直接delete
这个demuxer
并返回; -
使用上面获取的数据来初始化自己的成员变量;在
_framesLock
信号量中设置_frames
=frames
;至此,所有帧的数据都保存在YYImageCoder
中了;
-
-
-
- (void)_updateSourceAPNG
-
APNG
是对PNG
格式的一种用来支持动图的扩展,从iOS8
开始就支持ImageIO
了;为了兼容旧版本的系统可以解码APNG
的图片,作者并没有使用ImageIO
的APNG
帧数据信息,而是用了自定义的解码器,实际上,作者自定义的解码器比ImageIO
解码还要快一些;(在看具体解码之前,如果你看不懂了,可以先看下PNG格式详解,APNG格式去了解图片格式,因为我不会详细的分析 YY 是如何对APNG
图片进行建模的) -
使用
yy_png_info_release
函数释放_apngSource
资源,并置为nil
; -
使用
- (void)_updateSourceImageIO
方法解码拿到的数据;此方法在 4 中有详解;其实 YY 对于PNG
图片的渲染是在拿到所有数据后,才开始的,可以看下面的操作; -
如果在上一步中获取的
_frameCount
为0或者是并不是最后一次数据,则直接返回;也就是说在数据没有接收完全时,作者并不做渲染操作; -
使用
_data
和函数static yy_png_info *yy_png_info_create(const uint8_t *data, uint32_t length)
来初始化结构体yy_png_info
的实例apng
, 如果apng
为空则说明解码失败,直接返回,如果为静态图则直接返回; -
到了这一步说明成功解码了,并且为动态图,那么所有的图片资源我们已经获取到了,可以释放指针
_source
了; -
接下来就是根据
apng
对于动图数据的解码并给YYImageDecoder
的属性赋值,所进行的操作类似于webP
格式的图片,不过这里作者是根据自己封装的结构体来接收二进制数据后赋值给每一个_YYImageDecoderFrame
;
-
-
- (void)_updateSourceImageIO
此方法是用来将除了webP
的图片进行解码的操作,使用了大量的CGImageSource
里面的API
;主要也是获取图片的数据来给YYImageDecoder
的属性赋值;- 清空属性的信息,如宽高置为0,循环次数为0,方向为
up
,清空_frames
;- 如果
_source
为空,则做如下判断:-
如果
_finalized
为 YES ,证明是最后一帧,也就是说为静态图,则直接使用IMAGEIO_EXTERN CGImageSourceRef __nullable CGImageSourceCreateWithData(CFDataRef __nonnull data, CFDictionaryRef __nullable options)
函数去初始化_source
;此函数就是根据传入的data
数据来创建一个CGImage
,需要添加的额外信息在当前的头文件中有定义,可自行查看; -
否则,说明为动图;则使用
IMAGEIO_EXTERN CGImageSourceRef __nonnull CGImageSourceCreateIncremental(CFDictionaryRef __nullable options)
函数去初始化_source
;此函数会创建一个递增的image
资源;最终我们会在CGImageSourceUpdateDataProvider
或者CGImageSourceUpdateData
函数中提供生成图片的二进制数据;- 如果生成的
_source
不为空,则使用CGImageSourceUpdateData
函数来加入图片数据;
- 如果生成的
-
- 如果
_source
不为空,则直接使用CGImageSourceUpdateData
函数来更新数据; - 使用
IMAGEIO_EXTERN size_t CGImageSourceGetCount(CGImageSourceRef __nonnull isrc)
函数来获取图片的帧数,并用此来赋值属性_frameCount
,如果为0,直接返回; - 如果当前帧数据不是最后一帧,则在最后获取全部数据之前忽略此图片为动态图,将
_frameCount
设置为1; - 如果为最后一帧数据,则判断图片类型:
- 如果为
YYImageTypePNG
则强制令_frameCount
= 1;这是为了要使用自己的解码器来解码; - 如果为
YYImageTypeGIF
则使用CGImageSourceCopyProperties
函数获取_source
的属性字典,从此字典中取出value
为kCGImagePropertyGIFDictionary
的值,即gif
图片的属性字典,若此字典不为空,则取出value
为kCGImagePropertyGIFLoopCount
的属性,得到循环次数后赋值给_loopCount
;
- 如果为
- 接下来就是对于动图数据的解码并给
YYImageDecoder
的属性赋值,所进行的操作类似于webP
格式的图片,不过图片数据的获取是通过ImageIO
里面的一些API
, 大家有兴趣的可以看下源码;
- 如果
- 清空属性的信息,如宽高置为0,循环次数为0,方向为
-
- (BOOL)_updateData:(NSData *)data final:(BOOL)final
该方法用来边下边解码图片-
如果已经是
_finalized
的状态则直接返回 NO ; -
如果当前参数中的
data.length
小于_data.length
那么直接返回 NO ; -
将
_finalized
和_data
两个属性赋值; -
使用自己写的 C 函数
YYImageType YYImageDetectType(CFDataRef data)
来判断图片的实际类型;(实际判断就是通过二进制数据头文件匹配得带的,有兴趣的可以去看下源码); -
如果需要判断原数据类型,则判断
_type
与参数type
是否一致,不一致直接返回 NO ,一致则调用私有方法- (void)_updateSource
来更新数据,刷新图片的显示;这个判断主要是用来判断来的数据是否是第一次,进入else
后就是数据第一次来的时候的操作; -
如果不需要判断原数据类型,那么先判断给到的数据长度是否大于16,不然就无法准确得知图片的类型;当大于16时,我们会给
_type
赋值,并设置_sourceTypeDetected
以保证以后的数据都会进入上一个流程;最后调用- (void)_updateSource
来更新数据;
-
-
- (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index extendToCanvas:(BOOL)extendToCanvas decoded:(BOOL *)decoded CF_RETURNS_RETAINED
这个方法主要用在获取第index
帧数据,并且此帧数据不需要混合模式时调用的;-
如果
_finalized
为 NO 并且index
> 0,则说明在数据为完全接收的时候去获取某一帧,这个不允许,直接返回 NULL ;如果index
>=_frames.count
,返回 NULL; -
取到
_frames
第index
的数据_YYImageDecoderFrame *frame
;如果_source
不为空则进行如下步骤:-
通过
CGImageRef __nullable CGImageSourceCreateImageAtIndex(CGImageSourceRef __nonnull isrc, size_t index, CFDictionaryRef __nullable options)
方法来生成第index
张图片的CGImageRef
并在option
中设置kCGImageSourceShouldCache
的value
为 YES 用来保证解码后的数据在缓存中,因为64位机器上默认是开启的,但是32位是默认关闭的; -
通过上一步获取的
imageRef
来获取图片的长宽信息,若与画布的长宽相等,则调用CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay)
方法来获取解码后的数据;这个方法做了两件事:-
如果
decodeForDisplay
为 YES ,则通过CGImageGetAlphaInfo
函数和imageRef
找到图片的alpha
通道信息,再通过此信息来设置位图信息CGBitmapInfo
,有了此信息后我们就可以根据CGContextRef __nullable CGBitmapContextCreate(void * __nullable data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
方法来生成位图信息的上下文CGContextRef
,有了contextRef
就可以在此基础上使用void CGContextDrawImage(CGContextRef cg_nullable c, CGRect rect, CGImageRef cg_nullable image)
进行绘制图像了(即解码图像数据),然后根据此上下文就可以使用CGImageRef __nullable CGBitmapContextCreateImage(CGContextRef cg_nullable context)
方法生成图片数据CGImageRef
,然后返回此图片数据指针; -
如果不需要解码出来进行展示,则根据参数中的
imageRef
找到图片数据的一个像素中每个独立的颜色分量使用的 bit 数,一个像素使用的总 bit 数,位图中的每一行使用的字节数,颜色空间模式和位图信息;再根据CGImageGetDataProvider
获取这个图片的数据源,然后调用CGDataProviderCopyData
从数据源获取直接解码的数据,最后根据这个方法来生成图片CGImageRef __nullable CGImageCreate(size_t width, size_t height, size_t bitsPerComponent, size_t bitsPerPixel, size_t bytesPerRow, CGColorSpaceRef cg_nullable space, CGBitmapInfo bitmapInfo, CGDataProviderRef cg_nullable provider, const CGFloat * __nullable decode, bool shouldInterpolate, CGColorRenderingIntent intent)
,最后返回这个图片;
-
-
-
如果
_apngSource
不为空,则进行如下操作,此时是动图的解压:-
使用函数
yy_png_copy_frame_data_at_index
来从_data
复制一帧 PNG 数据; -
使用此帧数据创建
CGDataProviderRef
,然后根据此provider
生成CGImageSourceRef
;接下来的操作就跟上述 1 步骤差不多了,将最后得到的新的CGImageRef
返回;
-
-
如果为
webP
格式的动图则进行如下步骤:-
通过方法
WebPDemuxGetFrame
获取_webpSource
第index
的图片数据,若获取不到直接返回 NULL;获取到的数据使用WebPIterator
接收; -
使用
WebPDecoderConfig
来得到图片数据和像素信息;然后使用CGDataProviderCreateWithData
函数得到CGDataProviderRef
,后面的就跟上面的操作类似了,得到CGImageRef
并返回;
-
具体要了解这里都做了哪些事的,建议看下 CGImage 的头文件,另外这里有个博客也可以看下 谈谈 iOS 中图片的解压缩
-
-
- (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay
-
如果
index
大于等于_frames.count
则直接返回 0 ; -
找到
_frames
的第index
帧数据_YYImageDecoderFrame *frame
; -
如果
_type
不为YYImageTypeICO
并且decodeForDisplay
为 YES ,则设置标志量extendToCanvas
为 YES ,表示需要绘制到画布上;因为对于ICON
类型的图片, 是不需要绘制到画布上的; -
如果不需要混合,则走如下流程:
- 调用
- (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index extendToCanvas:(BOOL)extendToCanvas decoded:(BOOL *)decoded CF_RETURNS_RETAINED
方法来生成CGImageRef
指针; - 将得到的图片使用
YYCGImageCreateDecodedCopy
方法进行解压得到imageRefDecoded
; - 使用
imageRefDecoded
生成UIImage
并复制给frame
,然后返回此frame
;
- 调用
-
如果需要混合,则走如下流程:
- 调用
_createBlendContextIfNeeded
方法生成混合模式的上下文(即调用CGBitmapContextCreate
方法)并初始化_blendFrameIndex
为0; - 如果是前一帧,直接调用
_newBlendedImageWithFrame
方法,即根据相应的混合和清除模式对图片进行做处理;这里的代码都是根据CGBitmapContextCreateImage
,CGContextDrawImage
和CGContextClearRect
这几个函数来操作的,有兴趣的可以看下这几个函数和源码; - 如果不是前一帧则判断是否
frame.blendFromIndex == frame.index
- 如果是的则先用
- (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index extendToCanvas:(BOOL)extendToCanvas decoded:(BOOL *)decoded CF_RETURNS_RETAINED
方法生成index
图像,再使用CGContextDrawImage
函数合成图片数据; - 如果不是的,则要将
blendFromIndex
的数据开始都要混合,即都调用_newBlendedImageWithFrame
方法;
- 如果是的则先用
- 最后使用生成的
imageRef
得到UIImage
之后赋值给frame
返回此frame
即可;
- 调用
-
Pubulic Funcs
-
- (instancetype)init
实际调用的是initWithScale
方法; scale 为屏幕的 scale; -
- (instancetype)initWithScale:(CGFloat)scale
指定的初始化方法,用来创建一个图片解码器;- 如果 scale <= 0,则将 scale 设置为1;
- 初始化
_scale
为 scale ; - 初始化
_framesLock
信号量为1; - 使用
pthread_mutexattr_t
初始化_lock
,类型为PTHREAD_MUTEX_RECURSIVE
;
-
- (BOOL)updateData:(nullable NSData *)data final:(BOOL)final
- 根据增量更新的数据来解码图片;
data
会被decoder
retain 持有,因此我们不应该在其他线程中修改data
; - 当我们没有完整的图像数据时,我们可以使用此方法来逐渐解码/隔行扫描/基线 图片;
-
data
: 每次调用这个方法时,我们要保证data
参数是包含目前为止所有累积的图片数据; -
final
: 表示是否是该图片数据的最后一个数据,如果为YES,则可以继续更新图片数据,否则不可以再更新图片数据; - 实现过程:
- 使用
pthread_mutex_lock
给_lock
加锁来保护数据; - 使用
- (BOOL)_updateData:(NSData *)data final:(BOOL)final
来更新数据; - 使用
pthread_mutex_unlock
给_lock
解锁来释放资源; - 返回
- (BOOL)_updateData:(NSData *)data final:(BOOL)final
方法的返回值;
- 使用
- 根据增量更新的数据来解码图片;
-
+ (instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale
便利构造方法,实际调用initWithScale
方法,然后调用- (BOOL)_updateData:(NSData *)data final:(BOOL)final
方法来解码和给属性赋值;最后返回此YYImageDecoder
;如果解码发送错误,返回nil; -
- (nullable YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay
解码第index
帧数据,并生成YYImageFrame
对象返回;index
从0开始;decodeForDisplay
表明是否需要解码图片到内存用来展示;如果为 NO ,则会尝试返回没有混合的原始帧数据;如果发生错误则返回nil
;
实际调用的是- (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay
方法; -
- (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index
直接返回_frames[index]).duration
即可; -
- (nullable NSDictionary *)framePropertiesAtIndex:(NSUInteger)index
使用CGImageSourceCopyPropertiesAtIndex
函数获取到_source
指定index
的字典,然后转为NSDictionary
即可;详细信息可以看ImageIO.framework
里面的CGImageProperties.h
头文件; -
- (nullable NSDictionary *)imageProperties
直接使用CGImageSourceCopyProperties
返回_source
的属性字典,转为NSDictionary
返回即可;
网友评论