本文源自本人的學習記錄整理與理解,其中參考閱讀了部分優秀的博客和書籍,盡量以通俗簡單的語句轉述。引用到的地方如有遺漏或未能一一列舉原文出處還望見諒與指出,另文章內容如有不妥之處還望指教,萬分感謝 !
寫在前面:
Mach-O文件簡介
-
Mach object
的縮寫,是Mac
、iOS
上用于存儲程序、庫的標準格式 ;Mach-O文件
是一種叫法,就像以.text
結尾的文件,被叫做為text文件
常見的Mach-O文件有:
MH_OBJECT
:目標文件(.o
)、靜態庫文件(.a
) 靜態庫其實就是N個.o合并在一起MH_EXECUTE
:可執行文件.app/xx
MH_DYLIB
:動態庫文件.dylib
或.framework/xx
MH_DYLINKER
:動態鏈接編輯器/usr/lib/dyld
MH_DSYM
:存儲著二進制文件符號信息的文件
.dSYM/Contents/Resources/DWARF/xx(常用于分析APP的崩潰信息)
dyld簡介
在iOS系統中,幾乎所有的程序都會用到動態庫,而動態庫在加載的時候都需要用dyld(位于/usr/lib/dyld)程序進行鏈接。很多系統庫幾乎都是每個程序都要用到的,與其在每個程序運行的時候一個一個將這些動態庫都加載進來,還不如先把它們打包好,一次加載進來來的快。
dynamic link editor
的縮寫, Apple的動態鏈接器
,macOS和iOS通用; 動態庫不能直接運行 ,需要通過系統的動態鏈接加載器進行加載到內存后執行;主要用來裝載Mach-O文件; 比如:動態庫 和 可執行文件。動態鏈接器在系統中以一個
用戶態
的可執行文件形式存在, 一般應用程序會在Mach-O文件部分指定一個LC_LOAD_DYLINKER
的加載命令,此加載命令指定了dyld的路徑,通常它的默認值是“/usr/lib/dyld”共享緩存:從iOS 3.1開始,為了提高系統的性能,所有的系統庫文件都被打包保存到了一個很大的緩存文件當中;所有默認的動態鏈接庫被合并成一個大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下,按不同的架構分開存放
dyld加載時,為了優化程序啟動,啟用了共享緩存(
shared cache
)技術。共享緩存
會在進程啟動時被dyld映射到內存中
每當任何Mach-O鏡像加載時,dyld首先會檢查該Mach-O鏡像與所需的動態庫是否在共享緩存中,如果存在,則直接將它在共享內存中的內存地址映射到進程的內存地址空間。在程序依賴的系統動態庫很多的情況下,這種做法對程序啟動性能是有明顯提升的。
可執行文件:
- 平時編寫的代碼最終會被編譯成為一個Mach-O格式的文件
- 開發過程中所用到的動態庫(比如:UIKit、Foundation) 依賴信息也會存儲在可執行文件中
動態庫:
- 程序運行時由系統動態加載到內存,而不是復制,供程序調用。
- 系統只加載一次,多個程序共用,節省內存。因此,編譯內容更小,而且因為動態庫是需要時才被引用,所以更快。
簡單認識:系統的UIKit框架最終被dyld以動態庫的形式加載到內存 !
系統使用動態鏈接有幾點好處:
代碼共用:很多程序都動態鏈接了這些 lib
,但它們在內存和磁盤中中只有一份。 易于維護:由于被依賴的 lib
是程序執行時才鏈接的,所以這些 lib 很容易做更新,比如libSystem.dylib
是 libSystem.B.dylib
的替身,哪天想升級直接換成libSystem.C.dylib
然后再替換替身就行了。 減少可執行文件體積:相比靜態鏈接,動態鏈接在編譯時不需要打進去,所以可執行文件的體積要小很多。
如上圖所示,不同進程之間共用系統dylib的_TEXT
區,但是各自維護對應的_DATA
區。
所有動態鏈接庫和我們App中的靜態庫.a
和所有類文件編譯后的.o
文件最終都是由dyld,Apple的動態鏈接器來加載到內存中。每個image
都是由一個叫做ImageLoader的類
來負責加載(一一對應)
1. 應用啟動
APP啟動時間長短,直接會影響用戶對APP的第一體驗;如果啟動時間過長,不但會影響體驗導致用戶直接奧利給,同時可能會觸發蘋果的 watch dog
機制 kill
掉APP;這就很尷尬了,掉粉啊這 ! APP啟動卡死接著直接崩潰了。。。。。。
- Xcode在
debug模式
下默認不開啟watch dog
,所以有條件還是走一走真機測試
APP的啟動可以分為2種
- 冷啟動:
Cold Launch
, 從零開始啟動APP - 熱啟動:
Warm Launch
, APP已經在內存中,在后臺存活著,再次點擊圖標啟動APP
在衡量APP的啟動時間之前先了解下,APP的啟動流程:
Mach-O文件加載
mach-o文件有如下幾個部分組成:
Header
:保存了一些基本信息,包括了該文件運行的平臺、文件類型、LoadCommands
的個數等等。LoadCommands
:可以理解為加載命令,在加載Mach-O
文件時會使用這里的數據來確定內存的分布以及相關的加載命令。比如我們的main函數的加載地址,程序所需的dyld的文件路徑,以及相關依賴庫的文件路徑。Data
: 這里包含了具體的代碼、數據等等。
APP的啟動從用戶態
來說可以分為三個階段,即 dyld加載依賴庫
、runtime初始化
、main函數
總結如下:
Launch time = dyld
+ runtime
+ main()
當然也有部分同學將其分為兩個階段:main()之前
、main()之后
;這都沒有問題,只是概括粒度
的差別而已;也可以直接將其分為:內核態
、用戶態
APP加載過程:
- 系統會開啟一個進程,然后讀取可執行文件(
Mach-O文件
),從里面獲得dyld
的路徑 - 加載dyld,dyld先初始化運行環境,開啟緩存策略,加載(遞歸)程序相關依賴庫(其中也包含我們的可執行文件)到內存中,并生成相應的鏡像
C++靜態對象初始化構造器:initializer
對依賴庫進行鏈接 -->
link()
,調用每個依賴庫的初始化方法Initalizer,在這一步,runtime被初始化
---->
(libSystem.dylib庫
的libdispatch_init
里調用了runtime的初始化方法_objc_init
);
runtime初始化后不會閑著,在_objc_init
中注冊
了幾個通知
,從dyld這里接手了幾個活,其中包括:
初始化相應依賴庫里的類結構
調用依賴庫里所有的load方法
。當所有依賴庫的初始化后,輪到
最后一位(程序可執行文件)
進行初始化,在這時runtime會對項目中所有類進行類結構初始化
,然后調用所有的load方法
。
- 調用map_images(鏡像)進行可執行文件內容的解析和處理
- 在load_images中調用call_load_methods,調用所有Class(包括分類)的+load方法
- 進行各種objc結構的初始化(注冊Objc類、初始化類對象)
- dyld返回
main函數地址
,這時進程進入就緒狀態;main函數被調用后進程進入執行狀態,至此便來到了熟悉的程序入口。
這些事情大多數在dyld:_main
方法中被發生;
main():調用UIKit庫中的UIApplicationMain()
找到應用的委托方法執行開發者自定義的任務,比如:獲取主控制器顯示到UIWindow
Xcode提供的主函數調用UIKit的
UIApplicationMain函數
。UIApplicationMain函數創建
UIApplication對象
和你的AppDelegate
。UIKit從
主故事板
或nib文件
加載應用程序的默認入口。UIKit調用
AppDelegate
的:willFinishLaunchingWithOptions:
方法。UIKit執行狀態恢復,它調用你的
AppDelegate
和UIWindow
的附加方法。UIKit調用AppDelegate的:
didFinishLaunchingWithOptions:
方法。初始化完成后,系統使用場景委托或應用程序委托來顯示UI并管理應用程序的生命周期。
總結:
APP的啟動由dyld主導
,將可執行文件加載到內存,順便加載所有依賴的動態庫
并由runtime負責加載完成OC類結構初始化
所有初始化工作結束
后,dyld就會調用main函數
接下來就是UIApplicationMain函數
,AppDelegate
的application:didFinishLaunchingWithOptions:
方法
補充:
dyld是蘋果操作系統一個重要組成部分,而且令人興奮的是,它是開源的,任何人可以通過蘋果官網下載它的源碼來閱讀理解它的運作方式(下載地址:Source Browser),了解系統加載動態庫的細節。
dyld詳細流程:
XNU加載程序可執行文件后,通過分析文件來獲得dyld所在路徑來加載dyld,同時也把當前主程序的Mach-O頭部信息給了dyld;有了頭部信息,加載器就可以從頭開始,遍歷整個Mach-O文件的信息,獲取LoadCommands
(加載命令)、Data
(數據、代碼);有了這些就正式開始搭建初始化程序環境
! ! ! !
- 加載
dyld
-->__dyld_start()
-->dyldbootstrap::start()
-->_main()函數
; dyld的加載動態庫的代碼就是從_main()
開始
共九個步驟如下:
第一步: 設置運行環境,處理環境變量
第二步:初始化主程序
核心API -- instantiateFromLoadedImage()
- 將實例化好的主程序添加到全局主列表sAllImages中,最后調用addMappedRange()申請內存,更新主程序映像映射的內存區。做完這些工作,第二步初始化主程序就算完成了。
第三步:加載共享緩存
- 主要執行
mapSharedCache()
來完成映射共享緩存 - 進行動態庫的版本化重載,這主要通過函數
checkVersionedPaths()
完成
第四步:加載插入的動態庫
- 循環遍歷
DYLD_INSERT_LIBRARIES
環境變量中指定的動態庫列表,并調用loadInsertedDylib()
將其加載
第五步:鏈接主程序
- 執行
link()
完成主程序的鏈接操作,該函數調用了ImageLoader
自身的link()
函數;主要目的是將實例化的主程序的動態數據進行修正
,達到讓進程
可用的目的,其中典型的就是主程序中的符號表修正操作
rebase和bind
rebase修復的是指向當前鏡像內部的資源指針。
bind修復的是指向鏡像外部的資源指針。
由于ASLR
(address space layout randomization)的存在,可執行文件和動態鏈接庫在虛擬內存中的加載地址每次啟動都不固定,所以需要這兩步來修復鏡像中的資源指針,來指向正確的地址。
rebase步驟先進行,需要把鏡像讀入內存,并以page為單位進行加密驗證,保證不會被篡改,所以這一步的瓶頸在IO。bind在其后進行,由于要查詢符號表,來指向跨鏡像的資源,加上在rebase階段,鏡像已被讀入和加密驗證,所以這一步的瓶頸在于CPU計算
備注:recursiveBind()
完成遞歸綁定符號表的操作。此處的符號表針對的是非延遲加載的符號表,它的核心是調用了doBind()
,在ImageLoaderMachOCompressed
中,該函數讀取映像動態鏈接信息的bind_off
與bind_size
來確定需要綁定的數據偏移與大小,然后挨個對它們進行綁定,綁定操作具體使用bindAt()
函數,它主要通過調用resolve()
解析完符號表后,調用bindLocation()
完成最終的綁定操作,需要綁定的符號信息有三種:
BIND_TYPE_POINTER
:需要綁定的是一個指針。直接將計算好的新值嶼值即可。
BIND_TYPE_TEXT_ABSOLUTE32
:一個32位的值。取計算的值的低32位賦值過去。
BIND_TYPE_TEXT_PCREL32
:重定位符號。需要使用新值減掉需要修正的地址值來計算出重定位值。
第六步:鏈接插入的動態庫
- 鏈接插入的動態庫與鏈接主程序一樣,都是使用的
link()
,插入的動態庫列表是前面調用addImage()
保存到sAllImages
中的,之后,循環獲取每一個動態庫的ImageLoader
,調用link()
對其進行鏈接,注意:sAllImages
中保存的第一項是主程序的映像。
第七步:執行弱符號綁定
- 調用
weakBind()
函數執行弱符號綁定。
首先通過調用context
的getCoalescedImages()
將sAllImages
中所有含有弱符號的映像合并成一個列表,合并完后調用initializeCoalIterator()
對映像進行排序,排序完成后調用incrementCoalIterator()
收集需要進行綁定的弱符號
incrementCoalIterator ()
是一個虛函數,在ImageLoaderMachOCompressed
中,該函數讀取映像動態鏈接信息的weak_bind_off
與weak_bind_size
來確定弱符號的數據偏移與大小,然后挨個計算它們的地址信息。之后調用getAddressCoalIterator()
,按照映像的加載順序在導出表中查找符號的地址,找到后調用updateUsesCoalIterator()
執行最終的綁定操作,執行綁定的是bindLocation()
第八步:執行初始化方法
- 執行初始化的方法入口是
initializeMainExecutable()
,主要實現了doImageInit()
與doModInitFunctions()
執行映像
與模塊
中設置為init的函數
與靜態初始化方法
第九步:查找程序入口函數并返回
- 這一步調用主程序映像的
getThreadPC()
函數來查找主程序的LC_MAIN
加載命令獲取程序的入口點,沒找到就調用getMain()
到LC_UNIXTHREAD
加載命令中去找,找到后就跳轉到入口點指定的地址返回 !
到這里,dyld整個加載動態庫的過程就算完成了。
2. 啟動時間優化主要針對冷啟動
查看main()函數執行之前
的耗時:
Xcode提供通過添加環境變量
來打印APP啟動時間分析
-
Edit scheme
-->Run
-->Arguments
-->Environment Variables
添加DYLD_PRINT_STATISTICS
設置為 1 - 如果需要更加詳細的時間信息,添加
DYLD_PRINT_STATISTICS_DETAILS
設置為 1 - 以上環境變量本質上插入到 dyld全局的環境變量sEnv中,
輸出結果示例
Total pre-main time: 36.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 : 3.20 milliseconds (6.4%)
libBacktraceRecording.dylib : 3.90 milliseconds (8.4%)
libMainThreadChecker.dylib : 6.55 milliseconds (19.1%)
------------------------- 分析 ------------------------
pre: previous 在…以前
在執行main函數之前所用的時間:36.22毫秒
動態庫加載:14.43 毫秒
rebase綁定:1.82毫秒
ObjC結構準備:3.89毫秒
初始化:13.99毫秒
比較慢的加載:
libSystem.B.dylib : 3.20 毫秒
libBacktraceRecording.dylib : 3.90 毫秒
libMainThreadChecker.dylib : 6.55 毫秒
優化方案:
dyld優化:
- 減少動態庫、合并一些動態庫(定期清理不必要的動態庫)
- 減少
Objc類
、分類
的數量、減少Selector
數量(定期清理不必要的類、分類) - 減少
C++虛函數
數量,一旦虛函數會需要多維護一張虛表
-Swift
盡量使用struct
- 使用AppCode工具檢查未被使用的文件
虛函數
C++中的虛函數的作用主要是實現了多態的機制。關于多態,簡而言之就是用父類指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。這種技術可以讓父類的指針有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法。比如:模板技術,RTTI技術,虛函數技術,要么是試圖做到在編譯時決議,要么試圖做到運行時決議。
- 和oc的多態不同,雖然也可以認為父類擁有多種形態,但OC只支持單繼承;父類指針可以指向子類示例,但是不能調用沒有在父類中聲明的方法;只能調用子類重寫的方法
runtime
- 用
+initialize
方法和dispatch_once
取代所有的__attribute__((constructor))
、C++靜態構造器
、Objc的load
方
main
- 在不影響用戶體驗的前提下,盡可能將一些操作延遲,不要全部都放在
finishLaunching
方法中 - 按需加載,用到的時候再加載
3. 電池能耗
手機電池電量是極其有限的,沒有電的手機就像一塊沒有實際作用的模型;一個APP如果對電池消耗影響很大就可能會被用戶奧利給 !
一般開發中對電池消耗比較大的幾個方面:
CPU處理,
Processing
; 高頻的處理會加快電量的消耗網絡,
Networking
, 長連接發送接收數據,手機需要持續的保持信號接收和發送對手機電量的消耗會比較大;比如:微信、QQ等APP定位,
Location
; 持續定位不斷的獲取GPS信息,和刷新對電量消耗也會很快 比如:高德導航、百度地圖等軟件使用起來電量就消耗很快圖像,
Graphics
; GPU的圖像渲染是會占用大量的資源,同時也很耗電
可以通過對一下方面做出相應的優化措施:
- 盡可能降低
CPU、GPU
的功耗,即CPU、GPU的優化 - 少用定時器, 定時器會持續循環的做事情,這樣的操作會對電量消耗較快
- 優化
I/O
(文件讀寫)操作
盡量不要頻繁寫入小數據,可以把小數據整理在空閑時一次性寫入;在不影響結果的情況下
讀寫大量重要數據時,考慮用
dispatch_io
,其提供了基于GCD的異步操作文件I/O
的API, 用dispatch_io
系統會優化對磁盤訪問;數據量比較多的情況,不建議直接存儲在文件里;可以考慮是用數據庫,比如
SQLite
、CoreData
; 因為數據庫對數據讀寫都是有優化過的,有相應的算法,會比直接讀寫更有優勢的多-
網絡優化
- 減少、壓縮網絡數據,比如網絡傳輸早期用
XM
L:體積比較大 ,后來使用JSON
: 體積就比較小;現在也有人在用protocl buffer
這種格式傳輸,但前提是服務器也使用相同的格式接收 - 上傳圖片,文件等數據先進行
壓縮
,或者分片上傳
; - 如果多次請求的結果相同,盡量使用緩存
-
斷點續傳
,100MB的文件,下載了一半,突然關機了;下次下載的時候可以從50MB開始繼續下載,不需要從頭開始 - 如果網絡狀態變為不可用或者是未知網絡時,不要嘗試頻繁執行網絡請求;
- 讓用戶可以取消長時間運行或者網速很慢的網絡操作;并設置合適的超時時間
-
批量傳輸
,比如 下載視頻流時,不要傳輸很小的數據包,直接下載整個文件或者一大塊一大塊地下載。 如果下載廣告,一次性多下載一些,然后再慢慢展示。如果下載電子郵件,一次下載多封,不要一封一封下載
- 減少、壓縮網絡數據,比如網絡傳輸早期用
-
定位優化
- 如果只需要快速確定用戶位置,最好用
CoreLocation
框架中CLLocationManager
的requestLocation
方法獲取位置信息;
此方法優勢
:定位完成后,會自動讓定位硬件斷電 - 如果不是導航應用,盡量不要實時獲取定位;定位完畢就關掉定位服務
- 盡量降低定位精度,比如盡量不要使用精度較高的
kCLLocationAccuracyBest
- 需要用到后臺定位時,盡量設置
pausesLocationUpdatesAutomatically
為YES
:用來停止當用戶靜止時
位置的自動更新;
- 如果只需要快速確定用戶位置,最好用
-
硬件檢測優化
- 用戶移動、搖晃、傾斜設備是,會產生動作(
motion
)事件; 這些事件由加速度計、陀螺儀、磁力計等硬件檢測;在不需要檢測的場景,應該及時關閉這些的使用
- 用戶移動、搖晃、傾斜設備是,會產生動作(
今日頭條iOS客戶端啟動速度優化
iOS 底層 - 性能優化之CPU、GPU
iOS 底層 - 性能優化之安裝包瘦身(App Thinning)