iOS 系統(tǒng)架構(gòu)
Mac系統(tǒng)是基于Unix內(nèi)核的圖形化操作系統(tǒng)拟淮,Mac OS 和 iOS 系統(tǒng)架構(gòu)的對(duì)比分析發(fā)現(xiàn),Mac OS和iOS的系統(tǒng)架構(gòu)層次只有最上面一層不同把兔,Mac是Cocoa框架,而iOS是Cocoa Touch框架,其余的架構(gòu)層次都是一樣的僵闯。
Core OS是用FreeBSD和Mach所改寫(xiě)的一個(gè)名叫Darwin的開(kāi)放原始碼操作系統(tǒng), 是開(kāi)源、符合POSIX標(biāo)準(zhǔn)的一個(gè)Unix核心藤滥。這一層包含并提供了整個(gè)iPhone OS的一些基礎(chǔ)功能鳖粟,比如:硬件驅(qū)動(dòng), 內(nèi)存管理,程序管理拙绊,線程管理(POSIX)向图,文件系統(tǒng),網(wǎng)絡(luò)(BSD Socket),以及標(biāo)準(zhǔn)輸入輸出等标沪,所有這些功能都會(huì)通過(guò)C語(yǔ)言的API來(lái)提供榄攀。
核心OS層的驅(qū)動(dòng)提供了硬件和系統(tǒng)框架之間的接口。然而金句,由于安全的考慮檩赢,只有有限的系統(tǒng)框架類能訪問(wèn)內(nèi)核和驅(qū)動(dòng)。iPhone OS提供了許多訪問(wèn)操作系統(tǒng)低層功能的接口集违寞,iPhone 應(yīng)用通過(guò)LibSystem庫(kù)來(lái)訪問(wèn)這些功能贞瞒,這些接口集有線程(POSIX線程)、網(wǎng)絡(luò)(BSD sockets)趁曼、文件系統(tǒng)訪問(wèn)军浆、標(biāo)準(zhǔn)I/O、Bonjour和DNS服務(wù)挡闰、現(xiàn)場(chǎng)信息(Locale Information)瘾敢、內(nèi)存分配和數(shù)學(xué)計(jì)算等。
Core Services在Core OS基礎(chǔ)上提供了更為豐富的功能尿这, 它包含了Foundation.Framework和Core Foundation.Framework, 之所以叫Foundation簇抵,就是因?yàn)樗峁┝艘幌盗刑幚碜址帕猩渲冢M合碟摆,日歷,時(shí)間等等的基本功能叨橱。
Foundation是屬于Objective-C的API典蜕,Core Fundation是屬于C的API断盛。另外Core servieces還提供了如Security(用來(lái)處理認(rèn)證,密碼管理愉舔,安全性管理等), Core Location, SQLite和Address Book等功能钢猛。
核心基礎(chǔ)框架(CoreFoundation.framework)是基于C語(yǔ)言的接口集,提供iPhone應(yīng)用的基本數(shù)據(jù)管理和服務(wù)功能轩缤。該框架支持Collection數(shù)據(jù)類型(Arrays命迈、 Sets等)、Bundles火的、字符串管理壶愤、日期和時(shí)間管理、原始數(shù)據(jù)塊管理馏鹤、首選項(xiàng)管理征椒、URL和Stream操作、線程和運(yùn)行循環(huán)(Run Loops)湃累、端口和Socket通信勃救。
核心基礎(chǔ)框架與基礎(chǔ)框架是緊密相關(guān)的,它們?yōu)橄嗤幕竟δ芴峁┝薕bjective-C接口治力。如果開(kāi)發(fā)者混合使用Foundation Objects 和Core Foundation類型剪芥,就能充分利用存在兩個(gè)框架中的"toll-free bridging"技術(shù)(橋接)。toll-free bridging使開(kāi)發(fā)者能使用這兩個(gè)框架中的任何一個(gè)的核心基礎(chǔ)和基礎(chǔ)類型琴许。
程序啟動(dòng)之前
從應(yīng)用圖標(biāo)被用戶點(diǎn)擊開(kāi)始,直到應(yīng)用可以開(kāi)始響應(yīng)發(fā)生了很多事情溉躲。
很多文章中大家都提到說(shuō)dyld加載了主程序和動(dòng)態(tài)庫(kù)榜田,這個(gè)理解明顯是錯(cuò)誤的,在XNU加載Mach-O和dyld過(guò)程中锻梳,是內(nèi)核加載了主程序箭券,dyld只會(huì)負(fù)責(zé)動(dòng)態(tài)庫(kù)的加載,雖然主程序也會(huì)作為鏡像形式被dyld來(lái)管理起來(lái)疑枯。當(dāng)一個(gè)App啟動(dòng)時(shí)辩块,dyld把App需要的dylib加載進(jìn)App的內(nèi)存空間。App運(yùn)行所需要的信息荆永,一般都存放在其MachO頭部44中废亭,其中dylib的信息是由load commands指定的,App得到執(zhí)行時(shí)具钥,dyld會(huì)查看其MachO頭部中的load commands豆村,并把里面LC_LOAD_DYLIB相關(guān)的dylib給加載到進(jìn)程的內(nèi)存空間。
一般來(lái)說(shuō)骂删,逆向工程會(huì)在dyld階段入手掌动,之前版本的dyld中確實(shí)存在一些漏洞四啰,使App能夠繞過(guò)代碼簽名,例如dyld-353.2.1版本粗恢,漏洞編號(hào)CVE-2015-5876柑晒,漏洞存在于Mach-O頭的處理過(guò)程中,一個(gè)畸形的Mach-O文件可以導(dǎo)致內(nèi)存段被替換眷射,從而導(dǎo)致任意代碼執(zhí)行匙赞,當(dāng)然目前dyld的已知漏洞都已修復(fù)。監(jiān)控啟動(dòng)崩潰會(huì)在Objc階段之后凭迹,具體方法具體分析罚屋。
總結(jié)來(lái)說(shuō),大體分為如下步驟:
(1) 系統(tǒng)為程序啟動(dòng)做好準(zhǔn)備
(2) 系統(tǒng)將控制權(quán)交給 Dyld嗅绸,Dyld 會(huì)負(fù)責(zé)后續(xù)的工作
(3) Dyld 加載程序所需的動(dòng)態(tài)庫(kù)
(3) Dyld 對(duì)程序進(jìn)行 rebase 以及 bind 操作
(4) Objc SetUp
(5) 運(yùn)行初始化函數(shù)
(6) 執(zhí)行程序的 main 函數(shù)
需要注意的是脾猛,dyld2和dyld3的加載方式略有不同。dyld2是純粹的in-process鱼鸠,也就是在程序進(jìn)程內(nèi)執(zhí)行的猛拴,也就意味著只有當(dāng)應(yīng)用程序被啟動(dòng)的時(shí)候,dyld2才能開(kāi)始執(zhí)行任務(wù)蚀狰。dyld3則是部分out-of-process愉昆,部分in-process。
dyld2的過(guò)程是:加載dyld到App進(jìn)程麻蹋,加載動(dòng)態(tài)庫(kù)(包括所依賴的所有動(dòng)態(tài)庫(kù))跛溉,Rebase,Bind扮授,初始化Objective C Runtime和其它的初始化代碼芳室。
dyld3的out-of-process會(huì)做如下事情:分析Mach-o Headers,分析依賴的動(dòng)態(tài)庫(kù)刹勃,查找需要Rebase & Bind之類的符號(hào)堪侯,把上述結(jié)果寫(xiě)入緩存。這樣荔仁,在應(yīng)用啟動(dòng)的時(shí)候伍宦,就可以直接從緩存中讀取數(shù)據(jù),加快加載速度乏梁。
從exec()開(kāi)始
Mach-O是 OS X 系統(tǒng)的可執(zhí)行文件次洼,Mach-O有多種文件類型,比如MH_DYLIB文件遇骑、MH_BUNDLE文件滓玖、MH_EXECUTE文件,MH_OBJECT(內(nèi)核加載)等质蕉∈拼郏可執(zhí)行文件離不開(kāi)進(jìn)程翩肌,在 Linux 中,我們會(huì)通過(guò) Fork()來(lái)新創(chuàng)建子進(jìn)程禁悠,然后執(zhí)行鏡像通過(guò)exec()來(lái)替換為另一個(gè)可執(zhí)行程序念祭。在用戶態(tài)會(huì)通過(guò)exec*系列函數(shù)來(lái)加載一個(gè)可執(zhí)行文件。
main()函數(shù)是整個(gè)程序的入口碍侦,在程序啟動(dòng)之前粱坤,系統(tǒng)會(huì)調(diào)用exec()函數(shù)。在Unix中exec和system的不同在于瓷产,system是用shell來(lái)調(diào)用程序站玄,相當(dāng)于fork+exec+waitpid,fork 函數(shù)創(chuàng)建子進(jìn)程后通常都會(huì)調(diào)用 exec 函數(shù)來(lái)執(zhí)行一個(gè)新程序濒旦;而exec是直接讓你的程序代替原來(lái)的程序運(yùn)行株旷。
system 是在單獨(dú)的進(jìn)程中執(zhí)行命令,完了還會(huì)回到你的程序中尔邓。而exec函數(shù)是直接在你的進(jìn)程中執(zhí)行新的程序晾剖,新的程序會(huì)把你的程序覆蓋,除非調(diào)用出錯(cuò)梯嗽,否則你再也回不到exec后面的代碼齿尽,也就是當(dāng)前的程序變成了exec調(diào)用的那個(gè)程序了。
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[]);
通過(guò)分析我們發(fā)現(xiàn)循头,含有 l 和 v 的 exec 函數(shù)的參數(shù)表傳遞方式是不同的。含有 e 結(jié)尾的 exec 函數(shù)會(huì)傳遞一個(gè)環(huán)境變量列表炎疆。含有 p 結(jié)尾的 exec 函數(shù)取的是新程序的文件名作為參數(shù)卡骂,而其他exec 函數(shù)取的是新程序的路徑。
如果函數(shù)出錯(cuò)則返回-1磷雇,若成功則沒(méi)有返回值。其中只有execve是真正意義上的系統(tǒng)調(diào)用躏救,其它都是在此基礎(chǔ)上經(jīng)過(guò)包裝的庫(kù)函數(shù)唯笙。
exec函數(shù)族的作用是根據(jù)指定的文件名找到可執(zhí)行文件,并用它來(lái)取代調(diào)用進(jìn)程的內(nèi)容盒使,換句話說(shuō)崩掘,就是在調(diào)用進(jìn)程內(nèi)部執(zhí)行一個(gè)可執(zhí)行文件。這里的可執(zhí)行文件既可以是二進(jìn)制文件少办,也可以是任何Unix下可執(zhí)行的腳本文件苞慢。
Dyld
Dyld 是 iOS 系統(tǒng)的動(dòng)態(tài)鏈接器, 在dyldStartup.s 文件中有個(gè)名為 __dyld_start 的方法英妓,它會(huì)去調(diào)用 dyldbootstrap::start() 方法挽放,然后進(jìn)一步調(diào)用 dyld::_main() 方法绍赛,里面包含 App 的整個(gè)啟動(dòng)流程,該函數(shù)最終返回應(yīng)用程序 main 函數(shù)的地址辑畦,最后 Dyld 會(huì)去調(diào)用它吗蚌。
之后會(huì)去加載可執(zhí)行文件,二進(jìn)制文件常被稱為 image纯出,包括可執(zhí)行文件蚯妇、動(dòng)態(tài)庫(kù)等,ImageLoader 的作用就是將二進(jìn)制文件加載進(jìn)內(nèi)存暂筝。dyld::_main() 方法在設(shè)置好運(yùn)行環(huán)境后箩言,會(huì)調(diào)用instantiateFromLoadedImage 函數(shù)將可執(zhí)行文件加載進(jìn)內(nèi)存中,加載過(guò)程分為三步:
合法性檢查焕襟。主要是檢查可執(zhí)行文件是否合法陨收,是否能在當(dāng)前的 CPU 架構(gòu)下運(yùn)行。
選擇 ImageLoader 加載可執(zhí)行文件胧洒。系統(tǒng)會(huì)去判斷可執(zhí)行文件的類型畏吓,選擇相應(yīng)的 ImageLoader 將其加載進(jìn)內(nèi)存空間中。
注冊(cè) image 信息卫漫》票可執(zhí)行文件加載完成后,系統(tǒng)會(huì)調(diào)用 addImage 函數(shù)將其管理起來(lái)列赎,并更新內(nèi)存分布信息宏悦。
以上三步完成后,Dyld 會(huì)調(diào)用 link 函數(shù)開(kāi)始之后的處理流程包吝。
靜態(tài)鏈接庫(kù)與動(dòng)態(tài)鏈接庫(kù)
iOS中的相關(guān)文件有如下幾種:Dylib饼煞,動(dòng)態(tài)鏈接庫(kù)(又稱 DSO 或 DLL);Bundle诗越,不能被鏈接的 Dylib砖瞧,只能在運(yùn)行時(shí)使用 dlopen() 加載,可當(dāng)做 macOS 的插件嚷狞。Framework块促,包含 Dylib 以及資源文件和頭文件的文件夾。
動(dòng)態(tài)鏈接庫(kù)是一組源代碼的模塊床未,每個(gè)模塊包含一些可供應(yīng)用程序或者其他動(dòng)態(tài)鏈接庫(kù)調(diào)用的函數(shù)竭翠,在應(yīng)用程序調(diào)用一個(gè)動(dòng)態(tài)鏈接庫(kù)里面的函數(shù)的時(shí)候,操作系統(tǒng)會(huì)將動(dòng)態(tài)鏈接庫(kù)的文件映像映射到進(jìn)程的地址空間中薇搁,這樣進(jìn)程中所有的線程就可以調(diào)用動(dòng)態(tài)鏈接庫(kù)中的函數(shù)了斋扰。動(dòng)態(tài)鏈接庫(kù)加載完成后,這個(gè)時(shí)候動(dòng)態(tài)鏈接庫(kù)對(duì)于進(jìn)程中的線程來(lái)說(shuō)只是一些被放在地址進(jìn)程空間附加的代碼和數(shù)據(jù),操作系統(tǒng)為了節(jié)省內(nèi)存空間传货,同一個(gè)動(dòng)態(tài)鏈接庫(kù)在內(nèi)存中只有一個(gè)屎鳍,操作系統(tǒng)也只會(huì)加載一次到內(nèi)存中。
因?yàn)榇a段在內(nèi)存中的權(quán)限都是為只讀的损离,所以當(dāng)多個(gè)應(yīng)用程序加載同一個(gè)動(dòng)態(tài)鏈接庫(kù)的時(shí)候哥艇,不用擔(dān)心應(yīng)用程序會(huì)修改動(dòng)態(tài)鏈接庫(kù)的代碼段。當(dāng)線程調(diào)用動(dòng)態(tài)鏈接庫(kù)的一個(gè)函數(shù)僻澎,函數(shù)會(huì)在線程棧中取得傳遞給他的參數(shù)貌踏,并使用線程棧來(lái)存放他需要的變量,動(dòng)態(tài)鏈接庫(kù)函數(shù)創(chuàng)建的任何對(duì)象都為調(diào)用線程或者調(diào)用進(jìn)程擁有窟勃,動(dòng)態(tài)鏈接庫(kù)不會(huì)擁有任何對(duì)象祖乳。如果動(dòng)態(tài)鏈接庫(kù)中的一個(gè)函數(shù)調(diào)用了VirtualAlloc,系統(tǒng)會(huì)從調(diào)用進(jìn)程的地址空間預(yù)定地址秉氧,即使撤銷了對(duì)動(dòng)態(tài)鏈接庫(kù)的映射眷昆,調(diào)用進(jìn)程的預(yù)定地址依然會(huì)存在,直到用戶取消預(yù)定或者進(jìn)程結(jié)束汁咏。
靜態(tài)鏈接庫(kù)與動(dòng)態(tài)鏈接庫(kù)都是共享代碼的方式亚斋,如果采用靜態(tài)鏈接庫(kù),則無(wú)論你愿不愿意攘滩,lib 中的指令都全部被直接包含在最終生成的包文件中了帅刊。但是若使用動(dòng)態(tài)鏈接庫(kù),該動(dòng)態(tài)鏈接庫(kù)不必被包含在最終包里漂问,包文件執(zhí)行時(shí)可以“動(dòng)態(tài)”地引用和卸載這個(gè)與安裝包獨(dú)立的動(dòng)態(tài)鏈接庫(kù)文件赖瞒。靜態(tài)鏈接庫(kù)和動(dòng)態(tài)鏈接庫(kù)的另外一個(gè)區(qū)別在于靜態(tài)鏈接庫(kù)中不能再包含其他的動(dòng)態(tài)鏈接庫(kù)或者靜態(tài)庫(kù),而在動(dòng)態(tài)鏈接庫(kù)中還可以再包含其他的動(dòng)態(tài)或靜態(tài)鏈接庫(kù)蚤假。
Linux中靜態(tài)函數(shù)庫(kù)的名字一般是libxxx.a栏饮,利用靜態(tài)函數(shù)庫(kù)編譯成的文件比較大,因?yàn)檎麄€(gè)函數(shù)庫(kù)的所有數(shù)據(jù)都會(huì)被整合進(jìn)目標(biāo)代碼中磷仰。編譯后的執(zhí)行程序不需要外部的函數(shù)庫(kù)支持袍嬉,因?yàn)樗惺褂玫暮瘮?shù)都已經(jīng)被編譯進(jìn)去了。當(dāng)然這也會(huì)成為他的缺點(diǎn)灶平,因?yàn)槿绻o態(tài)函數(shù)庫(kù)改變了伺通,那么你的程序必須重新編譯。
動(dòng)態(tài)函數(shù)庫(kù)的名字一般是libxxx.so民逼,相對(duì)于靜態(tài)函數(shù)庫(kù)泵殴,動(dòng)態(tài)函數(shù)庫(kù)在編譯的時(shí)候并沒(méi)有被編譯進(jìn)目標(biāo)代碼中涮帘,你的程序執(zhí)行到相關(guān)函數(shù)時(shí)才調(diào)用該函數(shù)庫(kù)里的相應(yīng)函數(shù)拼苍,因此動(dòng)態(tài)函數(shù)庫(kù)所產(chǎn)生的可執(zhí)行文件比較小。由于函數(shù)庫(kù)沒(méi)有被整合進(jìn)你的程序,而是程序運(yùn)行時(shí)動(dòng)態(tài)的申請(qǐng)并調(diào)用疮鲫,所以程序的運(yùn)行環(huán)境中必須提供相應(yīng)的庫(kù)吆你。動(dòng)態(tài)函數(shù)庫(kù)的改變并不影響你的程序,所以動(dòng)態(tài)函數(shù)庫(kù)的升級(jí)比較方便俊犯。
iOS開(kāi)發(fā)中靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)是相對(duì)編譯期和運(yùn)行期的妇多。靜態(tài)庫(kù)在程序編譯時(shí)會(huì)被鏈接到目標(biāo)代碼中,程序運(yùn)行時(shí)將不再需要載入靜態(tài)庫(kù)燕侠。而動(dòng)態(tài)庫(kù)在程序編譯時(shí)并不會(huì)被鏈接到目標(biāo)代碼中者祖,只是在程序運(yùn)行時(shí)才被載入,因?yàn)樵诔绦蜻\(yùn)行期間還需要?jiǎng)討B(tài)庫(kù)的存在绢彤。
iOS中靜態(tài)庫(kù)可以用.a或.Framework文件表示七问,動(dòng)態(tài)庫(kù)的形式有.dylib和.framework。系統(tǒng)的.framework是動(dòng)態(tài)庫(kù)茫舶,一般自己建立的.framework是靜態(tài)庫(kù)械巡。
.a是一個(gè)純二進(jìn)制文件,.framework中除了有二進(jìn)制文件之外還有資源文件饶氏。.a文件不能直接使用讥耗,至少要有.h文件配合。.framework文件可以直接使用疹启,.a + .h + sourceFile = .framework古程。
動(dòng)態(tài)庫(kù)的一個(gè)重要特性就是即插即用性台腥,我們可以選擇在需要的時(shí)候再加載動(dòng)態(tài)庫(kù)娩脾。如果不希望在軟件一啟動(dòng)就加載動(dòng)態(tài)庫(kù),需要將
Targets-->Build Phases-->Link Binary With Libraries
中 *.framework 對(duì)應(yīng)的Status由默認(rèn)的 Required 改成 Optional 愕掏;或者將 xx.framework 從 Link Binary With Libraries 列表中刪除贷祈。
可以使用dlopen加載動(dòng)態(tài)庫(kù)趋急,動(dòng)態(tài)庫(kù)中真正的可執(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來(lái)加載動(dòng)態(tài)庫(kù)势誊,實(shí)現(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);
}
}
可以為動(dòng)態(tài)庫(kù)的加載和移除添加監(jiān)聽(tīng)回調(diào)呜达,ImageLogger上有一個(gè)完整的示例代碼,從中可以發(fā)現(xiàn)粟耻,一個(gè)工程軟件啟動(dòng)的時(shí)候會(huì)加載多達(dá)一百二十多個(gè)動(dòng)態(tài)庫(kù)查近,即使是一個(gè)空白的項(xiàng)目。
但是挤忙,需要注意的一點(diǎn)是霜威,不要在初始化方法中調(diào)用 dlopen(),對(duì)性能有影響册烈。因?yàn)?dyld 在 App 開(kāi)始前運(yùn)行戈泼,由于此時(shí)是單線程運(yùn)行所以系統(tǒng)會(huì)取消加鎖,但 dlopen() 開(kāi)啟了多線程,系統(tǒng)不得不加鎖大猛,這就嚴(yán)重影響了性能扭倾,還可能會(huì)造成死鎖以及產(chǎn)生未知的后果。所以也不要在初始化器中創(chuàng)建線程挽绩。
據(jù)說(shuō)膛壹,iOS現(xiàn)在可以使用自定義的動(dòng)態(tài)庫(kù),低版本的需要手動(dòng)的使用dlopen()加載唉堪。動(dòng)態(tài)庫(kù)上架會(huì)有一些審核的規(guī)則模聋,如不要把x86/i386的包和arm架構(gòu)的包lipo在一起使用。如:
lipo –create Release-iphoneos/libiphone.a Debig-iphonesimulator/libiphone.a –output libiphone.a
如此便將模擬器和設(shè)備的靜態(tài)庫(kù)文件合并成一個(gè)文件輸出了唠亚。
dylib加載調(diào)用
基于上面的分析撬槽,在exec()時(shí),系統(tǒng)內(nèi)核把應(yīng)用映射到新的地址空間趾撵,每次起始位置都是隨機(jī)的侄柔。然后使用dyld 加載 dylib 文件(動(dòng)態(tài)鏈接庫(kù)),dyld 在應(yīng)用進(jìn)程中運(yùn)行的工作就是加載應(yīng)用依賴的所有動(dòng)態(tài)鏈接庫(kù)占调,準(zhǔn)備好運(yùn)行所需的一切暂题,它擁有和應(yīng)用一樣的權(quán)限。
加載 Dylib時(shí)究珊,先從主執(zhí)行文件的 header 中獲取需要加載的所依賴動(dòng)態(tài)庫(kù)的列表薪者,從中找到每個(gè) dylib,然后打開(kāi)文件讀取文件起始位置剿涮,確保它是 Mach-O 文件(針對(duì)不同運(yùn)行時(shí)可執(zhí)行文件的文件類型)言津。然后找到代碼簽名并將其注冊(cè)到內(nèi)核。
應(yīng)用所依賴的 dylib 文件可能會(huì)再依賴其他 dylib取试,因此動(dòng)態(tài)庫(kù)列表是一個(gè)遞歸依賴的集合悬槽。一般應(yīng)用會(huì)加載 100 到 400 個(gè) dylib 文件,但大部分都是系統(tǒng) dylib瞬浓,它們會(huì)被預(yù)先計(jì)算和緩存起來(lái)初婆,加載速度很快。但加載內(nèi)嵌(embedded)的 dylib 文件很占時(shí)間猿棉,所以盡可能把多個(gè)內(nèi)嵌 dylib 合并成一個(gè)來(lái)加載磅叛,或者使用 static archive。
在加載所有的動(dòng)態(tài)鏈接庫(kù)之后萨赁,它們只是處在相互獨(dú)立的狀態(tài)弊琴,代碼簽名使得我們不能修改指令,那樣就不能讓一個(gè) dylib 調(diào)用另一個(gè) dylib杖爽。通過(guò)fix-up可以將它們結(jié)合起來(lái)敲董,dyld 所做的事情就是修正(fix-up)指針和數(shù)據(jù)详瑞。Fix-up 有兩種類型,rebasing(在鏡像內(nèi)部調(diào)整指針的指向) 和 binding(將指針指向鏡像外部的內(nèi)容)臣缀。
因?yàn)榈刂房臻g加載隨機(jī)化(ASLR,Address Space Layout Randomization)的緣故泻帮,二進(jìn)制文件最終的加載地址與預(yù)期地址之間會(huì)存在偏移精置,所以需要進(jìn)行 rebase 操作,對(duì)那些指向文件內(nèi)部符號(hào)的指針進(jìn)行修正锣杂。rebase 完成之后脂倦,就會(huì)進(jìn)行 bind 操作,修正那些指向其他二進(jìn)制文件所包含的符號(hào)的指針元莫。因?yàn)?dylib 之間有依賴關(guān)系赖阻,所以 動(dòng)態(tài)庫(kù)中的好多操作都是沿著依賴鏈遞歸操作的,Rebasing 和 Binding 分別對(duì)應(yīng)著 recursiveRebase() 和 recursiveBind() 這兩個(gè)方法踱蠢。因?yàn)槭沁f歸火欧,所以會(huì)自底向上地分別調(diào)用 doRebase() 和 doBind() 方法,這樣被依賴的 dylib 總是先于依賴它的 dylib 執(zhí)行 Rebasing 和 Binding茎截。
Rebaing 消耗了大量時(shí)間在 I/O 上苇侵,在 Rebasing 和 Binding 前會(huì)判斷是否已經(jīng) 預(yù)綁定。如果已經(jīng)進(jìn)行過(guò)預(yù)綁定(Prebinding)企锌,那就不需要 Rebasing 和 Binding 這些 Fix-up 流程了榆浓,因?yàn)橐呀?jīng)在預(yù)先綁定的地址加載好了。
Binding 處理那些指向 dylib 外部的指針撕攒,它們實(shí)際上被符號(hào)(symbol)名稱綁定陡鹃,是一個(gè)字符串。dyld 需要找到 symbol 對(duì)應(yīng)的實(shí)現(xiàn)抖坪,在符號(hào)表里查找時(shí)需要很多計(jì)算萍鲸,找到后會(huì)將內(nèi)容存儲(chǔ)起來(lái)。Binding 看起來(lái)計(jì)算量比 Rebasing 更大擦俐,但其實(shí)需要的 I/O 操作很少猿推,因?yàn)橹?Rebasing 已經(jīng)替 Binding 做過(guò)了。Objective-C 中有很多數(shù)據(jù)結(jié)構(gòu)都是靠 Rebasing 和 Binding 來(lái)修正(fix-up)的捌肴,比如 Class 中指向超類的指針和指向方法的指針蹬叭。
Objc
C++ 會(huì)為靜態(tài)創(chuàng)建的對(duì)象生成初始化器,與靜態(tài)語(yǔ)言不同状知,OC基于Runtime機(jī)制可以用類的名字來(lái)實(shí)例化一個(gè)類的對(duì)象秽五。Runtime 維護(hù)了一張映射類名與類的全局表,當(dāng)加載一個(gè) dylib 時(shí)饥悴,其定義的所有的類都需要被注冊(cè)到這個(gè)全局表中坦喘。ObjC 在加載時(shí)可以通過(guò) fix-up 在動(dòng)態(tài)類中改變實(shí)例變量的偏移量盲再,利用這個(gè)技術(shù)可以在不改變dylib的情況下添加另一個(gè) dylib 中類的方法,而非常見(jiàn)的通過(guò)定義類別(Category)的方式改變一個(gè)類的方法瓣铣。
Dyld 在 bind 操作結(jié)束之后答朋,會(huì)發(fā)出 dyld_image_state_bound 通知,然后與之綁定的回調(diào)函數(shù) map_2_images 就會(huì)被調(diào)用棠笑,它主要做以下幾件事來(lái)完成 Objc Setup:
讀取二進(jìn)制文件的 DATA 段內(nèi)容梦碗,找到與 objc 相關(guān)的信息
注冊(cè) Objc 類
確保 selector 的唯一性
讀取 protocol 以及 category 的信息
除了 map_2_images,我們注意到 _objc_init 還注冊(cè)了 load_images 函數(shù)蓖救,它的作用就是調(diào)用 Objc 的 + load 方法洪规,它監(jiān)聽(tīng) dyld_image_state_dependents_initialized 通知。
dyld 是運(yùn)行在用戶態(tài)的循捺, 這里由內(nèi)核態(tài)切到了用戶態(tài)斩例。每當(dāng)有新的鏡像加載之后,都會(huì)執(zhí)行 load-images 方法進(jìn)行回調(diào)从橘,這里的回調(diào)是在整個(gè)ObjC runtime 初始化時(shí) -objc-init 注冊(cè)的念赶。有新的鏡像被 map 到 runtime 時(shí),調(diào)用 load-images 方法恰力,并傳入最新鏡像的信息列表 infoList晶乔。調(diào)用 prepare-load-methods 對(duì) load 方法的調(diào)用進(jìn)行準(zhǔn)備(將需要調(diào)用 load 方法的類添加到一個(gè)列表中),調(diào)用 -getObjc2NonlazyClassList 獲取所有的類的列表之后牺勾,會(huì)通過(guò) remapClass 獲取類對(duì)應(yīng)的指針正罢,然后調(diào)用 schedule-class-load 遞歸地 將當(dāng)前類和沒(méi)有調(diào)用 + load 父類進(jìn)入列表。在執(zhí)行 add-class-to-loadable-list(cls) 將當(dāng)前類加入加載列表之前驻民,會(huì)先把父類加入待加載的列表翻具,保證父類在子類前調(diào)用 load 方法。在執(zhí)行 add-class-to-loadable-list(cls) 將當(dāng)前類加入加載列表之前回还,會(huì)先把父類加入待加載的列表裆泳,保證父類在子類前調(diào)用 load 方法。在將鏡像加載到運(yùn)行時(shí)柠硕、對(duì) load 方法的準(zhǔn)備就緒工禾,執(zhí)行 call-load-methods,開(kāi)始調(diào)用 load 方法蝗柔。
由于iOS開(kāi)發(fā)時(shí)基于Cocoa Touch的闻葵,所以絕大多數(shù)的類起始都是系統(tǒng)類,大多數(shù)的Runtime初始化起始在Rebase和Bind中已經(jīng)完成癣丧。
Initializers
Objc SetUp 結(jié)束后槽畔,Dyld 便開(kāi)始運(yùn)行程序的初始化函數(shù),該任務(wù)由 initializeMainExecutable 函數(shù)執(zhí)行胁编。整個(gè)初始化過(guò)程是一個(gè)遞歸的過(guò)程厢钧,順序是先將依賴的動(dòng)態(tài)庫(kù)初始化鳞尔,然后在對(duì)自己初始化。初始化需要做的事情包括:
調(diào)用 Objc 類的 + load 函數(shù)
調(diào)用 C++ 中帶有 constructor 標(biāo)記的函數(shù)
非基本類型的 C++ 靜態(tài)全局變量的創(chuàng)建
所謂執(zhí)行監(jiān)控啟動(dòng)crash的思路都是在這里構(gòu)建的早直。下面是一些方法的執(zhí)行順序寥假,initialize的順序可能在更早,但總是會(huì)在load和launch之間霞扬。
程序啟動(dòng)邏輯
主執(zhí)行文件和相關(guān)的 dylib的依賴關(guān)系構(gòu)成了一張巨大的有向圖糕韧,執(zhí)行初始化器先加載葉子節(jié)點(diǎn),然后逐步向上加載中間節(jié)點(diǎn)祥得,直至最后加載根節(jié)點(diǎn)。這種加載順序確保了安全性蒋得,加載某個(gè) dylib 前级及,其所依賴的其余 dylib 文件肯定已經(jīng)被預(yù)先加載。最后 dyld 會(huì)調(diào)用 main() 函數(shù)额衙。main() 會(huì)調(diào)用 UIApplicationMain()饮焦,程序啟動(dòng)。
使用Xcode打開(kāi)一個(gè)項(xiàng)目窍侧,很容易會(huì)發(fā)現(xiàn)一個(gè)文件--main.m文件县踢,此處就是應(yīng)用的入口了。程序啟動(dòng)時(shí)伟件,先執(zhí)行main函數(shù)硼啤,main函數(shù)是ios程序的入口點(diǎn),內(nèi)部會(huì)調(diào)用UIApplicationMain函數(shù)斧账,UIApplicationMain里會(huì)創(chuàng)建一個(gè)UIApplication對(duì)象 谴返,然后創(chuàng)建UIApplication的delegate對(duì)象 —–(您的)AppDelegate ,開(kāi)啟一個(gè)消息循環(huán)(main runloop)咧织,每當(dāng)監(jiān)聽(tīng)到對(duì)應(yīng)的系統(tǒng)事件時(shí)嗓袱,就會(huì)通知AppDelegate。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplication對(duì)象是應(yīng)用程序的象征习绢,每一個(gè)應(yīng)用都有自己的UIApplication對(duì)象渠抹,而且是單例的。通過(guò)[UIApplication sharedApplication]可以獲得這個(gè)單例對(duì)象闪萄,一個(gè)iOS程序啟動(dòng)后創(chuàng)建的第一個(gè)對(duì)象就是UIApplication對(duì)象梧却,利用UIApplication對(duì)象,能進(jìn)行一些應(yīng)用級(jí)別的操作败去。
UIApplicationMain函數(shù)實(shí)現(xiàn)如下:
int UIApplicationMain{
int argc,
char *argv[],
NSString *principalClassName,
NSString *delegateClassName
}
第一個(gè)參數(shù)表示參數(shù)的個(gè)數(shù)篮幢,第二個(gè)參數(shù)表示裝載函數(shù)的數(shù)組,第三個(gè)參數(shù)为迈,是UIApplication類名或其子類名三椿,若是nil缺菌,則默認(rèn)使用UIApplication類名。第四個(gè)參數(shù)是協(xié)議UIApplicationDelegate的實(shí)例化對(duì)象名搜锰,這個(gè)對(duì)象就是UIApplication對(duì)象監(jiān)聽(tīng)到系統(tǒng)變化的時(shí)候通知其執(zhí)行的相應(yīng)方法伴郁。
啟動(dòng)完畢會(huì)調(diào)用 didFinishLaunching方法,并在這個(gè)方法中創(chuàng)建UIWindow蛋叼,設(shè)置AppDelegate的window屬性焊傅,并設(shè)置UIWindow的根控制器。如果有storyboard狈涮,會(huì)根據(jù)info.plist中找到應(yīng)用程序的入口storyboard并加載箭頭所指的控制器狐胎,顯示窗口。storyboard和xib最大的不同在于storyboard是基于試圖控制器的歌馍,而非視圖或窗口握巢。展示之前會(huì)將添加rootViewController的view到UIWindow上面(在這一步才會(huì)創(chuàng)建控制器的view)
[window addSubview: window.rootViewControler.view];
每個(gè)應(yīng)用程序至少有一個(gè)UIWindow,這window負(fù)責(zé)管理和協(xié)調(diào)應(yīng)用程序的屏幕顯示松却,rootViewController的view將會(huì)作為UIWindow的首視圖暴浦。
程序啟動(dòng)的完整過(guò)程如下:
1.main 函數(shù)
2.UIApplicationMain
創(chuàng)建UIApplication對(duì)象
創(chuàng)建UIApplication的delegate對(duì)象
- delegate對(duì)象開(kāi)始處理(監(jiān)聽(tīng))系統(tǒng)事件(沒(méi)有storyboard)
程序啟動(dòng)完畢的時(shí)候, 就會(huì)調(diào)用代理的application:didFinishLaunchingWithOptions:方法
在application:didFinishLaunchingWithOptions:中創(chuàng)建UIWindow
創(chuàng)建和設(shè)置UIWindow的rootViewController
顯示窗口
3.根據(jù)Info.plist獲得最主要storyboard的文件名,加載最主要的storyboard(有storyboard)
創(chuàng)建UIWindow
創(chuàng)建和設(shè)置UIWindow的rootViewController
顯示窗口
AppDelegate的代理方法
//app啟動(dòng)完畢后就會(huì)調(diào)用
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
}
//app程序失去焦點(diǎn)就會(huì)調(diào)用
- (void)applicationWillResignActive:(UIApplication *)application
{
}
//app進(jìn)入后臺(tái)的時(shí)候調(diào)用, 一般在這里保存應(yīng)用的數(shù)據(jù)(游戲數(shù)據(jù),比如暫停游戲)
- (void)applicationDidEnterBackground:(UIApplication *)application
{
}
//app程序程序從后臺(tái)回到前臺(tái)就會(huì)調(diào)用
- (void)applicationWillEnterForeground:(UIApplication *)application
{
}
//app程序獲取焦點(diǎn)就會(huì)調(diào)用
- (void)applicationDidBecomeActive:(UIApplication *)application
{
}
// 內(nèi)存警告晓锻,可能要終止程序歌焦,清除不需要再使用的內(nèi)存
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
}
// 程序即將退出調(diào)用
- (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(如果沒(méi)有storyboard就會(huì)調(diào)用initWithFrame,這里兩種方法視為一種)
2.awakeFromNib
3.layoutSubviews
4.drawRect
一些方法的使用時(shí)機(jī)
+ (void)load;
應(yīng)用程序啟動(dòng)就會(huì)調(diào)用的方法砚哆,在這個(gè)方法里寫(xiě)的代碼最先調(diào)用独撇。
+ (void)initialize;
用到本類時(shí)才調(diào)用,這個(gè)方法里一般設(shè)置導(dǎo)航控制器的主題等躁锁,如果在后面的方法設(shè)置導(dǎo)航欄主題就太遲了券勺!
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions;
這個(gè)方法里面會(huì)創(chuàng)建UIWindow,設(shè)置根控制器并展現(xiàn)灿里,比如某些應(yīng)用程序要加載授權(quán)頁(yè)面也是在這加关炼,也可以設(shè)置觀察者,監(jiān)聽(tīng)到通知切換根控制器等匣吊。
- (void)awakeFromNib;
在使用IB的時(shí)候才會(huì)涉及到此方法的使用儒拂,當(dāng).nib文件被加載的時(shí)候,會(huì)發(fā)送一個(gè)awakeFromNib的消息到.nib文件中的每個(gè)對(duì)象色鸳,每個(gè)對(duì)象都可以定義自己的awakeFromNib函數(shù)來(lái)響應(yīng)這個(gè)消息社痛,執(zhí)行一些必要的操作。在這個(gè)方法里設(shè)置view的背景等一系列普通操作命雀。
- (void)loadView;
創(chuàng)建視圖的層次結(jié)構(gòu)蒜哀,在沒(méi)有創(chuàng)建控制器的view的情況下不能直接寫(xiě) self.view 因?yàn)閟elf.view的底層是:
if(_view == nil){
_view = [self loadView]
}
這么寫(xiě)會(huì)直接造成死循環(huán)。
如果重寫(xiě)這個(gè)loadView方法里面什么都不寫(xiě)吏砂,會(huì)顯示黑屏撵儿。
- (void)viewWillLayoutSubviews;
視圖將要布局子視圖乘客,蘋果建議的設(shè)置界面布局屬性的方法,這個(gè)方法和viewWillAppear里淀歇,系統(tǒng)的底層都是沒(méi)有寫(xiě)任何代碼的易核,也就是說(shuō)這里面不寫(xiě)super 也是可以的。
- (void)layoutSubviews;
在這個(gè)方法里一般設(shè)置子控件的frame浪默。
- (void)drawRect:(CGRect)rect;
UI控件都是畫(huà)上去的牡直,在這一步就是把所有的東西畫(huà)上去。drawRect方法只能在加載時(shí)調(diào)用一次纳决,如果后面還需要調(diào)用碰逸,比如下載進(jìn)度的圓弧,需要一直刷幀阔加,就要使用setNeedsDisplay來(lái)定時(shí)多次調(diào)用本方法饵史。
- (void)applicationDidBecomeActive:(UIApplication *)application;
這是AppDelegate的應(yīng)用程序獲取焦點(diǎn)方法,真正到了這里掸哑,才是所有東西全部加載完畢约急。
啟動(dòng)分析
應(yīng)用啟動(dòng)時(shí)零远,會(huì)播放一個(gè)啟動(dòng)動(dòng)畫(huà)苗分。iPhone上是400ms,iPad上是500ms牵辣。如果應(yīng)用啟動(dòng)過(guò)慢摔癣,用戶就會(huì)放棄使用,甚至永遠(yuǎn)都不再回來(lái)纬向。為了防止一個(gè)應(yīng)用占用過(guò)多的系統(tǒng)資源择浊,開(kāi)發(fā)iOS的蘋果工程師門設(shè)計(jì)了一個(gè)“看門狗”的機(jī)制。在不同的場(chǎng)景下逾条,“看門狗”會(huì)監(jiān)測(cè)應(yīng)用的性能琢岩。如果超出了該場(chǎng)景所規(guī)定的運(yùn)行間,“看門狗”就會(huì)強(qiáng)制終結(jié)這個(gè)應(yīng)用的進(jìn)程师脂。
iOS App啟動(dòng)時(shí)會(huì)鏈接并加載Framework和static lib担孔,執(zhí)行UIKit初始化,然后進(jìn)入應(yīng)用程序回調(diào)吃警,執(zhí)行Core Animation transaction等糕篇。每個(gè)Framework都會(huì)增加啟動(dòng)時(shí)間和占用的內(nèi)存,不要鏈接不必要的Framework酌心,必要的Framework不要標(biāo)記為Optional拌消。避免創(chuàng)建全局的C++對(duì)象。
初始化UIKit時(shí)字體安券、狀態(tài)欄墩崩、user defaults氓英、Main.storyboard會(huì)被初始化。User defaults本質(zhì)上是一個(gè)plist文件泰鸡,保存的數(shù)據(jù)是同時(shí)被反序列化的债蓝,不要在user defaults里面保存圖片等大數(shù)據(jù)。
對(duì)于 OC 來(lái)說(shuō)應(yīng)盡量減少 Class,selector 和 category 這些元數(shù)據(jù)的數(shù)量盛龄。編碼原則和設(shè)計(jì)模式之類的理論會(huì)鼓勵(lì)大家多寫(xiě)精致短小的類和方法饰迹,并將每部分方法獨(dú)立出一個(gè)類別,但這會(huì)增加啟動(dòng)時(shí)間余舶。在調(diào)用的地方使用初始化器啊鸭,不要使用\\atribute((constructor)) 將方法顯式標(biāo)記為初始化器,而是讓初始化方法調(diào)用時(shí)才執(zhí)行匿值。比如使用 dispatch_once(),pthread_once() 或 std::once()赠制。也就是在第一次使用時(shí)才初始化,推遲了一部分工作耗時(shí)挟憔。
建立網(wǎng)絡(luò)連接前需要做域名解析钟些,如果網(wǎng)關(guān)出現(xiàn)問(wèn)題,dns解析不正常時(shí)绊谭,dns的超時(shí)時(shí)間是應(yīng)用控制不了的政恍。在程序設(shè)計(jì)時(shí)要考慮這些問(wèn)題,如果程序啟動(dòng)時(shí)有網(wǎng)絡(luò)連接达传,應(yīng)盡快的結(jié)束啟動(dòng)過(guò)程篙耗,網(wǎng)絡(luò)訪問(wèn)通過(guò)線程解決,而不阻塞主線程的運(yùn)行宪赶。