iOS界面優(yōu)化

界面優(yōu)化

本文主要介紹界面卡頓的原理以及優(yōu)化

界面卡頓

通常來說,計算機中的顯示過程是下面這樣的,通過CPU、GPU、顯示器協(xié)同工作來將圖片顯示到屏幕上

1、CPU計算好顯示內(nèi)容,提交至GPU

2、GPU經(jīng)過渲染完成后將渲染的結(jié)果放入FrameBuffer(幀緩存區(qū))

3、隨后視頻控制器會按照VSync信號逐行讀取FrameBuffer的數(shù)據(jù)

4、經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器進行顯示

最開始時,F(xiàn)rameBuffer只有一個,這種情況下FrameBuffer的讀取和刷新有很大的效率問題,為了解決這個問題,引入了雙緩存區(qū)。即雙緩沖機制。在這種情況下,GPU會預(yù)先渲染好一幀放入FrameBuffer,讓視頻控制器讀取,當(dāng)下一幀渲染好后,GPU會直接將視頻控制器的指針指向第二個FrameBuffer。

雙緩存機制雖然解決了效率問題,但是隨之而言的是新的問題,當(dāng)視頻控制器還未讀取完成時,例如屏幕內(nèi)容剛顯示一半,GPU將新的一幀內(nèi)容提交到FrameBuffer,并將兩個FrameBuffer而進行交換后,視頻控制器就會將新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成屏幕撕裂現(xiàn)象

為了解決這個問題,采用了垂直同步信號機制。當(dāng)開啟垂直同步后,GPU會等待顯示器的VSync信號發(fā)出后,才進行新的一幀渲染和FrameBuffer更新。而目前iOS設(shè)備中采用的正是雙緩存區(qū)+VSync

屏幕卡頓原因

在 VSync信號到來后,系統(tǒng)圖形服務(wù)會通過 CADisplayLink 等機制通知 App,App 主線程開始在CPU中計算顯示內(nèi)容。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由GPU進行變換、合成、渲染。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏?xí)A糁暗膬?nèi)容不變。所以可以簡單理解掉幀為過時不候

如下圖所示,是一個顯示過程,第1幀在VSync到來前,處理完成,正常顯示,第2幀在VSync到來后,仍在處理中,此時屏幕不刷新,依舊顯示第1幀,此時就出現(xiàn)了掉幀情況,渲染時就會出現(xiàn)明顯的卡頓現(xiàn)象

從圖中可以看出,CPU和GPU不論是哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象,所以為了給用戶提供更好的體驗,在開發(fā)中,我們需要進行卡頓檢測以及相應(yīng)的優(yōu)化

卡頓監(jiān)控

卡頓監(jiān)控的方案一般有兩種:

  • FPS監(jiān)控:為了保持流程的UI交互,App的刷新拼搏應(yīng)該保持在60fps左右,其原因是因為iOS設(shè)備默認的刷新頻率是60次/秒,而1次刷新(即VSync信號發(fā)出)的間隔是 1000ms/60 = 16.67ms,所以如果在16.67ms內(nèi)沒有準備好下一幀數(shù)據(jù),就會產(chǎn)生卡頓

  • 主線程卡頓監(jiān)控:通過子線程監(jiān)測主線程的RunLoop,判斷兩個狀態(tài)(kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting)之間的耗時是否達到一定閾值

FPS監(jiān)控 案例

FPS的監(jiān)控,參照YYKit中的YYFPSLabel,主要是通過CADisplayLink實現(xiàn)。借助link的時間差,來計算一次刷新刷新所需的時間,然后通過 刷新次數(shù) / 時間差 得到刷新頻次,并判斷是否其范圍,通過顯示不同的文字顏色來表示卡頓嚴重程度。代碼實現(xiàn)如下:

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
        //刷新次數(shù) / 時間差 = 刷新頻次
        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
        }
    }

}

如果只是簡單的監(jiān)測,使用FPS足夠了。

主線程卡頓監(jiān)控

除了FPS,還可以通過RunLoop來監(jiān)控,因為卡頓的是事務(wù),而事務(wù)是交由主線程的RunLoop處理的。

實現(xiàn)思路:檢測主線程每次執(zhí)行消息循環(huán)的時間,當(dāng)這個時間大于規(guī)定的閾值時,就記為發(fā)生了一次卡頓。這個也是微信卡頓三方matrix的原理

以下是一個簡易版RunLoop監(jiān)控的實現(xiàn)


import UIKit

class 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(){
        //監(jiān)控兩個狀態(tài)
        registerObserver()
        
        //啟動監(jiān)控
        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(){
        //創(chuàng)建信號
        semaphore = DispatchSemaphore(value: 0)
        //在子線程監(jiān)控時長
        DispatchQueue.global().async {
            while(true){
                // 超時時間是 1 秒,沒有等到信號量,st 就不等于 0, RunLoop 所有的任務(wù)
                let st = self.semaphore.wait(timeout: DispatchTime.now()+1.0)
                if st != DispatchTimeoutResult.success {
                    //監(jiān)聽兩種狀態(tài)kCFRunLoopBeforeSources 、kCFRunLoopAfterWaiting,
                    if self.activity == CFRunLoopActivity.beforeSources || self.activity == CFRunLoopActivity.afterWaiting {
                        
                        self.timeoutCount += 1
                        
                        if self.timeoutCount < 2 {
                            print("timeOutCount = \(self.timeoutCount)")
                            continue
                        }
                        // 一秒左右的衡量尺度 很大可能性連續(xù)來 避免大規(guī)模打印!
                        print("檢測到超過兩次連續(xù)卡頓")
                    }
                }
                self.timeoutCount = 0
            }
        }
    }
}

使用時,直接調(diào)用即可

RCBlockMonitor.share.start()

也可以直接使用三方庫

  • Swift的卡頓檢測第三方ANREye,其主要思路是:創(chuàng)建子線程進行循環(huán)監(jiān)測,每次檢測時設(shè)置標記置為true,然后派發(fā)任務(wù)到主線程,標記置為false,接著子線程睡眠超過閾值時,判斷標記是否為false,如果沒有,說明主線程發(fā)生了卡頓

  • OC可以使用 微信matrix、滴滴DoraemonKit

界面優(yōu)化

  • CPU層面的優(yōu)化

  • 1、盡量用輕量級的對象代替重量級的對象,可以對性能有所優(yōu)化,例如 不需要相應(yīng)觸摸事件的控件,用CALayer代替UIView

  • 2.盡量減少對UIView和CALayer的屬性修改

    • CALayer內(nèi)部并沒有屬性,當(dāng)調(diào)用屬性方法時,其內(nèi)部是通過運行時resolveInstanceMethod為對象臨時添加一個方法,并將對應(yīng)屬性值保存在內(nèi)部的一個Dictionary中,同時還會通知delegate、創(chuàng)建動畫等,非常耗時
  • UIView相關(guān)的顯示屬性,例如frame、bounds、transform等,實際上都是從CALayer映射來的,對其進行調(diào)整時,消耗的資源比一般屬性要大

  • 3、當(dāng)有大量對象釋放時,也是非常耗時的,盡量挪到后臺線程去釋放

  • 4、盡量提前計算視圖布局,即預(yù)排版,例如cell的行高

  • 5、Autolayout在簡單頁面情況下們可以很好的提升開發(fā)效率,但是對于復(fù)雜視圖而言,會產(chǎn)生嚴重的性能問題,隨著視圖數(shù)量的增長,Autolayout帶來的CPU消耗是呈指數(shù)上升的。所以盡量使用代碼布局。如果不想手動調(diào)整frame等,也可以借助三方庫,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit

  • 6、文本處理的優(yōu)化:當(dāng)一個界面有大量文本時,其行高的計算、繪制也是非常耗時的

    • 1)如果對文本沒有特殊要求,可以使用UILabel內(nèi)部的實現(xiàn)方式,且需要放到子線程中進行,避免阻塞主線程

      • 計算文本寬高:[NSAttributedString boundingRectWithSize:options:context:]

      • 文本繪制:[NSAttributedString drawWithRect:options:context:]

    • 2)自定義文本控件,利用TextKit 或最底層的CoreText 對文本異步繪制。并且CoreText 對象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計算(調(diào)整和繪制都需要計算一次)。CoreText直接使用了CoreGraphics占用內(nèi)存小,效率高

  • 7、圖片處理(解碼 + 繪制)

  • 1)當(dāng)使用UIImage 或 CGImageSource 的方法創(chuàng)建圖片時,圖片的數(shù)據(jù)不會立即解碼,而是在設(shè)置時解碼(即圖片設(shè)置到UIImageView/CALayer.contents中,然后在CALayer提交至GPU渲染前,CGImage中的數(shù)據(jù)才進行解碼)。這一步是無可避免的,且是發(fā)生在主線程中的。想要繞開這個機制,常見的做法是在子線程中先將圖片繪制到CGBitmapContext,然后從Bitmap 直接創(chuàng)建圖片,例如SDWebImage三方框架中對圖片編解碼的處理。這就是Image的預(yù)解碼

  • 2)當(dāng)使用CG開頭的方法繪制圖像到畫布中,然后從畫布中創(chuàng)建圖片時,可以將圖像的繪制在子線程中進行

  • 8、圖片優(yōu)化

  • 1)盡量使用PNG圖片,不使用JPGE圖片

  • 2)通過子線程預(yù)解碼,主線程渲染,即通過Bitmap創(chuàng)建圖片,在子線程賦值image

  • 3)優(yōu)化圖片大小,盡量避免動態(tài)縮放

  • 4)盡量將多張圖合為一張進行顯示

  • 9、盡量避免使用透明view,因為使用透明view,會導(dǎo)致在GPU中計算像素時,會將透明view下層圖層的像素也計算進來,即顏色混合處理,可以參考 OpenGL 渲染技巧:深度測試、多邊形偏移、 混合這篇文章中提及的混合

  • 10、按需加載,例如在TableView中滑動時不加載圖片,使用默認占位圖,而是在滑動停止時加載

  • 11、少使用addView 給cell動態(tài)添加view

GPU層面優(yōu)化

相對于CPU而言,GPU主要是接收CPU提交的紋理+頂點,經(jīng)過一系列transform,最終混合并渲染,輸出到屏幕上。

  • 1、盡量減少在短時間內(nèi)大量圖片的顯示,盡可能將多張圖片合為一張顯示,主要是因為當(dāng)有大量圖片進行顯示時,無論是CPU的計算還是GPU的渲染,都是非常耗時的,很可能出現(xiàn)掉幀的情況

  • 2、盡量避免圖片的尺寸超過4096×4096,因為當(dāng)圖片超過這個尺寸時,會先由CPU進行預(yù)處理,然后再提交給GPU處理,導(dǎo)致額外CPU資源消耗

  • 3、盡量減少視圖數(shù)量和層次,主要是因為視圖過多且重疊時,GPU會將其混合,混合的過程也是非常耗時的

  • 4、盡量避免離屏渲染,深入剖析【離屏渲染】原理

  • 異步渲染,例如可以將cell中的所有控件、視圖合成一張圖片進行顯示??梢詤⒖?a href="" target="_blank">Graver

注:上述這些優(yōu)化方式的落地實現(xiàn),需要根據(jù)自身項目進行評估,合理的使用進行優(yōu)化

補充:

一.卡頓的原理

VSync垂直同步:本質(zhì)是同步的時間段內(nèi)完成一次->計算(CPU)和渲染(GPU)

二.卡頓的監(jiān)測

1.YYKit -> YYFPSLabel卡頓監(jiān)測

CADisplayLink 譯為:綁定在垂直同步信號的計時器timer, VSync(16.67ms/次)

YYFPSLabel:可以單獨拷貝到工程,做debug

2.runloop卡頓監(jiān)測

依賴于 CFRunloopActivity activity;

CFRunloopOberserverCreate 添加觀察,觀察它 kCFRunloopAllActivities 回調(diào)之后發(fā)送信號semahpore

CFRunloopAddObserver(which-one-runloop,observer,kCFRunloopCommonModes)

3.Matrix (微信的方法)

4.監(jiān)測主線程(滴滴方案) 主要監(jiān)測主線程:在主線程發(fā)送信號,子線程接收信號,主線程卡頓時則無法發(fā)出信號,子線程的任務(wù)就沒辦法執(zhí)行。

三.界面優(yōu)化之 預(yù)排版-預(yù)計算

請求網(wǎng)絡(luò) / 獲取數(shù)據(jù)(json + frame_height + 富文本) /

主要理解思路:mode 改變成 layoutMode

預(yù)排版

四.界面優(yōu)化之 預(yù)解碼

1.圖片為什么要預(yù)解碼?
UIimage模型(dataBuffer,imageBuffer)

預(yù)解碼 處理圖片data -> dataBuffer -> decode -> imageBuffer -> frameBuffer(渲染)

預(yù)解碼的原理:即是將原圖解碼的動作放到子線程區(qū)完成,

2.按需加載 通過scollview的滾動狀態(tài)決議加載的需要。

3.異步渲染 框架Graver

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文主要介紹界面卡頓的原理以及優(yōu)化 界面卡頓 通常來說,計算機中的顯示過程是下面這樣的,通過CPU、GPU、顯示器...
    輝輝歲月閱讀 257評論 0 0
  • 本文主要介紹界面卡頓的原理以及優(yōu)化 界面卡頓 通常來說,計算機中的顯示過程是下面這樣的,通過CPU、GPU協(xié)同工作...
    大菠蘿_DABLO閱讀 169評論 0 2
  • 卡頓原因 計算機通過CPU、GPU、顯示器三者協(xié)同工作將試圖顯示到屏幕上 1、CPU將需要顯示的內(nèi)容計算出來,提交...
    木揚音閱讀 776評論 0 9
  • 資料來源iOS 保持界面流暢的技巧[https://blog.ibireme.com/2015/11/12/smo...
    Mjs閱讀 797評論 1 0
  • 轉(zhuǎn)載自:iOS 保持界面流暢的技巧 屏幕顯示圖像的原理 卡頓產(chǎn)生的原因和解決方案 從上面的圖中可以看到,CPU 和...
    荒漠現(xiàn)甘泉閱讀 1,621評論 0 0

友情鏈接更多精彩內(nèi)容