iOS-底层原理 34:界面优化方案

简介: iOS-底层原理 34:界面优化方案

本文主要介绍界面卡顿的原理以及优化


界面卡顿


通常来说,计算机中的显示过程是下面这样的,通过CPUGPU显示器协同工作来将图片显示到屏幕上

image.png

  • 1、CPU计算好显示内容,提交至GPU
  • 2、GPU经过渲染完成后将渲染的结果放入FrameBuffer(帧缓存区)
  • 3、随后视频控制器会按照VSync信号逐行读取FrameBuffer的数据
  • 4、经过可能的数模转换传递给显示器进行显示


最开始时,FrameBuffer只有一个,这种情况下FrameBuffer的读取和刷新有很大的效率问题,为了解决这个问题,引入了双缓存区。即双缓冲机制。在这种情况下,GPU会预先渲染好一帧放入FrameBuffer,让视频控制器读取,当下一帧渲染好后,GPU会直接将视频控制器的指针指向第二个FrameBuffer


双缓存机制虽然解决了效率问题,但是随之而言的是新的问题,当视频控制器还未读取完成时,例如屏幕内容刚显示一半,GPU将新的一帧内容提交到FrameBuffer,并将两个FrameBuffer而进行交换后,视频控制器就会将新的一帧数据的下半段显示到屏幕上,造成屏幕撕裂现象


为了解决这个问题,采用了垂直同步信号机制。当开启垂直同步后,GPU会等待显示器的VSync信号发出后,才进行新的一帧渲染和FrameBuffer更新。而目前iOS设备中采用的正是双缓存区+VSync


更多的关于屏幕卡顿渲染流程,请查看二、屏幕卡顿 及 iOS中的渲染流程解析文章


屏幕卡顿原因


下面我们来说说,屏幕卡顿的原因


VSync信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在CPU中计算显示内容。随后 CPU 会将计算好的内容提交到 GPU 去,由GPU进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。所以可以简单理解掉帧过时不候


如下图所示,是一个显示过程,第1帧在VSync到来前,处理完成,正常显示,第2帧在VSync到来后,仍在处理中,此时屏幕不刷新,依旧显示第1帧,此时就出现了掉帧情况,渲染时就会出现明显的卡顿现象

image.png


从图中可以看出,CPU和GPU不论是哪个阻碍了显示流程,都会造成掉帧现象,所以为了给用户提供更好的体验,在开发中,我们需要进行卡顿检测以及相应的优化


卡顿监控


卡顿监控的方案一般有两种:


  • FPS监控:为了保持流程的UI交互,App的刷新拼搏应该保持在60fps左右,其原因是因为iOS设备默认的刷新频率是60次/秒,而1次刷新(即VSync信号发出)的间隔是 1000ms/60 = 16.67ms,所以如果在16.67ms内没有准备好下一帧数据,就会产生卡顿
  • 主线程卡顿监控:通过子线程监测主线程的RunLoop,判断两个状态(kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting)之间的耗时是否达到一定阈值


FPS监控


FPS的监控,参照YYKit中的YYFPSLabel,主要是通过CADisplayLink实现。借助link的时间差,来计算一次刷新刷新所需的时间,然后通过 刷新次数 / 时间差 得到刷新频次,并判断是否其范围,通过显示不同的文字颜色来表示卡顿严重程度。代码实现如下:

class CJLFPSLabel: UILabel {
    fileprivate var link: CADisplayLink = {
        let link = CADisplayLink.init()
        return link
    }()
    fileprivate var count: Int = 0
    fileprivate var lastTime: TimeInterval = 0.0
    fileprivate var fpsColor: UIColor = {
        return UIColor.green
    }()
    fileprivate var fps: Double = 0.0
    override init(frame: CGRect) {
        var f = frame
        if f.size == CGSize.zero {
            f.size = CGSize(width: 80.0, height: 22.0)
        }
        super.init(frame: f)
        self.textColor = UIColor.white
        self.textAlignment = .center
        self.font = UIFont.init(name: "Menlo", size: 12)
        self.backgroundColor = UIColor.lightGray
        //通过虚拟类
        link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
        link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    deinit {
        link.invalidate()
    }
    @objc func tick(_ link: CADisplayLink){
        guard lastTime != 0 else {
            lastTime = link.timestamp
            return
        }
        count += 1
        //时间差
        let detla = link.timestamp - lastTime
        guard detla >= 1.0 else {
            return
        }
        lastTime = link.timestamp
        //刷新次数 / 时间差 = 刷新频次
        fps = Double(count) / detla
        let fpsText = "\(String.init(format: "%.2f", fps)) FPS"
        count = 0
        let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
        if fps > 55.0 {
            //流畅
            fpsColor = UIColor.green
        }else if (fps >= 50.0 && fps <= 55.0){
            //一般
            fpsColor = UIColor.yellow
        }else{
            //卡顿
            fpsColor = UIColor.red
        }
        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
        DispatchQueue.main.async {
            self.attributedText = attrMStr
        }
    }
}

如果只是简单的监测,使用FPS足够了。


主线程卡顿监控


除了FPS,还可以通过RunLoop来监控,因为卡顿的是事务,而事务是交由主线程RunLoop处理的。


实现思路:检测主线程每次执行消息循环的时间,当这个时间大于规定的阈值时,就记为发生了一次卡顿。这个也是微信卡顿三方matrix的原理


以下是一个简易版RunLoop监控的实现

//
//  CJLBlockMonitor.swift
//  UIOptimizationDemo
//
//  Created by 陈嘉琳 on 2020/12/2.
//
import UIKit
class CJLBlockMonitor: NSObject {
    static let share = CJLBlockMonitor.init()
    fileprivate var semaphore: DispatchSemaphore!
    fileprivate var timeoutCount: Int!
    fileprivate var activity: CFRunLoopActivity!
    private override init() {
        super.init()
    }
    public func start(){
        //监控两个状态
        registerObserver()
        //启动监控
        startMonitor()
    }
}
fileprivate extension CJLBlockMonitor{
    func registerObserver(){
        let controllerPointer = Unmanaged<CJLBlockMonitor>.passUnretained(self).toOpaque()
        var context: CFRunLoopObserverContext = CFRunLoopObserverContext(version: 0, info: controllerPointer, retain: nil, release: nil, copyDescription: nil)
        let observer: CFRunLoopObserver = CFRunLoopObserverCreate(nil, CFRunLoopActivity.allActivities.rawValue, true, 0, { (observer, activity, info) in
            guard info != nil else{
                return
            }
            let monitor: CJLBlockMonitor = Unmanaged<CJLBlockMonitor>.fromOpaque(info!).takeUnretainedValue()
            monitor.activity = activity
            let sem: DispatchSemaphore = monitor.semaphore
            sem.signal()
        }, &context)
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes)
    }
    func  startMonitor(){
        //创建信号
        semaphore = DispatchSemaphore(value: 0)
        //在子线程监控时长
        DispatchQueue.global().async {
            while(true){
                // 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
                let st = self.semaphore.wait(timeout: DispatchTime.now()+1.0)
                if st != DispatchTimeoutResult.success {
                    //监听两种状态kCFRunLoopBeforeSources 、kCFRunLoopAfterWaiting,
                    if self.activity == CFRunLoopActivity.beforeSources || self.activity == CFRunLoopActivity.afterWaiting {
                        self.timeoutCount += 1
                        if self.timeoutCount < 2 {
                            print("timeOutCount = \(self.timeoutCount)")
                            continue
                        }
                        // 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
                        print("检测到超过两次连续卡顿")
                    }
                }
                self.timeoutCount = 0
            }
        }
    }
}

使用时,直接调用即可

CJLBlockMonitor.share.start()

也可以直接使用三方库


  • Swift的卡顿检测第三方ANREye,其主要思路是:创建子线程进行循环监测,每次检测时设置标记置为true,然后派发任务到主线程,标记置为false,接着子线程睡眠超过阈值时,判断标记是否为false,如果没有,说明主线程发生了卡顿
  • OC可以使用 微信matrix滴滴DoraemonKit


界面优化


CPU层面的优化


  • 1、尽量用轻量级的对象代替重量级的对象,可以对性能有所优化,例如 不需要相应触摸事件的控件,用CALayer代替UIView
  • 2、尽量减少对UIViewCALayer的属性修改
  • CALayer内部并没有属性,当调用属性方法时,其内部是通过运行时resolveInstanceMethod为对象临时添加一个方法,并将对应属性值保存在内部的一个Dictionary中,同时还会通知delegate、创建动画等,非常耗时
  • UIView相关的显示属性,例如frame、bounds、transform等,实际上都是从CALayer映射来的,对其进行调整时,消耗的资源比一般属性要大
  • 3、当有大量对象释放时,也是非常耗时的,尽量挪到后台线程去释放
  • 4、尽量提前计算视图布局,即预排版,例如cell的行高
  • 5、Autolayout在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的。所以尽量使用代码布局。如果不想手动调整frame等,也可以借助三方库,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
  • 6、文本处理的优化:当一个界面有大量文本时,其行高的计算、绘制也是非常耗时的
  • 1)如果对文本没有特殊要求,可以使用UILabel内部的实现方式,且需要放到子线程中进行,避免阻塞主线程
  • 计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
  • 文本绘制:[NSAttributedString drawWithRect:options:context:]
  • 2)自定义文本控件,利用TextKit 或最底层的 CoreText 对文本异步绘制。并且CoreText 对象创建好后,能直接获取文本的宽高等信息,避免了多次计算(调整和绘制都需要计算一次)。CoreText直接使用了CoreGraphics占用内存小,效率高
  • 7、图片处理(解码 + 绘制)
  • 1)当使用UIImageCGImageSource 的方法创建图片时,图片的数据不会立即解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的数据才进行解码)。这一步是无可避免的,且是发生在主线程中的。想要绕开这个机制,常见的做法是在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap 直接创建图片,例如SDWebImage三方框架中对图片编解码的处理。这就是Image的预解码
  • 当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的绘制子线程中进行
  • 8、图片优化
  • 1)尽量使用PNG图片,不使用JPGE图片
  • 2)通过子线程预解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
  • 3)优化图片大小,尽量避免动态缩放
  • 4)尽量将多张图合为一张进行显示
  • 9、尽量避免使用透明view,因为使用透明view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来,即颜色混合处理,可以参考六、OpenGL 渲染技巧:深度测试、多边形偏移、 混合这篇文章中提及的混合
  • 10、按需加载,例如在TableView中滑动时不加载图片,使用默认占位图,而是在滑动停止时加载
  • 11、少使用addViewcell动态添加view


GPU层面优化


相对于CPU而言,GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上。


  • 1、尽量减少在短时间内大量图片的显示,尽可能将多张图片合为一张显示,主要是因为当有大量图片进行显示时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很可能出现掉帧的情况
  • 2、尽量避免图片的尺寸超过4096×4096,因为当图片超过这个尺寸时,会先由CPU进行预处理,然后再提交给GPU处理,导致额外CPU资源消耗
  • 3、尽量减少视图数量和层次,主要是因为视图过多且重叠时,GPU会将其混合,混合的过程也是非常耗时的
  • 4、尽量避免离屏渲染,可以查看这篇文章四、深入剖析【离屏渲染】原理
  • 5、异步渲染,例如可以将cell中的所有控件、视图合成一张图片进行显示。可以参考Graver三方框架


注:上述这些优化方式的落地实现,需要根据自身项目进行评估,合理的使用进行优化


相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
20天前
|
传感器 iOS开发 UED
探索iOS生态系统:从App Store优化到用户体验提升
本文旨在深入探讨iOS生态系统的多个方面,特别是如何通过App Store优化(ASO)和改进用户体验来提升应用的市场表现。不同于常规摘要仅概述文章内容的方式,我们将直接进入主题,首先介绍ASO的重要性及其对开发者的意义;接着分析当前iOS平台上用户行为的变化趋势以及这些变化如何影响应用程序的设计思路;最后提出几点实用建议帮助开发者更好地适应市场环境,增强自身竞争力。
|
24天前
|
安全 Swift iOS开发
Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法
本文深入探讨了 Swift 与 UIKit 在 iOS 应用界面开发中的关键技术和实践方法。Swift 以其简洁、高效和类型安全的特点,结合 UIKit 丰富的组件和功能,为开发者提供了强大的工具。文章从 Swift 的语法优势、类型安全、编程模型以及与 UIKit 的集成,到 UIKit 的主要组件和功能,再到构建界面的实践技巧和实际案例分析,全面介绍了如何利用这些技术创建高质量的用户界面。
27 2
|
1月前
|
安全 Android开发 iOS开发
深入探索iOS与Android系统的差异性及优化策略
在当今数字化时代,移动操作系统的竞争尤为激烈,其中iOS和Android作为市场上的两大巨头,各自拥有庞大的用户基础和独特的技术特点。本文旨在通过对比分析iOS与Android的核心差异,探讨各自的优势与局限,并提出针对性的优化策略,以期为用户提供更优质的使用体验和为开发者提供有价值的参考。
|
4月前
|
测试技术 Linux 虚拟化
iOS自动化测试方案(五):保姆级VMware虚拟机安装MacOS
详细的VMware虚拟机安装macOS Big Sur的保姆级教程,包括下载VMware和macOS镜像、图解安装步骤和遇到问题时的解决方案,旨在帮助读者顺利搭建macOS虚拟机环境。
170 3
iOS自动化测试方案(五):保姆级VMware虚拟机安装MacOS
|
4月前
|
测试技术 开发工具 iOS开发
iOS自动化测试方案(三):WDA+iOS自动化测试解决方案
这篇文章是iOS自动化测试方案的第三部分,介绍了在没有MacOS系统条件下,如何使用WDA(WebDriverAgent)结合Python客户端库facebook-wda和tidevice工具,在Windows系统上实现iOS应用的自动化测试,包括环境准备、问题解决和扩展应用的详细步骤。
358 1
iOS自动化测试方案(三):WDA+iOS自动化测试解决方案
|
4月前
|
测试技术 数据安全/隐私保护 iOS开发
iOS自动化测试方案(四):保姆级搭建iOS自动化开发环境
iOS自动化测试方案的第四部分,涵盖了基础环境准备、iPhone虚拟机设置、MacOS虚拟机与iPhone真机的连接,以及扩展问题和代码示例,确保读者能够顺利完成环境搭建并进行iOS自动化测试。
379 0
iOS自动化测试方案(四):保姆级搭建iOS自动化开发环境
|
4月前
|
测试技术 虚拟化 iOS开发
iOS自动化测试方案(二):Xcode开发者工具构建WDA应用到iphone
这篇文章是iOS自动化测试方案的第二部分,详细介绍了在Xcode开发者工具中构建WebDriverAgent(WDA)应用到iPhone的全过程,包括环境准备、解决构建过程中可能遇到的错误,以及最终成功安装WDA到设备的方法。
211 0
iOS自动化测试方案(二):Xcode开发者工具构建WDA应用到iphone
|
4月前
|
测试技术 开发工具 虚拟化
iOS自动化测试方案(一):MacOS虚拟机保姆级安装Xcode教程
这篇文章提供了一份保姆级的教程,指导如何在MacOS虚拟机上安装Xcode,包括环境准备、基础软件安装以及USB扩展插件的使用,以实现iOS自动化测试方案的第一步。
208 0
iOS自动化测试方案(一):MacOS虚拟机保姆级安装Xcode教程
|
4月前
|
Android开发 iOS开发 C#
Xamarin.Forms:从零开始的快速入门指南——打造你的首个跨平台移动应用,轻松学会用C#和XAML构建iOS与Android通用界面的每一个步骤
【8月更文挑战第31天】Xamarin.Forms 是一个强大的框架,让开发者通过单一共享代码库构建跨平台移动应用,支持 iOS、Android 和 Windows。使用 C# 和 XAML,它简化了多平台开发流程并保持一致的用户体验。本指南通过创建一个简单的 “HelloXamarin” 应用演示了 Xamarin.Forms 的基本功能和工作原理。
105 0
|
6月前
|
编解码 安全 Android开发
探索iOS与Android开发的差异:从界面到性能
【6月更文挑战第10天】在移动应用开发的广阔天地中,iOS和Android两大平台各占山头,它们在设计理念、用户体验、性能优化等方面展现出独特的魅力。本文将深入探讨这两大系统在开发过程中的主要差异,从用户界面设计到性能调优,揭示各自背后的技术逻辑与创新策略,为开发者提供全面的视角和实用的开发指南。