美文网首页
YYImage源码阅读—YYImageCoder

YYImage源码阅读—YYImageCoder

作者: AppleTTT | 来源:发表于2017-05-09 18:31 被阅读488次

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 这几个函数以完成锁的初始化,锁的销毁,上锁和释放锁操作。

  1. 锁的创建

    • 宏创建:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
    • 通过锁的属性动态创建: int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
  2. 锁的属性 pthread_mutexattr_t

    • 互斥锁属性可以由 pthread_mutexattr_init(pthread_mutexattr_t *mattr) 来初始化,然后可以调用其他的属性设置方法来设置其属性;

    • 互斥锁的范围:可以指定是该进程与其他进程的同步还是同一进程内不同的线程之间的同步。可以设置为 PTHREAD_PROCESS_SHAREPTHREAD_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) 获取或设置锁的类型;

  3. 锁的释放

    • 调用 pthread_mutex_destory 之后,可以释放锁占用的资源,但这有一个前提上锁当前是没有被锁的状态;
  4. 锁操作

    • 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,否则返回一个错误提示码错误;

大端小端

在很多时候,服务器返回的二进制数据是以大端模式存储的,而机器是小端模式,必须进行转换,否则使用时会出问题。

  1. 大端模式(Big Endian):数据的高字节,保存在内存的低地址中;数据的低字节,保存在内存的高地址中。
  2. 小端模式(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个字节组成:RedGreenBlue

对于索引图像,调色板信息是必须的,调色板的颜色索引从0开始编号,然后是1、2……,调色板的颜色数不能超过色深中规定的颜色数(如图像色深为4的时候,调色板中的颜色数不可以超过2^4=16),否则,这将导致PNG图像不合法。

真彩色图像和带α通道数据的真彩色图像也可以有调色板数据块,目的是便于非真彩色显示程序用它来量化图像数据,从而显示该图像。

IDAT

图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。

IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。

IEND

图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。

APNG 作为 PNG 的扩展在此基础上添加了 acTLfcTLfdAT 三个字段,作用分别如下:

  • acTL : 动画控制块,包括了帧数和循环动画次数;

  • fcTL : 每一帧的控制模块,包含了如下信息:

    • sequence_number : 动画块的个数,从0开始;
    • width : 帧的宽度;
    • height : 帧的高度;
    • x_offset : 渲染在此帧上的图片的原点 X 坐标;
    • y_offset : 渲染在此帧上的图片的原点 Y 坐标;
    • delay_num : 帧延迟的分子;
    • delay_den : 帧延迟的分母;(这两个属性主要用来生成一个浮点数来表示动图显示时长)
    • dispose_op : 渲染模式;
    • blend_op : 混合模式;
  • fdAT : 每一帧的数据块,包括动画块的个数,从0开始和此帧的二进制数据

YY用到的枚举

  1. 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就是这么做的。具体可以看下这两篇博客 通过文件头标识判断图片格式通过文件头判断图片类型

  1. YYImageDisposeMethod: 在画布上渲染下一帧之前,如何处理当前帧使用的区域;
    • YYImageDisposeNone: 把当前帧增量绘制到画布上,不清空画布;
    • YYImageDisposeBackground:绘制当前帧之前,先把画布清空为默认背景色;
    • YYImageDisposePrevious:绘制下一帧前,把先把画布恢复为当前帧的前一帧;
  1. YYImageBlendOperation: 混合操作指定当前帧的透明像素如何与先前画布的像素混合。
    • YYImageBlendNone: 绘制时,全部通道(包含Alpha通道)都会覆盖到画布,相当于绘制前先清空画布的指定区域;
    • YYImageBlendOver:绘制时,Alpha 通道会被合成到画布,即通常情况下两张图片重叠的效果;

YYImageFrame

An image frame object.

一个实现了NSCopying协议,用来表示图片中帧的类,如果是动图,会包含所有帧数据;

Property
  1. index: 帧的索引
  2. width: 帧的宽度
  3. height: 帧的高度
  4. offsetX: 帧在画布上的X方向上的偏移量(左下角为原点)
  5. offsetY: 帧在画布上的Y方向上的偏移量
  6. duration: 帧经过的秒数;
  7. dispose: YYImageDisposeMethod;
  8. blend: YYImageBlendOperation;
  9. image: the UIImage;
Public func
  1. + (instancetype)frameWithImage:(UIImage *)image;
    直接调用new来初始化,并将image复制给image属性;

  2. - (id)copyWithZone:(NSZone *)zone
    实现NSCopying协议,new一个YYImageFrame实例,赋值后返回;

_YYImageDecoderFrame

YY 在内部实现的时候还给了 YYImageFrame 一个子类,来更好的表示每一帧数据;并在 YYImageFrame 的基础上添加了如下属性:

  1. hasAlpha : 图片是否有 alhpa 通道;
  2. isFullSize : 图片是否填充了整个画布;
  3. blendFromIndex : 图片从第几帧到当前帧开始混合;

YYImageDecoder

Public Property
  1. data: 图片的二进制数据;
  2. type: 图片的类型(YYImageType);
  3. scale: 图片的缩放比例;
  4. frameCount: 图片的帧数;
  5. loopCount: 动图时,循环显示的次数;如果为0则是无限循环;
  6. width: 图片画布的宽;
  7. height: 图片画布的高;
  8. finalized: 当边下边显示图片时,用来表示是否为图片的最后一次数据;
Private Property
  1. _lock: 一个 pthread_mutex_t 类型的递归锁;
  2. _sourceTypeDetected:
  3. _source: 指向在操作的当前帧数据资源,CGImageSourceRef 指针;
  4. _apngSource: APNG 的信息;yy_png_info 结构体,用来表示 APNG 的所有信息,因为 iOS 是在8.0只有 ImageIO 才支持APNG的图片,为了支持老版本,作者自己做了解码工作,所以才自己添加了表示 APNG 的信息模块;
  5. _webpSource: WebPDemuxer 类型;如果 webP 要支持动图,首先在编译 libwebp 时需要加上 demux 模块,解码 WebP 时需要先用 WebPDemuxer 尝试拆包,之后再把拆出来的单帧用 WebPDecode 解码;
  6. _orientation: 图片的方向;
  7. _framesLock: 操作每一帧数据时的锁;dispatch_semaphore_t 类型;
  8. _frames: 原始帧数据;< Array<GGImageDecoderFrame>
  9. _needBlend: 是否需要混合;
  10. _blendFrameIndex: 当前混合的帧的索引;
  11. _blendCanvas: 混合到画布的上下文;CGContextRef 类型;
Fuctions

由于实现的代码有2000多行,所以我们直接看对外的接口,看到了用到的私有方法,再去分析每个私有方法具体实现的内容,看这样效果如何

Private Funcs

还是先看下一些私有的功能方法到底是实现了什么东西,怎么实现的,不然直接去看对外的接口,还真的是跳来跳去的;

  1. - (void)_updateSource
    这个方法是 YYImageCoder 里一个非常重要的方法,这个方法里面根据用户提供的二进制数据来增量更新图片数据;后面我们会跟大家分析下不同类型的更新图片的方法;

    • 如果 _type 则调用 - (void)_updateSourceWebP 方法;
    • 如果 _type 则调用 - (void)_updateSourceAPNG 方法;
    • 为其他类型则滴啊用 - (void)_updateSourceImageIO 方法;
  2. - (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;使用 WebPDemuxerwebPData 数据进行解码;

    • 使用 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通道等;

    • 使用 WebPDemuxGetFrameWebPDemuxNextFrame 两个函数来迭代每一帧数据;

      • 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 中了;

  3. - (void)_updateSourceAPNG

    • APNG 是对 PNG 格式的一种用来支持动图的扩展,从 iOS8 开始就支持 ImageIO 了;为了兼容旧版本的系统可以解码 APNG 的图片,作者并没有使用 ImageIOAPNG 帧数据信息,而是用了自定义的解码器,实际上,作者自定义的解码器比 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;

  4. - (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 的属性字典,从此字典中取出 valuekCGImagePropertyGIFDictionary 的值,即 gif 图片的属性字典,若此字典不为空,则取出 valuekCGImagePropertyGIFLoopCount 的属性,得到循环次数后赋值给 _loopCount ;
      • 接下来就是对于动图数据的解码并给 YYImageDecoder 的属性赋值,所进行的操作类似于 webP 格式的图片,不过图片数据的获取是通过 ImageIO 里面的一些 API, 大家有兴趣的可以看下源码;
  5. - (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 来更新数据;

  6. - (CGImageRef)_newUnblendedImageAtIndex:(NSUInteger)index extendToCanvas:(BOOL)extendToCanvas decoded:(BOOL *)decoded CF_RETURNS_RETAINED
    这个方法主要用在获取第 index 帧数据,并且此帧数据不需要混合模式时调用的;

    • 如果 _finalized 为 NO 并且 index > 0,则说明在数据为完全接收的时候去获取某一帧,这个不允许,直接返回 NULL ;如果 index >= _frames.count,返回 NULL;

    • 取到 _framesindex 的数据 _YYImageDecoderFrame *frame;如果 _source 不为空则进行如下步骤:

      • 通过 CGImageRef __nullable CGImageSourceCreateImageAtIndex(CGImageSourceRef __nonnull isrc, size_t index, CFDictionaryRef __nullable options)方法来生成第 index 张图片的 CGImageRef 并在 option 中设置 kCGImageSourceShouldCachevalue 为 YES 用来保证解码后的数据在缓存中,因为64位机器上默认是开启的,但是32位是默认关闭的;

      • 通过上一步获取的 imageRef 来获取图片的长宽信息,若与画布的长宽相等,则调用 CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) 方法来获取解码后的数据;这个方法做了两件事:

        1. 如果 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 ,然后返回此图片数据指针;

        2. 如果不需要解码出来进行展示,则根据参数中的 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 获取 _webpSourceindex 的图片数据,若获取不到直接返回 NULL;获取到的数据使用 WebPIterator 接收;

      • 使用 WebPDecoderConfig 来得到图片数据和像素信息;然后使用 CGDataProviderCreateWithData 函数得到 CGDataProviderRef ,后面的就跟上面的操作类似了,得到 CGImageRef 并返回;

    具体要了解这里都做了哪些事的,建议看下 CGImage 的头文件,另外这里有个博客也可以看下 谈谈 iOS 中图片的解压缩

  7. - (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 , CGContextDrawImageCGContextClearRect 这几个函数来操作的,有兴趣的可以看下这几个函数和源码;
      • 如果不是前一帧则判断是否 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
  1. - (instancetype)init
    实际调用的是 initWithScale 方法; scale 为屏幕的 scale;

  2. - (instancetype)initWithScale:(CGFloat)scale
    指定的初始化方法,用来创建一个图片解码器;

    • 如果 scale <= 0,则将 scale 设置为1;
    • 初始化 _scale 为 scale ;
    • 初始化 _framesLock 信号量为1;
    • 使用 pthread_mutexattr_t 初始化 _lock ,类型为 PTHREAD_MUTEX_RECURSIVE;
  3. - (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 方法的返回值;
  4. + (instancetype)decoderWithData:(NSData *)data scale:(CGFloat)scale
    便利构造方法,实际调用 initWithScale 方法,然后调用 - (BOOL)_updateData:(NSData *)data final:(BOOL)final 方法来解码和给属性赋值;最后返回此 YYImageDecoder;如果解码发送错误,返回nil;

  5. - (nullable YYImageFrame *)frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay
    解码第 index 帧数据,并生成 YYImageFrame 对象返回;index 从0开始;decodeForDisplay 表明是否需要解码图片到内存用来展示;如果为 NO ,则会尝试返回没有混合的原始帧数据;如果发生错误则返回 nil;
    实际调用的是 - (YYImageFrame *)_frameAtIndex:(NSUInteger)index decodeForDisplay:(BOOL)decodeForDisplay 方法;

  6. - (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index
    直接返回 _frames[index]).duration 即可;

  7. - (nullable NSDictionary *)framePropertiesAtIndex:(NSUInteger)index
    使用 CGImageSourceCopyPropertiesAtIndex 函数获取到 _source 指定 index 的字典,然后转为 NSDictionary 即可;详细信息可以看 ImageIO.framework 里面的 CGImageProperties.h 头文件;

  8. - (nullable NSDictionary *)imageProperties
    直接使用 CGImageSourceCopyProperties 返回 _source 的属性字典,转为 NSDictionary 返回即可;

参考文献

  1. 雷纯峰:谈谈 iOS 中图片的解压缩
  2. YY:移动端图片格式调研
  3. ISUX:WebP 探寻之路
  4. 使用 FFmpeg 处理高质量 GIF 图片
  5. Linux线程-互斥锁pthread_mutex_t
  6. 互斥锁属性PTHREAD_MUTEX_RECURSIVE
  7. pthread_mutex_lock
  8. YY:iOS 处理图片的一些小 Tip
  9. 二次采样设置的四种彩色模式RGB565,ARGB8888,ARGB4444,ALPHA_8

相关文章

  • YYImage源码阅读—YYImageCoder

    YYImageCoder An image decoder to decode image data. YYIma...

  • YYImage 源码剖析:图片处理技巧

    YYImage 源码剖析:图片处理技巧 YYImage 源码剖析:图片处理技巧

  • YYImage

    YYImage 一、源码分析 1、来到YYImage.m的+ (nullable YYImage *)imageN...

  • YYImage/YYModel/YYCache

    1.YYImage源码分析2.YYModel源码分析3.郑钦洪_:YYModel 源码历险记model属性赋值过程...

  • YYImage源码分析

    YYImage和SDWebIMage的功能是相同的,通过为系统的UIImageView、UIButton、CALa...

  • YYImage源码分析

    前言 YYImage 是一个强大的iOS图像框架,是YYKit 的组件之一。具体用法可以参考Demo。 ** 特性...

  • YYImage源码浅析

    简介 YYImage是与UIImage完全兼容的子类,YYImage扩展了UIImage使其支持WebP,GIF,...

  • IOS源码解析:YYImage

    原创:知识点总结性文章创作不易,请珍惜,之后会持续更新,不断完善个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈...

  • YYImage 阅读总结

    特性 支持以下类型动画图像的播放/编码/解码:WebP, APNG, GIF。 支持以下类型静态图像的显示/编码/...

  • YYKit之YYImageCoder

    YYImageCoder是YYKit中图片解码器。

网友评论

      本文标题:YYImage源码阅读—YYImageCoder

      本文链接:https://www.haomeiwen.com/subject/uusitxtx.html