一文讀懂iOS圖像顯示原理與優化

站在巨人的肩膀上,總結原理實現與優化及卡頓監測,一鼓作氣,氣脈通暢,還要啥自行車~huaixiao~php

圖像圖形渲染原理

圖形渲染主要是利用GPU並行運算能力,實現圖形渲染並顯示在屏幕的每個像素上。渲染過程最經常使用的就是光柵化,即將數據轉化爲可見像素的過程。GPU及相關驅動實現了圖形處理的OpenGLDirectX模型,其實OpenGL不是函數API而是一種標準,制定了相關函數API及其實現的功能,具體的函數庫由第三方來實現,一般是由顯卡製造商來提供。html

GPU渲染過程以下圖所示:ios

主要包括:頂點着色器(包含了3D座標系的轉換,每一個頂點屬性值設定)、形狀(圖元)裝配(造成基本的圖形)、幾何着色器(構造新的頂點來造成其餘形狀,如上圖的另外一個三角形)、光柵化(將形狀映射到屏幕的相應的像素生成片斷,片斷包含了像素結構全部的數據)、片斷着色器(丟棄超過視圖之外的像素並着色)、測試與混合(判斷像素位置如是否在其餘像素的後面及透明度等決定是否丟棄及混合)。git

要想圖形更加真實逼真須要更多的頂點及顏色屬性,這樣就增長了性能開銷,爲提高成產和執行效率,常常會使用紋理來表現細節。github

紋理是一個 2D 圖片(甚至也有 1D 和 3D 的紋理),紋理通常能夠直接做爲圖形渲染流水線的*第五階段(即片斷着色器)*的輸入;objective-c

GPU內部包含了若干處理核來實現併發執行,其內部使用了二級緩存(L1L2 cache),其與CPU的架構模型包含以下兩種形式:分離式及耦合式,以下圖所示:編程

  • 分離式的結構緩存

    CPU 和 GPU 擁有各自的存儲系統,二者經過 PCI-e 總線進行鏈接。這種結構的缺點在於 PCI-e 相對於二者具備低帶寬和高延遲,數據的傳輸成了其中的性能瓶頸。目前使用很是普遍,如PC、智能手機等。安全

  • 耦合式的結構性能優化

    CPU 和 GPU 共享內存和緩存。AMD 的 APU 採用的就是這種結構,目前主要使用在遊戲主機中,如 PS4。

屏幕圖形顯示結構以下:

CPU將圖形數據經過總線BUS提交至GPUGPU通過渲染處理轉化爲一幀幀的數據並提交至幀緩衝區,視頻控制器會經過垂直同步信號VSync逐幀讀取幀緩衝區的數據並提交至屏幕控制器最終顯示在屏幕上。爲解決一個幀緩衝區效率問題(讀取和寫入都是一個沒法有效的併發處理),採用雙緩衝機制,在這種狀況下,GPU 會預先渲染一幀放入一個緩衝區中,用於視頻控制器的讀取。當下一幀渲染完畢後,GPU 會直接把視頻控制器的指針指向第二個緩衝器,以下圖所示:

雙緩衝機制雖然提高了效率但也引入了畫面撕裂問題,即當視頻控制器還未讀取完成時,即屏幕內容剛顯示一半時,GPU 將新的一幀內容提交到幀緩衝區並把兩個緩衝區進行交換後,視頻控制器就會把新的一幀數據的下半段顯示到屏幕上,形成畫面撕裂現象,以下圖:

爲了解決這個問題,GPU 一般有一個機制叫作垂直同步(簡寫也是 V-Sync),當開啓垂直同步後,GPU 會等待顯示器的 VSync 信號發出後,才進行新的一幀渲染和緩衝區更新。這樣能解決畫面撕裂現象,也增長了畫面流暢度,但須要消費更多的計算資源,也會帶來部分延遲。

iOS 設備會始終使用雙緩存,並開啓垂直同步。而安卓設備直到 4.1 版本,Google 纔開始引入這種機制,目前安卓系統是三緩存+垂直同步。

卡頓

VSync 信號到來後,系統圖形服務會經過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,好比視圖的建立、佈局計算、圖片解碼、文本繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 信號到來時顯示到屏幕上。因爲垂直同步的機制,若是在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留以前的內容不變。這就是界面卡頓的緣由。

圖像顯示

圖形渲染技術棧

整個圖形渲染技術棧:App 使用 Core GraphicsCore AnimationCore Image 等框架來繪製可視化內容,這些軟件框架相互之間也有着依賴關係。這些框架都須要經過 OpenGL 來調用 GPU 進行繪製,最終將內容顯示到屏幕之上,結構以下圖所示:

框架介紹:

  • UIKit

    UIKit 自身並不具有在屏幕成像的能力,其主要負責對用戶操做事件的響應UIView 繼承自 UIResponder),事件響應的傳遞大致是通過逐層的 視圖樹 遍歷實現的。

  • Core Animation

    Core Animation 是一個複合引擎,其職責是 儘量快地組合屏幕上不一樣的可視內容,這些可視內容可被分解成獨立的圖層(即 CALayer),這些圖層會被存儲在一個叫作圖層樹的體系之中。從本質上而言,CALayer 是用戶所能在屏幕上看見的一切的基礎。

  • Core Graphics

    Core Graphics 基於 Quartz 高級繪圖引擎,主要用於運行時繪製圖像。開發者可使用此框架來處理基於路徑的繪圖,轉換,顏色管理,離屏渲染,圖案,漸變和陰影,圖像數據管理,圖像建立和圖像遮罩以及 PDF 文檔建立,顯示和分析。

  • Core Image

    Core ImageCore Graphics 偏偏相反,Core Graphics 用於在運行時建立圖像,而 Core Image 是用來處理運行前建立的圖像 的。Core Image 框架擁有一系列現成的圖像過濾器,能對已存在的圖像進行高效的處理。

  • OpenGL(ES)

    OpenGL ES(OpenGL for Embedded Systems,簡稱 GLES),是 OpenGL 的子集。

  • Metal

    Metal 相似於 OpenGL ES,也是一套第三方標準,具體實現由蘋果實現。大多數開發者都沒有直接使用過 Metal,但其實全部開發者都在間接地使用 MetalCore AnimationCore ImageSceneKitSpriteKit 等等渲染框架都是構建於 Metal 之上的。當在真機上調試 OpenGL 程序時,控制檯會打印出啓用 Metal 的日誌。根據這一點能夠猜想,Apple 已經實現了一套機制將 OpenGL 命令無縫橋接到 Metal 上,由 Metal 擔任真正於硬件交互的工做

UIView與CALayer關係

UIKit中的每個視圖控件其內部都有一個關聯的CALayer,即backing layer;因爲這種一一對應的關係,視圖採用視圖樹形式呈現,與之對應的圖層也是採用圖層樹形式。

視圖的職責是建立並管理圖層,以確保當子視圖在層級關係中 添加或被移除 時,其關聯的圖層在圖層樹中也有相同的操做,即保證視圖樹和圖層樹在結構上的一致性。

蘋果採用這種結構的目的是保證iOS/Mac平臺底層CALayer通用,避免重複代碼且職責分離,畢竟採用多點觸摸形式與基於鼠標鍵盤的交互有着本質的區別;

CALayer

CALayer基本等同於紋理,本質上是一張圖片,所以 CALayer 也包含一個 contents 屬性指向一塊緩存區,稱爲 backing store,能夠存放位圖(Bitmap)。iOS 中將該緩存區保存的圖片稱爲 寄宿圖

位圖(英語:Bitmap,臺灣稱爲點陣圖),又稱柵格圖(Raster graphics),是使用像素陣列(Pixel-array/Dot-matrix點陣)來表示的圖像。位圖也可指:一種數據結構,表明了有限域中的稠集(dense set),每個元素至少出現一次,沒有其餘的數據和元素相關聯。在索引,數據壓縮等方面有普遍應用,位圖的像素都分配有特定的位置和顏色值。

圖形渲染流水線支持從頂點開始進行繪製(在流水線中,頂點會被處理生成紋理),也支持直接使用紋理(圖片)進行渲染。相應地,在實際開發中,繪製界面也有兩種方式:一種是 手動繪製(custom drawing);另外一種是 使用圖片(contents image)

Contents Image 是指經過 CALayercontents 屬性來配置圖片,典型的是經過CGImage來指定其內容。Custom Drawing 是指使用 Core Graphics 直接繪製寄宿圖。實際開發中,通常經過繼承 UIView 並實現 -drawRect:方法來自定義繪製。

雖然 -drawRect: 是一個 UIView 方法,但事實上都是底層的 CALayer 完成了重繪工做並保存了產生的圖片。下圖所示爲 -drawRect: 繪製定義寄宿圖的基本原理。

  • UIView都有一個CALayer屬性

  • CALayer存在弱引用delegate屬性,實現了<CALayerDelegate>協議,由UIView來代理實現協議方法;

  • 當須要重繪時,CALayer首先調用-displayLayer方法,此時代理能夠直接設置contents屬性;

    須要重繪指:好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法;

  • 若是代理沒有實現 -displayLayer: 方法,CALayer 則會嘗試調用 -drawLayer:inContext: 方法。在調用該方法前,CALayer 會建立一個空的寄宿圖(尺寸由 boundscontentScale 決定)和一個 Core Graphics 的繪製上下文CGContextRef,爲繪製寄宿圖作準備,做爲 ctx 參數傳入。

  • -drawLayer:inContext內部會調用-drawRect,細節代碼以下:

    - (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
        UIGraphicsPushContext(context);
    
        CGRect bounds;
        bounds = CGContextGetClipBoundingBox(context);
        [self drawRect:bounds];
    
        UIGraphicsPopContext();
    }
    複製代碼

    具體的函數調用棧以下:

  • 最後,由 Core Graphics 繪製生成的寄宿圖會存入 backing store

Core Animation Pipeline

瞭解完CALayer本質及流程後,詳細介紹下Core Animation Pipeline工做原理,以下圖:

其中iOS中應用並不負責渲染而是由專門的渲染進程負責,即Render Server

在 iOS 5 之前這個進程叫 SpringBoard,在 iOS 6 以後叫 BackBoard或者backboardd;

越獄查看系統進程,確實存在此進程,以下圖:

主要處理流程以下:

  • 首先,由 App 處理事件(Handle Events),如:用戶的點擊操做,在此過程當中 app 可能須要更新視圖樹,相應地,圖層樹 也會被更新;

  • 其次,App 經過 CPU 完成對顯示內容的計算,如:視圖的建立、佈局計算、圖片解碼、文本繪製等。在完成對顯示內容的計算以後,App 對圖層進行打包,並在下一次 RunLoop 時將其發送至 Render Server,即完成了一次 Commit Transaction 操做。

    具體commit transcation能夠細分爲以下步驟:

    • Layout,主要進行視圖構建,包括:LayoutSubviews 方法的重載,addSubview: 方法填充子視圖等;
    • Display,主要進行視圖繪製,這裏僅僅是設置最要成像的圖元數據。重載視圖的 drawRect: 方法能夠自定義 UIView 的顯示,其原理是在 drawRect: 方法內部繪製寄宿圖,該過程使用 CPU 和內存;
    • Prepare,屬於附加步驟,通常處理圖像的解碼和轉換等操做;
    • Commit,主要將圖層打包,並將它們經過IPC發送至 Render Server。該過程會遞歸執行,由於圖層和視圖都是以樹形結構存在。
  • Render Server執行OpenGLCore Graphics相關操做,如根據layer的各類屬性(若是是動畫屬性,則會計算動畫layer的屬性的中間值)並用OpenGL準備渲染;

  • GPU經過Frame Buffer、視頻控制器等相關組件對圖層進行渲染到屏幕;

爲了知足屏幕60FPS刷新率,RunLoop每次操做的時間間隔不該超過16.67ms,且上述步驟須要並行執行。

渲染與RunLoop

iOS 的顯示系統是由 VSync 信號驅動的,VSync 信號由硬件時鐘生成,每秒鐘發出 60 次(這個值取決設備硬件,好比 iPhone 真機上一般是 59.97)。iOS 圖形服務接收到 VSync 信號後,會經過 IPC 通知到 App 內。App 的 Runloop 在啓動後會註冊對應的 CFRunLoopSource 經過 mach_port 接收傳過來的時鐘信號通知,隨後 Source 的回調會驅動整個 App 的動畫與顯示。

備註:實際觀察App啓動後未註冊相關的VSync相關的Source,所以上述應用應該是Render Server渲染進程註冊Source監聽VSync信號來驅動圖層的渲染,進而提交至GPU。

Core AnimationRunLoop 中註冊了一個 Observer,監聽了 BeforeWaitingExit 事件。這個 Observer 的優先級是 2000000,低於常見的其餘 Observer。當一個觸摸事件到來時,RunLoop 被喚醒,App 中的代碼會執行一些操做,好比建立和調整視圖層級、設置 UIView 的 frame、修改 CALayer 的透明度、爲視圖添加一個動畫;這些操做最終都會被 CALayer 捕獲,並經過 CATransaction 提交到一箇中間狀態去(CATransaction 的文檔略有提到這些內容,但並不完整)。當上面全部操做結束後,RunLoop 即將進入休眠(或者退出)時,關注該事件的 Observer 都會獲得通知。這時 Core Animation 註冊的那個 Observer 就會在回調中,把全部的中間狀態合併提交到 GPU 去顯示;若是此處有動畫,Core Animation 會經過 CADisplayLink 等機制屢次觸發相關流程。

渲染性能優化

爲了保證渲染性能,主要是保證CPUGPU不會阻礙上述渲染流程進而引起「掉幀」現象,所以須要分別針對CPUGPU影響渲染過程進行分析、評估及優化。

CPU資源消耗緣由及解決方案

對象建立

對象建立會分配內存、調整屬性、甚至還有讀取文件(如建立UIViewController讀取xib文件)等操做,比較消耗CPU資源。所以,儘可能使用輕量的對象替代重量的對象,如CALayerUIView不須要響應觸摸事件;若是對象不涉及UI操做,則儘可能放到後臺線程執行;性能敏感的視圖對象,儘可能使用代碼建立而不是Storyboard來建立;若是對象能夠複用,可使用緩存池來複用。

對象調整

對象調整也常常是消耗CPU資源的地方,如CALayer屬性修改、視圖層次調整、添加和移除視圖等;

CALayer內部並無屬性方法,其內部是經過runtime動態接收方法resoleInstanceMethod方法爲對象臨時添加一個方法,並把對應屬性值保存到內部的一個Dictionary字典裏,同時還會通知delegate、建立動畫等。UIView的關於顯示相關的屬性(好比frame/bounds/transform)等其實是CALayer屬性映射來的。

對象銷燬

雖然對象銷燬銷燬資源很少,但累積起來也不容忽視。一般當容器類持有大量對象時,其銷燬時的資源消耗就很是明顯,所以,可見用於後臺線程去釋放的對象挪動後臺線程去。技巧代碼以下:

//將對象捕獲到block中,而後扔到後臺隊列中隨便發個消息以免編譯器警告;
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
    [tmp class];
});
複製代碼
佈局計算

視圖佈局計算是應用最爲常見的銷燬CPU資源的地方,其最終實現都會經過UIView.frame/bounds/center等屬性的調整上,所以,避免CPU資源消耗盡可能提早計算好佈局,在須要時一次性調整好對應屬性,而不要屢次、頻繁的計算和調整這些屬性。

Autolayout

Auotlayout是蘋果提倡的技術,可大部分狀況下能很好地提高開發效率,可是其對於複雜視圖來講嚐嚐會帶來嚴重的性能問題,具體可參閱pilky.me/36/,所以對於性能要求高的視圖儘可能使用代碼實現視圖。

文本計算

若是頁面包含大量文本,文本寬高計算會佔用很大一部分資源,而且不可避免。能夠經過UILabel內部的實現方式:[NSAttributedString boundingRectWithSize:options:context]富文本AttributedString來計算文本寬高,用[NSAttributeString drawWithRect:options:context:]來繪製文本,並放在後臺線程執行避免阻塞主線程;或者使用CoreText基於c的跨平臺API來繪製文本。

Core Text 是爲一些必須處理底層字體處理和文字佈局的開發者準備,如無必要,你應該使用 TextKit(Text Programming Guide for iOS)、CocoaText(Cocoa Text Architecture Guide)等框架開發你的 App 或 Mac 應用。Core Text 是以上兩種文本框架的底層實現,所以它們的速度和效率是共享的。除此以外,以上兩種文本框架提供了富文本編輯及頁面佈局引擎。若是你的 App 只使用 Core Text,則須要爲其提供其餘的基礎實現。Core Text 編程指南

文本渲染

屏幕上能看到的全部文本內容控件,包括UIWebView,在底層都是經過CoreText排版、繪製爲Bitmap顯示。常見的文本控件,如UILabelUITextView等,其排版和繪製都是在主線程進行,當顯示大量文本時,CPU的壓力會很是大。解決方案只有一個,就是自定義文本控件,並用TextKit或最底層的CoreText對文本異步繪製

圖片解碼

當使用UIImageCGImageSource的那幾個方法建立圖片時,圖片數據並不會當即解碼。只有圖片設置到UIImageView或者CALayer.contents中去,而且CALayer被提交到GPU前,CGImage中的數據纔會獲得解碼,且須要在主線程執行。

解決方法:後臺線程先把圖片繪製到CGBitmapContext中,而後從Bitmap直接建立圖片。目前常見的網絡圖片庫都自帶這個功能。

圖像繪製

圖像的繪製一般是指用CGxx開頭的方法將圖像繪製到畫布中,而後從畫布建立圖片並顯示這樣的一個過程。這個最多見的就是[UIView drawRect:]方法,因爲CoreCraphic方法一般都是線程安全的,因此圖像的繪製能夠很容易放到後臺線程進行,示例以下:

- (void)display {
	dispatch_async(backgroudQueu, ^{
		CGContextRef ctx = CGBitmapContextCreate(...);
		//draw in context ....
		CGImageRef img = CGBitmapContextCreateImage(ctx);
		CFRelease(ctx);
		dispatch_async(mainQueue, ^{
			layer.contents = img;
		});
	});
}
複製代碼

GPU資源消耗緣由及解決方案

相對於CPU來講,GPU主要就是:接收提交的紋理頂點描述(三角形)應用變換混合渲染,而後輸出到屏幕上。一般你所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。

紋理的渲染

全部的Bitmap,包括圖片、文本、柵格化的內容,最終都要從內存提交到顯存,綁定爲GPU紋理。不管是提交到顯存的過程,仍是GPU調製和渲染紋理的過程,都要消耗很多GPU資源。當在較短期內顯示大量圖片時(如UITableView存在很是多的圖片而且快速滑動時),CPU佔用率很低,GPU佔用很是高,所以會致使界面掉幀卡頓。有效避免此狀況的方法就是儘可能減小在短期內大量圖片的顯示,儘量將多張圖片合併爲一張進行顯示。

視圖的混合

多存在多視圖且多層次重疊顯示時,GPU會首先將其混合在一塊兒。若是視圖結構很複雜,混合的過程也會消耗不少的GPU資源。爲了減輕GPU的消耗,應儘可能減小視圖數量級層次,並在不透明的視圖裏標明opaque屬性以免無用的Alpha通道合成。

圖形的生成

CALayerborder、圓角、陰影、遮罩(mask),CASharpLayer的矢量圖形顯示,一般會觸發離屏渲染(offscreen rendering),而離屏渲染一般發生在GPU中。當一個列表視圖中存在大量圓角的CALayer且款式滑動時,會消耗大量的GPU資源,進而引起界面卡頓。爲避免此種狀況,能夠嘗試開始CALayer.shouldRasterize屬性,這會吧離屏渲染的操做轉嫁到CPU上;最好是儘可能避免使用圓角、陰影、遮罩等屬性。

GPU屏幕渲染存在兩種方式:當前屏幕渲染(On-Screen Rendering)離屏渲染(Off-Screen Rendering),其中當前屏幕渲染就是正常的GPU渲染流程,GPU將渲染完成的幀放到幀緩衝區,而後顯示到屏幕;而離屏渲染會額外建立一個離屏渲染緩衝區(如保存後續複用的數據),後續仍會提交至幀緩衝區進而顯示到屏幕。

離屏渲染須要建立新的緩衝區,渲染過程當中會涉及從當前屏幕切換到離屏環境屢次上下文環境切換,等到離屏渲染完成後還須要將渲染結果切換到當前屏幕環境,所以付出的代價較高。

AsyncDisplayKit

AsyncDisplayKit(簡寫ASDK)是Facebook開源的一個用於保持iOS界面流暢的開源庫,其基本原理以下:

將不須要主線程執行的消耗性能的經過異步執行方式執行,如文本寬高和視圖佈局計算,文本渲染、圖片界面和圖形繪製,對象建立、屬性調製和銷燬;但UIKitCore Animation相關操做必須在主線程執行,對於不能後臺執行的就優化性能。

UIView CALayer封裝

在原有UIViewCALayer基礎上,封裝了ASDisplayNode類(簡寫ASNode),包裝了常見的視圖屬性(如frame/bounds/aplphs/transform/backgroudColor/superNode/subNodes等),創建ASNodeCALayer的對應關係,當CALayer屬性改變或者動畫產生時,會經過delegate通知的UIVIew進而通知ASNode。因爲UIviewCALayer不是線程安全的,而且只能在主線程建立、訪問和銷燬,但ASNode是線程安全的,能夠在後臺線程建立和修改。ASNode還提供了layer backed屬性,當不須要觸摸事件時,就省去了UIView的中間層功能。同時還提供了大量優化後的子類封裝,如Button/Control/Cell/Image/ImageView/Text/TableView/CollectView等。

圖層預合成

對於多層級CALayer狀況,GPU須要圖層合成,但對於多層級圖層中不須要動畫和位置調整的狀況,就會致使不必的GPU性能消耗,所以ASDK爲此實現了一個pr-composing的技術,將多層級圖層合併渲染成一張圖片,有效下降了GPU的消耗。

異步併發操做

上文提到的能夠後臺線程的任務經過GCD異步併發執行,有效利用iPhone處理器多核的特色。

RunLoop任務分發

ASDK 在此處模擬了 Core Animation 的這個機制:全部針對 ASNode 的修改和提交,總有些任務是必需放入主線程執行的。當出現這種任務時,ASNode 會把任務用 ASAsyncTransaction(Group) 封裝並提交到一個全局的容器去。ASDK 也在 RunLoop 中註冊了一個 Observer,監視的事件和 CA 同樣,但優先級比 CA 要低。當 RunLoop 進入休眠前、CA 處理完事件後,ASDK 就會執行該 loop 內提交的全部任務。經過這種機制,ASDK 能夠在合適的機會把異步、併發的操做同步到主線程去,而且能得到不錯的性能。

卡頓檢測

instrument工具

主要工具使用以下:

Time Profiler,用來檢測CPU的使用狀況。它能夠告訴咱們程序中的哪一個方法正在消耗大量的CPU時間。使用大量的CPU並不必定是個問題 - 你可能指望動畫路徑對CPU很是依賴,由於動畫每每是iOS設備中最苛刻的任務。 可是若是你有性能問題,查看CPU時間對於判斷性能是否是和CPU相關,以及定位到函數都頗有幫助。

Core Animation,用來監測Core Animation性能。它給咱們提供了週期性的FPS。

以下圖使用Core Animation工具查看FPS(Frames Per Second)每秒幀渲染數;

基於RunLoop檢測

主要有兩種方案:

  • FPS監控

    原理就是添加CADisplayLink對象至runloop中統計每秒回調次數,經過次數/時間來獲取屏幕刷新率FPS,具體實現以下:

// 建立CADisplayLink,並添加到當前run loop的NSRunLoopCommonModes
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
  	// 統計每秒的回調次數_count
    if (delta < 1) return;
    _lastTime = link.timestamp;
  	// FPS=次數/時間間隔
    float fps = _count / delta;
    _count = 0;    
    NSLog(@"current FPS: %d", (int)round(fps));
}
複製代碼

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 並不同,其內部實際是操做了一個 Source)。若是在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 類似),形成界面卡頓的感受。在快速滑動TableView時,即便一幀的卡頓也會讓用戶有所察覺。經過對比CADisplayLink添加至runloop先後modes變化,發現其實現是向runloop中添加Source1回調爲IODispatchCalloutFromCFMessage

UI繪製並不必定FPS爲滿60幀,如動畫片FPS爲24,所以,經過FPs方案監測卡頓是存在問題的。

FPS 是一秒顯示的幀數,也就是一秒內畫面變化數量。若是按照動畫片來講,動畫片的 FPS 就是 24,是達不到 60 滿幀的。也就是說,對於動畫片來講,24 幀時雖然沒有 60 幀時流暢,但也已是連貫的了,因此並不能說 24 幀時就算是卡住了。

  • 主線程卡頓監控

    經過子線程監測主線程的runloop,判斷kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting兩個狀態之間的耗時是否達到必定閾值,若監測到卡頓則記錄此時的函數調用信息,具體代碼以下:

    static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        MyClass *object = (__bridge MyClass*)info;
        
        // 記錄狀態值
        object->activity = activity;
        
        // 發送信號
        dispatch_semaphore_t semaphore = moniotr->semaphore;
        dispatch_semaphore_signal(semaphore);
    }
    
    - (void)registerObserver
    {
        CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
        CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                                kCFRunLoopAllActivities,
                                                                YES,
                                                                0,
                                                                &runLoopObserverCallBack,
                                                                &context);
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
        
        // 建立信號
        semaphore = dispatch_semaphore_create(0);
        
        // 在子線程監控時長
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            while (YES)
            {
                // 假定連續5次超時50ms認爲卡頓(固然也包含了單次超時250ms)
                long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
                if (st != 0)
                {
                    if (activity==kCFRunLoopBeforeSources || 
                        activity==kCFRunLoopAfterWaiting)
                    {
                        if (++timeoutCount < 5)
                            continue;
                        //使用第三方crash收集庫PLCrashReporter,其不只會收集crash信息也能夠用於實施獲取各線程的調用堆棧
                      	PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc]
                                                       initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD                                  
                                                       symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
                        PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
                        
                        NSData *data = [crashReporter generateLiveReport];
                        PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
                        NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
                                                                                  withTextFormat:PLCrashReportTextFormatiOS];
    
                        NSLog(@"好像有點兒卡哦");
                    }
                }
                timeoutCount = 0;
            }
        });
    }
    複製代碼

    爲啥須要監測kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting間的耗時,主要由於二者之間處理了APP內部事件處理的Source0時間,如觸摸事件、CFSocketRef,還有中間監聽kCFRunLoopBeforeWaiting狀態Core Animation提交全部的圖層中間狀態至GPU,大部分致使卡頓的場景都在這二者之間;

    而主線程RunLoop閒置時處在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之間的kCFRunLoopBeforeWaiting狀態,所以致使錯誤的判斷爲卡頓,所以優化解決此問題出現了子線程ping方案。具體的原理以下:建立一個子線程經過信號量去ping主線程,由於ping的時候主線程確定是在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之間。每次檢測時設置標記位爲YES,而後派發任務到主線程中將標記位設置爲NO。接着子線程沉睡超時闕值時長,判斷標誌位是否成功設置成NO,若是沒有說明主線程發生了卡頓,ANREye中就是使用子線程Ping的方式監測卡頓的,具體代碼以下:

    @interface PingThread : NSThread
    ......
    @end
    
    @implementation PingThread
    
    - (void)main {
        [self pingMainThread];
    }
    
    - (void)pingMainThread {
        while (!self.cancelled) {
            @autoreleasepool {
              __block BOOL timeOut = YES;
                dispatch_async(dispatch_get_main_queue(), ^{
                  	timeOut = NO;
                  	dispatch_semaphore_signal(_semaphore);
                });
                [NSThread sleepForTimeInterval: lsl_time_out_interval];
                if (timeOut) {
                    NSArray *callSymbols = [StackBacktrace backtraceMainThread];
                  	...
                }
                dispatch_wait(_semaphore, DISPATCH_TIME_FOREVER);
            }
        }
    }
    @end
    複製代碼

Reference

iOS-Core-Animation-Advanced-Techniques

計算機那些事(8)——圖形圖像渲染原理

iOS 圖像渲染原理

iOS 保持界面流暢的技巧

關於drawRect

深刻理解 iOS Rendering Process

iOS 視圖、動畫渲染機制探究

iOS Core Animation: Advanced Techniques中文譯本

iOS離屏渲染

iOS 的離屏渲染

離屏渲染優化詳解:實例示範+性能測試

iOS 核心動畫高級及技巧

iOS性能優化 - 工具Instruments之Time Profiler

CADisplayLink

backboardd

質量監控-卡頓檢測

iOS開發--APP性能檢測方案彙總(一)

相關文章
相關標籤/搜索