深入探索 iOS 卡顿优化

认识卡顿

一些概念

  • FPS:Frames Per Second,表示每秒渲染的帧数,通过用于衡量画面的流畅度,数值越高则表示画面越流畅。
  • CPU:负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)。
  • GPU: 负责纹理的渲染(将数据渲染到屏幕)。
  • 垂直同步技术: 让CPU和GPU在收到vSync信号后再开始准备数据,防止撕裂感和跳帧,通俗来讲就是保证每秒输出的帧数不高于屏幕显示的帧数。
  • 双缓冲技术:iOS是双缓冲机制,前帧缓存和后帧缓存,cpu计算完GPU渲染后放入缓冲区中,当gpu下一帧已经渲染完放入缓冲区,且视频控制器已经读完前帧,GPU会等待vSync(垂直同步信号)信号发出后,瞬间切换前后帧缓存,并让cpu开始准备下一帧数据,安卓4.0后采用三重缓冲,多了一个后帧缓冲,可降低连续丢帧的可能性,但会占用更多的CPU和GPU 。

图像显示

图像的显示可以简单理解成先经过 CPU 的计算/排版/编解码等操作,然后交由 GPU 去完成渲染放入缓冲中,当视频控制器接受到 vSync 时会从缓冲中读取已经渲染完成的帧并显示到屏幕上。
在这里插入图片描述

卡顿原因

正常情况下,iOS 手机默认显示刷新频率是 60hz,所以 GPU 渲染只要达到 60fps 就不会产生卡顿。

以 60fps 为例,vSync 会每 16.67ms 渲染一次,如在16.67ms内没有准备好下一帧数据就会使画面停留在上一帧,这就造成了卡顿。例如图中第3帧渲染完成之前一直显示的是第2帧的内容。

优化卡顿

CPU

  • 尽量用轻量级的对象,比如用不到事件处理的地方使用CALayer取代UIView
  • 尽量提前计算好布局(例如cell行高)
  • 不要频繁地调用和调整UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的调用和修改(UIView的显示属性实际都是CALayer的映射,而CALayer本身是没有这些属性的,都是初次调用属性时通过resolveInstanceMethod添加并创建Dictionry保存的,耗费资源)
  • Autolayout会比直接设置frame消耗更多的CPU资源,当视图数量增长时会呈指数级增长
  • 图片的size最好刚好跟UIImageView的size保持一致,减少图片显示时的处理计算
  • 控制一下线程的最大并发数量
  • 尽量把耗时的操作放到子线程
  • 文本处理(尺寸计算、绘制、CoreText和YYText)
    1. 计算文本宽高boundingRectWithSize:options:context: 和文本绘制drawWithRect:options:context:放在子线程操作
    2. 使用CoreText自定义文本空间,在对象创建过程中可以缓存宽高等信息,避免像UILabel/UITextView需要多次计算(调整和绘制都要计算一次),且CoreText直接使用了CoreGraphics占用内存小,效率高。(YYText)

GPU

  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
  • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
  • GPU会将多个视图混合在一起再去显示,混合的过程会消耗CPU资源,尽量减少视图数量和层次
  • 减少透明的视图(alpha<1),不透明的就设置opaque为YES,GPU就不会去进行alpha的通道合成
  • 尽量避免出现离屏渲染

离屏渲染

这里特别说下离屏渲染,对 GPU 的资源消耗极大。 在OpenGL中,GPU有2种渲染方式,分别是屏幕渲染(On-Screen Rendering)和离屏渲染(Off-Screen Rendering),区别在于渲染操作是在当前用于显示的屏幕缓冲区进行还是新开辟一个缓冲区进行渲染,渲染完成后再在当前显示的屏幕展示。

离屏渲染消耗性能的原因,在于需要创建新的缓冲区,并且在渲染的整个过程中,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕,造成了资源的及大小消耗。

为什么会需要创建离屏缓存区?

当某个图层的渲染结果需要多次处理或者与其他图层合成

一些会触发离屏渲染的操作:

  • 光栅化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0 考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
  • 阴影,layer.shadowXXX 如果设置了layer.shadowPath就不会产生离屏渲染

一些对于离屏渲染的探究

//
//  ViewController.swift
//  test1
//
//  Created by Unlimited_z on 2024/12/17.
//

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        let test = UIImageView()
       
        test.backgroundColor = .red
        test.image = UIImage(named: "Image")
        test.layer.cornerRadius = 100
        test.clipsToBounds = true
        
        let imageWidth: CGFloat = 200  // 设置宽度
        let imageHeight: CGFloat = 200  // 设置高度
        test.frame = CGRect(x: (view.frame.size.width - imageHeight) / 2,
                            y: 10,
                            width: imageWidth,
                            height: imageHeight)
        let test1 = UIButton();
        
        test1.backgroundColor = .clear
        test1.setImage(UIImage(named: "Image"), for: .normal)
        test1.layer.cornerRadius = 100
        test1.clipsToBounds = true
        test1.frame = CGRect(x: (view.frame.size.width - imageHeight) / 2,
                            y: 220,
                            width: imageWidth,
                            height: imageHeight)
        
        
        let test2 = UIButton();
        
//        test2.backgroundColor = .clear
        test2.setImage(UIImage(named: "Image"), for: .normal)
        test2.layer.cornerRadius = 100
        test2.clipsToBounds = true
        test2.frame = CGRect(x: (view.frame.size.width - imageHeight) / 2,
                            y: 420,
                            width: imageWidth,
                            height: imageHeight)
        let test3 = UIButton();
        
        test3.backgroundColor = .red
//        test3.setImage(UIImage(named: "Image"), for: .normal)
        test3.layer.cornerRadius = 100
        test3.clipsToBounds = true
        test3.frame = CGRect(x: (view.frame.size.width - imageHeight) / 2,
                            y: 620,
                            width: imageWidth,
                            height: imageHeight)
        view.addSubview(test1)
        view.addSubview(test)
        view.addSubview(test2)
        view.addSubview(test3)

    }

    
}


在这里插入图片描述

在这里插入图片描述
可以发现,并不是设置圆角就会直接产生离屏渲染,可以简单的理解为,只对一个图层设置圆角的话,不会有影响,但对于button这种,你给他设置了图片后,就相当于添加了一个子视图,这时候圆角的设置相当于需要操作多个图层,所以会产生离屏渲染

根据示例可以得出只是控件设置了圆角或(圆角+裁剪)并不会触发离屏渲染,同时需要满足父layer需要裁剪时,子layer也因为父layer设置了圆角也需要被裁剪(即视图contents有内容并发生了多图层被裁剪)时才会触发离屏渲染。苹果官方文档对于cornerRadius的描述:

Setting the radius to a value greater than 0.0 causes the layer to
begin drawing rounded corners on its background. By default, the
corner radius does not apply to the image in the layer’s contents
property; it applies only to the background color and border of the
layer. However, setting the masksToBounds property to true causes the
content to be clipped to the rounded corners.

设置cornerRadius大于0时,只为layer的backgroundColor和border设置圆角;而不会对layer的contents设置圆角,除非同时设置了layer.masksToBounds为true(对应UIView的clipsToBounds属性)。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值