一.图片如何在屏幕上看到
从磁盘加载一张图片,使用UIImageView显示在屏幕上,加载流程如下:
1.使用imageWithContentsOfFile(使用Image I/O创建CGImageRef内存映射数据)方法从磁盘加载一张图
片,此时,图片尚未解码。在这个过程中先从磁盘拷贝数据到内核缓冲区,再从内核缓冲区复制数据到用户空间。
2.生成UIImageView,把图像数据赋值给UIImageView,(PNG/JPEG)如果未解码,就解码成位图
3.隐式CATransaction捕获到UIImageView layer树的变化。
4.在主线程下一个runloop到来时,Core Animation提交了这个隐式的transaction。这个过程可能会对图片进行copy操作:
个人理解:
1.将文件数据从磁盘读到内存中
2.分配内存缓冲区用于管理文件IO和解压缩操作(将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作)
3.Core Animation中CALayer使用未压缩的位图渲染UIImageView涂层,隐式CATransaction捕获到UIImageView layer树的变化。在主线程下一个runloop到来时,Core Animation提交了这个隐式的transaction
4.CPU计算好图片的Frame,对图片解压后,就交给GPU来做图片渲染
GPU渲染流程:
- GPU获取获取图片的坐标
- 将坐标交给顶点着色器(顶点计算)
- 将图片光栅化(获取图片对应屏幕上的像素点)
- 片元着色器计算(计算每个像素点的最终显示的颜色值)
- 从帧缓存区中渲染到屏幕上
拓展:
图片:(iOS 设备会始终使用双缓存,并开启垂直同步)
CPU将图形数据通过总线BUS提交至GPU,GPU经过渲染处理转化为一帧帧的数据并提交至帧缓冲区,视频控制器会通过垂直同步信号VSync逐帧读取帧缓冲区的数据并提交至屏幕控制器最终显示在屏幕上。为解决一个帧缓冲区效率问题(读取和写入都是一个无法有效的并发处理),采用双缓冲机制,在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。 双缓冲机制虽然提升了效率但也引入了画面撕裂问题,即当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象
为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟
卡顿:
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
1.为什么要解压缩
图片的本质就是由许多的像素点构成的,而前面所说的位图实际上就是一个装着这些像素点的数组。而我们平时开发经常用的PNG或者JPG图片,都是一种压缩的位图图形格式。只不过 PNG图片是无损压缩,并且支持 alpha 通道。而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。图片分辨率越高,图片包含的像素点就越高,从而图片解压缩成位图的过程耗时也就越长。这个解压缩的过程我们是必须经过的,因为图片显示的过程其实就是将构成图片的一个个像素点显示出来,从而组成我们的图片。 因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据(位图),才能执行后续的绘制操作
2.iOS 中图片解压缩的原理
既然图片的解压缩不可避免,而我们也不想让它在主线程执行,影响我们应用的响应性,那么是否有比较好的解决方案呢?当然有,想必你也想到了,在主线程执行解压缩影响性能,那放在子线程不就可以了吗?
我们前面已经提到了,当未解压缩的图片将要渲染到屏幕时,系统会在主线程对图片进行解压缩,而如果图片已经解压缩了,系统就不会再对图片进行解压缩。因此,也就有了业内的解决方案,在子线程提前对图片进行强制解压缩。
而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate :
总结:
-
图片文件只有在确认要显示时,CPU才会对齐进行解压缩.因为解压是非常消耗性能的事情.解压过的图片就不会重复解压,会缓存起来.
-
图片渲染到屏幕的过程:
a. CPU 读取文件->计算Frame->图片解码->解码后纹理图片位图数据通过数据总线交给GPU
b. GPU获取图片Frame->顶点变换计算->光栅化->根据纹理坐标获取每个像素点的颜色值(如果出现透明值需要将每个像素点的颜色*透明度值)->渲染到帧缓存区->渲染到屏幕
3.苹果官方针对屏幕撕裂现象,目前一直采用的是 垂直同步+双缓存
二.UIView和CALayer的关系
UIView
1.UIView属于UIKit 2.负责绘制图形和动画操作 3.用于界面布局和子试图的管理 4.处理用户的点击事件
CALayer
1.CALayer属于CoreAnimation 2.只负责显示,且显示的是位图 3.CALayer既用于UIKit,也用于APPKit UIKit是iOS平台的渲染框架,AppKit是Mac OSX系统下的渲染框架
面试题:UIView和CALayer的关系
-
UIView基于UIKit框架,可以处理用户触摸事件,并管理子视图
-
CALayer基于CoreAnimation,而CoreAnimation是基于QuartzCode的。所以CALayer只负责显示,不能处理用户的触摸事件
-
从父类来说,CALayer继承的是NSObject,而UIView是直接继承自UIResponder的,所以UIVIew相比CALayer而言,只是多了事件处理功能,
-
从底层来说,UIView属于UIKit的组件,而UIKit的组件到最后都会被分解成layer,存储到图层树中
-
在应用层面来说,需要与用户交互时,使用UIView,不需要交互时,使用两者都可以
UIview和CALayer的渲染:
-
界面触发的方式有两种
==> 通过loadView中子View的drawRect方法触发:会回调CoreAnimation中监听Runloop的BeforeWaiting的RunloopObserver,通过RunloopObserver来进一步调用CoreAnimation内部的CA::Transaction::commit(),进而一步步走到drawRect方法
==> 用户点击事件触发:唤醒Runloop,由source1处理(__IOHIDEventSystemClientQueueCallback),并且在下一个runloop里由source0转发给UIApplication(_UIApplicationHandleEventQueue),从而能通过source0里的事件队列来调用CoreAnimation内部的CA::Transaction::commit();方法,进而一步一步的调用drawRect。
最终都会走到CoreAnimation中的CA::Transaction::commit()方法,从而来触发UIView和CALayer的渲染 -
这时,已经到了CoreAnimation的内部,即调用
CA::Transaction::commit();来创建CATrasaction,然后进一步调用CALayer drawInContext:() -
回调CALayer的Delegate(UIView),问UIView没有需要画的内容,即回调到
drawRect:方法 -
在drawRect:方法里可以通过CoreGraphics函数或UIKit中对CoreGraphics封装的方法进行画图操作
-
将绘制好的位图交由CALayer,由OpenGL ES 传送到GPU的帧缓冲区
-
等屏幕接收到垂直信号后,就读取帧缓冲区的数据,显示到屏幕上
二.动画原理
SVGA:
SVGAConverter 可以将 Flash 以及 After Effects 动画导出成 .SVGA 文件(实际上是 ZIP 包),供 SVGAPlayer 在各平台播放,SVGAPlayer 支持在 iOS / Android / Web / ReactNative / LayaBox 等平台、游戏引擎播放。
SVGA 做的事情,实际上,非常简单,Converter 会负责从 Flash 或 AE 源文件中提取所有动画元素(位图、矢量),并将其在时间轴中的每帧表现(位移、缩放、旋转、透明度)导出。 Player 会负责将这些信息还原至画布上。
其实就一句话:通过帧率去刷每一帧的画面,这个思路跟gif很像,但是通过配置使得动画过程中图片都可以得到复用。性能就提升上来了。并且不用解析高阶插值(二次线性方程,贝塞尔曲线方程),这种思路真是清奇呀
1.导出的.svga文件
2.通过SVGAParser解析出SVGAVideoEntity(动画数据源); parse中是一整套的网络下载.
SVGAVideoEntity 定义了动画所需的所有信息,包括画布大小、帧率、总帧数、序列化后的图片数组,图层数组
SVGAVideoSpriteEntity 定义了当前图层所使用的 imageKey->帧信息数组(动画元素,每一帧都是关键帧)
SVGAVideoSpriteFrameEntity 定义了绘制当前帧所需信息
播放动画占用的内存大小一定程度取决于 总帧数和图层数量
4.draw()
设置 SVGAPlayer 数据源后,调用 draw() 创建图层,所有图层保存在 contentLayers
5.startAnimation()
调用 startAnimation 后,会开启 CADisplayLink 定时器,通过 fps 算出每间隔几帧调用 next 方法
next 方法算出当前播放第几帧 currentFrame 后调用 update 方法
update 方法遍历 contentLayers 更新每个图层当前帧的画面
6.stepToFrame()
SVGAContentLayer 会更新图层大小、位置,transform,遮罩,
SVGAContentLayer 持有 SVGABitmapLayer 和 SVGAVectorLayer
SVGABitmapLayer 负责图片绘制
SVGAVectorLayer 负责矢量元素绘制
SVGA 动画原理
- 通过设置帧率,来生成一个配置文件,使得每一帧都有一个配置,每一帧都是关键帧
- 通过帧率去刷每一帧的画面,这个思路跟 gif 很像,但是通过配置使得动画过程中图片都可以得到复用
mp4:
webp:
序列帧:
gif: