前言
这篇文章写在我毕业工作的第五个年头,从前一直觉得自己挺牛的,什么东西都一学就会。
实至今日,才发现自己的能力在各方面是有多弱,和牛逼的人差距是有多大。计算机的很多基础知识都极为薄弱。
青春期最最最后悔的事,就是没有好好读书,天天想着怎么逃课,泡妞。
其实15年刚刚做iOS的时候就接触过YYImage,当时就是觉得挺屌的,大家都在吹,我也用呗。也没有好好读过源码。
最近因为业务需要,对各种图片格式也有了新的认识,索性重新来读一遍YYImage,顺便总结一些感想
YYImage是YYKit系列中的其中一个组件,大神自己口述是在大概搞iOS的第五个年头写出来的。
我自认为我在第五个年头,才能静下心来把源码好好地从头到尾理顺,而人家已经在第五个年头发布了这个iOS中国最优秀之一的框架。
其实自己跟别人比起来,真是没什么天赋,只有多花时间,少一些玩的时间。
正文
其实纵观整个YYImage还是基于ImageIO和CoreGraphics的相关api进行一系列操作的。
- 加载一张普通静态图的大致步骤:
- 通过分辨率和图片格式加载图片进内存
- 通过源数据判断判断图片类型
- 通过图片类型进行源数据的信息获取,这里作者大概分为三类,webp:依赖于libwebp ,png和apng作者自己写了一套数据解析的算法(这里我暂时放弃了..下次一定学),剩下的都基于ImageIO,通过
_YYImageDecoderFrame
私有属性保存源数据信息。 - 调用ImageIO相关api进行解码,之后就可以愉快地使用了。
- 加载一张动图的大致步骤:
- 和加载静态的图流程差不多,源数据会多记录一些东西,比如每一帧的druation,每一帧的渲染方式。详见这篇,原作者讲的很详细(在解码动图时,解码器通常采用所谓“画布模式”进行渲染。想象一下:播放的区域是一张画布,第一帧播放前先把画布清空,然后完整的绘制上第一帧图;播放第二帧时,不再清空画布,而是只把和第一帧不同的区域覆盖到画布上,就像油画的创作那样。像这样的第一帧就被称为关键帧(即 I 帧,帧内编码帧),而后续的那些通过补偿计算得到的帧被称为预测编码帧(P帧)。一个压缩的比较好的动图内,通常只有少量的关键帧,而其余都是预测编码帧;一个较差的压缩工具制作的动图内,则基本都是关键帧。不同的动图压缩工具通常能得到不同的结果。)这些其实都很详细地记录在图片的原数据里。
- 显示动图必须用YYAnimatedImageView承接,用UIImageView只会显示第一帧(这是YYImage内部实现的原因),每个YYAnimatedImageView里都有一个CADisplayLink,通过屏幕绘制频率进行进度计算和异步解码。实现边播放边解码的高性能操作。
疑点总结
-
对于动图来说,假设其中一帧进度时间小于0.011秒,强制将这一帧时间提升到0.1秒。至于原因其实源码当中是有备注的。
大致原因在于:许多恼人的广告指定0秒持续时间,以到达最快速度的闪烁,我们遵循Safari和Firefox的行为,对于指定持续时间<=10毫秒的任何帧,使用100毫秒的持续时间
这也是ImageIO源码中的原话
// Many annoying ads specify a 0 duration to make an image flash as quickly as possible. // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify // a duration of <= 10 ms. See <rdar://problem/7689300> and <http://webkit.org/b/36082> // for more information.
至于为什么是100毫秒,下面其实也有说到,
没有一个现代浏览器支持0.02秒以下的帧延迟。因此,当创建动画GIF时,绝不应使用低于此阈值的帧延迟,因为这将完全无效。另一个有趣的发现是Safari是唯一一个在GIF动画播放方面性能下降的浏览器。
对于那些对创建和观看流畅、快速的动画感兴趣的人来说,Chrome、Firefox和Opera无疑是不错的选择。这些浏览器都支持0.02秒的最小帧延迟,从而使动画GIF以每秒50帧的速度运行。然而,这种能力需要与交叉兼容性问题进行权衡。
由于Safari和internetexplorer决定只支持0.06秒的帧延迟(低于此值的四舍五入为0.10秒),动画的观看速度有可能大大低于预期。所讨论的GIF将以每秒10帧的速度播放,而不是期望的每秒50帧。只有20%的真实速度,这成为一个认真考虑美学影响。(翻译而来差不多看看)
-
对于单张静态图片的加载,YYImage和UImage的加载方式略有不同,同样是
imageNamed:(NSString *)name
方法,UImage不会解码,而是在UIImageView addView的时候才进行解码,而YYImage会在调用该方法后当场解码,因此放入异步线程进行解码还可以减少主线程压力。 -
几种常见的图片保存格式
第一种是 baseline,即逐行扫描。默认情况下,JPEG、PNG、GIF 都是这种保存方式。
第二种是 interlaced,即隔行扫描。PNG 和 GIF 在保存时可以选择这种格式。
第三种是 progressive,即渐进式。JPEG 在保存时可以选择这种方式。
调用CGImageSourceUpdateData(data, false) 可以实现类似于网页中的渐进式显示的效果。 -
preloadAllAnimatedImageFrames
该属性可支持提前解开所有帧图片,使在动图播放过程中减少cpu的开销,但是可能会造成oom,所以视情况而定进行使用。 -
强如webp也有他的性能瓶颈,那就是解码效率,没有方案是万能的,bitmap,png,wep各自都有他们的存在场景,假如以后计算机的存储量和网络流量都无限大,随便用,那么我完全可以使用bitmap,我为什么要使用webp去消耗额外的cpu或者gpu的开销呢。
-
其实YYImage当中还有很强大的encode功能,支持多种格式和参数(quality质量,lossless无损等),不过我个人在实际业务中用到的比较少。
遗留问题
读源码的过程中还是碰到某几处地方还是没有特别的理解,希望后续有机会能得到答案
-
关于在解码类的入口函数递归锁的使用
- (BOOL)updateData:(NSData *)data final:(BOOL)final { BOOL result = NO; pthread_mutex_lock(&_lock); result = [self _updateData:data final:final]; pthread_mutex_unlock(&_lock); return result; }
我认为对于递归锁的本质来说,其实就是给递归方法用的。使得同一线程在同一区块可以进行重入,但是对于该方法,我来来回回看了很多遍都没发现怎么会出现重入的情况,所以对于这里还是挺疑惑的,这里用递归锁的意义。
-
关于缓冲区的下一帧的移除操作
在CADisplayLink每次触发的step方法中有这么一段
bufferedImage = buffer[@(nextIndex)]; if (bufferedImage) { if ((int)_incrBufferCount < _totalFrameCount) { [buffer removeObjectForKey:@(nextIndex)]; } .... } else { _bufferMiss = YES; }
经过多次的调试,没太明白
[buffer removeObjectForKey:@(nextIndex)]
这句话的意义,因为由于子线程的解码速度一定是大于当前播放的帧数的,所以其实每次每次来到这个地方,nextIndex
基本是解完了的,但这里先是在buffer中移除下一帧图片,但后续又在Operation
的main
重新拿到(虽然有缓存)进行添加的,因为buffer最终还是会保存所有的帧图片。而且我移除这句话也没发现啥问题,所以没有很搞懂移除的操作是为了啥 -
待学部分:png的图片格式数据解析,包括图片的解码算法。
网友评论