iOS界面优化

藏宝库编辑 2024-10-5 18:44:40 103 0 来自 中国
界面优化

本文重要先容界面卡顿的原理以及优化
界面卡顿
通常来说,计算机中的表现过程是下面如许的,通过CPU、GPU、表现器协同工作来将图片表现到屏幕上
1.png 1、CPU计算好表现内容,提交至GPU2、GPU颠末渲染完成后将渲染的效果放入FrameBuffer(帧缓存区)3、随后视频控制器会按照VSync信号逐行读取FrameBuffer的数据4、颠末大概的数模转换通报给表现器举行表现最开始时,FrameBuffer只有一个,这种环境下FrameBuffer的读取和革新有很大的服从题目,为相识决这个题目,引入了双缓存区。即双缓冲机制。在这种环境下,GPU会预先渲染好一帧放入FrameBuffer,让视频控制器读取,当下一帧渲染好后,GPU会直接将视频控制器的指针指向第二个FrameBuffer。
双缓存机制虽然办理了服从题目,但是随之而言的是新的题目,当视频控制器还未读取完成时,比方屏幕内容刚表现一半,GPU将新的一帧内容提交到FrameBuffer,并将两个FrameBuffer而举行互换后,视频控制器就会将新的一帧数据的下半段表现到屏幕上,造成屏幕扯破征象
为相识决这个题目,采用了垂直同步信号机制。当开启垂直同步后,GPU会等候表现器的VSync信号发出后,才举行新的一帧渲染和FrameBuffer更新。而现在iOS装备中采用的正是双缓存区+VSync
屏幕卡顿缘故起因
在 VSync信号到来后,体系图形服务会通过 CADisplayLink 等机制关照 App,App 主线程开始在CPU中计算表现内容。随后 CPU 会将计算好的内容提交到 GPU 去,由GPU举行变动、合成、渲染。随后 GPU 会把渲染效果提交到帧缓冲区去,等候下一次 VSync 信号到来时表现到屏幕上。由于垂直同步的机制,假如在一个 VSync 时间内,CPU 大概 GPU 没有完成内容提交,则那一帧就会被抛弃,等候下一次时机再表现,而这时表现屏会保留之前的内容稳定。以是可以简朴明白掉帧为逾期不候
如下图所示,是一个表现过程,第1帧在VSync到来前,处置惩罚完成,正常表现,第2帧在VSync到来后,仍在处置惩罚中,此时屏幕不革新,仍旧表现第1帧,此时就出现了掉帧环境,渲染时就会出现显着的卡顿征象
从图中可以看出,CPU和GPU岂论是哪个拦阻了表现流程,都会造成掉帧征象,以是为了给用户提供更好的体验,在开辟中,我们必要举行卡顿检测以及相应的优化
卡顿监控

卡顿监控的方案一样平常有两种:

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

FPS的监控,参照YYKit中的YYFPSLabel,重要是通过CADisplayLink实现。借助link的时间差,来计算一次革新革新所需的时间,然后通过 革新次数 / 时间差 得到革新频次,并判定是否其范围,通过表现差别的笔墨颜色来表现卡顿严肃程度。代码实现如下:
CADisplayLink 译为:绑定在垂直同步信号的计时器timer, 60fps环境下->VSync(16.67ms/次)
class RCFPSLabel: 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: RCLWeakProxy(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监控的实现
import UIKitclass RCBlockMonitor: NSObject {        static let share = RCBlockMonitor.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 RCBlockMonitor{        func registerObserver(){        let controllerPointer = Unmanaged<RCBlockMonitor>.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: RCBlockMonitor = Unmanaged<RCBlockMonitor>.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            }        }    }}使用时,直接调用即可
RCBlockMonitor.share.start()也可以直接使用三方库

  • Swift的卡顿检测第三方ANREye,其重要思绪是:创建子线程举行循环监测,每次检测时设置标志置为true,然后派发使命到主线程,标志置为false,接着子线程就寝高出阈值时,判定标志是否为false,假如没有,阐明主线程发生了卡顿
  • OC可以使用 微信matrix、滴滴DoraemonKit
界面优化


  • CPU层面的优化
  • 1、只管用轻量级的对象取代重量级的对象,可以对性能有所优化,比方 不必要相应触摸事件的控件,用CALayer取代UIView
  • 2.只管镌汰对UIView和CALayer的属性修改

    • 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 boundingRectWithSizeptions:context:]
      • 文本绘制:[NSAttributedString drawWithRectptions:context:]

    • 2)自界说文本控件,使用TextKit 或最底层的CoreText 对文本异步绘制。而且CoreText 对象创建好后,能直接获取文本的宽高等信息,克制了多次计算(调解和绘制都必要计算一次)。CoreText直接使用了CoreGraphics占用内存小,服从高

  • 7、图片处置惩罚(解码 + 绘制)
  • 1)当使用UIImage 或 CGImageSource 的方法创建图片时,图片的数据不会立刻解码,而是在设置时解码(即图片设置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的数据才举行解码)。这一步是无可克制的,且是发生在主线程中的。想要绕开这个机制,常见的做法是在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap 直接创建图片,比方SDWebImage三方框架中对图片编解码的处置惩罚。这就是Image的预解码
  • 2)当使用CG开头的方法绘制图像到画布中,然后从画布中创建图片时,可以将图像的绘制在子线程中举行
  • 8、图片优化
  • 1)只管使用PNG图片,倒霉用JPGE图片
  • 2)通过子线程预解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
  • 3)优化图片大小,只管克制动态缩放
  • 4)只管将多张图合为一张举行表现
  • 9、只管克制使用透明view,由于使用透明view,会导致在GPU中计算像素时,会将透明view下层图层的像素也计算进来,即颜色混淆处置惩罚,可以参考 OpenGL 渲染本领:深度测试、多边形偏移、 混淆这篇文章中提及的混淆
  • 10、按需加载,比方在TableView中滑动时不加载图片,使用默认占位图,而是在滑动克制时加载
  • 11、少使用addView 给cell动态添加view
GPU层面优化

相对于CPU而言,GPU重要是接收CPU提交的纹理+极点,颠末一系列transform,终极混归并渲染,输出到屏幕上。

  • 1、只管镌汰在短时间内大量图片的表现,尽大概将多张图片合为一张表现,重要是由于当有大量图片举行表现时,无论是CPU的计算还是GPU的渲染,都是非常耗时的,很大概出现掉帧的环境
  • 2、只管克制图片的尺寸高出4096×4096,由于当图片高出这个尺寸时,会先由CPU举行预处置惩罚,然后再提交给GPU处置惩罚,导致额外CPU资源斲丧
  • 3、只管镌汰视图数目和条理,重要是由于视图过多且重叠时,GPU会将其混淆,混淆的过程也是非常耗时的
  • 4、只管克制离屏渲染,深入分析【离屏渲染】原理
  • 异步渲染,比方可以将cell中的全部控件、视图合成一张图片举行表现。可以参考Graver
注:上述这些优化方式的落地实现,必要根据自身项目举行评估,公道的使用举行优化
增补:
一.卡顿的原理
VSync垂直同步:本质是同步的时间段内完成一次->计算(CPU)和渲染(GPU)
二.卡顿的监测
1.YYKit -> YYFPSLabel卡顿监测
CADisplayLink 译为:绑定在垂直同步信号的计时器timer, VSync(16.67ms/次)
YYFPSLabel:可以单独拷贝到工程,做debug
2.runloop卡顿监测
依赖于 CFRunloopActivity activity;
CFRunloopOberserverCreate 添加观察,观察它 kCFRunloopAllActivities 回调之后发送信号semahpore
CFRunloopAddObserver(which-one-runloop,observer,kCFRunloopCommonModes)
4.png 3.Matrix (微信的方法)
4.监测主线程(滴滴方案) 重要监测主线程:在主线程发送信号,子线程接收信号,主线程卡顿时则无法发出信号,子线程的使命就没办法实行。
三.界面优化之 预排版-预计算
哀求网络 / 获取数据(json + frame_height + 富文本) /
重要明白思绪:mode 改酿成 layoutMode
四.界面优化之 预解码
1.图片为什么要预解码?
UIimage模子(dataBuffer,imageBuffer)
预解码 处置惩罚图片data -> dataBuffer -> decode -> imageBuffer -> frameBuffer(渲染)
预解码的原理:便是将原图解码的动作放到子线程区完成,
2.按需加载 通过scollview的滚动状态决议加载的必要。
3.异步渲染 框架Graver
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-11-22 01:59, Processed in 0.197762 second(s), 36 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表