图像显示原理
显示器通过从上到下一行一行的进行扫描,扫描完成后就显示一帧画面,随后回到起始位置继续扫描。为了显示同步,显示器会产生一系列的定时信号。当换行的时候会发出一个水平同步信号HSync,当一帧画面绘制完成后重新开始绘制下一帧之前,会发出一个垂直同步信号VSync。我们通常所说的屏幕刷新率,指的就是VSync信号的产生频率
在计算机系统中CPU、GPU、显示器以上述方式进行协同工作。CPU计算显示内容然后提交至GPU,GPU渲染完成后将渲染结果放入帧缓冲区,随后显示器会按照VSync信号逐行读取帧缓冲区的数据,然后经过转换传递显示到屏幕
通常GPU会开启垂直同步机制,GPU会等待显示器的VSync信号发出后,才进行新的一帧的渲染和缓冲区更新,这样能解决画面撕裂现象,也增加了画面流畅度,
卡顿原因
在垂直同步机制下,当VSync信号到来后,系统通知CPU在主线程中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因,基于这个,我们分别来分析CPU和GPU中可能存在的问题,并给出一定的解决方案和实践
解决方案
CPU
对象创建
对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗 CPU 资源。尽量采取轻量级对象,尽量放到后台线程处理,尽量推迟对象的创建时间。例如使用懒加载等等
lazy var tableView: UITableView = {
let tableView = UITableView(frame: self.view.bounds)
return tableView
}()
对象调整
对象的调整也经常是消耗 CPU 资源的地方。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。尽可能的避免频繁的调整属性,调整视图层次和删除添加等等
布局计算
我相信平常开发过程中一定是大量使用Autolayout,无论使用何种布局方式,最终都是对于View.frame/bounds/center 等属性的调整,尽可能的提前计算好布局,在需要调整的时候也一次性调整,不要高频率的调整和计算这些属性。虽然带来的性能提升不大,但是当视图层级较为复杂的时候,产生的消耗是巨大的
文本计算与渲染
如果一个界面中包含大量文本,文本的宽高计算会占用很大一部分资源,并且不可避免。你可以提前计算好高度来避免主线程阻塞。在文本渲染上也可以使用CoreText来异步绘制,尽管非常麻烦,但是可以大幅度的提升性能,这里我实现了提前计算高度的功能
func setSize(with text: String) -> CGFloat {
self.text = text
let size = sizeThatFits(CGSize(width: 200, height: CGFloat(MAXFLOAT)))
return size.height + 1
}
图片加载
由于网络请求的存在,参考了SDWebImage简单实现了图片的异步请求和缓存,将图片存入缓存和沙盒中,提高加载的速度。当你创建一个UIImage的时候,默认发生实际的解码,只有当图片要被显示到屏幕上的时候,才会发生实际的解码,解码是在CPU上进行了。同时也附带了异步解码的方法
class ImageTool: NSObject {
static let shared = ImageTool()
override private init() {
cache = NSCache<AnyObject, AnyObject>()
}
typealias imageHandler = (_ image: UIImage?, _ url: String) -> Void
private var cache: NSCache<AnyObject, AnyObject>
}
extension ImageTool {
public func setImageWithUrl(url: String, handler: @escaping imageHandler) {
if let image = cache.object(forKey: url as AnyObject) as? UIImage {
DispatchQueue.global(qos: .userInteractive).async {
let decodeImage = self.DecodedImage(image: image)
DispatchQueue.main.async {
handler(decodeImage, url)
}
}
return
} else {
let filePath = NSHomeDirectory().appending("/Documents/").appending(url)
if let image = UIImage(contentsOfFile: filePath) {
DispatchQueue.global(qos: .userInteractive).async {
let decodeImage = self.DecodedImage(image: image)
DispatchQueue.main.async {
handler(decodeImage, url)
}
}
cache.setObject(image, forKey: url as AnyObject)
} else {
let downloadTask: URLSessionDataTask = URLSession.shared.dataTask(with: URL(string: url)!) { data, _, error in
if error != nil {
handler(nil, url)
return
} else {
let image = UIImage(data: data!)!
self.cache.setObject(image, forKey: url as AnyObject)
DispatchQueue.global(qos: .userInteractive).async {
let decodeImage = self.DecodedImage(image: image)
DispatchQueue.main.async {
handler(decodeImage, url)
}
}
let filePath = NSHomeDirectory().appending("/Documents/").appending(url)
let imageData = image.jpegData(compressionQuality: 400) as NSData?
imageData?.write(toFile: filePath, atomically: true)
return
}
}
downloadTask.resume()
}
}
}
public func DecodedImage(image: UIImage) -> UIImage?{
guard let cgImage = image.cgImage else { return nil}
guard let colorSpace = cgImage.colorSpace else { return nil}
let width = cgImage.width
let height = cgImage.height
let bytesPerRow = width * 4
let ctx = CGContext(data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue)
guard let context = ctx else { return nil}
let rect = CGRect(x: 0, y: 0, width: width, height: height)
context.draw(cgImage, in: rect)
guard let drawedImage = context.makeImage() else { return nil}
let result = UIImage(cgImage: drawedImage, scale: image.scale, orientation: image.imageOrientation)
return result
}
}
GPU
视图混合
当多个视图重叠在一起显示时,GPU 会首先把他们混合到一起。如果视图结构过于复杂,混合的过程也会消耗很多 GPU 资源。为了减轻这种情况的 GPU 消耗,应用应当尽量减少视图数量和层次,并在不透明的视图里标明 opaque 属性以避免无用的 Alpha 通道合成。例如如果UIlabel的背景为纯色的话,应该给UIlabel设置背景色来避免图层混合。
图形的生成
CALayer 的 、圆角、阴影、遮罩,CASharpLayer 的矢量图形显示,通常会触发离屏渲染,而离屏渲染通常发生在 GPU 中。减少使用圆角、阴影、遮罩等属性,或者将这些属性用图片来实现上述这些属性。这里也简单提一下离屏渲染的概念
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染
关于这方面,有一篇更有深度的文章有体现,我在这里也只是简单的提一下。
总结
最终效果如下:
对于提高界面流畅度方面,就是充分发挥CPU和GPU的性能,将一些能够异步的任务放入后台,不能异步的提前计算好。善用Instuments的Time Profiler来分析CPU在整个过程中都干了什么,有哪些能够改善的地方,以上都只是我个人的一些浅薄理解,如果有错误的地方欢迎指出,谢谢。