iOS App從點擊到啟動

iOS 系統(tǒng)架構

Mac系統(tǒng)是基于Unix內(nèi)核的圖形化操作系統(tǒng),Mac OS 和 iOS 系統(tǒng)架構的對比分析發(fā)現(xiàn),Mac OS和iOS的系統(tǒng)架構層次只有最上面一層不同,Mac是Cocoa框架,而iOS是Cocoa Touch框架,其余的架構層次都是一樣的。

Core OS是用FreeBSD和Mach所改寫的一個名叫Darwin的開放原始碼操作系統(tǒng), 是開源、符合POSIX標準的一個Unix核心。這一層包含并提供了整個iPhone OS的一些基礎功能,比如:硬件驅動, 內(nèi)存管理,程序管理,線程管理(POSIX),文件系統(tǒng),網(wǎng)絡(BSD Socket),以及標準輸入輸出等,所有這些功能都會通過C語言的API來提供。

核心OS層的驅動提供了硬件和系統(tǒng)框架之間的接口。然而,由于安全的考慮,只有有限的系統(tǒng)框架類能訪問內(nèi)核和驅動。iPhone OS提供了許多訪問操作系統(tǒng)低層功能的接口集,iPhone 應用通過LibSystem庫來訪問這些功能,這些接口集有線程(POSIX線程)、網(wǎng)絡(BSD sockets)、文件系統(tǒng)訪問、標準I/O、Bonjour和DNS服務、現(xiàn)場信息(Locale Information)、內(nèi)存分配和數(shù)學計算等。

Core Services在Core OS基礎上提供了更為豐富的功能, 它包含了Foundation.Framework和Core Foundation.Framework, 之所以叫Foundation,就是因為它提供了一系列處理字符串,排列,組合,日歷,時間等等的基本功能。

Foundation是屬于Objective-C的API,Core Fundation是屬于C的API。另外Core servieces還提供了如Security(用來處理認證,密碼管理,安全性管理等), Core Location, SQLite和Address Book等功能。

核心基礎框架(CoreFoundation.framework)是基于C語言的接口集,提供iPhone應用的基本數(shù)據(jù)管理和服務功能。該框架支持Collection數(shù)據(jù)類型(Arrays、 Sets等)、Bundles、字符串管理、日期和時間管理、原始數(shù)據(jù)塊管理、首選項管理、URL和Stream操作、線程和運行循環(huán)(Run Loops)、端口和Socket通信。

核心基礎框架與基礎框架是緊密相關的,它們?yōu)橄嗤幕竟δ芴峁┝薕bjective-C接口。如果開發(fā)者混合使用Foundation Objects 和Core Foundation類型,就能充分利用存在兩個框架中的"toll-free bridging"技術(橋接)。toll-free bridging使開發(fā)者能使用這兩個框架中的任何一個的核心基礎和基礎類型。

程序啟動之前

從應用圖標被用戶點擊開始,直到應用可以開始響應發(fā)生了很多事情。

啟動

很多文章中大家都提到說dyld加載了主程序和動態(tài)庫,這個理解明顯是錯誤的,在XNU加載Mach-O和dyld過程中,是內(nèi)核加載了主程序,dyld只會負責動態(tài)庫的加載,雖然主程序也會作為鏡像形式被dyld來管理起來。當一個App啟動時,dyld把App需要的dylib加載進App的內(nèi)存空間。App運行所需要的信息,一般都存放在其MachO頭部44中,其中dylib的信息是由load commands指定的,App得到執(zhí)行時,dyld會查看其MachO頭部中的load commands,并把里面LC_LOAD_DYLIB相關的dylib給加載到進程的內(nèi)存空間。

一般來說,逆向工程會在dyld階段入手,之前版本的dyld中確實存在一些漏洞,使App能夠繞過代碼簽名,例如dyld-353.2.1版本,漏洞編號CVE-2015-5876,漏洞存在于Mach-O頭的處理過程中,一個畸形的Mach-O文件可以導致內(nèi)存段被替換,從而導致任意代碼執(zhí)行,當然目前dyld的已知漏洞都已修復。監(jiān)控啟動崩潰會在Objc階段之后,具體方法具體分析。

總結來說,大體分為如下步驟:

(1) 系統(tǒng)為程序啟動做好準備

(2) 系統(tǒng)將控制權交給 Dyld,Dyld 會負責后續(xù)的工作

(3) Dyld 加載程序所需的動態(tài)庫

(3) Dyld 對程序進行 rebase 以及 bind 操作

(4) Objc SetUp

(5) 運行初始化函數(shù)

(6) 執(zhí)行程序的 main 函數(shù)

需要注意的是,dyld2和dyld3的加載方式略有不同。dyld2是純粹的in-process,也就是在程序進程內(nèi)執(zhí)行的,也就意味著只有當應用程序被啟動的時候,dyld2才能開始執(zhí)行任務。dyld3則是部分out-of-process,部分in-process。

dyld2的過程是:加載dyld到App進程,加載動態(tài)庫(包括所依賴的所有動態(tài)庫),Rebase,Bind,初始化Objective C Runtime和其它的初始化代碼。

dyld3的out-of-process會做如下事情:分析Mach-o Headers,分析依賴的動態(tài)庫,查找需要Rebase & Bind之類的符號,把上述結果寫入緩存。這樣,在應用啟動的時候,就可以直接從緩存中讀取數(shù)據(jù),加快加載速度。

從exec()開始

Mach-O是 OS X 系統(tǒng)的可執(zhí)行文件,Mach-O有多種文件類型,比如MH_DYLIB文件、MH_BUNDLE文件、MH_EXECUTE文件,MH_OBJECT(內(nèi)核加載)等。可執(zhí)行文件離不開進程,在 Linux 中,我們會通過 Fork()來新創(chuàng)建子進程,然后執(zhí)行鏡像通過exec()來替換為另一個可執(zhí)行程序。在用戶態(tài)會通過exec*系列函數(shù)來加載一個可執(zhí)行文件。

加載

main()函數(shù)是整個程序的入口,在程序啟動之前,系統(tǒng)會調用exec()函數(shù)。在Unix中exec和system的不同在于,system是用shell來調用程序,相當于fork+exec+waitpid,fork 函數(shù)創(chuàng)建子進程后通常都會調用 exec 函數(shù)來執(zhí)行一個新程序;而exec是直接讓你的程序代替原來的程序運行。

system 是在單獨的進程中執(zhí)行命令,完了還會回到你的程序中。而exec函數(shù)是直接在你的進程中執(zhí)行新的程序,新的程序會把你的程序覆蓋,除非調用出錯,否則你再也回不到exec后面的代碼,也就是當前的程序變成了exec調用的那個程序了。

UNIX 提供了 6 種不同的 exec 函數(shù)供我們使用。

#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */);
int execvp(cosnt char *filename, char *const argv[]);

通過分析我們發(fā)現(xiàn),含有 l 和 v 的 exec 函數(shù)的參數(shù)表傳遞方式是不同的。含有 e 結尾的 exec 函數(shù)會傳遞一個環(huán)境變量列表。含有 p 結尾的 exec 函數(shù)取的是新程序的文件名作為參數(shù),而其他exec 函數(shù)取的是新程序的路徑。

如果函數(shù)出錯則返回-1,若成功則沒有返回值。其中只有execve是真正意義上的系統(tǒng)調用,其它都是在此基礎上經(jīng)過包裝的庫函數(shù)。

exec函數(shù)族的作用是根據(jù)指定的文件名找到可執(zhí)行文件,并用它來取代調用進程的內(nèi)容,換句話說,就是在調用進程內(nèi)部執(zhí)行一個可執(zhí)行文件。這里的可執(zhí)行文件既可以是二進制文件,也可以是任何Unix下可執(zhí)行的腳本文件。

Dyld

Dyld 是 iOS 系統(tǒng)的動態(tài)鏈接器, 在dyldStartup.s 文件中有個名為 __dyld_start 的方法,它會去調用 dyldbootstrap::start() 方法,然后進一步調用 dyld::_main() 方法,里面包含 App 的整個啟動流程,該函數(shù)最終返回應用程序 main 函數(shù)的地址,最后 Dyld 會去調用它。

之后會去加載可執(zhí)行文件,二進制文件常被稱為 image,包括可執(zhí)行文件、動態(tài)庫等,ImageLoader 的作用就是將二進制文件加載進內(nèi)存。dyld::_main() 方法在設置好運行環(huán)境后,會調用instantiateFromLoadedImage 函數(shù)將可執(zhí)行文件加載進內(nèi)存中,加載過程分為三步:

合法性檢查。主要是檢查可執(zhí)行文件是否合法,是否能在當前的 CPU 架構下運行。
選擇 ImageLoader 加載可執(zhí)行文件。系統(tǒng)會去判斷可執(zhí)行文件的類型,選擇相應的 ImageLoader 將其加載進內(nèi)存空間中。
注冊 image 信息。可執(zhí)行文件加載完成后,系統(tǒng)會調用 addImage 函數(shù)將其管理起來,并更新內(nèi)存分布信息。

以上三步完成后,Dyld 會調用 link 函數(shù)開始之后的處理流程。

靜態(tài)鏈接庫與動態(tài)鏈接庫

iOS中的相關文件有如下幾種:Dylib,動態(tài)鏈接庫(又稱 DSO 或 DLL);Bundle,不能被鏈接的 Dylib,只能在運行時使用 dlopen() 加載,可當做 macOS 的插件。Framework,包含 Dylib 以及資源文件和頭文件的文件夾。

動態(tài)鏈接庫是一組源代碼的模塊,每個模塊包含一些可供應用程序或者其他動態(tài)鏈接庫調用的函數(shù),在應用程序調用一個動態(tài)鏈接庫里面的函數(shù)的時候,操作系統(tǒng)會將動態(tài)鏈接庫的文件映像映射到進程的地址空間中,這樣進程中所有的線程就可以調用動態(tài)鏈接庫中的函數(shù)了。動態(tài)鏈接庫加載完成后,這個時候動態(tài)鏈接庫對于進程中的線程來說只是一些被放在地址進程空間附加的代碼和數(shù)據(jù),操作系統(tǒng)為了節(jié)省內(nèi)存空間,同一個動態(tài)鏈接庫在內(nèi)存中只有一個,操作系統(tǒng)也只會加載一次到內(nèi)存中。

因為代碼段在內(nèi)存中的權限都是為只讀的,所以當多個應用程序加載同一個動態(tài)鏈接庫的時候,不用擔心應用程序會修改動態(tài)鏈接庫的代碼段。當線程調用動態(tài)鏈接庫的一個函數(shù),函數(shù)會在線程棧中取得傳遞給他的參數(shù),并使用線程棧來存放他需要的變量,動態(tài)鏈接庫函數(shù)創(chuàng)建的任何對象都為調用線程或者調用進程擁有,動態(tài)鏈接庫不會擁有任何對象。如果動態(tài)鏈接庫中的一個函數(shù)調用了VirtualAlloc,系統(tǒng)會從調用進程的地址空間預定地址,即使撤銷了對動態(tài)鏈接庫的映射,調用進程的預定地址依然會存在,直到用戶取消預定或者進程結束。

靜態(tài)鏈接庫與動態(tài)鏈接庫都是共享代碼的方式,如果采用靜態(tài)鏈接庫,則無論你愿不愿意,lib 中的指令都全部被直接包含在最終生成的包文件中了。但是若使用動態(tài)鏈接庫,該動態(tài)鏈接庫不必被包含在最終包里,包文件執(zhí)行時可以“動態(tài)”地引用和卸載這個與安裝包獨立的動態(tài)鏈接庫文件。靜態(tài)鏈接庫和動態(tài)鏈接庫的另外一個區(qū)別在于靜態(tài)鏈接庫中不能再包含其他的動態(tài)鏈接庫或者靜態(tài)庫,而在動態(tài)鏈接庫中還可以再包含其他的動態(tài)或靜態(tài)鏈接庫。

Linux中靜態(tài)函數(shù)庫的名字一般是libxxx.a,利用靜態(tài)函數(shù)庫編譯成的文件比較大,因為整個函數(shù)庫的所有數(shù)據(jù)都會被整合進目標代碼中。編譯后的執(zhí)行程序不需要外部的函數(shù)庫支持,因為所有使用的函數(shù)都已經(jīng)被編譯進去了。當然這也會成為他的缺點,因為如果靜態(tài)函數(shù)庫改變了,那么你的程序必須重新編譯。

動態(tài)函數(shù)庫的名字一般是libxxx.so,相對于靜態(tài)函數(shù)庫,動態(tài)函數(shù)庫在編譯的時候并沒有被編譯進目標代碼中,你的程序執(zhí)行到相關函數(shù)時才調用該函數(shù)庫里的相應函數(shù),因此動態(tài)函數(shù)庫所產(chǎn)生的可執(zhí)行文件比較小。由于函數(shù)庫沒有被整合進你的程序,而是程序運行時動態(tài)的申請并調用,所以程序的運行環(huán)境中必須提供相應的庫。動態(tài)函數(shù)庫的改變并不影響你的程序,所以動態(tài)函數(shù)庫的升級比較方便。

iOS開發(fā)中靜態(tài)庫和動態(tài)庫是相對編譯期和運行期的。靜態(tài)庫在程序編譯時會被鏈接到目標代碼中,程序運行時將不再需要載入靜態(tài)庫。而動態(tài)庫在程序編譯時并不會被鏈接到目標代碼中,只是在程序運行時才被載入,因為在程序運行期間還需要動態(tài)庫的存在。

iOS中靜態(tài)庫可以用.a或.Framework文件表示,動態(tài)庫的形式有.dylib和.framework。系統(tǒng)的.framework是動態(tài)庫,一般自己建立的.framework是靜態(tài)庫。

.a是一個純二進制文件,.framework中除了有二進制文件之外還有資源文件。.a文件不能直接使用,至少要有.h文件配合。.framework文件可以直接使用,.a + .h + sourceFile = .framework。

動態(tài)庫的一個重要特性就是即插即用性,我們可以選擇在需要的時候再加載動態(tài)庫。如果不希望在軟件一啟動就加載動態(tài)庫,需要將

Targets-->Build Phases-->Link Binary With Libraries

中 *.framework 對應的Status由默認的 Required 改成 Optional ;或者將 xx.framework 從 Link Binary With Libraries 列表中刪除。

可以使用dlopen加載動態(tài)庫,動態(tài)庫中真正的可執(zhí)行代碼為 xx.framework/xx 文件。

- (IBAction)useDlopenLoad:(id)sender
{
NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/xx.framework/xx",NSHomeDirectory()];
[self dlopenLoadlib:documentsPath];
}

- (void)dlopenLoadlib:(NSString *)path
{
libHandle = NULL;
libHandle = dlopen([path cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);
if (libHandle == NULL) {
    char *error = dlerror();
    NSLog(@"dlopen error: %s", error);
} else {
    NSLog(@"dlopen load framework success.");
}
}

也可以使用NSBundle來加載動態(tài)庫,實現(xiàn)代碼如下:

- (IBAction)useBundleLoad:(id)sender
{
NSString *documentsPath = [NSString stringWithFormat:@"%@/Documents/xx.framework",NSHomeDirectory()];
[self bundleLoadlib:documentsPath];
}

- (void)bundleLoadlib:(NSString *)path
{
_libPath = path;
NSError *err = nil;
NSBundle *bundle = [NSBundle bundleWithPath:path];
if ([bundle loadAndReturnError:&err]) {
    NSLog(@"bundle load framework success.");
} else {
    NSLog(@"bundle load framework err:%@",err);
}
}

可以為動態(tài)庫的加載和移除添加監(jiān)聽回調,ImageLogger上有一個完整的示例代碼,從中可以發(fā)現(xiàn),一個工程軟件啟動的時候會加載多達一百二十多個動態(tài)庫,即使是一個空白的項目。

但是,需要注意的一點是,不要在初始化方法中調用 dlopen(),對性能有影響。因為 dyld 在 App 開始前運行,由于此時是單線程運行所以系統(tǒng)會取消加鎖,但 dlopen() 開啟了多線程,系統(tǒng)不得不加鎖,這就嚴重影響了性能,還可能會造成死鎖以及產(chǎn)生未知的后果。所以也不要在初始化器中創(chuàng)建線程。

據(jù)說,iOS現(xiàn)在可以使用自定義的動態(tài)庫,低版本的需要手動的使用dlopen()加載。動態(tài)庫上架會有一些審核的規(guī)則,如不要把x86/i386的包和arm架構的包lipo在一起使用。如:

lipo –create Release-iphoneos/libiphone.a Debig-iphonesimulator/libiphone.a –output libiphone.a

如此便將模擬器和設備的靜態(tài)庫文件合并成一個文件輸出了。

dylib加載調用

基于上面的分析,在exec()時,系統(tǒng)內(nèi)核把應用映射到新的地址空間,每次起始位置都是隨機的。然后使用dyld 加載 dylib 文件(動態(tài)鏈接庫),dyld 在應用進程中運行的工作就是加載應用依賴的所有動態(tài)鏈接庫,準備好運行所需的一切,它擁有和應用一樣的權限。

加載 Dylib時,先從主執(zhí)行文件的 header 中獲取需要加載的所依賴動態(tài)庫的列表,從中找到每個 dylib,然后打開文件讀取文件起始位置,確保它是 Mach-O 文件(針對不同運行時可執(zhí)行文件的文件類型)。然后找到代碼簽名并將其注冊到內(nèi)核。

應用所依賴的 dylib 文件可能會再依賴其他 dylib,因此動態(tài)庫列表是一個遞歸依賴的集合。一般應用會加載 100 到 400 個 dylib 文件,但大部分都是系統(tǒng) dylib,它們會被預先計算和緩存起來,加載速度很快。但加載內(nèi)嵌(embedded)的 dylib 文件很占時間,所以盡可能把多個內(nèi)嵌 dylib 合并成一個來加載,或者使用 static archive。

在加載所有的動態(tài)鏈接庫之后,它們只是處在相互獨立的狀態(tài),代碼簽名使得我們不能修改指令,那樣就不能讓一個 dylib 調用另一個 dylib。通過fix-up可以將它們結合起來,dyld 所做的事情就是修正(fix-up)指針和數(shù)據(jù)。Fix-up 有兩種類型,rebasing(在鏡像內(nèi)部調整指針的指向) 和 binding(將指針指向鏡像外部的內(nèi)容)。

因為地址空間加載隨機化(ASLR,Address Space Layout Randomization)的緣故,二進制文件最終的加載地址與預期地址之間會存在偏移,所以需要進行 rebase 操作,對那些指向文件內(nèi)部符號的指針進行修正。rebase 完成之后,就會進行 bind 操作,修正那些指向其他二進制文件所包含的符號的指針。因為 dylib 之間有依賴關系,所以 動態(tài)庫中的好多操作都是沿著依賴鏈遞歸操作的,Rebasing 和 Binding 分別對應著 recursiveRebase() 和 recursiveBind() 這兩個方法。因為是遞歸,所以會自底向上地分別調用 doRebase() 和 doBind() 方法,這樣被依賴的 dylib 總是先于依賴它的 dylib 執(zhí)行 Rebasing 和 Binding。

Rebaing 消耗了大量時間在 I/O 上,在 Rebasing 和 Binding 前會判斷是否已經(jīng) 預綁定。如果已經(jīng)進行過預綁定(Prebinding),那就不需要 Rebasing 和 Binding 這些 Fix-up 流程了,因為已經(jīng)在預先綁定的地址加載好了。

Binding 處理那些指向 dylib 外部的指針,它們實際上被符號(symbol)名稱綁定,是一個字符串。dyld 需要找到 symbol 對應的實現(xiàn),在符號表里查找時需要很多計算,找到后會將內(nèi)容存儲起來。Binding 看起來計算量比 Rebasing 更大,但其實需要的 I/O 操作很少,因為之前 Rebasing 已經(jīng)替 Binding 做過了。Objective-C 中有很多數(shù)據(jù)結構都是靠 Rebasing 和 Binding 來修正(fix-up)的,比如 Class 中指向超類的指針和指向方法的指針。

Objc

C++ 會為靜態(tài)創(chuàng)建的對象生成初始化器,與靜態(tài)語言不同,OC基于Runtime機制可以用類的名字來實例化一個類的對象。Runtime 維護了一張映射類名與類的全局表,當加載一個 dylib 時,其定義的所有的類都需要被注冊到這個全局表中。ObjC 在加載時可以通過 fix-up 在動態(tài)類中改變實例變量的偏移量,利用這個技術可以在不改變dylib的情況下添加另一個 dylib 中類的方法,而非常見的通過定義類別(Category)的方式改變一個類的方法。

Dyld 在 bind 操作結束之后,會發(fā)出 dyld_image_state_bound 通知,然后與之綁定的回調函數(shù) map_2_images 就會被調用,它主要做以下幾件事來完成 Objc Setup:

讀取二進制文件的 DATA 段內(nèi)容,找到與 objc 相關的信息
注冊 Objc 類
確保 selector 的唯一性
讀取 protocol 以及 category 的信息

除了 map_2_images,我們注意到 _objc_init 還注冊了 load_images 函數(shù),它的作用就是調用 Objc 的 + load 方法,它監(jiān)聽 dyld_image_state_dependents_initialized 通知。

dyld 是運行在用戶態(tài)的, 這里由內(nèi)核態(tài)切到了用戶態(tài)。每當有新的鏡像加載之后,都會執(zhí)行 load-images 方法進行回調,這里的回調是在整個ObjC runtime 初始化時 -objc-init 注冊的。有新的鏡像被 map 到 runtime 時,調用 load-images 方法,并傳入最新鏡像的信息列表 infoList。調用 prepare-load-methods 對 load 方法的調用進行準備(將需要調用 load 方法的類添加到一個列表中),調用 -getObjc2NonlazyClassList 獲取所有的類的列表之后,會通過 remapClass 獲取類對應的指針,然后調用 schedule-class-load 遞歸地 將當前類和沒有調用 + load 父類進入列表。在執(zhí)行 add-class-to-loadable-list(cls) 將當前類加入加載列表之前,會先把父類加入待加載的列表,保證父類在子類前調用 load 方法。在執(zhí)行 add-class-to-loadable-list(cls) 將當前類加入加載列表之前,會先把父類加入待加載的列表,保證父類在子類前調用 load 方法。在將鏡像加載到運行時、對 load 方法的準備就緒,執(zhí)行 call-load-methods,開始調用 load 方法。

load 函數(shù)調用

由于iOS開發(fā)時基于Cocoa Touch的,所以絕大多數(shù)的類起始都是系統(tǒng)類,大多數(shù)的Runtime初始化起始在Rebase和Bind中已經(jīng)完成。

Initializers

Objc SetUp 結束后,Dyld 便開始運行程序的初始化函數(shù),該任務由 initializeMainExecutable 函數(shù)執(zhí)行。整個初始化過程是一個遞歸的過程,順序是先將依賴的動態(tài)庫初始化,然后在對自己初始化。初始化需要做的事情包括:

調用 Objc 類的 + load 函數(shù)
調用 C++ 中帶有 constructor 標記的函數(shù)
非基本類型的 C++ 靜態(tài)全局變量的創(chuàng)建

所謂執(zhí)行監(jiān)控啟動crash的思路都是在這里構建的。下面是一些方法的執(zhí)行順序,initialize的順序可能在更早,但總是會在load和launch之間。


方法流

程序啟動邏輯

主執(zhí)行文件和相關的 dylib的依賴關系構成了一張巨大的有向圖,執(zhí)行初始化器先加載葉子節(jié)點,然后逐步向上加載中間節(jié)點,直至最后加載根節(jié)點。這種加載順序確保了安全性,加載某個 dylib 前,其所依賴的其余 dylib 文件肯定已經(jīng)被預先加載。最后 dyld 會調用 main() 函數(shù)。main() 會調用 UIApplicationMain(),程序啟動。

使用Xcode打開一個項目,很容易會發(fā)現(xiàn)一個文件--main.m文件,此處就是應用的入口了。程序啟動時,先執(zhí)行main函數(shù),main函數(shù)是ios程序的入口點,內(nèi)部會調用UIApplicationMain函數(shù),UIApplicationMain里會創(chuàng)建一個UIApplication對象 ,然后創(chuàng)建UIApplication的delegate對象 —–(您的)AppDelegate ,開啟一個消息循環(huán)(main runloop),每當監(jiān)聽到對應的系統(tǒng)事件時,就會通知AppDelegate。

int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

UIApplication對象是應用程序的象征,每一個應用都有自己的UIApplication對象,而且是單例的。通過[UIApplication sharedApplication]可以獲得這個單例對象,一個iOS程序啟動后創(chuàng)建的第一個對象就是UIApplication對象,利用UIApplication對象,能進行一些應用級別的操作。

UIApplicationMain函數(shù)實現(xiàn)如下:

int UIApplicationMain{

  int argc,

  char *argv[],

  NSString *principalClassName,

  NSString *delegateClassName

}

第一個參數(shù)表示參數(shù)的個數(shù),第二個參數(shù)表示裝載函數(shù)的數(shù)組,第三個參數(shù),是UIApplication類名或其子類名,若是nil,則默認使用UIApplication類名。第四個參數(shù)是協(xié)議UIApplicationDelegate的實例化對象名,這個對象就是UIApplication對象監(jiān)聽到系統(tǒng)變化的時候通知其執(zhí)行的相應方法。

啟動完畢會調用 didFinishLaunching方法,并在這個方法中創(chuàng)建UIWindow,設置AppDelegate的window屬性,并設置UIWindow的根控制器。如果有storyboard,會根據(jù)info.plist中找到應用程序的入口storyboard并加載箭頭所指的控制器,顯示窗口。storyboard和xib最大的不同在于storyboard是基于試圖控制器的,而非視圖或窗口。展示之前會將添加rootViewController的view到UIWindow上面(在這一步才會創(chuàng)建控制器的view)

[window addSubview: window.rootViewControler.view];

每個應用程序至少有一個UIWindow,這window負責管理和協(xié)調應用程序的屏幕顯示,rootViewController的view將會作為UIWindow的首視圖。

未使用storyboard的啟動

程序啟動的完整過程如下:

1.main 函數(shù)

2.UIApplicationMain

  • 創(chuàng)建UIApplication對象

  • 創(chuàng)建UIApplication的delegate對象

  1. delegate對象開始處理(監(jiān)聽)系統(tǒng)事件(沒有storyboard)
  • 程序啟動完畢的時候, 就會調用代理的application:didFinishLaunchingWithOptions:方法

  • 在application:didFinishLaunchingWithOptions:中創(chuàng)建UIWindow

  • 創(chuàng)建和設置UIWindow的rootViewController

  • 顯示窗口

3.根據(jù)Info.plist獲得最主要storyboard的文件名,加載最主要的storyboard(有storyboard)

  • 創(chuàng)建UIWindow

  • 創(chuàng)建和設置UIWindow的rootViewController

  • 顯示窗口

AppDelegate的代理方法

//app啟動完畢后就會調用
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
}

//app程序失去焦點就會調用                    
- (void)applicationWillResignActive:(UIApplication *)application
{
}

//app進入后臺的時候調用, 一般在這里保存應用的數(shù)據(jù)(游戲數(shù)據(jù),比如暫停游戲)
- (void)applicationDidEnterBackground:(UIApplication *)application
{
}

//app程序程序從后臺回到前臺就會調用
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}

//app程序獲取焦點就會調用
- (void)applicationDidBecomeActive:(UIApplication *)application
{
 }

// 內(nèi)存警告,可能要終止程序,清除不需要再使用的內(nèi)存
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
}

// 程序即將退出調用
- (void)applicationWillTerminate:(UIApplication *)application
{
}

AppDelegate加載順序
1.application:didFinishLaunchingWithOptions:
2.applicationDidBecomeActive:

ViewController中的加載順序
1.loadView
2.viewDidLoad
3.viewWillAppear
4.viewWillLayoutSubviews
5.viewDidLayoutSubviews
6.viewDidAppear

View中的加載順序
1.initWithCoder(如果沒有storyboard就會調用initWithFrame,這里兩種方法視為一種)
2.awakeFromNib
3.layoutSubviews
4.drawRect

一些方法的使用時機

+ (void)load;

應用程序啟動就會調用的方法,在這個方法里寫的代碼最先調用。

+ (void)initialize;

用到本類時才調用,這個方法里一般設置導航控制器的主題等,如果在后面的方法設置導航欄主題就太遲了!

 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions;

這個方法里面會創(chuàng)建UIWindow,設置根控制器并展現(xiàn),比如某些應用程序要加載授權頁面也是在這加,也可以設置觀察者,監(jiān)聽到通知切換根控制器等。

- (void)awakeFromNib;

在使用IB的時候才會涉及到此方法的使用,當.nib文件被加載的時候,會發(fā)送一個awakeFromNib的消息到.nib文件中的每個對象,每個對象都可以定義自己的awakeFromNib函數(shù)來響應這個消息,執(zhí)行一些必要的操作。在這個方法里設置view的背景等一系列普通操作。

- (void)loadView;

創(chuàng)建視圖的層次結構,在沒有創(chuàng)建控制器的view的情況下不能直接寫 self.view 因為self.view的底層是:

if(_view == nil){
_view = [self loadView]
}

這么寫會直接造成死循環(huán)。

如果重寫這個loadView方法里面什么都不寫,會顯示黑屏。

- (void)viewWillLayoutSubviews;

視圖將要布局子視圖,蘋果建議的設置界面布局屬性的方法,這個方法和viewWillAppear里,系統(tǒng)的底層都是沒有寫任何代碼的,也就是說這里面不寫super 也是可以的。

 - (void)layoutSubviews;

在這個方法里一般設置子控件的frame。

- (void)drawRect:(CGRect)rect;

UI控件都是畫上去的,在這一步就是把所有的東西畫上去。drawRect方法只能在加載時調用一次,如果后面還需要調用,比如下載進度的圓弧,需要一直刷幀,就要使用setNeedsDisplay來定時多次調用本方法。

- (void)applicationDidBecomeActive:(UIApplication *)application;

這是AppDelegate的應用程序獲取焦點方法,真正到了這里,才是所有東西全部加載完畢。

啟動分析

應用啟動時,會播放一個啟動動畫。iPhone上是400ms,iPad上是500ms。如果應用啟動過慢,用戶就會放棄使用,甚至永遠都不再回來。為了防止一個應用占用過多的系統(tǒng)資源,開發(fā)iOS的蘋果工程師門設計了一個“看門狗”的機制。在不同的場景下,“看門狗”會監(jiān)測應用的性能。如果超出了該場景所規(guī)定的運行間,“看門狗”就會強制終結這個應用的進程。

iOS App啟動時會鏈接并加載Framework和static lib,執(zhí)行UIKit初始化,然后進入應用程序回調,執(zhí)行Core Animation transaction等。每個Framework都會增加啟動時間和占用的內(nèi)存,不要鏈接不必要的Framework,必要的Framework不要標記為Optional。避免創(chuàng)建全局的C++對象。

初始化UIKit時字體、狀態(tài)欄、user defaults、Main.storyboard會被初始化。User defaults本質上是一個plist文件,保存的數(shù)據(jù)是同時被反序列化的,不要在user defaults里面保存圖片等大數(shù)據(jù)。

對于 OC 來說應盡量減少 Class,selector 和 category 這些元數(shù)據(jù)的數(shù)量。編碼原則和設計模式之類的理論會鼓勵大家多寫精致短小的類和方法,并將每部分方法獨立出一個類別,但這會增加啟動時間。在調用的地方使用初始化器,不要使用\\atribute((constructor)) 將方法顯式標記為初始化器,而是讓初始化方法調用時才執(zhí)行。比如使用 dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用時才初始化,推遲了一部分工作耗時。

建立網(wǎng)絡連接前需要做域名解析,如果網(wǎng)關出現(xiàn)問題,dns解析不正常時,dns的超時時間是應用控制不了的。在程序設計時要考慮這些問題,如果程序啟動時有網(wǎng)絡連接,應盡快的結束啟動過程,網(wǎng)絡訪問通過線程解決,而不阻塞主線程的運行。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內(nèi)容