@[TOC]linux
APP的啓動時間,直接影響用戶對你的APP的第一體驗和判斷。若是啓動時間過長,不僅僅體驗直線降低,並且可能會激發蘋果的watch dog機制kill掉你的APP,那就悲劇了,用戶會以爲APP怎麼一啓動就卡死而後崩潰了,不能用,而後長按APP點擊刪除鍵。(Xcode在debug模式下是沒有開啓watch dog的,因此咱們必定要鏈接真機測試咱們的APP)ios
在衡量APP的啓動時間以前咱們先了解下,APP的啓動流程:git
咱們將 App 啓動方式分爲:程序員
APP的啓動能夠分爲兩個階段,即main()
執行以前和main()
執行以後。總結以下:github
- t(App 總啓動時間) = t1( main()以前的加載時間 ) + t2( main()以後的加載時間 )。
- t1 = 系統的
dylib
(動態連接庫)和 App 可執行文件的加載時間;- t2 =
main()
函數執行以後到AppDelegate
類中的applicationDidFinishLaunching:withOptions:
方法執行結束前這段時間。
因此咱們對APP啓動時間的獲取和優化都是從這兩個階段着手,下面先看看main()
函數執行以前如何獲取啓動時間。面試
main()
函數執行以前衡量main()
函數執行以前的耗時 對於衡量main()
以前也就是time1
的耗時,蘋果官方提供了一種方法,即在真機調試的時候,勾選DYLD_PRINT_STATISTICS
選項(若是想獲取更詳細的信息能夠使用DYLD_PRINT_STATISTICS_DETAILS
),以下圖:算法
Total pre-main time: 34.22 milliseconds (100.0%)
dylib loading time: 14.43 milliseconds (42.1%)
rebase/binding time: 1.82 milliseconds (5.3%)
ObjC setup time: 3.89 milliseconds (11.3%)
initializer time: 13.99 milliseconds (40.9%)
slowest intializers :
libSystem.B.dylib : 2.20 milliseconds (6.4%)
libBacktraceRecording.dylib : 2.90 milliseconds (8.4%)
libMainThreadChecker.dylib : 6.55 milliseconds (19.1%)
libswiftCoreImage.dylib : 0.71 milliseconds (2.0%)
複製代碼
系統級別的動態連接庫,由於蘋果作了優化,因此耗時並很少,而大多數時候,t1的時間大部分會消耗在咱們自身App中的代碼上和連接第三方庫上。 因此咱們應如何減小main()
調用以前的耗時呢,咱們能夠優化的點有:shell
- 合併動態庫,減小沒必要要的
framework
,特別是第三方的,由於動態連接比較耗時;- check
framework
應設爲optional
和required
,若是該framework
在當前App支持的全部iOS系統版本都存在,那麼就設爲required
,不然就設爲optional
,由於optional
會有些額外的檢查;- 合併或者刪減一些
OC
類,關於清理項目中沒用到的類,能夠藉助AppCode
代碼檢查工具:- 刪減一些無用的靜態變量
- 刪減沒有被調用到或者已經廢棄的方法
- 將沒必要須在+load方法中作的事情延遲到+initialize中
- 儘可能不要用C++虛函數(建立虛函數表有開銷)
- 避免使用
attribute((constructor))
,可將要實現的內容放在初始化方法中配合dispatch_once
使用。- 減小非基本類型的 C++ 靜態全局變量的個數。(由於這類全局變量一般是類或者結構體,若是在構造函數中有繁重的工做,就會拖慢啓動速度)
咱們能夠從原理上分析main
函數執行以前作了一些什麼事情:
Mach-O
格式文件,既 App 中全部類編譯後生成的格式爲 .o
的目標文件集合。
- 分析 App 依賴的全部
dylib
。- 找到
dylib
對應的Mach-O
文件。- 打開、讀取這些
Mach-O
文件,並驗證其有效性。- 在系統內核中註冊代碼簽名
- 對
dylib
的每個segment
調用mmap()
。
系統依賴的動態庫因爲被優化過,能夠較快的加載完成,而開發者引入的動態庫須要耗時較久。
Rebase和Bind操做: 因爲使用了ASLR
技術,在 dylib
加載過程當中,須要計算指針偏移獲得正確的資源地址。 Rebase
將鏡像讀入內存,修正鏡像內部的指針,消耗 IO
性能;Bind
查詢符號表,進行外部鏡像的綁定,須要大量 CPU
計算。
Objc setup : 進行 Objc 的初始化,包括註冊 Objc 類、檢測 selector 惟一性、插入分類方法等。
Initializers : 往應用的堆棧中寫入內容,包括執行 +load
方法、調用 C/C++ 中的構造器函數(用 attribute((constructor))
修飾的函數)、建立非基本類型的 C++ 靜態全局變量等。
main()
函數執行以後衡量main()函數執行以後的耗時 第二階段的耗時統計,咱們認爲是從main ()
執行以後到applicationDidFinishLaunching:withOptions:
方法最後,那麼咱們能夠經過打點的方式進行統計。 Objective-C項目由於有main
文件,因此我麼直接能夠經過添加代碼獲取:
// 1. 在 main.m 添加以下代碼:
CFAbsoluteTime AppStartLaunchTime;
int main(int argc, char * argv[]) {
AppStartLaunchTime = CFAbsoluteTimeGetCurrent();
.....
}
// 2. 在 AppDelegate.m 的開頭聲明
extern CFAbsoluteTime AppStartLaunchTime;
// 3. 最後在AppDelegate.m 的 didFinishLaunchingWithOptions 中添加
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"App啓動時間--%f",(CFAbsoluteTimeGetCurrent()-AppStartLaunchTime));
});
複製代碼
Swift項目是沒有main文件,但咱們能夠經過添加@UIApplicationMain
標誌的方式,幫咱們添加了main
函數了。因此若是是咱們須要在main
函數中作一些其它操做的話,須要咱們本身來建立main.swift
文件,這個也是蘋果容許的。 咱們能夠刪除AppDelegate
類中的 @UIApplicationMain
標誌;
而後自行建立main.swift
文件,並添加程序入口:
import UIKit
var appStartLaunchTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()
UIApplicationMain(
CommandLine.argc,
UnsafeMutableRawPointer(CommandLine.unsafeArgv)
.bindMemory(
to: UnsafeMutablePointer<Int8>.self,
capacity: Int(CommandLine.argc)),
nil,
NSStringFromClass(AppDelegate.self)
)
複製代碼
而後在AppDelegate
的didFinishLaunchingWithOptions :
方法最後添加:
// APP啓動時間耗時,從mian函數開始到didFinishLaunchingWithOptions方法結束
DispatchQueue.main.async {
print("APP啓動時間耗時,從mian函數開始到didFinishLaunchingWithOptions方法:(CFAbsoluteTimeGetCurrent() - appStartLaunchTime)。")
}
複製代碼
總的說來,main
函數以後的優化有如下方式:
- 儘可能使用純代碼編寫,減小xib的使用;
- 啓動階段的網絡請求,是否都放到異步請求;
- 一些耗時的操做是否能夠放到後面去執行,或異步執行等。
- 使用簡單的廣告頁做爲過渡,將首頁的計算操做及網絡請求放在廣告頁展現時異步進行。
- 涉及活動需變動頁面展現時(例如雙十一),提早下發數據緩存
- 首頁控制器用純代碼方式來構建,而不是 xib/Storyboard,避免佈局轉換耗時。
- 避免在主線程進行大量的計算,將與首屏無關的計算內容放在頁面展現後進行,縮短 CPU 計算時間。
- 避免使用大圖片,減小視圖數量及層級,減輕 GPU 的負擔。
- 作好網絡請求接口優化(DNS 策略等),只請求與首屏相關數據。
- 本地緩存首屏數據,待渲染完成後再去請求新數據。
上面1.1.1和1.1.2將的App啓動相關的優化,都是基於一些代碼層面,設計方面儘可能作到好的優化減小啓動時間。咱們還有一種重操做系統底層原理方面的優化,也是屬於main
函數執行以前階段的優化。
學過操做系統原理,咱們就會知道咱們操做系統加載內存的時候有分頁和分段兩種方式,因爲手機的實際內存的限制,通常操做系統給咱們的內存都是虛擬內存,也就是說內存須要作映射。如分頁存儲的方式,若是咱們App須要的內存很大,App一次只能加載有限的內存頁數,不能一次性將App全部的內存所有加載到內存中。 若是在APP啓動過程當中發現開始加載的頁面沒有在內存中,會發生缺頁中斷,去從磁盤找到缺乏的頁,重新加入內存。而缺頁中斷是很耗時的,雖然是毫秒級別的,可是,若是連續發生了屢次這樣的中斷,則用戶會明顯感受到啓動延遲的問題。
知道了這個原理,咱們就須要從分頁這個方面來解決。咱們用二進制重排的思想就是要將咱們APP啓動所須要的相關類,在編譯階段都從新排列,排到最前面,儘可能避免,減小缺頁中斷髮生的次數,從而達到啓動優化的目的。
下面咱們來詳細分析一下內存加載的原理
在早期的計算機中 , 並無虛擬內存的概念 , 任何應用被從磁盤中加載到運行內存中時 , 都是完整加載和按序排列的 . 可是這樣直接使用物理內存會存在一些問題:
- 安全問題 : 因爲在內存條中使用的都是真實物理地址 , 並且內存條中各個應用進程都是按順序依次排列的 . 那麼在 進程1 中經過地址偏移就能夠訪問到 其餘進程 的內存 .
- 效率問題 : 隨着軟件的發展 , 一個軟件運行時須要佔用的內存愈來愈多 , 但每每用戶並不會用到這個應用的全部功能 , 形成很大的內存浪費 , 然後面打開的進程每每須要排隊等待 .
爲了解決上面物理內存存在的問題,引入了虛擬內存的概念。引用了虛擬內存後 , 在咱們進程中認爲本身有一大片連續的內存空間其實是虛擬的 , 也就是說從 0x000000 ~ 0xffffff 咱們是均可以訪問的 . 可是實際上這個內存地址只是一個虛擬地址 , 而這個虛擬地址經過一張映射表映射後才能夠獲取到真實的物理地址 .
整個虛擬內存的工做原理這裏用一張圖來展現 :
引用虛擬內存後就不存在經過偏移能夠訪問到其餘進程的地址空間的問題了 。由於每一個進程的映射表是單獨的 , 在你的進程中隨便你怎麼訪問 , 這些地址都是受映射表限制的 , 其真實物理地址永遠在規定範圍內 , 也就不存在經過偏移獲取到其餘進程的內存空間的問題了 .
引入虛擬內存後 , cpu 在經過虛擬內存地址訪問數據須要經過映射來找到真實的物理地址。過程以下:
- 經過虛擬內存地址 , 找到對應進程的映射表 .
- 經過映射表找到其對應的真實物理地址 , 進而找到數據 .
學過操做系統,咱們知道cpu內存尋址有兩種方式:分頁和分段兩種方式。
虛擬內存和物理內存經過映射表進行映射 , 可是這個映射並不多是一一對應的 , 那樣就太過浪費內存了 ,咱們知道物理內存實際就是一段連續的空間,若是所有分配給一個應用程序使用,這樣會致使其餘應用得不到響應. 爲了解決效率問題 , 操做系統爲了高效使用內存採用了分頁和分段兩種方式來管理內存。
對於咱們這種多用戶多進程的大部分都是採用分頁的方式,操做系統將內存一段連續的內存分紅不少頁,每一頁的大小都相同,如在 linux 系統中 , 一頁內存大小爲 4KB
, 在不一樣平臺可能各有不一樣 . Mac OS 系統內核也是基於linux的, 所以也是一頁爲 4KB
。可是在iOS 系統中 , 一頁爲 16KB
。
內存被分紅不少頁後,就像咱們的一本很厚的書本,有不少頁,可是這麼多頁,若是沒有目錄,咱們很難找到咱們真正須要的那一頁。而操做系統採用一個高速緩存來存放須要提早加載的頁數。因爲CPU的時間片很寶貴,CPU要負責作不少重要的事情,而直接從磁盤讀取數據到內存的IO操做很是耗時,爲了提升效率,採用了高速緩存模式,就是先將一部分須要的分頁加載到高速緩存中,CPU須要讀取的時候直接從高速緩存讀取,而不去直接方法磁盤,這樣就大大提升了CPU的使用效率,可是咱們高速緩存大小也是頗有限的,加載的頁數是有限的,若是CPU須要讀取的分頁不在高速緩存中,則會發生缺頁中斷,從磁盤將須要的頁加載到高速緩存。
以下圖,是兩個進程的虛擬頁表映射關係:
當應用被加載到內存中時 , 並不會將整個應用加載到內存中 . 只會放用到的那一部分 . 也就是懶加載的概念 , 換句話說就是應用使用多少 , 實際物理內存就實際存儲多少 .
當應用訪問到某個地址 , 映射表中爲 0 , 也就是說並無被加載到物理內存中時 , 系統就會馬上阻塞整個進程 , 觸發一個咱們所熟知的 缺頁中斷 - Page Fault
.
當一個缺頁中斷被觸發 , 操做系統會從磁盤中從新讀取這頁數據到物理內存上 , 而後將映射表中虛擬內存指向對應 ( 若是當前內存已滿 , 操做系統會經過置換頁算法 找一頁數據進行覆蓋 , 這也是爲何開再多的應用也不會崩掉 , 可是以前開的應用再打開時 , 就從新啓動了的根本緣由 ).
操做系統經過這種分頁和覆蓋機制 , 就完美的解決了內存浪費和效率問題,可是因爲採用了虛擬內存 , 那麼其中一個函數不管如何運行 , 運行多少次 , 都會是虛擬內存中的固定地址 . 這樣就會有漏洞,黑客能夠很輕易的提早寫好程序獲取固定函數的實現進行修改 hook
操做 . 因此產生這個很是嚴重的安全性問題。
例如:假設應用有一個函數 , 基於首地址偏移量爲
0x00a000
, 那麼虛擬地址從0x000000 ~ 0xffffff
, 基於這個 , 那麼這個函數我不管如何只須要經過0x00a000
這個虛擬地址就能夠拿到其真實實現地址 .
爲了解決上面安全問題,引入了ASLR
技術 . 其原理就是 每次 虛擬地址在映射真實地址以前 , 增長一個隨機偏移值。
Android 4.0 , Apple iOS4.3 , OS X Mountain Lion10.8
開始全民引入 ASLR 技術 , 而實際上自從引入ASLR
後 , 黑客的門檻也自此被拉高 . 再也不是人人均可作黑客的年代
經過上面對內存加載原理的講解,咱們瞭解了分頁和缺頁中斷。而咱們接下來要講解的啓動優化--二進制重排技術
就是基於上面的原理,儘可能減小缺頁中斷髮生的次數,從而達到減小啓動時間的損耗,最終達到啓動時間優化的目的。
在瞭解了內存分頁會觸發中斷異常 Page Fault
會阻塞進程後 , 咱們就知道了這個問題是會對性能產生影響的 . 實際上在 iOS 系統中 , 對於生產環境的應用 , 當產生缺頁中斷進行從新加載時 , iOS 系統還會對其作一次簽名驗證 . 所以 iOS 生產環境的應用 page fault
所產生的耗時要更多 .
抖音團隊分享的一個
Page Fault
,開銷在0.6 ~ 0.8ms
, 實際測試發現不一樣頁會有所不一樣 , 也跟 cpu 負荷狀態有關 , 在0.1 ~ 1.0 ms
之間 。
當用戶使用應用時 , 第一個直接印象就是啓動 app
耗時 , 而恰巧因爲啓動時期有大量的類 , 分類 , 三方 等等須要加載和執行 , 多個 page fault
所產生的的耗時每每是不能小覷的 . 這也是二進制重排進行啓動優化的必要性 .
假設在啓動時期咱們須要調用兩個函數 method1
與 method4
. 函數編譯在 mach-o
中的位置是根據 ld
( Xcode
的連接器) 的編譯順序並不是調用順序來的 . 所以極可能這兩個函數分佈在不一樣的內存頁上 .
那麼啓動時 , page1
與 page2
則都須要從無到有加載到物理內存中 , 從而觸發兩次 page fault
.
而二進制重排的作法就是將 method1
與 method4
放到一個內存頁中 , 那麼啓動時則只須要加載 page1
便可 , 也就是隻觸發一次 page fault
, 達到優化目的 .
實際項目中的作法是將啓動時須要調用的函數放到一塊兒 ( 好比 前10頁中 ) 以儘量減小 page fault
, 達到優化目的 . 而這個作法就叫作 : 二進制重排 .
若是想查看真實 page fault
次數 , 應該將應用卸載 , 查看第一次應用安裝後的效果 , 或者先打開不少個其餘應用 .
由於以前運行過 app
, 應用其中一部分已經被加載到物理內存並作好映射表映射 , 這時再啓動就會少觸發一部分缺頁中斷 , 而且殺掉應用再打開也是如此 .
其實就是但願將物理內存中以前加載的覆蓋/清理掉 , 減小偏差 .
查看步驟以下:
打開 Instruments
, 選擇 System Trace
.
選擇真機 , 選擇工程 , 點擊啓動 , 當首個頁面加載出來點擊中止 . 這裏注意 , 最好是將應用殺掉從新安裝 , 由於冷熱啓動的界定其實因爲進程的緣由並不必定後臺殺掉應用從新打開就是冷啓動 .
以下圖是後臺殺掉重啓應用的狀況:
以下圖是第一次安裝啓動應用的狀況:
此外,你還能夠經過添加 DYLD_PRINT_STATISTICS
來查看 pre-main
階段總耗時來作一個側面輔證 .
二進制重排具體操做,其實很簡單 , Xcode 已經提供好這個機制 , 而且 libobjc
實際上也是用了二進制重排進行優化 .
在objc4-750源碼中提供了libobjc.order
以下圖:
咱們在Xcode中經過以下步驟來進行二進制重排:
首先 , Xcode 是用的連接器叫作 ld
, ld
有一個參數叫 Order File
, 咱們能夠經過這個參數配置一個 order
文件的路徑 .
在這個 order
文件中 , 將你須要的符號按順序寫在裏面
當工程 build
的時候 , Xcode 會讀取這個文件 , 打的二進制包就會按照這個文件中的符號順序進行生成對應的 mach-O
.
如何查看本身工程的符號順序
重排先後咱們須要查看本身的符號順序有沒有修改爲功 , 這時候就用到了 Link Map
.
Link Map
是編譯期間產生的產物 , ( ld
的讀取二進制文件順序默認是按照 Compile Sources - GUI
裏的順序 ) , 它記錄了二進制文件的佈局 . 經過設置 Write Link Map File
來設置輸出與否 , 默認是 no
.
clean
一下 , 運行工程 ,
Products - show in finder
, 找到
macho
的上上層目錄.
按下圖依次找到最新的一個 .txt 文件並打開.
這個文件中就存儲了全部符號的順序 , 在 # Symbols: 部分:
page fault
的次數從而實現時間上的優化.
能夠看到 , 這個符號順序明顯是按照 Compile Sources
的文件順序來排列的 .
在瞭解卡頓產生的緣由以前,先看下屏幕顯示圖像的原理。
咱們先要鏈接一些關於CPU,GPU的相關概念:
- GPU是一個專門爲圖形高併發計算而量身定作的處理單元,比CPU使用更少的電來完成工做而且GPU的浮點計算能力要超出CPU不少。
- GPU的渲染性能要比CPU高效不少,同時對系統的負載和消耗也更低一些,因此在開發中,咱們應該儘可能讓CPU負責主線程的UI調動,把圖形顯示相關的工做交給GPU來處理,當涉及到光柵化等一些工做時,CPU也會參與進來,這點在後面再詳細描述。
- 相對於CPU來講,GPU能幹的事情比較單一:接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合(合成)並渲染,而後輸出到屏幕上。一般你所能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
CPU 和 GPU 的協做:
垂直同步技術:讓CPU和GPU在收到vSync信號後再開始準備數據,防止撕裂感和跳幀,通俗來說就是保證每秒輸出的幀數不高於屏幕顯示的幀數。
雙緩衝技術:iOS是雙緩衝機制,前幀緩存和後幀緩存,cpu計算完GPU渲染後放入緩衝區中,當gpu下一幀已經渲染完放入緩衝區,且視頻控制器已經讀完前幀,GPU會等待vSync(垂直同步信號)信號發出後,瞬間切換先後幀緩存,並讓cpu開始準備下一幀數據 安卓4.0後採用三重緩衝,多了一個後幀緩衝,可下降連續丟幀的可能性,但會佔用更多的CPU和GPU
如今的手機設備基本都是採用雙緩存+垂直同步(即V-Sync)屏幕顯示技術。 如上圖所示,系統內CPU、GPU和顯示器是協同完成顯示工做的。其中CPU負責計算顯示的內容,例如視圖建立、佈局計算、圖片解碼、文本繪製等等。隨後CPU將計算好的內容提交給GPU,由GPU進行變換、合成、渲染。GPU會預先渲染好一幀放入一個緩衝區內,讓視頻控制器讀取,當下一幀渲染好後,GPU會直接將視頻控制器的指針指向第二個容器(雙緩存原理)。這裏,GPU會等待顯示器的VSync(即垂直同步)信號發出後,才進行新的一幀渲染和緩衝區更新(這樣能解決畫面撕裂現象,也增長了畫面流暢度,但須要消費更多的計算資源,也會帶來部分延遲)。
- CPU: 計算視圖frame,圖片解碼,須要繪製紋理圖片經過數據總線交給GPU
- GPU: 紋理混合,頂點變換與計算,像素點的填充計算,渲染到幀緩衝區。
- 時鐘信號:垂直同步信號V-Sync / 水平同步信號H-Sync。
- iOS設備雙緩衝機制:顯示系統一般會引入兩個幀緩衝區,雙緩衝機制
- 圖片顯示到屏幕上是CPU與GPU的協做完成
總的說來,圖片渲染到屏幕的過程:
讀取文件->計算Frame->圖片解碼->解碼後紋理圖片位圖數據經過數據總線交給GPU->GPU獲取圖片Frame->頂點變換計算->光柵化->根據紋理座標獲取每一個像素點的顏色值(若是出現透明值須要將每一個像素點的顏色*透明度值)->渲染到幀緩存區->渲染到屏幕
- 假設咱們使用 +imageWithContentsOfFile: 方法從磁盤中加載一張圖片,這個時候的圖片並無解壓縮;
- 而後將生成的 UIImage 賦值給 UIImageView
- 接着一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化
- 在主線程的下一個 runloop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進行 copy 操做,而受圖片是否字節對齊等因素的影響,這個 copy 操做可能會涉及如下部分或所有步驟: (1).分配內存緩衝區用於管理文件 IO 和解壓縮操做 (2). 將文件數據從磁盤讀到內存中; (3).將壓縮的圖片數據解碼成未壓縮的位圖形式,這是一個很是耗時的 CPU 操做; (4). 最後 Core Animation 中CALayer使用未壓縮的位圖數據渲染 UIImageView 的圖層。 (5). CPU計算好圖片的Frame,對圖片解壓以後.就會交給GPU來作圖片渲染
- 渲染流程: (1).GPU獲取獲取圖片的座標 (2).將座標交給頂點着色器(頂點計算) (3).將圖片光柵化(獲取圖片對應屏幕上的像素點) (4). 片元着色器計算(計算每一個像素點的最終顯示的顏色值) (5).從幀緩存區中渲染到屏幕上
既然圖片的解壓縮須要消耗大量的 CPU 時間,那麼咱們爲何還要對圖片進行解壓縮呢?是否能夠不通過解壓縮,而直接將圖片顯示到屏幕上呢?答案是否認的。要想弄明白這個問題,咱們首先須要知道什麼是位圖
其實,位圖就是一個像素數組,數組中的每一個像素就表明着圖片中的一個點。咱們在應用中常常用到的 JPEG 和 PNG 圖片就是位圖
你們能夠嘗試
UIImage *image = [UIImage imageNamed:@"text.png"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
複製代碼
打印rawData,這裏就是圖片的原始數據.
事實上,無論是 JPEG 仍是 PNG 圖片,都是一種壓縮的位圖圖形格式。只不過 PNG 圖片是無損壓縮,而且支持 alpha 通道,而 JPEG 圖片則是有損壓縮,能夠指定 0-100% 的壓縮比。值得一提的是,在蘋果的 SDK 中專門提供了兩個函數用來生成 PNG 和 JPEG 圖片:
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);
// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);
複製代碼
所以,在將磁盤中的圖片渲染到屏幕以前,必須先要獲得圖片的原始像素數據,才能執行後續的繪製操做,這就是爲何須要對圖片解壓縮的緣由。
既然圖片的解壓縮不可避免,而咱們也不想讓它在主線程執行,影響咱們應用的響應性,那麼是否有比較好的解決方案呢?
咱們前面已經提到了,當未解壓縮的圖片將要渲染到屏幕時,系統會在主線程對圖片進行解壓縮,而若是圖片已經解壓縮了,系統就不會再對圖片進行解壓縮。所以,也就有了業內的解決方案,在子線程提早對圖片進行強制解壓縮。
而強制解壓縮的原理就是對圖片進行從新繪製,獲得一張新的解壓縮後的位圖。其中,用到的最核心的函數是 CGBitmapContextCreate
:
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
複製代碼
函數參數解釋:
data
:若是不爲 NULL ,那麼它應該指向一塊大小至少爲 bytesPerRow * height 字節的內存;若是 爲 NULL ,那麼系統就會爲咱們自動分配和釋放所需的內存,因此通常指定 NULL 便可;
width 和height
:位圖的寬度和高度,分別賦值爲圖片的像素寬度和像素高度便可;
bitsPerComponent
:像素的每一個顏色份量使用的 bit 數,在 RGB 顏色空間下指定 8 便可;
bytesPerRow
:位圖的每一行使用的字節數,大小至少爲 width * bytes per pixel 字節。當咱們指定 0/NULL 時,系統不只會爲咱們自動計算,並且還會進行 cache line alignment 的優化
space
:就是咱們前面提到的顏色空間,通常使用 RGB 便可;
bitmapInfo
:位圖的佈局信息.kCGImageAlphaPremultipliedFirst
YYImage中解壓圖片的代碼: YYImage
用於解壓縮圖片的函數 YYCGImageCreateDecodedCopy
存在於 YYImageCoder
類中,核心代碼以下:
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
...
if (decodeForDisplay) { // decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage;
} else {
...
}
}
複製代碼
它接受一個原始的位圖參數 imageRef ,最終返回一個新的解壓縮後的位圖 newImage ,中間主要通過了如下三個步驟:
- 使用 CGBitmapContextCreate 函數建立一個位圖上下文;
- 使用 CGContextDrawImage 函數將原始位圖繪製到上下文中;
- 使用 CGBitmapContextCreateImage 函數建立一張新的解壓縮後的位圖。
事實上,SDWebImage 中對圖片的解壓縮過程與上述徹底一致,只是傳遞給 CGBitmapContextCreate 函數的部分參數存在細微的差異. SDWebImage和YYImage解壓圖片性能對比:
- 在解壓PNG圖片,SDWebImage>YYImage
- 在解壓JPEG圖片,SDWebImage<YYImage
SDWebImage
解壓圖片的核心代碼以下:
SDWebImage的使用:
CGImageRef imageRef = image.CGImage;
// device color space
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
// iOS display alpha info (BRGA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (context == NULL) {
return image;
}
// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);
return imageWithoutAlpha;
複製代碼
上面講解了圖片顯示的原理和屏幕渲染的原理,形成卡頓的緣由有不少,最主要的緣由是由於發生了掉幀,以下圖:
由上面屏幕顯示的原理,採用了垂直同步機制的手機設備。在 VSync 信號到來後,系統圖形服務會經過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,好比視圖的建立、佈局計算、圖片解碼、文本繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 信號到來時顯示到屏幕上。因爲垂直同步的機制,若是在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留以前的內容不變。這就是界面卡頓的緣由。
在開發中,CPU和GPU中任何一個壓力過大,都會致使掉幀現象,因此在開發時,也須要分別對CPU和GPU壓力進行評估和優化。
卡頓監控通常有兩種實現方案:
- 主線程卡頓監控。經過子線程監測主線程的runLoop,判斷兩個狀態區域之間的耗時是否達到必定閾值。
- FPS監控。要保持流暢的UI交互,App 刷新率應該當努力保持在 60fps。FPS的監控實現原理,上面已經探討過這裏略過。
在使用FPS監控性能的實踐過程當中,發現 FPS 值抖動較大,形成偵測卡頓比較困難。爲了解決這個問題,經過採用檢測主線程每次執行消息循環的時間,當這一時間大於規定的閾值時,就記爲發生了一次卡頓的方式來監控。 這也是美團的移動端採用的性能監控Hertz 方案,微信團隊也在實踐過程當中提出來相似的方案--微信讀書 iOS 性能優化總結。
以下圖是美團Hertz方案流程圖:
方案的提出,是根據滾動引起的Sources事件或其它交互事件老是被快速的執行完成,而後進入到kCFRunLoopBeforeWaiting狀態下;假如在滾動過程當中發生了卡頓現象,那麼RunLoop必然會保持kCFRunLoopAfterWaiting或者kCFRunLoopBeforeSources這兩個狀態之一。 因此監控主線程卡頓的方案一:
開闢一個子線程,而後實時計算 kCFRunLoopBeforeSources
和 kCFRunLoopAfterWaiting
兩個狀態區域之間的耗時是否超過某個閥值,來判定主線程的卡頓狀況。 可是因爲主線程的RunLoop
在閒置時基本處於Before Waiting
狀態,這就致使了即使沒有發生任何卡頓,這種檢測方式也總能認定主線程處在卡頓狀態。 爲了解決這個問題寒神(南梔傾寒)給出了本身的解決方案,Swift
的卡頓檢測第三方ANREye
。這套卡頓監控方案大體思路爲:建立一個子線程進行循環檢測,每次檢測時設置標記位爲YES,而後派發任務到主線程中將標記位設置爲NO。接着子線程沉睡超時闕值時長,判斷標誌位是否成功設置成NO,若是沒有說明主線程發生了卡頓。 結合這套方案,當主線程處在Before Waiting
狀態的時候,經過派發任務到主線程來設置標記位的方式處理常態下的卡頓檢測:
#define lsl_SEMAPHORE_SUCCESS 0
static BOOL lsl_is_monitoring = NO;
static dispatch_semaphore_t lsl_semaphore;
static NSTimeInterval lsl_time_out_interval = 0.05;
@implementation LSLAppFluencyMonitor
static inline dispatch_queue_t __lsl_fluecy_monitor_queue() {
static dispatch_queue_t lsl_fluecy_monitor_queue;
static dispatch_once_t once;
dispatch_once(&once, ^{
lsl_fluecy_monitor_queue = dispatch_queue_create("com.dream.lsl_monitor_queue", NULL);
});
return lsl_fluecy_monitor_queue;
}
static inline void __lsl_monitor_init() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lsl_semaphore = dispatch_semaphore_create(0);
});
}
#pragma mark - Public
+ (instancetype)monitor {
return [LSLAppFluencyMonitor new];
}
- (void)startMonitoring {
if (lsl_is_monitoring) { return; }
lsl_is_monitoring = YES;
__lsl_monitor_init();
dispatch_async(__lsl_fluecy_monitor_queue(), ^{
while (lsl_is_monitoring) {
__block BOOL timeOut = YES;
dispatch_async(dispatch_get_main_queue(), ^{
timeOut = NO;
dispatch_semaphore_signal(lsl_semaphore);
});
[NSThread sleepForTimeInterval: lsl_time_out_interval];
if (timeOut) {
[LSLBacktraceLogger lsl_logMain]; // 打印主線程調用棧
// [LSLBacktraceLogger lsl_logCurrent]; // 打印當前線程的調用棧
// [LSLBacktraceLogger lsl_logAllThread]; // 打印全部線程的調用棧
}
dispatch_wait(lsl_semaphore, DISPATCH_TIME_FOREVER);
}
});
}
- (void)stopMonitoring {
if (!lsl_is_monitoring) { return; }
lsl_is_monitoring = NO;
}
@end
其中LSLBacktraceLogger是獲取堆棧信息的類,詳情見代碼Github。
打印日誌以下:
2018-08-16 12:36:33.910491+0800 AppPerformance[4802:171145] Backtrace of Thread 771:
======================================================================================
libsystem_kernel.dylib 0x10d089bce __semwait_signal + 10
libsystem_c.dylib 0x10ce55d10 usleep + 53
AppPerformance 0x108b8b478 $S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtF + 1144
AppPerformance 0x108b8b60b $S14AppPerformance25LSLFPSTableViewControllerC05tableD0_12cellForRowAtSo07UITableD4CellCSo0kD0C_10Foundation9IndexPathVtFTo + 155
UIKitCore 0x1135b104f -[_UIFilteredDataSource tableView:cellForRowAtIndexPath:] + 95
UIKitCore 0x1131ed34d -[UITableView _createPreparedCellForGlobalRow:withIndexPath:willDisplay:] + 765
UIKitCore 0x1131ed8da -[UITableView _createPreparedCellForGlobalRow:willDisplay:] + 73
UIKitCore 0x1131b4b1e -[UITableView _updateVisibleCellsNow:isRecursive:] + 2863
UIKitCore 0x1131d57eb -[UITableView layoutSubviews] + 165
UIKitCore 0x1133921ee -[UIView(CALayerDelegate) layoutSublayersOfLayer:] + 1501
QuartzCore 0x10ab72eb1 -[CALayer layoutSublayers] + 175
QuartzCore 0x10ab77d8b _ZN2CA5Layer16layout_if_neededEPNS_11TransactionE + 395
QuartzCore 0x10aaf3b45 _ZN2CA7Context18commit_transactionEPNS_11TransactionE + 349
QuartzCore 0x10ab285b0 _ZN2CA11Transaction6commitEv + 576
QuartzCore 0x10ab29374 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv + 76
CoreFoundation 0x109dc3757 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
CoreFoundation 0x109dbdbde __CFRunLoopDoObservers + 430
CoreFoundation 0x109dbe271 __CFRunLoopRun + 1537
CoreFoundation 0x109dbd931 CFRunLoopRunSpecific + 625
GraphicsServices 0x10f5981b5 GSEventRunModal + 62
UIKitCore 0x112c812ce UIApplicationMain + 140
AppPerformance 0x108b8c1f0 main + 224
libdyld.dylib 0x10cd4dc9d start + 1
複製代碼
方案二: 是結合CADisplayLink
的方式實現
經過維基百科咱們知道,FPS是Frames Per Second 的簡稱縮寫,意思是每秒傳輸幀數,也就是咱們常說的「刷新率(單位爲Hz)。 FPS是測量用於保存、顯示動態視頻的信息數量。每秒鐘幀數愈多,所顯示的畫面就會愈流暢,FPS值越低就越卡頓,因此這個值在必定程度上能夠衡量應用在圖像繪製渲染處理時的性能。通常咱們的APP的FPS只要保持在 50-60之間,用戶體驗都是比較流暢的。 蘋果手機屏幕的正常刷新頻率是每秒60次,便可以理解爲FPS值爲60。咱們都知道
CADisplayLink
是和屏幕刷新頻率保存一致,因此咱們是否能夠經過它來監控咱們的FPS呢?
CADisplayLink
是什麼?
CADisplayLink
是CoreAnimation
提供的另外一個相似於NSTimer
的類,它老是在屏幕完成一次更新以前啓動,它的接口設計的和NSTimer
很相似,因此它實際上就是一個內置實現的替代,可是和timeInterval
以秒爲單位不一樣,CADisplayLink
有一個整型的frameInterval
屬性,指定了間隔多少幀以後才執行。默認值是1,意味着每次屏幕更新以前都會執行一次。可是若是動畫的代碼執行起來超過了六十分之一秒,你能夠指定frameInterval
爲2,就是說動畫每隔一幀執行一次(一秒鐘30幀)。
使用CADisplayLink
監控界面的FPS
值,參考自YYFPSLabel
:
import UIKit
class LSLFPSMonitor: UILabel {
private var link: CADisplayLink = CADisplayLink.init()
private var count: NSInteger = 0
private var lastTime: TimeInterval = 0.0
private var fpsColor: UIColor = UIColor.green
public var fps: Double = 0.0
// MARK: - init
override init(frame: CGRect) {
var f = frame
if f.size == CGSize.zero {
f.size = CGSize(width: 55.0, height: 22.0)
}
super.init(frame: f)
self.textColor = UIColor.white
self.textAlignment = .center
self.font = UIFont.init(name: "Menlo", size: 12.0)
self.backgroundColor = UIColor.black
link = CADisplayLink.init(target: LSLWeakProxy(target: self), selector: #selector(tick))
link.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)
}
deinit {
link.invalidate()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - actions
@objc func tick(link: CADisplayLink) {
guard lastTime != 0 else {
lastTime = link.timestamp
return
}
count += 1
let delta = link.timestamp - lastTime
guard delta >= 1.0 else {
return
}
lastTime = link.timestamp
fps = Double(count) / delta
let fpsText = "(String.init(format: "%.3f", 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([NSAttributedStringKey.foregroundColor:fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
attrMStr.setAttributes([NSAttributedStringKey.foregroundColor:UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
DispatchQueue.main.async {
self.attributedText = attrMStr
}
}
}
複製代碼
經過CADisplayLink
的實現方式,並真機測試以後,確實是能夠在很大程度上知足了監控FPS
的業務需求和爲提升用戶體驗提供參考,可是和Instruments
的值可能會有些出入。下面咱們來討論下使用CADisplayLink
的方式,可能存在的問題。 (1). 和Instruments
值對比有出入,緣由以下: CADisplayLink
運行在被添加的那個RunLoop
之中(通常是在主線程中),所以它只能檢測出當前RunLoop
下的幀率。RunLoop中所管理的任務的調度時機,受任務所處的RunLoopMode
和CPU的繁忙程度所影響。因此想要真正定位到準確的性能問題所在,最好仍是經過Instrument
來確認。 (2). 使用CADisplayLink
可能存在的循環引用問題。
例如如下寫法:
let link = CADisplayLink.init(target: self, selector: #selector(tick))
let timer = Timer.init(timeInterval: 1.0, target: self, selector: #selector(tick), userInfo: nil, repeats: true)
複製代碼
緣由:以上兩種用法,都會對 self
強引用,此時 timer
持有 self
,self
也持有 timer
,循環引用致使頁面 dismiss
時,雙方都沒法釋放,形成循環引用。此時使用 weak
也不能有效解決:
weak var weakSelf = self
let link = CADisplayLink.init(target: weakSelf, selector: #selector(tick))
複製代碼
那麼咱們應該怎樣解決這個問題,有人會說在deinit
(或dealloc
)中調用定時器的invalidate
方法,可是這是無效的,由於已經形成循環引用了,不會走到這個方法的。
YYKit
做者提供的解決方案是使用 YYWeakProxy
,這個YYWeakProxy
不是繼承自NSObject
而是繼承NSProxy
。
NSProxy
是一個爲對象定義接口的抽象父類,而且爲其它對象或者一些不存在的對象扮演了替身角色。
修改後代碼以下:
let link = CADisplayLink.init(target: LSLWeakProxy(target: self), selector: #selector(tick))
複製代碼
卡頓優化從 CPU層面的 相關優化,有下面這些方式:
- 儘可能用輕量級的對象,好比用不到事件處理的地方使用
CALayer
取代UIView
- 儘可能提早計算好佈局(例如
cell
行高)- 不要頻繁地調用和調整
UIView
的相關屬性,好比frame
、bounds
、transform
等屬性,儘可能減小沒必要要的調用和修改(UIView
的顯示屬性實際都是CALayer
的映射,而CALayer
自己是沒有這些屬性的,都是初次調用屬性時經過resolveInstanceMethod
添加並建立Dictionary
保存的,耗費資源)Autolayout
會比直接設置frame
消耗更多的CPU
資源,當視圖數量增加時會呈指數級增加.- 圖片的
size
最好恰好跟UIImageView
的size
保持一致,減小圖片顯示時的處理計算- 控制一下線程的最大併發數量
- 儘可能把耗時的操做放到子線程
- 文本處理(尺寸計算、繪製、
CoreText
和YYText
): (1). 計算文本寬高boundingRectWithSize:options:context:
和文本繪製drawWithRect:options:context:
放在子線程操做 (2). 使用CoreText
自定義文本空間,在對象建立過程當中能夠緩存寬高等信息,避免像UILabel/UITextView
須要屢次計算(調整和繪製都要計算一次),且CoreText
直接使用了CoreGraphics
佔用內存小,效率高。(YYText
)- 圖片處理(解碼、繪製) 圖片都須要先解碼成
bitmap
才能渲染到UI上,iOS建立UIImage
,不會馬上進行解碼,只有等到顯示前纔會在主線程進行解碼,固能夠使用CoreGraphics
中的CGBitmapContextCreate
相關操做提早在子線程中進行強制解壓縮得到位圖.- TableViewCell 複用: 在
cellForRowAtIndexPath:
回調的時候只建立實例,快速返回cell
,不綁定數據。在willDisplayCell: forRowAtIndexPath:
的時候綁定數據(賦值)- 高度緩存: 在
tableView
滑動時,會不斷調用heightForRowAtIndexPath:
,當cell
高度須要自適應時,每次回調都要計算高度,會致使 UI 卡頓。爲了不重複無心義的計算,須要緩存高度。- 視圖層級優化: 不要動態建立視圖,在內存可控的前提下,緩存
subview
。善用hidden
。- 減小視圖層級: 減小
subviews
個數,用layer
繪製元素. 少用clearColor
,maskToBounds
,陰影效果等。- 減小多餘的繪製操做.
- 圖片優化: (1)不要用
JPEG
的圖片,應當使用PNG
圖片。 (2)子線程預解碼(Decode
),主線程直接渲染。由於當image
沒有Decode
,直接賦值給imageView
會進行一個Decode操做。 (3)優化圖片大小,儘可能不要動態縮放(contentMode
)。 (4)儘量將多張圖片合成爲一張進行顯示。- 減小透明
view
: 使用透明view會引發blending
,在iOS的圖形處理中,blending
主要指的是混合像素顏色的計算。最直觀的例子就是,咱們把兩個圖層疊加在一塊兒,若是第一個圖層的透明的,則最終像素的顏色計算須要將第二個圖層也考慮進來。這一過程即爲Blending
。- 理性使用
-drawRect
: 當你使用UIImageView
在加載一個視圖的時候,這個視圖雖然依然有CALayer
,可是卻沒有申請到一個後備的存儲,取而代之的是使用一個使用屏幕外渲染,將CGImageRef
做爲內容,並用渲染服務將圖片數據繪製到幀的緩衝區,就是顯示到屏幕上,當咱們滾動視圖的時候,這個視圖將會從新加載,浪費性能。因此對於使用-drawRect:
方法,更傾向於使用CALayer
來繪製圖層。由於使用CALayer
的-drawInContext:,Core Animation
將會爲這個圖層申請一個後備存儲,用來保存那些方法繪製進來的位圖。那些方法內的代碼將會運行在 CPU上,結果將會被上傳到GPU。這樣作的性能更爲好些。 靜態界面建議使用-drawRect:
的方式,動態頁面不建議。- 按需加載: 局部刷新,刷新一個cell就能解決的,堅定不刷新整個
section
或者整個tableView
,刷新最小單元元素。 利用runloop
提升滑動流暢性,在滑動中止的時候再加載內容,像那種一閃而過的(快速滑動),就沒有必要加載,能夠使用默認的佔位符填充內容。
Blending
補充:會致使
blending
的緣由:
UIView
的alpha
< 1。UIImageView
的image含有alpha channel
(即便UIImageView
的alpha
是1,但只要image
含有透明通道,則仍會致使blending
)。
爲啥
blending
會致使性能的損失?
- 緣由是很直觀的,若是一個圖層是不透明的,則系統直接顯示該圖層的顏色便可。而若是圖層是透明的,則會引發更多的計算,由於須要把另外一個的圖層也包括進來,進行混合後的顏色計算。
opaque
設置爲YES,減小性能消耗,由於GPU將不會作任何合成,而是簡單從這個層拷貝。
GPU
層面的卡頓相關優化有下面這些方式:
- 儘可能避免短期內大量圖片的顯示,儘量將多張圖片合成一張進行顯示
GPU
能處理的最大紋理尺寸是4096x4096,一旦超過這個尺寸,就會佔用CPU
資源進行處理,因此紋理儘可能不要超過這個尺寸GPU
會將多個視圖混合在一塊兒再去顯示,混合的過程會消耗CPU資源,儘可能減小視圖數量和層次- 減小透明的視圖(
alpha
<1),不透明的就設置opaque
爲YES
,GPU
就不會去進行alpha
的通道合成- 儘可能避免出現離屏渲染.
- 合理使用光柵化
shouldRasterize
: 光柵化是把GPU的操做轉到CPU上,生成位圖緩存,直接讀取複用。CALayer
會被光柵化爲bitmap
,shadows
、cornerRadius
等效果會被緩存。 更新已經光柵化的layer
,會形成離屏渲染。bitmap
超過100ms沒有使用就會移除。 受系統限制,緩存的大小爲 2.5X Screen Size。shouldRasterize
適合靜態頁面顯示,動態頁面會增長開銷。若是設置了shouldRasterize
爲 YES,那也要記住設置rasterizationScale
爲contentsScale
。- 異步渲染.在子線程繪製,主線程渲染。例如 VVeboTableViewDemo
- 在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
就不會產生離屏渲染.layer.allowsGroupOpacity
爲YES,layer.opacity
的值小於1.0
- 使用
ShadowPath
指定layer
陰影效果路徑。- 使用異步進行
layer
渲染(Facebook開源的異步繪製框架AsyncDisplayKit
)。- 設置
layer
的opaque
值爲YES,減小複雜圖層合成。- 儘可能使用不包含透明(
alpha
)通道的圖片資源。- 儘可能設置
layer
的大小值爲整形值。- 直接讓美工把圖片切成圓角進行顯示,這是效率最高的一種方案。
- 不少狀況下用戶上傳圖片進行顯示,能夠在客戶端處理圓角。
- 使用代碼手動生成圓角
image
設置到要顯示的View
上,利用UIBezierPath
(Core Graphics
框架)畫出來圓角圖片。
Instuments
是Xcode
套件中沒有被充分利用的工具,不少iOS開發者歷來沒用過Instrument
,特別是經過短暫培訓出來的同窗們,因此,不少面試官也會問性能條調優方面的知識,來判斷面試的同窗是否真正應用對年開發經驗。
- 第一種:爲對象A申請了內存空間,以後再也沒用過對象A,也沒釋放過A致使內存泄漏,這種是
Leaked Memory
內存泄漏- 第二種:相似於遞歸,不斷地申請內存空間致使的內存泄漏,這種狀況是
Abandoned Momory
Allocations
工具可讓開發者很好的瞭解每一個方法佔用內存的狀況,並定位相關的代碼,以下圖:
右鍵就能夠打開Xcode自動定位到相關佔用內存方法的代碼上
圈着數字紅色方框中的數字,表明着FPS值,理論上60最佳,實際過程當中59就能夠了,說明就是很流暢的,說明一下操做方式:在手指不離開屏幕的狀況下,上下滑動屏幕列表 介紹一下Deug Display中選項的做用
這個選項基於渲染程度對屏幕中的混合區域進行綠到紅的高亮(也就是多個半透明圖層的疊加),因爲重繪的緣由,混合對GPU性能會有影響,同時也是滑動或者動畫掉幀的罪魁禍首之一 GPU每一幀的繪製的像素有最大限制,這個狀況下能夠輕易繪製整個屏幕的像素,但若是發生重疊像素的關係須要不停的重繪同一區域的,掉幀和卡頓就有可能發生。 GPU會放棄繪製那些徹底被其餘圖層遮擋的像素,可是要計算出一個圖層是否被遮擋也是至關複雜而且會消耗CPU的資源,一樣,合併不一樣圖層的透明重疊元素消耗的資源也很大,因此,爲了快速處理,通常不要使用透明圖層, 1). 給View添加一個固定、不透明的顏色 2). 設置opaque 屬性爲true 可是這對性能調優的幫助並不大,由於UIView的opaque 屬性默認爲true,也就是說,只要不是認爲設置成透明,都不會出現圖層混合 而對於UIIimageView來講,不只須要自身須要不是透明的,它的圖片也不能含有alpha通道,這也上圖9張圖片是綠色的緣由,所以圖像自身的性質也可能會對結果有影響,因此你肯定本身的代碼沒問題,還出現了混合圖層可能就是圖片的問題了 而針對於屏幕中的文字高亮成紅色,是由於一沒有給文字的label增長不透明的背景顏色,而是當UILabel內容爲中文時,label的實際渲染區域要大於label的size,由於外圍有了一圈的陰影,纔會出現圖層混合咱們須要給中文的label加上以下代碼:
retweededTextLab?.layer.masksToBounds = true
retweededTextLab?.backgroundColor = UIColor.groupTableViewBackground
statusLab.layer.masksToBounds = true
statusLab.backgroundColor = UIColor.white
複製代碼
看下效果圖:
statusLab.layer.masksToBounds = true
單獨使用不會出現離屏渲染 2). 若是對
label
設置了圓角的話,圓角部分會離屏渲染,離屏渲染的前提是位圖發生了形變
這個選項主要是檢測咱們有無濫用或正確使用layer的shouldRasterize屬性.成功被緩存的layer會標註爲綠色,沒有成功緩存的會標註爲紅色。 不少視圖Layer因爲Shadow、Mask和Gradient等緣由渲染很高,所以UIKit提供了API用於緩存這些Layer,self.layer.shouldRasterize = true系統會將這些Layer緩存成Bitmap位圖供渲染使用,若是失效時便丟棄這些Bitmap從新生成。圖層Rasterization柵格化好處是對刷新率影響較小,壞處是刪格化處理後的Bitmap緩存須要佔用內存,並且當圖層須要縮放時,要對刪格化後的Bitmap作額外計算。 使用這個選項後時,若是Rasterized的Layer失效,便會標註爲紅色,若是有效標註爲綠色。當測試的應用頻繁閃現出紅色標註圖層時,代表對圖層作的Rasterization做用不大。 在測試的過程當中,第一次加載時,開啓光柵化的layer會顯示爲紅色,這是很正常的,由於尚未緩存成功。可是若是在接下來的測試,。例如咱們來回滾動TableView時,咱們仍然發現有許多紅色區域,那就須要謹慎對待了
這個選項主要檢查咱們有無使用不正確圖片格式,因爲手機顯示都是基於像素的,因此當手機要顯示一張圖片的時候,系統會幫咱們對圖片進行轉化。好比一個像素佔用一個字節,故而RGBA則佔用了4個字節,則1920 x 1080的圖片佔用了7.9M左右,可是平時jpg或者png的圖片並無那麼大,由於它們對圖片作了壓縮,可是是可逆的。因此此時,若是圖片的格式不正確,則系統將圖片轉化爲像素的時間就有可能變長。而該選項就是檢測圖片的格式是不是系統所支持的,如果GPU不支持的色彩格式的圖片則會標記爲青色,則只能由CPU來進行處理。CPU被強制生成了一些圖片,而後發送到渲染服務器,而不是簡單的指向原始圖片的的指針。咱們不但願在滾動視圖的時候,CPU實時來進行處理,由於有可能會阻塞主線程。
一般 Core Animation 以每秒10此的頻率更新圖層的調試顏色,對於某些效果來講,這可能太慢了,這個選項能夠用來設置每一幀都更新(可能會影響到渲染性能,因此不要一直都設置它)
這裏會高亮那些被縮放或者拉伸以及沒有正確對齊到像素邊界的圖片,即圖片Size和imageView中的Size不匹配,會使圖過程片縮放,而縮放會佔用CPU,因此在寫代碼的時候保證圖片的大小匹配好imageView,以下圖所示: 圖片尺寸 170 * 220px
能夠看到圖片高亮成黃色顯示,更改下imageView的大小: ![]()
let imageView = UIImageView(frame: CGRect(x: 50, y: 100, width: 85, height: 110))
imageView.image = UIImage(named: "cat")
view.addSubview(imageView)
複製代碼
看下效果圖:
/* 圓角處理 */
view.layer.maskToBounds = truesomeView.clipsToBounds = true
/* 設置陰影 */
view.shadow..
/* 柵格化 */
view.layer.shouldRastarize = true
複製代碼
針對柵格化處理,咱們須要指定屏幕的分辨率
//離屏渲染 - 異步繪製 耗電
self.layer.drawsAsynchronously = true
//柵格化 - 異步繪製以後 ,會生成一張獨立的圖片 cell 在屏幕上滾動的時候,本質上滾動的是這張圖片
//cell 優化,要儘可能減小圖層的數量,想當於只有一層
//中止滾動以後,能夠接受監聽
self.layer.shouldRasterize = true
//使用 「柵格化」 必須指定分辨率
self.layer.rasterizationScale = UIScreen.main.scale
複製代碼
指定陰影的路徑,能夠防止離屏渲染
// 指定陰影曲線,防止陰影效果帶來的離屏渲染
imageView.layer.shadowPath = UIBezierPath(rect: imageView.bounds).cgPath
複製代碼
這行代碼制定了陰影路徑,若是沒有手動指定,Core Animation會去自動計算,這就會觸發離屏渲染。若是人爲指定了陰影路徑,就能夠免去計算,從而避免產生離屏渲染。 設置cornerRadius自己並不會致使離屏渲染,但不少時候它還須要配合layer.masksToBounds = true使用。根據以前的總結,設置masksToBounds會致使離屏渲染。解決方案是儘量在滑動時避免設置圓角,若是必須設置圓角,能夠使用光柵化技術將圓角緩存起來:
// 設置圓角
label.layer.masksToBounds = true
label.layer.cornerRadius = 8
label.layer.shouldRasterize = true
label.layer.rasterizationScale = layer.contentsScale
複製代碼
若是界面中有不少控件須要設置圓角,好比tableView中,當tableView有超過25個圓角,使用以下方法
view.layer.cornerRadius = 10
view.maskToBounds = Yes
複製代碼
那麼fps將會降低不少,特別是對某些控件還設置了陰影效果,更會加重界面的卡頓、掉幀現象,對於不一樣的控件將採用不一樣的方法進行處理: 1). 對於label類,能夠經過CoreGraphics來畫出一個圓角的label 2). 對於imageView,經過CoreGraphics對繪畫出來的image進行裁邊處理,造成一個圓角的imageView,代碼以下:
/// 建立圓角圖片
///
/// - parameter radius: 圓角的半徑
/// - parameter size: 圖片的尺寸
/// - parameter backColor: 背景顏色 默認 white
/// - parameter lineWith: 圓角線寬 默認 1
/// - parameter lineColor: 線顏色 默認 darkGray
///
/// - returns: image
func yw_drawRectWithRoundCornor(radius: CGFloat, size: CGSize, backColor: UIColor = UIColor.white, lineWith: CGFloat = 1, lineColor: UIColor = UIColor.darkGray) -> UIImage? {
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
UIGraphicsBeginImageContextWithOptions(rect.size, true, 0)
let bezier = UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.allCorners, cornerRadii: CGSize(width: radius, height: radius))
backColor.setFill()
UIRectFill(rect)
bezier.addClip()
draw(in: rect)
bezier.lineWidth = 1
lineColor.setStroke()
bezier.stroke()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return result
}
複製代碼
這個選項會對任何直接使用OpenGL繪製的圖層進行高亮,若是僅僅使用UIKit或者Core Animation的API,不會有任何效果
Leaks
主要用來檢查內存泄漏,在前面Allcations
裏面咱們提到內存泄漏分兩種,如今咱們研究Leaked Memory
, 從用戶使用角度來看,內存泄漏自己不會產生什麼危害,做爲用戶,根本感受不到內存泄漏的存在,真正的危害在於內存泄漏的堆積,最終會耗盡系統全部的內存。咱們直接看圖:
instruments
中,雖然選擇了
Leaks
模板,但默認狀況下也會添加
Allocations
模板.基本上凡是內存分析都會使用
Allocations
模板, 它能夠監控內存分佈狀況。 選中
Allocations
模板3區域會顯示隨着時間的變化內存使用的折線圖,同時在4區域會顯示內存使用的詳細信息,以及對象分配狀況. 點擊
Leaks
模板, 能夠查看內存泄露狀況。若是在3區域有 紅X 出現, 則有內存泄露, 4區域則會顯示泄露的對象. 打用
leaks
進行監測:點擊泄露對象能夠在(下圖)看到它們的內存地址, 佔用字節, 所屬框架和響應方法等信息.打開擴展視圖, 能夠看到右邊的跟蹤堆棧信息,4 黑色代碼最有可能出現內存泄漏的方法
監測結果的分析:
Time Profiler
是Xcode自帶的工具,原理是定時抓取線程的堆棧信息,經過統計比較時間間隔之間的堆棧狀態,計算一段時間內各個方法的近似耗時。精確度取決於設置的定時間隔。
經過 Xcode → Open Developer Tool → Instruments → Time Profiler 打開工具,注意,需將工程中 Debug Information Format 的 Debug 值改成 DWARF with dSYM File,不然只能看到一堆線程沒法定位到函數。
正常Time Profiler是每1ms採樣一次, 默認只採集全部在運行線程的調用棧,最後以統計學的方式彙總。因此會沒法統計到耗時太短的函數和休眠的線程,好比下圖中的5次採樣中,method3都沒有采樣到,因此最後聚合到的棧裏就看不到method3。
咱們能夠將 File -> Recording Options 中的配置調高,便可獲取更精確的調用棧。
static void add(const struct mach_header* header, intptr_t imp) {
usleep(10000);
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
_dyld_register_func_for_add_image(add);
});
....
}
複製代碼
能夠看到整個記錄過程耗時7s,但 Time Profiler 上只顯示了1.17s,且看到啓動後有一段時間是空白的。這時經過 System Trace 查看各個線程的具體狀態。
接着咱們觀察 0x5d39c 線程,發如今主線程阻塞的這段時間,該線程執行了屢次10ms的 sleep 操做,到此就找到了主線程被子線程阻塞致使啓動緩慢的緣由。
從此,當咱們想更清楚的看到各個線程之間的調度就能夠使用 System Trace,但仍是建議優先使用 Time Profiler,使用簡單易懂,排查問題效率更高。
App Launch是Xcode11 以後新出的工具,功能至關於 Time Profiler 和 System Trace 的整合。
能夠對 objc_msgSend
進行 Hook
獲取每一個函數的具體耗時,優化在啓動階段耗時多的函數或將其置後調用。實現方法可查看筆者以前的文章 經過objc_msgSend
實現iOS方法耗時監控。
可能形成
tableView
卡頓的緣由有:
- 最經常使用的就是
cell
的重用, 註冊重用標識符 若是不重用cell
時,每當一個cell
顯示到屏幕上時,就會從新建立一個新的cell; 若是有不少數據的時候,就會堆積不少cell
。 若是重用cell
,爲cell建立一個ID,每當須要顯示cell 的時候,都會先去緩衝池中尋找可循環利用的cell
,若是沒有再從新建立cell
- 避免
cell
的從新佈局cell
的佈局填充等操做 比較耗時,通常建立時就佈局好 如能夠將cell
單獨放到一個自定義類,初始化時就佈局好- 提早計算並緩存
cell
的屬性及內容 當咱們建立cell
的數據源方法時,編譯器並非先建立cell 再定cell的高度 而是先根據內容一次肯定每個cell的高度,高度肯定後,再建立要顯示的cell,滾動時,每當cell進入憑虛都會計算高度,提早估算高度告訴編譯器,編譯器知道高度後,緊接着就會建立cell,這時再調用高度的具體計算方法,這樣能夠方式浪費時間去計算顯示之外的cell- 減小
cell
中控件的數量 儘可能使cell得佈局大體相同,不一樣風格的cell能夠使用不用的重用標識符,初始化時添加控件, 不適用的能夠先隱藏- 不要使用
ClearColor
,無背景色,透明度也不要設置爲0 渲染耗時比較長- 使用局部更新 若是隻是更新某組的話,使用
reloadSection
進行局部更新- 加載網絡數據,下載圖片,使用異步加載,並緩存
- 少使用
addView
給cell
動態添加view
- 按需加載cell,cell滾動很快時,只加載範圍內的cell
- 不要實現無用的代理方法,
tableView
只遵照兩個協議- 緩存行高:
estimatedHeightForRow
不能和HeightForRow
裏面的layoutIfNeed
同時存在,這二者同時存在纔會出現「竄動」的bug。因此個人建議是:只要是固定行高就寫預估行高來減小行高調用次數提高性能。若是是動態行高就不要寫預估方法了,用一個行高的緩存字典來減小代碼的調用次數便可- 不要作多餘的繪製工做。 在實現
drawRect
:的時候,它的rect參數就是須要繪製的區域,這個區域以外的不須要進行繪製。例如上例中,就能夠用CGRectIntersectsRect
、CGRectIntersection
或CGRectContainsRect
判斷是否須要繪製image
和text
,而後再調用繪製方法。- 預渲染圖像。 當新的圖像出現時,仍然會有短暫的停頓現象。解決的辦法就是在
bitmap context
裏先將其畫一遍,導出成UIImage
對象,而後再繪製到屏幕;- 使用正確的數據結構來存儲數據。
繪製像素到屏幕上 iOS圖形原理與離屏渲染 iOS 保持界面流暢的技巧 Advanced Graphics and Animations for iOS Apps(session 419) 使用 ASDK 性能調優 - 提高 iOS 界面的渲染性能 Designing for iOS: Graphics & Performance iOS離屏渲染之優化分析 iOS視圖渲染以及性能優化總結 iOS 離屏渲染 深入理解移動端優化之離屏渲染 iOS 流暢度性能優化、CPU、GPU、離屏渲染 離屏渲染優化詳解:實例示範+性能測試
專題內容比較多,後面細份內容會有部分重複。