重读YYImage

Posted by wbq on September 20, 2020

前言

实至今日,才发现自己的能力在各方面和牛逼的人差距还是挺大的。计算机的很多基础知识都很薄弱。

其实15年刚刚做iOS的时候就接触过YYImage,但也没有好好读过源码。

最近因为业务需要,对各种图片格式也有了新的认识,索性重新来读一遍YYImage,顺便总结一些感想

YYImage是YYKit系列中的其中一个组件。

正文

其实纵观整个YYImage还是基于ImageIO和CoreGraphics的相关api进行一系列操作的。

  • 加载一张普通静态图的大致步骤:
    1. 通过分辨率和图片格式加载图片进内存
    2. 通过源数据判断判断图片类型
    3. 通过图片类型进行源数据的信息获取,这里作者大概分为三类,webp:依赖于libwebp ,png和apng作者自己写了一套数据解析的算法(这里我暂时放弃了..下次一定学),剩下的都基于ImageIO,通过_YYImageDecoderFrame私有属性保存源数据信息。
    4. 调用ImageIO相关api进行解码,之后就可以愉快地使用了。
  • 加载一张动图的大致步骤:
    1. 和加载静态的图流程差不多,源数据会多记录一些东西,比如每一帧的druation,每一帧的渲染方式。详见这篇,原作者讲的很详细(在解码动图时,解码器通常采用所谓“画布模式”进行渲染。想象一下:播放的区域是一张画布,第一帧播放前先把画布清空,然后完整的绘制上第一帧图;播放第二帧时,不再清空画布,而是只把和第一帧不同的区域覆盖到画布上,就像油画的创作那样。像这样的第一帧就被称为关键帧(即 I 帧,帧内编码帧),而后续的那些通过补偿计算得到的帧被称为预测编码帧(P帧)。一个压缩的比较好的动图内,通常只有少量的关键帧,而其余都是预测编码帧;一个较差的压缩工具制作的动图内,则基本都是关键帧。不同的动图压缩工具通常能得到不同的结果。)这些其实都很详细地记录在图片的原数据里。
    2. 显示动图必须用YYAnimatedImageView承接,用UIImageView只会显示第一帧(这是YYImage内部实现的原因),每个YYAnimatedImageView里都有一个CADisplayLink,通过屏幕绘制频率进行进度计算和异步解码。实现边播放边解码的高性能操作。

疑点总结

  1. 对于动图来说,假设其中一帧进度时间小于0.011秒,强制将这一帧时间提升到0.1秒。至于原因其实源码当中是有备注的。

    大致原因在于:许多恼人的广告指定0秒持续时间,以到达最快速度的闪烁,我们遵循Safari和Firefox的行为,对于指定持续时间<=10毫秒的任何帧,使用100毫秒的持续时间

    这也是ImageIO源码中的原话

    1
    2
    3
    4
    
    // 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%的真实速度,这成为一个认真考虑美学影响。(翻译而来差不多看看)

  2. 对于单张静态图片的加载,YYImage和UImage的加载方式略有不同,同样是imageNamed:(NSString *)name方法,UImage不会解码,而是在UIImageView addView的时候才进行解码,而YYImage会在调用该方法后当场解码,因此放入异步线程进行解码还可以减少主线程压力。

  3. 几种常见的图片保存格式

    第一种是 baseline,即逐行扫描。默认情况下,JPEG、PNG、GIF 都是这种保存方式。 第二种是 interlaced,即隔行扫描。PNG 和 GIF 在保存时可以选择这种格式。 第三种是 progressive,即渐进式。JPEG 在保存时可以选择这种方式。 调用CGImageSourceUpdateData(data, false) 可以实现类似于网页中的渐进式显示的效果。

  4. preloadAllAnimatedImageFrames该属性可支持提前解开所有帧图片,使在动图播放过程中减少cpu的开销,但是可能会造成oom,所以视情况而定进行使用。

  5. 强如webp也有他的性能瓶颈,那就是解码效率,没有方案是万能的,bitmap,png,wep各自都有他们的存在场景,假如以后计算机的存储量和网络流量都无限大,随便用,那么我完全可以使用bitmap,我为什么要使用webp去消耗额外的cpu或者gpu的开销呢。

  6. 其实YYImage当中还有很强大的encode功能,支持多种格式和参数(quality质量,lossless无损等),不过我个人在实际业务中用到的比较少。

遗留问题

读源码的过程中还是碰到某几处地方还是没有特别的理解,希望后续有机会能得到答案

  1. 关于在解码类的入口函数递归锁的使用

    1
    2
    3
    4
    5
    6
    7
    
    - (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;
    }
    

    我认为对于递归锁的本质来说,其实就是给递归方法用的。使得同一线程在同一区块可以进行重入,但是对于该方法,我来来回回看了很多遍都没发现怎么会出现重入的情况,所以对于这里还是挺疑惑的,这里用递归锁的意义。

  2. 关于缓冲区的下一帧的移除操作

    在CADisplayLink每次触发的step方法中有这么一段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    bufferedImage = buffer[@(nextIndex)];
    if (bufferedImage) {
    	if ((int)_incrBufferCount < _totalFrameCount) {
    		[buffer removeObjectForKey:@(nextIndex)];
    	}
    ....
    } else {
    		_bufferMiss = YES;
    }
    

    经过多次的调试,没太明白[buffer removeObjectForKey:@(nextIndex)]这句话的意义,因为由于子线程的解码速度一定是大于当前播放的帧数的,所以其实每次每次来到这个地方,nextIndex基本是解完了的,但这里先是在buffer中移除下一帧图片,但后续又在Operationmain重新拿到(虽然有缓存)进行添加的,因为buffer最终还是会保存所有的帧图片。而且我移除这句话也没发现啥问题,所以没有很搞懂移除的操作是为了啥

  3. 待学部分:png的图片格式数据解析,包括图片的解码算法。

感谢

移动端图片格式调研

iOS 处理图片的一些小 Tip