1. 啟動流程
1.1 準備知識
Mach-O
Executable | 可執行文件 |
Dylib | 動態庫 |
Bundle | 無法被連接的動態庫,只能通過dlopen() 加載 |
Image | 指的是Executable,Dylib或者Bundle的一種 |
Framework | 動態庫和對應的頭文件和資源文件的集合 |
Apple的操作系統的可執行文件格式幾乎都是mach-o,mach-o可以大致的分為三部分:
Header | 頭部包含可以執行的CPU架構,比如x86,arm64 |
Load commands | 加載命令,包含文件的組織架構和在虛擬內存中的布局方式 |
Data | 數據,包含load commands中需要的各個段(segment)的數據,每一個Segment都得大小是Page的整數倍。 |
絕大多數mach-o包括以下三個段(支持用戶自定義Segment,但是很少使用)
__TEXT 代碼段 | 只讀,包括函數,和只讀的字符串,上圖中類似__TEXT,__text的都是代碼段 |
__DATA 數據段 | 讀寫,包括可讀寫的全局變量等,上圖類似中的__DATA,__data都是數據段 |
__LINKEDIT | 包含了方法和變量的元數據(位置,偏移量),以及代碼簽名等信息。 |
dyld
dyld的全稱是dynamic loader,它的作用是加載一個進程所需要的image,它是開源的。
-
Virtual Memory
虛擬內存是在物理內存上建立的一個邏輯地址空間,它向上(應用)提供了一個連續的邏輯地址空間,向下隱藏了物理內存的細節。
虛擬內存使得邏輯地址可以沒有實際的物理地址,也可以讓多個邏輯地址對應到一個物理地址。虛擬內存被劃分為一個個大小相同的Page(64位系統上是16KB),提高管理和讀寫的效率。 Page又分為只讀和讀寫的Page。
虛擬內存是建立在物理內存和進程之間的中間層。在iOS上,當內存不足的時候,會嘗試釋放那些只讀的Page,因為只讀的Page在下次被訪問的時候,可以再從磁盤讀取。如果沒有可用內存,會通知在后臺的App(也就是在這個時候收到了memory warning),如果在這之后仍然沒有可用內存,則會殺死在后臺的App。
Page fault
在應用執行的時候,它被分配的邏輯地址空間都是可以訪問的,當應用訪問一個邏輯Page,而在對應的物理內存中并不存在的時候,這時候就發生了一次Page fault。當Page fault發生的時候,會中斷當前的程序,在物理內存中尋找一個可用的Page,然后從磁盤中讀取數據到物理內存,接著繼續執行當前程序。Dirty Page & Clean Page
如果一個Page可以從磁盤上重新生成,那么這個Page稱為Clean Page
如果一個Page包含了進程相關信息,那么這個Page稱為Dirty Page
像代碼段這種只讀的Page就是Clean Page。而像數據段(_DATA)這種讀寫的Page,當寫數據發生的時候,會觸發COW(Copy on write),也就是寫時復制,Page會被標記成Dirty,同時會被復制。
1.2 dyld2啟動流程
dyld2啟動流程 |
---|
加載dyld到App進程 |
加載動態庫(包括所依賴的所有動態庫) |
Rebase |
Bind |
初始化Objective-C Runtime |
其它的初始化代碼 |
加載動態庫
dyld會首先讀取mach-o文件的Header和load commands。
接著就知道了這個可執行文件依賴的動態庫。例如加載動態庫A到內存,接著檢查A所依賴的動態庫,就這樣的遞歸加載,直到所有的動態庫加載完畢。通常一個App所依賴的動態庫在100-400個左右,其中大多數都是系統的動態庫,它們會被緩存到dyld shared cache,這樣讀取的效率會很高。
查看mach-o文件所依賴的動態庫,可以通過MachOView的圖形化界面(展開Load Command就能看到),也可以通過命令行otool。
Rebase && Bind
有兩種主要的技術來保證應用的安全:ASLR和Code Sign。
ASLR的全稱是Address space layout randomization,翻譯過來就是“地址空間布局隨機化”。App被啟動的時候,程序會被影射到邏輯的地址空間,這個邏輯的地址空間有一個起始地址,而ASLR技術使得這個起始地址是隨機的。如果是固定的,那么黑客很容易就可以由起始地址+偏移量找到函數的地址。
Code Sign相信大多數開發者都知曉,這里要提一點的是,在進行Code sign的時候,加密哈希不是針對于整個文件,而是針對于每一個Page的。這就保證了在dyld進行加載的時候,可以對每一個page進行獨立的驗證。
mach-o中有很多符號,有指向當前mach-o的,也有指向其他dylib的,比如printf。那么,在運行時,代碼如何準確的找到printf的地址呢?
mach-o中采用了PIC技術,全稱是Position Independ code。當你的程序要調用printf的時候,會先在__DATA段中建立一個指針指向printf,在通過這個指針實現間接調用。dyld這時候需要做一些fix-up工作,即幫助應用程序找到這些符號的實際地址。主要包括兩部分
Rebase 修正內部(指向當前mach-o文件)的指針指向
Bind 修正外部指針指向
之所以需要Rebase,是因為剛剛提到的ASLR使得地址隨機化,導致起始地址不固定,另外由于Code Sign,導致不能直接修改Image。Rebase的時候只需要增加對應的偏移量即可。待Rebase的數據都存放在__LINKEDIT中。
Rebase解決了內部的符號引用問題,而外部的符號引用則是由Bind解決。在解決Bind的時候,是根據字符串匹配的方式查找符號表,所以這個過程相對于Rebase來說是略慢的。
Objective-C
Objective C是動態語言,所以在執行main函數之前,需要把類的信息注冊到一個全局的Table中。同時,Objective C支持Category,在初始化的時候,也會把Category中的方法注冊到對應的類中,同時會唯一Selector,這也是為什么當你的Cagegory實現了類中同名的方法后,類中的方法會被覆蓋。
另外,由于iOS開發是基于Cocoa Touch的,所以絕大多數的類起始都是系統類,所以大多數的Runtime初始化起始在Rebase和Bind中已經完成。
Initializers
接下來就是必要的初始化部分了,主要包括幾部分:
- load(Swift已棄用,只能使用
initialize
) - C/C++靜態初始化對象和標記為
__attribute__(constructor)
的方法
1.3 dyld3啟動流程
上文的講解是dyld2的加載方式。而最新的是dyld3加載方式略有不同:
加載方式 | process | 注釋 |
---|---|---|
dyld2 | in-process | 只有當應用程序被啟動的時候,dyld2才能開始執行任務。 |
dyld3 | 部分out-of-process和in-process。 | out-of-process在App下載安裝和版本更新的時候會去執行。 |
out-of-process會做如下事情:
- 分析Mach-o Headers
- 分析依賴的動態庫
- 查找需要Rebase & Bind之類的符號
- 把上述結果寫入緩存
這樣,在應用啟動的時候,就可以直接從緩存中讀取數據,加快加載速度。
1.4 main
之后
相對于開發者來說,main
才是程序入口。下面是加載流程:
-
main
函數
-
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain
函數有四個參數,最后一個參數是AppDelegate的類名,通常使用模板創建的AppDelegate是AppDelegate
,如果我們想要改變它的名字,我們同樣需要在這里傳入對應的類名。
UIApplicationMain(int argc, char *argv[], NSString *principalClassName, NSString *delegateClassName);
參數名 | 作用 |
---|---|
argc 和argv
|
ISO C標準的main函數的參數,直接傳遞給UIApplicationMain進行相關處理。參數包含應用程序何時從系統啟動等信息。這些參數是由UIKit的基礎設施解析,否則可以忽略不計。該參數一般不會修改。 |
principalClassName |
這個參數標識了應用程序的類的名稱(該類必須繼承自UIApplication類)。這是負責運行應用程序的類。建議為這個參數傳nil。如果principalClassName是nil,那么它的值將從Info.plist去獲取,如果Info.plist沒有,則默認為UIApplication。principalClass這個類除了管理整個程序的生命周期之外什么都不做,它只負責監聽事件然后交給delegateClass去做。該參數一般使用nil。 |
delegateClassName |
delegateClass是應用程序類的代理類。應用程序的代理負責管理系統和你的代碼之間的高層次的互動。 |
- 程序完成加載
我們一般會在這里進行一些初始化配置,例如創建window
。
- 程序完成加載
- [AppDelegate application:didFinishLaunchingWithOptions:]
- 創建window窗口
我們所有的畫面最終都會顯示在該窗口上,makeKeyAndVisible
是window顯示的關鍵。
- 創建window窗口
_window = UIWindow.new;
_window.backgroundColor = [UIColor whiteColor];
[_window makeKeyAndVisible];
- 程序被激活
最后該方法會被調用,宣布程序處于激活狀態。
- 程序被激活
- [AppDelegate applicationDidBecomeActive:]
2. AppDelegate
AppDelegate類有以下常用的函數,這里是我們與系統進行交互的場所,一般在這里創建視圖以及監聽部分設備狀態。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSLog(@"%s",__func__);
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
NSLog(@"%s",__func__);
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
NSLog(@"%s",__func__);
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
NSLog(@"%s",__func__);
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
NSLog(@"%s",__func__);
}
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application {
NSLog(@"%s",__func__);
}
- (void)applicationWillTerminate:(UIApplication *)application {
NSLog(@"%s",__func__);
}
APP狀態更改后會收到一些通知。
// 啟動APP會調用
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[AppDelegate applicationDidBecomeActive:]
// 點擊Home鍵會調用
-[AppDelegate applicationWillResignActive:]
-[AppDelegate applicationDidEnterBackground:]
// APP從后臺返回前臺
-[AppDelegate applicationWillEnterForeground:]
-[AppDelegate applicationDidBecomeActive:]
// 收到內存警告
-[AppDelegate applicationDidReceiveMemoryWarning:]
函數 | 分析 | 注意事項 | ||
---|---|---|---|---|
application:willFinishLaunchingWithOptions: application:didFinishLaunchingWithOptions:
|
分別是程序首次將要和已經完成啟動時執行,一般在這個函數里創建window對象,將程序內容通過window呈現給用戶。 ①檢查啟動選項字典中的內容,查看程序啟動的方式,并做出適當的反應。 ②初始化應用程序的關鍵數據結構。 ③準備好你的應用程序的窗口和視圖進行顯示。 |
1. 使用OpenGL ES的應用程序不應該使用這個方法來準備他們的繪圖環境。相反,他們應該推遲到application:DidBecomeActive: 方法調用時啟動OpenGL ES繪圖方法。2. 您的應用程序方法應該總是盡可能為輕量,以減少你的應用程序的啟動時間。應用預期將啟動并初始化自身,并開始處理不到5秒的事件。如果一個應用程序沒有及時完成它的啟動周期,系統會殺死它。因此,有可能你的啟動慢下來(如接入網絡)的任何任務,應在異步輔助線程執行。 3. 當程序啟動到前臺,該系統還會調用 applicationDidBecomeActive: 方法來完成過渡到前臺。因為這種方法既在啟動時與從后臺過渡到前臺時被調用,使用它來執行所共有的兩個轉變的任何任務。 |
||
applicationWillResignActive |
程序將要失去Active狀態時調用,比如有電話進來或者按下Home鍵,之后程序進入后臺狀態,對應的applicationWillEnterForeground(即將進入前臺)方法。 | 該函數里面主要執行操作: a . 暫停正在執行的任務 b. 禁止計時器 c. 減少OpenGL ES幀率 d. 若為游戲應暫停游戲 |
applicationDidEnterBackground |
該方法用來: a. 釋放共享資源 b. 保存用戶數據(寫到硬盤) c. 作廢計時器 d. 保存足夠的程序狀態以便下次修復; |
applicationWillEnterForeground |
這個方法用來: 撤銷applicationWillResignActive 中做的改變。 |
|||
applicationDidBecomeActive |
若程序之前在后臺,在此方法內刷新用戶界面 | |||
applicationWillTerminate |
程序即將退出時調用。記得保存數據,如applicationDidEnterBackground方法一樣。 |
3. 啟動優化
如果你剛剛啟動過App,這時候App的啟動所需要的數據仍然在緩存中,再次啟動的時候稱為熱啟動。如果設備剛剛重啟,然后啟動App,這時候稱為冷啟動。
啟動時間在小于400ms是最佳的,因為從點擊圖標到顯示Launch Screen,到Launch Screen消失這段時間是400ms。啟動時間不可以大于20s,否則會被系統殺掉。
以main函數作為分水嶺,啟動時間其實包括了兩部分:main函數之前和main函數到第一個界面的viewDidAppear:。不過一般情況下都是耗時都產生在自己的代碼,優先考慮優化main之后的過程。
優化這些初始化的核心思想就是:能延遲初始化的盡量延遲初始化,不能延遲初始化的盡量放到后臺初始化。