上一篇筆記記錄了 session 的理論部分,這里接著記錄實踐部分。
重述
簡要地捋一下應用啟動的過程:首先是 dyld 解析得到所需的共享庫,然后將庫映射到應用的地址空間里面,通過 rebasing 和 binding 修正函數和數據指針的地址,接著注冊 ObjC 的類、調用所有的 initializers,最后才是調用 main()
函數。
測量
應用的啟動的方式有兩種——冷啟動和熱啟動:前者是啟動應用時沒有任何數據被內核緩存,比如說重啟設備后再打開應用就是冷啟動;后者在啟動時已經有數據被緩存,盡管你能退出這個應用,但是內核還是不一定會清除掉緩存。
要想測量調用 main()
函數之前的所耗費的時間,用我先前寫的 TICK TOCK
這兩個宏是不可能的。但是在 Xcode 中,可以添加一個環境變量 DYLD_PRINT_STATISTICS
:
DYLD_PRINT_STATISTICS.png
那么在啟動的時候就能看動態庫加載、rebase/binding 等等各部分的時間(Xcode 8,iOS 10 環境下):
pre_main_comsuming_hot.png
pre_main_comsuming_cold.png
提升
Dylib Loading
- 因為系統中的 dylibs 的數據都是經過 pre-calculate 的,在加載的時候會特別的快(不太清楚這一點,找不到相關資料)。而使用內嵌到 App Bundle 的 dylib 的開銷很高,而且 App Store 似乎不允許這么做;
- 合并一些動態庫以減少數量,異或考慮使用 .a 靜態庫。而實際中前者幾乎是不可行的,合并一些靜態庫還是可以的;
- 使用
dlopen()
加載 dylib,不過這也只是把一些工作延后而已,而且還可能會有其他的問題。
Rebas/Binding
- 減少 __DATA segment 中的數據指針,類似下面這種:
data_pointer.png
- 減少 ObjC 的元數據(class, categories, selector);
- 減少 C++ 虛函數;
- Swift 的結構體使用更少需要被修正的指針數據,所以盡量使用 Swift;(現在還不是很了解 Swift ??)。
ObjC Setup
- 啟動時 Objc runtime 要注冊 class 和 cacategory,開發的時候注意下別弄冗余的類就行了;
- 接下來更新 Non-fragile ivars 的偏移量,響應地就減少一點實例變量;
- 關于 Selector Uniquing, [objc explain]: Selector uniquing in the dyld shared cache 里面有說到這是 Objective-C 啟動開銷比較大的一塊,方法少一點就快一點。
- 感覺對于 ObjC Setup 的這一塊沒多少可優化的。
Initializers
Initializers 分為顯式的和隱式的,這些都會在啟動的時候被調用。我覺得這一塊才是實際開發中值得注意的一部分:
對于顯式的 initializers:
- 使用
+initialize
而不是+load
。前者在類接收到第一個發送給它的消息之前,后者這是在類被注冊的時候。 - 減少使用
__attribute__((constructor))
修飾的函數,這些函數會在 ObjC Setup 階段完之后調用。如果只是想執行一次這個函數,用dispatch_once()
,pthread_once()
,std::once()
這些 site initializers 代替它會比較好,一是將工作放到了啟動之后,二是系統對dispatch_once()
有優化。另外要寫 C++ 跨平臺的代碼就用std::once()
吧。
對于隱式的 initializers:
- C++ 中那些 static non-trivial constructors 可以用上面說的 site initializers 代替,或者用 Swift 重寫;
- 不要在 initializers 中調用
dlopen()
,也不要創建線程;
總結
在調用 main()
函數之前,我覺得能做的、對性能提升比較大的是在 initializers 這一塊。類、實例變量等等的數目對啟動的影響不大。而在進入 -application:didFinishLaunchingWithOptions:
之后,將一些不是必須在主線程進行的 IO 或計算任務放到低優先級的子線程中執行比較好,這樣可以盡快的返回。
話說,site initializers 是什么意思啊,Google 了半天沒找著……??