- iOS全解1:基礎(chǔ)/內(nèi)存管理/Block/GCD
- iOS全解2:Runloop
- iOS全解3:Runtime
-
iOS全解4:KVC/KVO、通知/推送/信號(hào)量、Delegate/Protocol、Singleton
iOS全解5:網(wǎng)絡(luò)協(xié)議 HTTP、Socket
iOS全解6:CoreAnimation/Layer
iOS全解7:音頻/視頻
iOS全解8:?jiǎn)?dòng)優(yōu)化、性能優(yōu)化、App后臺(tái)?;?、崩潰檢測(cè)(當(dāng)前位置)
iOS全解9:編程思想、架構(gòu)、組件化、RAC
iOS全解15: iOS編譯原理
一、基礎(chǔ)概念
程序編譯一般需經(jīng)幾個(gè)步驟:預(yù)處理、編譯、匯編、鏈接。
編譯:
是將人類可讀的程序代碼文本 --> 翻譯成為 --> 計(jì)算機(jī)可以執(zhí)行的二進(jìn)制指令「機(jī)器碼」。(即:源程序 --> 目標(biāo)程序)
解釋器:
是在運(yùn)行時(shí)才去解析「機(jī)器碼」,獲取一段代碼后就會(huì)將其翻譯成目標(biāo)代碼(就是字節(jié)碼:Bytecode),然后一句一句地執(zhí)行目標(biāo)代碼。解釋器可以在運(yùn)行時(shí)去執(zhí)行代碼,說(shuō)明它具有動(dòng)態(tài)性,程序運(yùn)行后能夠隨時(shí)通過(guò)增加和更新代碼來(lái)改變程序的邏輯。
編譯器 -> 每個(gè)文件進(jìn)行編譯 -> Mach-O(可執(zhí)行文件)
鏈接器 -> 項(xiàng)目中的多個(gè) Mach-O 文件 -> 合并成一
- 預(yù)處理(Preprocessing)處理源代碼中的預(yù)處理指令:Clang
- 宏展開(kāi):處理 #define 定義的宏。
- 頭文件包含:#include指令會(huì)將指定文件的內(nèi)容插入到當(dāng)前文件中。
- 條件編譯:根據(jù) #if、#ifdef 等條件編譯指令選擇代碼。
編譯(Compilation)將預(yù)處理后的源代碼轉(zhuǎn)換為
匯編代碼:Clang
詞法分析 --> 語(yǔ)法分析 --> 語(yǔ)義分析 --> 生成IR
-每個(gè)源碼文件(如 .c/.m 或 .swift文件)會(huì)被編譯成目標(biāo)文件(.o 文件)。
-每個(gè)目標(biāo)文件包含自己的符號(hào)表,記錄全局變量、函數(shù)等符號(hào)的定義和引用。匯編(Assembly)將匯編代碼轉(zhuǎn)換為
機(jī)器代碼:LLVM
對(duì)IR優(yōu)化 --> 目標(biāo)代碼--> 匯編器 --> 機(jī)器碼 --> -Mac-O文件(目標(biāo)文件)鏈接(Linking:Id鏈接)鏈接器
作用:將多個(gè)目標(biāo)文件和庫(kù)文件合并為一個(gè)可執(zhí)行文件(exec文件,解壓ipa包顯示包內(nèi)容,可見(jiàn)下圖)。

可執(zhí)行文件:
(Executable File) 指的是可以由操作系統(tǒng)進(jìn)行加載執(zhí)行的文件。在不同的操作系統(tǒng)環(huán)境下,可執(zhí)行程序的呈現(xiàn)方式不一樣。在windows操作系統(tǒng)下,可執(zhí)行程序可以是 .exe文件 .sys文件 .com等類型文件。
可執(zhí)行程序:
(Executable program,EXE File)是可在操作系統(tǒng)存儲(chǔ)空間中浮動(dòng)定位的二進(jìn)制可執(zhí)行程序。它可以加載到內(nèi)存中,由操作系統(tǒng)加載并執(zhí)行。特定的CPU指令集(如X86指令集)對(duì)應(yīng)的不同平臺(tái)之間的可執(zhí)行程序不可直接移植運(yùn)行。
- GNU:是一個(gè)自由的操作系統(tǒng),其內(nèi)容軟件完全以GPL方式發(fā)布。
- GCC: 由 GNU 開(kāi)發(fā)的編程語(yǔ)言編譯器。
- LLVM:是一個(gè)模塊化和可重用的編譯器和工具鏈技術(shù)的集合。Clang 是 LLVM 的子項(xiàng)目,是 C,C++ 和 Objective-C 編譯器,目的是提供驚人的快速編譯,比 GCC 快3倍。
- Clang:是一個(gè)C語(yǔ)言、C++、Objective-C語(yǔ)言的輕量級(jí)編譯器。
DSYM(Debug Symbol)
? dSYM 本質(zhì):是調(diào)試符號(hào)表文件,它建立了內(nèi)存地址與源代碼之間的映射關(guān)系。
? dSYM 作用: release 模式打包或上線后,崩潰錯(cuò)誤不直觀,這時(shí)就需要分析 crash report 文件,iOS 設(shè)備中會(huì)有日志文件保存我們每個(gè)應(yīng)用出錯(cuò)的函數(shù)內(nèi)存地址,通過(guò) Xcode 的 Organizer 可以將 iOS 設(shè)備中的 DeviceLog 導(dǎo)出成 crash 文件,這個(gè)時(shí)候我們就可以通過(guò)出錯(cuò)的函數(shù)地址去查詢 dSYM 文件中程序?qū)?yīng)的函數(shù)名和文件名。大前提是我們需要有軟件版本對(duì)應(yīng)的 dSYM 文件,所以要保存每個(gè)發(fā)布版本的 Archives 文件。
簡(jiǎn)單來(lái)說(shuō),它能將崩潰日志中那一串串看不懂的16進(jìn)制地址,還原成你熟悉的類名、方法名、文件名和行號(hào)
| 對(duì)比項(xiàng) | 原始崩潰堆棧 | 符號(hào)化后的堆棧 |
|---|---|---|
| 可讀性 | 難以理解,無(wú)法定位 | 清晰指出崩潰的類、方法和代碼行 |
| 內(nèi)容 | crash_ios_demo 0x0000000102a95d45 0x102a94000 + 7493 |
-[ViewController testCrash:] ViewController.m:29 |
幾乎所有崩潰收集服務(wù)(如Bugly、友盟統(tǒng)計(jì)、神策數(shù)據(jù)等)都需要你上傳 dSYM 文件。
核心原則:UUID必須匹配:每一次編譯都會(huì)生成一個(gè)唯一的UUID。.app 文件、.dSYM 文件和崩潰日志三者的UUID必須完全一致,才能成功進(jìn)行符號(hào)化。 即版本號(hào)要對(duì)齊。
獲取命令
xcrun dwarfdump --uuid YourApp.app.dSYM
Xcode 的Organizer ---> iOS設(shè)備中DeviceLog ---> 導(dǎo)出 (crash文件:應(yīng)用出錯(cuò)的函數(shù)內(nèi)存地址)
保存每個(gè)發(fā)布版本的 Archives 文件:軟件版本對(duì)應(yīng)的 dSYM 文件 --> 對(duì)應(yīng)的函數(shù)名和文件名
dsym 解析工具地址 :
1、1.0.3版下載
2、Git: dSYMTools
iOS 崩潰收集原理與自建方案
iOS 符號(hào)化腳本
App包文件:(Payload文件夾 )
- Executable:可執(zhí)行文件(含Mach_O文件)
- Dylib:動(dòng)態(tài)庫(kù) (.lib庫(kù))
- Framework:動(dòng)態(tài)庫(kù)和對(duì)應(yīng)的頭文件和資源文件的集合
- Bundle:無(wú)法被連接的動(dòng)態(tài)庫(kù),只能通過(guò)dlopen()加載
- Xib/StoryBoard文件
- 資源文件:圖片、音/視頻、json文件、plist、證書(shū)的配置文件
- Image:指的是Executable,Dylib或者Bundle的一種,文中會(huì)多次使用Image這個(gè)名詞。
1、iOS可執(zhí)行文件 初探 ipa包:(iPhone Application)
作為iOS客戶端開(kāi)發(fā)者,我們比較熟悉的一種文件是ipa包。但實(shí)際上這只是一個(gè)變相的zip壓縮包,我們可以把一個(gè)ipa文件直接通過(guò)unzip命令解壓。
解壓之后:會(huì)有一個(gè)Payload目錄,而Payload里則是一個(gè).app文件,而這個(gè)實(shí)際上又是一個(gè)目錄,或者說(shuō)是一個(gè)完整的App Bundle。
ipa包(zip壓縮包,用unzip命令解壓 )
--> Payload文件夾
--> .app文件
--> 顯示包內(nèi)容:
App Bundle: bundle、nib、plist、json、html、圖片、視頻、音頻、mac-O[exec] 等文件。
拆分二進(jìn)制文件:經(jīng)常用于整合靜態(tài)庫(kù)
瘦身
$ lipo 002--可執(zhí)行文件 -thin armv7 -output macho_armv7
$ lipo 002--可執(zhí)行文件 -thin armv64 -output macho_armv64
整合
$ lipo -create macho_armv7 macho_arm64 -output machO_v7_64
查看文件的方式:
方式1:終端查看
來(lái)到Mach-O文件所在位置,輸入相關(guān)命令得到Mach-O文件信息。
方式2: 工具查看(更直觀)
首先要下載一個(gè)可以查看Mach-O文件格式的工具 MachOView
常見(jiàn)的格式:
1. 可執(zhí)行文件
2. objcet庫(kù)文件
?.o 文件(目標(biāo)文件:.m -> .o)
?.a 靜態(tài)庫(kù)文件.其實(shí)就是N個(gè).o文件的集合
3. DYLIB: 動(dòng)態(tài)庫(kù)文件
? dylib
? framework
4. 動(dòng)態(tài)鏈接器 dyld
5. DSYM(打包上架用于監(jiān)測(cè)崩潰信息)
靜態(tài)庫(kù) 和 動(dòng)態(tài)庫(kù) 的存在形式和使用區(qū)別?
存在形式:
? 靜態(tài)庫(kù):以".a"或者“.framework”為文件后綴名 xx.a xx.framework
? 動(dòng)態(tài)庫(kù):以".dylib"或者“.framework”為文件后綴名(Xcode7 之后 .tbd 代替了 .dylib)
Mach-O的組成結(jié)構(gòu)包括:(MachOView 匯編分析)
? Mach64 Header (頭部)
? Load commands(加載命令): 加載的指令集,庫(kù)、數(shù)據(jù)表,描述了下面的數(shù)據(jù)
? Section64:全是數(shù)據(jù)
? Section64(__TEXT): 代碼段
? Section64(__DATA): 數(shù)據(jù)段,讀寫(xiě)全局變量等 (Data包含多個(gè)Segment)
? Section64(__LINKEDIT):linkedit
? Segment中包含多個(gè) Section(節(jié))

從這張圖上來(lái)看,Mach-O文件的數(shù)據(jù)主體可分為三大部分分別是:
1、頭部(Header)包含可以執(zhí)行的CPU架構(gòu)指令集,比如x86,arm64。
2、加載命令(Load commands)
3、數(shù)據(jù)(Data)數(shù)據(jù),包含load commands中需要的各個(gè)段(segment)的數(shù)據(jù)
? CS (Code Segment):代碼段寄存器
? DS (Data Segment):數(shù)據(jù)段寄存器
? SS (Stack Segment):堆棧段寄存器
? ES (Extra Segment):附加段寄存器
? SI: 源變址 寄存器(source)
? DI:目的變址 寄存器(destination)
IP:指令指針寄存器 IP = IP+所讀取指令的長(zhǎng)度,從而指向下一條指令
內(nèi)存分區(qū)域 特點(diǎn):
? 代碼區(qū): 可讀可寫(xiě)可執(zhí)行
? 棧區(qū)域: 放參數(shù)和局部變量
? 堆區(qū)域: 動(dòng)態(tài)申請(qǐng) 可讀可寫(xiě)
? 全局: 可讀可寫(xiě)
? 常量區(qū): 只讀!
App啟動(dòng)的過(guò)程:
- 點(diǎn)擊圖標(biāo):?jiǎn)?dòng)應(yīng)用程序
- 加載可執(zhí)行文件(Mach-O)到內(nèi)存中
-
動(dòng)態(tài)鏈接器(dyld):
-遞歸加載依賴的動(dòng)態(tài)庫(kù)
-dyld 會(huì)將 App 中的所有類、類別、協(xié)議等信息加載到內(nèi)存中。
-符號(hào)解析,綁定函數(shù)符號(hào)表 - 調(diào)用庫(kù)的初始化函數(shù)(如 +load 方法)
- main():入口函數(shù)
- UIApplicationMain():主應(yīng)用對(duì)象
- AppDelegate
-willFinishLaunchingWithOptions() :即將完成啟動(dòng)
-didFinishLaunchingWithOptions():已經(jīng)完成啟動(dòng) - Load main UI():加載主UI (xib、storyboard、tabbar等)
- Final intiallization:完成應(yīng)用初始化
- applicationDidBecomeActive():應(yīng)用程序已變?yōu)榛顒?dòng)狀態(tài)
-
執(zhí)行主程序:將控制權(quán)交給應(yīng)用程序的 main 函數(shù),開(kāi)始運(yùn)行。
——————————————————————————————
動(dòng)態(tài)鏈接器 (dyld)
dyld簡(jiǎn)介:
全稱(the dynamic link editor)動(dòng)態(tài)鏈接器,負(fù)責(zé)加載和鏈接應(yīng)用程序所需的共享庫(kù)(動(dòng)態(tài)庫(kù))。鏈接器
可以通過(guò)蘋果官網(wǎng)下載它的源碼(dyld下載地址)。
-
共享緩存:
使用共享緩存(Shared Cache)來(lái)加速系統(tǒng)庫(kù)的加載,減少內(nèi)存占用。 -
安全性:
支持代碼簽名和地址空間布局隨機(jī)化(ASLR),增強(qiáng)系統(tǒng)安全性。
加載動(dòng)態(tài)庫(kù)
dyld會(huì)首先讀取mach-o文件的
Header和load commands。
接著就知道了這個(gè)可執(zhí)行文件「依賴的動(dòng)態(tài)庫(kù)」。例如加載動(dòng)態(tài)庫(kù)A到內(nèi)存,接著檢查A所依賴的動(dòng)態(tài)庫(kù),就這樣的遞歸加載,直到所有的動(dòng)態(tài)庫(kù)加載完畢。通常一個(gè)App所依賴的動(dòng)態(tài)庫(kù)在 100~400 個(gè)左右,其中大多數(shù)都是系統(tǒng)的動(dòng)態(tài)庫(kù),它們會(huì)被緩存到dyld shared cache(動(dòng)態(tài)緩存庫(kù)),這樣讀取的效率會(huì)很高。
1、加載 Mach-O 文件:dyld 動(dòng)態(tài)加載(DyLib動(dòng)態(tài)庫(kù),在動(dòng)態(tài)緩存區(qū),每次啟動(dòng)地址不一樣?。?/p>
2、ASLR技術(shù):MachO文件加載的時(shí)候是隨機(jī)地址,使用動(dòng)態(tài)共享緩存庫(kù)(地址空間布局隨機(jī)化 ASLR是偏移地址)
3、PIC(Position independence code 代碼位置獨(dú)立)
3.1 如果MachO內(nèi)部需要調(diào)用 系統(tǒng)的庫(kù)函數(shù)時(shí)
3.2 先在 Data段中建立一個(gè)指針(符號(hào)),指向外部函數(shù)
3.3 DYLD會(huì)動(dòng)態(tài)的進(jìn)行綁定,將MachO中的Data段中的指針,指向外部函數(shù)!
類的信息怎么被加載至內(nèi)存中的,核心是map_images 與load_images。
-
map_images:管理文件和動(dòng)態(tài)庫(kù)中的符號(hào)-Selector(Class、Category、Protocol等) -
load_images:加載執(zhí)行l(wèi)oad方法

符號(hào)表
里面全是指針和地址
offset:地址的偏移量
Data: 符號(hào)表的下標(biāo)
Description:索引的描述
Value:值

動(dòng)態(tài)庫(kù):共享緩存
為了提高性能,系統(tǒng)的動(dòng)態(tài)庫(kù)文件都存在了動(dòng)態(tài)庫(kù)共享緩存里面!
共享緩存機(jī)制
在iOS系統(tǒng)中,每個(gè)程序依賴的動(dòng)態(tài)庫(kù)都需要通過(guò)dyld一個(gè)一個(gè)加載到內(nèi)存,然而,很多系統(tǒng)庫(kù)幾乎是每個(gè)程序都會(huì)用到的,如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次,勢(shì)必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能,共享緩存機(jī)制就應(yīng)運(yùn)而生。所有默認(rèn)的動(dòng)態(tài)鏈接庫(kù)被合并成一個(gè)大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構(gòu)保存分別保存著,iPhone6里面就有dyld_shared_cache_armv7s和dyld_shared_cache_armv64兩個(gè)文件,如下圖所示。

dyld:位于/usr/lib/dyld
二、啟動(dòng)優(yōu)化/性能優(yōu)化
2.1、 App啟動(dòng)鏈接的過(guò)程:
1、加載 Mach-O 文件 到進(jìn)程。
2、dyld 動(dòng)態(tài)加載 DyLib動(dòng)態(tài)庫(kù),鏈接多個(gè)目標(biāo)文件,創(chuàng)建一個(gè)函數(shù) 符號(hào)表(符號(hào)地址是隨機(jī)的)
3、Rebase:鏈接系統(tǒng)庫(kù)符號(hào),修正內(nèi)部(指向當(dāng)前mach-o文件)的指針指向。
4、Bind:綁定函數(shù)符號(hào),修正外部指針指向。根據(jù)字符串匹配的方式查找符號(hào)表,這個(gè)過(guò)程比Rebase慢。
然后把上述3、4綁定的函數(shù)結(jié)果寫(xiě)入緩存。
5、初始化OC Runtime,加載 +load 函數(shù),包括ObjC相關(guān)Class的注冊(cè)、category注冊(cè)、selector唯一性檢查等
6、其它的初始化代碼,加載靜態(tài)構(gòu)造器:constructor
main階段:加載所有的依賴庫(kù)庫(kù)的lnitializer。
7、首屏渲染

在執(zhí)行main函數(shù)之前,需要把類的信息注冊(cè)到一個(gè)全局的Table中。同時(shí),Objective C支持Category,在初始化的時(shí)候,也會(huì)把Category中的方法注冊(cè)到對(duì)應(yīng)的類中,同時(shí)會(huì)指向唯一Selector,這也是為什么當(dāng)你的Cagegory實(shí)現(xiàn)了類中同名的方法后,類中的方法會(huì)被覆蓋。
寄存器 > 高速緩存 > 內(nèi)存 > 磁盤(存放代碼)
點(diǎn)開(kāi)app (磁盤) --> 代碼讀入(內(nèi)存)里 --> 立刻馬上,將有8M的代碼 放入(高速緩存)--> 執(zhí)行命令(寄存器)
DyLib動(dòng)態(tài)庫(kù):在動(dòng)態(tài)緩存區(qū),每次啟動(dòng)地址不一樣!
所有關(guān)于多線程的安全操作:
防止資源資源搶奪!都是對(duì)內(nèi)存區(qū)域的操作進(jìn)行保護(hù)!
使用dyld2啟動(dòng)應(yīng)用的過(guò)程如圖:

2.2、App的 啟動(dòng)時(shí)間
總的啟動(dòng)時(shí)間 LaunchTime 包括main()調(diào)用之前的pre-main Time1(T1),加上從main()到applicationDidBecomeActive()的時(shí)間Time2(T2)。
LaunchTime = Time1 + Time2
1、main()之前:加載MacO文件,動(dòng)態(tài)鏈接函數(shù)的符號(hào)表
2、main()到 applicationDidBecomeActive()`:入口程序加載(UIApplication對(duì)象、開(kāi)啟Runloop、加載infol.plist、加載Main.stroyBoard/ tableBar、通知應(yīng)用程序代理、和自定義的配置邏輯)
獲取啟動(dòng)時(shí)間
我們可以通過(guò)環(huán)境變量的方法來(lái)獲取pre-main time。打開(kāi)Xcode->Product->Scheme->Edit Scheme或者直接command+shift+<(在鍵盤上是逗號(hào),按住shift就是小于號(hào)了)。在Edit Scheme 中添加DYLD_PRINT_STATISTICS 這個(gè)環(huán)境變量,如果要打印詳細(xì)的時(shí)間分布,可以將value設(shè)為1。(-FIRAnalyticsDebugEnabled)
注意:
DYLD_PRINT_STATISTICS:根據(jù) Apple 官方開(kāi)發(fā)論壇的信息,iOS 15 和 macOS Monterey 引入了新版本的 dyld。在 Xcode 13 和iOS 15+ 環(huán)境下反映該變量不生效,Apple 工程師當(dāng)時(shí)的回復(fù)是建議改用 Instruments 的 App Launch 模板進(jìn)行檢測(cè)
————————————————

運(yùn)行項(xiàng)目之后就會(huì)在控制臺(tái)會(huì)打印出每個(gè)階段都耗時(shí)多少。
打印啟動(dòng)時(shí)間
重定位(rebase)和符號(hào)綁定(binding)
Total
pre-maintime: 207.80 milliseconds (100.0%) //總啟動(dòng)時(shí)間
dylibloading time: 72.69 milliseconds (34.9%) //加載動(dòng)態(tài)庫(kù)時(shí)間
rebase/bindingtime: 8.99 milliseconds (4.3%) //修正內(nèi)部指針、綁定外部符號(hào)
ObjC setuptime: 25.89 milliseconds (12.4%) //ObjC類初始化
initializertime: 100.20 milliseconds (48.2%) //執(zhí)行 +load、靜態(tài)構(gòu)造函數(shù)
slowest intializers :
libSystem.B.dylib: 4.27 milliseconds (2.0%) //鏈接系統(tǒng)庫(kù)B
libMainThreadChecker.dylib : 51.87 milliseconds (24.9%) //debug時(shí)候檢查線程的
ProjectArchitecture : 65.38 milliseconds (31.4%)
main()函數(shù)之前耗時(shí)的影響因素
- 動(dòng)態(tài)庫(kù)加載越多,啟動(dòng)越慢。
- ObjC類越多,啟動(dòng)越慢
- C的constructor函數(shù)越多,啟動(dòng)越慢
- C++靜態(tài)對(duì)象越多,啟動(dòng)越慢
- ObjC的+load越多,啟動(dòng)越慢
Time Profiler 主要是用來(lái)分析 main() 函數(shù)之后 (post-main) 的代碼耗時(shí)。對(duì)于 main() 之前 (pre-main) 的時(shí)間,它只能提供一個(gè)概覽,無(wú)法進(jìn)行詳細(xì)的函數(shù)級(jí)分析。
我們已經(jīng)獲取到了main()函數(shù)之前的啟動(dòng)時(shí)間T1,至于T2,我們可以使用Xcode自帶工具Instruments里面的Time Profiler來(lái)獲取,也可以在main()的第一句和applicationDidBecomeActive()的最后一句加上獲取時(shí)間的代碼CFAbsoluteTimeGetCurrent()。
注意??:DYLD_PRINT_STATISTICS 只適配 iOS 10 - iOS 14,iOS 15 或更高版本,這個(gè)環(huán)境變量會(huì)完全失效,不會(huì)有任何輸出。



點(diǎn)擊左上角紅色按鈕運(yùn)行,勾選左下角Call Tree中Separate Thread和Hide System Libraries,等到第一個(gè)頁(yè)面顯示出來(lái)的之后,點(diǎn)擊左上角暫停按鈕,下面就會(huì)統(tǒng)計(jì)出每個(gè)步驟的耗時(shí)情況。這個(gè)時(shí)候我們就可以很容易得到啟動(dòng)時(shí)間 Time2。
然后就可以 根據(jù) Time1 和 Time2 時(shí)間段的內(nèi)容進(jìn)行優(yōu)化了。
| 項(xiàng)目類型 | Pre-Main 耗時(shí) | Post-Main 耗時(shí) | 優(yōu)化重點(diǎn) |
|---|---|---|---|
| 普通項(xiàng)目 | ~870ms | ~1.7s | 延遲加載、清理無(wú)用類 |
| 大型App | 1.8s(優(yōu)化前) | - | 二進(jìn)制重排、動(dòng)態(tài)庫(kù)治理 |
- 理想目標(biāo):將 Pre-Main 總耗時(shí)控制在 400ms 以內(nèi),Post-Main(首屏渲染)控制在 600ms 以內(nèi),總啟動(dòng)時(shí)間爭(zhēng)取小于 1秒。
- 底線紅線:Pre-Main 耗時(shí)不應(yīng)超過(guò) 2秒,否則很可能觸發(fā)系統(tǒng)看門狗機(jī)制。
-
優(yōu)先級(jí)排序:
Initializer:檢查+load方法,盡量遷移到+initialize或懶加載。
dylib loading:合并自定義動(dòng)態(tài)庫(kù),確保數(shù)量不超過(guò) 6 個(gè)。
rebase/binding & ObjC setup:借助工具掃描并移除未使用的類、方法和靜態(tài)變量。
Post-Main:將非首屏必需的 SDK 初始化、網(wǎng)絡(luò)請(qǐng)求延后執(zhí)行。
2.3、冷啟動(dòng)、熱啟動(dòng)
| 啟動(dòng)方式 | 定義 |
|---|---|
| 冷啟動(dòng) | 手機(jī)啟動(dòng)后或者相當(dāng)長(zhǎng)的時(shí)間間隔后,某個(gè)APP第一次啟動(dòng) |
| 熱啟動(dòng) | APP掛到后臺(tái),之后點(diǎn)擊APP再回來(lái)到前臺(tái),啟動(dòng)所需要的數(shù)據(jù)仍然在緩存中 |
啟動(dòng)時(shí)間在小于400ms是最佳的,因?yàn)閺狞c(diǎn)擊圖標(biāo)到顯示Launch Screen,到Launch Screen消失這段時(shí)間是400ms。啟動(dòng)時(shí)間不可以大于20s,否則會(huì)被系統(tǒng)殺掉。
引用博客解釋下
靜態(tài)構(gòu)造器 (constructor):
constructor 和 +load 都是在 main 函數(shù)執(zhí)行前調(diào)用,但 +load 比 constructor 更加早一丟丟,因?yàn)?code>dyld(動(dòng)態(tài)鏈接器,程序的最初起點(diǎn))在加載map_images、load_images(可以理解成Mach-O 文件)時(shí)會(huì)先通知 objc runtime 去加載其中所有的類,每加載一個(gè)類時(shí),它的 +load 隨之調(diào)用,全部加載完成后,dyld 才會(huì)調(diào)用這個(gè) images 中所有的 constructor 方法。
所以 constructor 是一個(gè)干壞事的絕佳時(shí)機(jī):
1、所有 Class 都已經(jīng)加載完成
2、main 函數(shù)還未執(zhí)行
3、無(wú)需像 +load 還得掛載在一個(gè) Class 中
總體原則無(wú)非就是減少啟動(dòng)的時(shí)候的步驟,以及每一步驟的時(shí)間消耗。
2.4、啟動(dòng)優(yōu)化 方向:
1. 減少動(dòng)態(tài)庫(kù)加載;
2. 優(yōu)化 +load 和 +initialize 方法
3. 減少啟動(dòng)時(shí)主線程渲染的任務(wù)
4. 延遲SDK初始化:支付/保險(xiǎn)SDk 等
5. 合并類,減少文件
6. 二進(jìn)制文件重排 (減少內(nèi)存頁(yè)的加載次數(shù))
1. 減少動(dòng)態(tài)庫(kù)加載;
-減少動(dòng)態(tài)庫(kù)的數(shù)量:盡量使用系統(tǒng)庫(kù),避免引入不必要的第三方動(dòng)態(tài)庫(kù)。
-合并動(dòng)態(tài)庫(kù):將多個(gè)動(dòng)態(tài)庫(kù)合并為一個(gè),減少加載次數(shù)。
-使用靜態(tài)庫(kù):將動(dòng)態(tài)庫(kù)改為靜態(tài)庫(kù)(.a 或 .framework),避免動(dòng)態(tài)鏈接的開(kāi)銷
示例:
使用 CocoaPods 時(shí),可以通過(guò) use_frameworks! :linkage => :static將動(dòng)態(tài)庫(kù)改為靜態(tài)庫(kù)。
2. 優(yōu)化 +load 和 +initialize 方法
-避免在 +load 方法中執(zhí)行耗時(shí)操作,因?yàn)?+load 是在啟動(dòng)時(shí)同步調(diào)用的。
-將初始化邏輯延遲到 +initialize 方法中,或者在使用時(shí)再初始化。
// 不推薦
+ (void)load {
[self setupHeavyConfiguration]; // 耗時(shí)的初始化操作
}
// 推薦
+ (void)initialize {
if (self == [MyClass class]) {
[self setupHeavyConfiguration]; // 延遲初始化
}
}
3. 減少啟動(dòng)時(shí)主線程渲染的任務(wù)
啟動(dòng)時(shí)主線程的任務(wù)過(guò)多會(huì)導(dǎo)致界面卡頓。優(yōu)化方法包括:
-任務(wù)放到后臺(tái)線程執(zhí)行。
-使用異步方式加載資源。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 在后臺(tái)線程執(zhí)行耗時(shí)操作
[self loadHeavyData];
});
4. 延遲SDK初始化
將非必要的初始化操作延遲到啟動(dòng)完成后執(zhí)行。例如:
-延遲加載第三方 SDK(支付、保險(xiǎn)、三方登錄等)。
-延遲加載非首屏所需的資源。
- (void)applicationDidBecomeActive:(UIApplication *)application {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self setupNonCriticalComponents]; // 延遲初始化非關(guān)鍵組件
});
}
5. 合并類,減少文件
不需要用到的類, 減少selector、category的數(shù)量,如合并功能類似的類和Category
使用純代碼,減少xib或者storyboard來(lái)進(jìn)行UI框架的搭建
因?yàn)閤ib和storyboard也還是要解析成代碼來(lái)渲染頁(yè)面,多了一些步驟。
6. 二進(jìn)制文件重排 (減少內(nèi)存頁(yè)的加載次數(shù))
減少二進(jìn)制文件的大小,優(yōu)化加載時(shí)間。
使用 Link Map 分析二進(jìn)制文件,移除未使用的代碼和資源。
Xcode自帶靜態(tài)分析器:點(diǎn)擊菜單欄 Product -> Analyze (或快捷鍵 Command+Shift+B),Xcode就會(huì)對(duì)項(xiàng)目進(jìn)行靜態(tài)分析,并在問(wèn)題導(dǎo)航器中列出:未使用的代碼、邏輯錯(cuò)誤等問(wèn)題。
iOS 如何使用 App Thinning 查看未使用的動(dòng)態(tài)庫(kù)?
iOS查詢不使用的資源/代碼/動(dòng)態(tài)庫(kù)等?
2.4、性能優(yōu)化
離屏渲染
在 OpenGL 中,GPU 有兩種渲染方式:
On-Screen Rendering:當(dāng)前屏幕渲染,在當(dāng)前用于顯示的屏幕緩沖區(qū)進(jìn)行渲染操作;
Off-Screen Rendering:離屏渲染,在當(dāng)前屏幕緩沖區(qū)外開(kāi)辟新的緩沖區(qū)進(jìn)行渲染操作;
離屏渲染消耗性能的原因:
離屏渲染的整個(gè)過(guò)程,需要多次切換上下文環(huán)境,先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen),渲染結(jié)束后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上,上下文環(huán)境從離屏切換到當(dāng)前屏幕,這個(gè)過(guò)程會(huì)造成性能的消耗。
哪些操作會(huì)觸發(fā)離屏渲染?
1、光柵化:layer.shouldRasterize = YES
2、遮罩:layer.mask
3、圓角:同時(shí)設(shè)置 layer.masksToBounds = YES,layer.cornerRadius > 0;(可以用 CoreGraphics 繪制裁剪圓角,可以用曲線)
4、陰影(如果設(shè)置了layer.shadowPath 不會(huì)產(chǎn)生離屏渲染)
- 可以考慮使用CALayer取代UIView;
- 不要頻繁地調(diào)用UIView的相關(guān)屬性,比如
frame、bounds、transform等屬性,盡量減少不必要的修改,提前計(jì)算這些屬性值;Autolayout會(huì)比直接設(shè)置frame消耗更多的CPU資源。
2.5、卡頓檢測(cè)
這里的卡頓檢測(cè)主要是針對(duì)在主線程執(zhí)行了耗時(shí)的操作所造成的,這樣可以通過(guò) RunLoop 來(lái)檢測(cè)卡頓:添加 Observer 到主線程 RunLoop 中,通過(guò)監(jiān)聽(tīng) RunLoop 狀態(tài)的切換的耗時(shí),達(dá)到監(jiān)控卡頓的目的。
一 檢測(cè)的方案根據(jù)線程是否相關(guān)分為兩大類:
1、執(zhí)行「耗時(shí)任務(wù)」會(huì)導(dǎo)致CPU短時(shí)間無(wú)法響應(yīng)其他任務(wù),檢測(cè)任務(wù)耗時(shí)來(lái)判斷是否可能導(dǎo)致卡頓
2、由于卡頓直接表現(xiàn)為操作無(wú)響應(yīng),界面動(dòng)畫(huà)遲緩,「檢測(cè)主線程」是否能響應(yīng)任務(wù)來(lái)判斷是否卡頓
與主線程相關(guān)的檢測(cè)方案包括:
1、fps(每秒傳輸幀數(shù) Frames Per Second)默認(rèn)刷新率都在60Hz(即75幀/秒)以上,要避免動(dòng)作不流暢的最低是30Hz。
2、ping:網(wǎng)絡(luò)測(cè)試工具
3、runloop:線程循環(huán)對(duì)象與主線程不相關(guān)的檢測(cè)包括:
1、stack backtrace:回溯調(diào)用棧
2、msgSend observe:觀察消息轉(zhuǎn)發(fā)
1、FPS 監(jiān)測(cè)
通常情況下,屏幕會(huì)保持60hz/s的刷新速度,每次刷新時(shí)會(huì)發(fā)出一個(gè)屏幕刷新信號(hào),CADisplayLink可以注冊(cè)一個(gè)與刷新信號(hào)同步的回調(diào)處理??梢酝ㄟ^(guò)屏幕刷新機(jī)制來(lái)展示fps值:
FPS 監(jiān)測(cè)CADisplayLink 核心代碼
1.通過(guò)打印,我們知道每次刷新時(shí)會(huì)發(fā)出一個(gè)屏幕刷新信號(hào),則與刷新信號(hào)同步的回調(diào)方法fpsDisplayLinkAction:會(huì)調(diào)用,然后count加一。
2.每隔一秒,我們計(jì)算一次 fps值,用一個(gè)變量_lastTime記錄上一次計(jì)算 fps 值的時(shí)間,然后將 count 的值除以時(shí)間間隔,就得到了 fps 的值,在將 _lastTime 重新賦值,_count 置成零。
3.正常情況下,屏幕會(huì)保持60hz/s的刷新速度,所以1秒內(nèi)fpsDisplayLinkAction:方法會(huì)調(diào)用60次。fps 計(jì)算的值為0,就不卡頓,流暢。
4.如果1秒內(nèi)fpsDisplayLinkAction:只回調(diào)了50次,計(jì)算出來(lái)的fps就是 _count / delta(時(shí)間間隔) 。
2.6、耗電優(yōu)化
耗電的主要來(lái)源為:
1、CPU 處理;
2、網(wǎng)絡(luò)請(qǐng)求;
3、定位;
4、圖像渲染;
優(yōu)化思路
- 盡可能降低 CPU、GPU 功耗;
- 少用定時(shí)器;
- 優(yōu)化 I/O 操作;
- 盡量不要頻繁寫(xiě)入小數(shù)據(jù),最好一次性批量寫(xiě)入;
- 讀寫(xiě)大量重要數(shù)據(jù)時(shí),可以用 dispatch_io,它提供了基于
- GCD 的異步操作文件的 API,使用該 API 會(huì)優(yōu)化磁盤訪問(wèn);
- 數(shù)據(jù)量大時(shí),用數(shù)據(jù)庫(kù)管理數(shù)據(jù);
- 網(wǎng)絡(luò)優(yōu)化;
- 減少、壓縮網(wǎng)絡(luò)數(shù)據(jù)(JSON 比 XML 文件性能更高);
- 若多次網(wǎng)絡(luò)請(qǐng)求結(jié)果相同,
盡量使用緩存; - 使用斷點(diǎn)續(xù)傳,否則網(wǎng)絡(luò)不穩(wěn)定時(shí)可能多次傳輸相同的內(nèi)容;
- 網(wǎng)絡(luò)不可用時(shí),不進(jìn)行網(wǎng)絡(luò)請(qǐng)求;
- 讓用戶可以取消長(zhǎng)時(shí)間運(yùn)行或者速度很慢的網(wǎng)絡(luò)操作,設(shè)置合適的超時(shí)時(shí)間;
- 批量傳輸,如下載視頻,不要傳輸很小的數(shù)據(jù)包,直接下載整個(gè)文件或者大塊下載,然后慢慢展示;
- 定位優(yōu)化;
- 如果只是需要快速確定用戶位置,用
CLLocationManager的requestLocation方法定位,定位完成后,定位硬件會(huì)自動(dòng)斷電; - 若不是導(dǎo)航應(yīng)用,盡量不要實(shí)時(shí)更新位置,并為
完畢就關(guān)掉定位服務(wù); - 盡量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest;
- 需要后臺(tái)定位時(shí),盡量設(shè)置
pausesLocationUpdatesAutomatically 為 YES,若用戶不怎么移動(dòng)的時(shí)候,系統(tǒng)會(huì)自暫停位置更新;
- 如果只是需要快速確定用戶位置,用
2.5、Tableview的優(yōu)化
1、提前計(jì)算好cell的高度
緩存在相應(yīng)的數(shù)據(jù)源模型中
2、 盡可能的降低 storyboard、xib等使用度
通過(guò)Interface知道xib/storyboard本身就是一個(gè)xml 文件,添加刪除控件必然中間多了一個(gè)encode/decode過(guò)程,增加了CPU的計(jì)算量。并且 還要避免臃腫的 XIB 文件,因?yàn)閄IB文件在主線程中進(jìn)行加載布局;當(dāng)用到一些自定義XIB文件時(shí),XIB的加載會(huì)把所有內(nèi)容加載進(jìn)來(lái),如果XIB里面的一些控件并不會(huì)用到,這就可能造成一些資源的消耗浪費(fèi)。
3、滑動(dòng)過(guò)程中盡量減少重新布局
約束最終還是轉(zhuǎn)換成frame,大量的約束重疊也會(huì)增加CPU的計(jì)算量
4、不要阻塞主線程
UIKit的工作基本上都是在主線程上進(jìn)行,界面繪制,用戶輸入響應(yīng)等等.當(dāng)所有的代碼邏輯都放在主線程時(shí),某些耗時(shí)任務(wù)可能會(huì)卡住主線程造成程序無(wú)法響應(yīng),流暢度降低等問(wèn)題;在主線程中繪制大量界面圖層,網(wǎng)絡(luò)I/O,磁盤I/O等都可以造成界面卡頓現(xiàn)象.
下面我們通過(guò)Xcode自帶的調(diào)試工具Instruments來(lái)看看項(xiàng)目界面的流暢度,及其一些建議,Instruments給我提供了各種各樣的調(diào)試查看工具,下面簡(jiǎn)單介紹一下:
1)Blank: 創(chuàng)建一個(gè)空的模板,可以從Library庫(kù)中添加其他模板.
2)Activity Monitor: 監(jiān)控進(jìn)程級(jí)別的CPU,內(nèi)存,磁盤,網(wǎng)絡(luò)使用情況,可以得到你的應(yīng)用程序在手機(jī)運(yùn)行時(shí)總共占用的內(nèi)存大小.
3)Allocations: 跟蹤過(guò)程的匿名虛擬內(nèi)存和堆的對(duì)象提供類名和可選保留/釋放歷史,可以檢測(cè)每一個(gè)堆對(duì)象的分配內(nèi)存情況.
4)Cocoa Layout : 觀察NSLayoutConstraint對(duì)象的改變,幫助我們判斷什么時(shí)間什么地點(diǎn)的constraint是否合理.觀察約束變化,找出布局代碼的問(wèn)題所在.
5)Core Animation: 這個(gè)模塊顯示程序顯卡性能以及CPU使用情況,查看界面流暢度.
6)CoreData: 這個(gè)模塊跟蹤C(jī)ore Data文件系統(tǒng)活動(dòng).
7)Counters : 收集使用時(shí)間或基于事件的抽樣方法的性能監(jiān)控計(jì)數(shù)器(PMC)事件.
8)Energy Log: 耗電量監(jiān)控.
9)File Activity: 檢測(cè)文件創(chuàng)建,移動(dòng),變化,刪除等.
10)Leak: 一般的措施內(nèi)存使用情況,檢查泄漏的內(nèi)存,并提供了所有活動(dòng)的分配和泄漏模塊的類對(duì)象分配統(tǒng)計(jì)信息以及內(nèi)存地址歷史記錄.
11)Metal System Trace: Metal API是apple 2014年在ios平臺(tái)上推出的高效底層的3D圖形API,它通過(guò)減少驅(qū)動(dòng)層的API調(diào)用CPU的消耗提高渲染效率.
12)Network: 用鏈接工具分析你的程序如何使用TCP/IP和UDP/IP鏈接.
13)SceneKit: 3D性能狀況分析.
14)System Trace: 系統(tǒng)跟蹤,通過(guò)顯示當(dāng)前被調(diào)度線程提供綜合的系統(tǒng)表現(xiàn),顯示從用戶到系統(tǒng)的轉(zhuǎn)換代碼通過(guò)兩個(gè)系統(tǒng)調(diào)用或內(nèi)存操作.
15)System Usage: 這個(gè)模板記錄關(guān)于文件讀寫(xiě),sockets,I/O系統(tǒng)活動(dòng),輸入輸出.
16)Time Profiler(時(shí)間探查): 執(zhí)行對(duì)系統(tǒng)的CPU上運(yùn)行的進(jìn)程低負(fù)載時(shí)間為基礎(chǔ)采樣.
17)Zombies: 測(cè)量一般的內(nèi)存使用,專注于檢測(cè)過(guò)度釋放的野指針對(duì)象,也提供對(duì)象分配統(tǒng)計(jì),以及主動(dòng)分配的內(nèi)存地址歷史.
本文主要使用的是Instruments中的第5個(gè)工具:Core Animation(圖形性能),這個(gè)模塊顯示程序顯卡性能以及CPU使用情況,查看界面流暢度.
首先我們必須要把源碼安裝到測(cè)試設(shè)備上,1)連接X(jué)code運(yùn)行程序;2)然后選擇快捷鍵(Command + Control + i)調(diào)出Instruments,選擇Core Animation.打開(kāi)后我們可以看到Debug Options里面有多個(gè)調(diào)試選項(xiàng)。
三、App后臺(tái)?;?/h2>
1、短時(shí)間保活的方式有beginBackgroundTaskWithName 和 endBackgroundTask
2、App長(zhǎng)時(shí)間?;畹姆绞接校翰シ艧o(wú)聲音樂(lè)、后臺(tái)持續(xù)定位、后臺(tái)下載資源、BGTaskScheduler等;
3、喚醒App的方式有:推送、VoIP等;
數(shù)據(jù)重用/緩存、盡量使用layer繪圖 代替View
四、崩潰檢測(cè)
void uncaughtExceptionHandler(NSException *exception)
{
NSArray *stackArray = [exception callStackSymbols]; //異常的堆棧信息
NSString *reason = [exception reason]; //出現(xiàn)異常的原因
NSString *name = [exception name]; //異常名稱
NSString *exceptionInfo = [NSString stringWithFormat:@"================異常崩潰報(bào)告================\n name:\n%@\n reason:\n%@\n callStackSymbols:\n%@ \n \n ",name,reason,[stackArray componentsJoinedByString:@"\n"]];
NSLog(@"---> exceptionInfo: %@",exceptionInfo);
// 保存到本地:當(dāng)app再次打開(kāi)后,獲取本地保存的崩潰信息并上傳到服務(wù)器即可。
[CatchCrash saveAsText:exceptionInfo];
// 發(fā)送郵件
[CatchCrash sendEmail:exceptionInfo];
}
引用:
iOS全解15: iOS編譯原理
類的加載