iOS App從點選到啟動

發表於2016-11-09

程式啟動之前

從exec()開始

main()函式是整個程式的入口,在程式啟動之前,系統會呼叫exec()函式。在Unix中exec和system的不同在於,system是用shell來呼叫程式,相當於fork+exec+waitpid,fork 函式建立子程式後通常都會呼叫 exec 函式來執行一個新程式;而exec是直接讓你的程式代替原來的程式執行。

system 是在單獨的程式中執行命令,完了還會回到你的程式中。而exec函式是直接在你的程式中執行新的程式,新的程式會把你的程式覆蓋,除非呼叫出錯,否則你再也回不到exec後面的程式碼,也就是當前的程式變成了exec呼叫的那個程式了。

UNIX 提供了 6 種不同的 exec 函式供我們使用。

通過分析我們發現,含有 l 和 v 的 exec 函式的參數列傳遞方式是不同的。含有 e 結尾的 exec 函式會傳遞一個環境變數列表。含有 p 結尾的 exec 函式取的是新程式的檔名作為引數,而其他exec 函式取的是新程式的路徑。

如果函式出錯則返回-1,若成功則沒有返回值。其中只有execve是真正意義上的系統呼叫,其它都是在此基礎上經過包裝的庫函式。

exec函式族的作用是根據指定的檔名找到可執行檔案,並用它來取代呼叫程式的內容,換句話說,就是在呼叫程式內部執行一個可執行檔案。這裡的可執行檔案既可以是二進位制檔案,也可以是任何Unix下可執行的指令碼檔案。

iOS 系統架構

Mac系統是基於Unix核心的圖形化作業系統,Mac OS 和 iOS 系統架構的對比分析發現,Mac OS和iOS的系統架構層次只有最上面一層不同,Mac是Cocoa框架,而iOS是Cocoa Touch框架,其餘的架構層次都是一樣的。

111170656-247c6478b7e43c22

Core OS是用FreeBSD和Mach所改寫的一個名叫Darwin的開放原始碼作業系統, 是開源、符合POSIX標準的一個Unix核心。這一層包含並提供了整個iPhone OS的一些基礎功能,比如:硬體驅動, 記憶體管理,程式管理,執行緒管理(POSIX),檔案系統,網路(BSD Socket),以及標準輸入輸出等等,所有這些功能都會通過C語言的API來提供。

121170656-273f4ba893a40054

核心OS層的驅動提供了硬體和系統框架之間的介面。然而,由於安全的考慮,只有有限的系統框架類能訪問核心和驅動。iPhone OS提供了許多訪問作業系統低層功能的介面集,iPhone 應用通過LibSystem庫來訪問這些功能,這些介面集有執行緒(POSIX執行緒)、網路(BSD sockets)、檔案系統訪問、標準I/O、Bonjour和DNS服務、現場資訊(Locale Information)、記憶體分配和數學計算等。

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應用的基本資料管理和服務功能。該框架支援Collection資料型別(Arrays、 Sets等)、Bundles、字串管理、日期和時間管理、原始資料塊管理、首選項管理、URL和Stream操作、執行緒和執行迴圈(Run Loops)、埠和Socket通訊。

核心基礎框架與基礎框架是緊密相關的,它們為相同的基本功能提供了Objective-C介面。如果開發者混合使用Foundation Objects 和Core Foundation型別,就能充分利用存在兩個框架中的”toll-free bridging”技術(橋接)。toll-free bridging使開發者能使用這兩個框架中的任何一個的核心基礎和基礎型別。

靜態連結庫與動態連結庫

iOS中的相關檔案有如下幾種:Dylib,動態連結庫(又稱 DSO 或 DLL);Bundle,不能被連結的 Dylib,只能在執行時使用 dlopen() 載入,可當做 macOS 的外掛。Framework,包含 Dylib 以及資原始檔和標頭檔案的資料夾。

動態連結庫是一組原始碼的模組,每個模組包含一些可供應用程式或者其他動態連結庫呼叫的函式,在應用程式呼叫一個動態連結庫裡面的函式的時候,作業系統會將動態連結庫的檔案映像對映到程式的地址空間中,這樣程式中所有的執行緒就可以呼叫動態連結庫中的函式了。動態連結庫載入完成後,這個時候動態連結庫對於程式中的執行緒來說只是一些被放在地址程式空間附加的程式碼和資料,作業系統為了節省記憶體空間,同一個動態連結庫在記憶體中只有一個,作業系統也只會載入一次到記憶體中。

因為程式碼段在記憶體中的許可權都是為只讀的,所以當多個應用程式載入同一個動態連結庫的時候,不用擔心應用程式會修改動態連結庫的程式碼段。當執行緒呼叫動態連結庫的一個函式,函式會線上程棧中取得傳遞給他的引數,並使用執行緒棧來存放他需要的變數,動態連結庫函式建立的任何物件都為呼叫執行緒或者呼叫程式擁有,動態連結庫不會擁有任何物件。如果動態連結庫中的一個函式呼叫了VirtualAlloc,系統會從呼叫程式的地址空間預定地址,即使撤銷了對動態連結庫的對映,呼叫程式的預定地址依然會存在,直到使用者取消預定或者程式結束。

靜態連結庫與動態連結庫都是共享程式碼的方式,如果採用靜態連結庫,則無論你願不願意,lib 中的指令都全部被直接包含在最終生成的包檔案中了。但是若使用 動態連結庫,該 動態連結庫 不必被包含在最終包裡,包檔案執行時可以“動態”地引用和解除安裝這個與 安裝包 獨立的 動態連結庫檔案。靜態連結庫和動態連結庫的另外一個區別在於靜態連結庫中不能再包含其他的動態連結庫或者靜態庫,而在動態連結庫中還可以再包含其他的動態或靜態連結庫。

Linux中靜態函式庫的名字一般是libxxx.a;利用靜態函式庫編譯成的檔案比較大,因為整個函式庫的所有資料都會被整合進目的碼中。編譯後的執行程式不需要外部的函式庫支援,因為所有使用的函式都已經被編譯進去了。當然這也會成為他的缺點,因為如果靜態函式庫改變了,那麼你的程式必須重新編譯。

動態函式庫的名字一般是libxxx.so,相對於靜態函式庫,動態函式庫在編譯的時候並沒有被編譯進目的碼中,你的程式執行到相關函式時才呼叫該函式庫裡的相應函式,因此動態函式庫所產生的可執行檔案比較小。由於函式庫沒有被整合進你的程式,而是程式執行時動態的申請並呼叫,所以程式的執行環境中必須提供相應的庫。動態函式庫的改變並不影響你的程式,所以動態函式庫的升級比較方便。

iOS開發中靜態庫和動態庫是相對編譯期和執行期的。靜態庫在程式編譯時會被連結到目的碼中,程式執行時將不再需要載入靜態庫。而動態庫在程式編譯時並不會被連結到目的碼中,只是在程式執行時才被載入,因為在程式執行期間還需要動態庫的存在。

iOS中靜態庫可以用.a或.Framework檔案表示,動態庫的形式有.dylib和.framework。系統的.framework是動態庫,一般自己建立的.framework是靜態庫。

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

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

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

可以使用dlopen載入動態庫,動態庫中真正的可執行程式碼為 xx.framework/xx 檔案。

也可以使用NSBundle來載入動態庫,實現程式碼如下:

可以為動態庫的載入和移除新增監聽回撥,github上有一個完整的示例程式碼,從中可以發現,一個工程軟體啟動的時候會載入多達一百二十多個動態庫,即使是一個空白的專案。

但是,需要注意的一點是,不要在初始化方法中呼叫 dlopen(),對效能有影響。因為 dyld 在 App 開始前執行,由於此時是單執行緒執行所以系統會取消加鎖,但 dlopen() 開啟了多執行緒,系統不得不加鎖,這就嚴重影響了效能,還可能會造成死鎖以及產生未知的後果。所以也不要在初始化器中建立執行緒。

據說,iOS現在可以使用自定義的動態庫,低版本的需要手動的使用dlopen()載入。動態庫上架會有一些稽核的規則,如不要把x86/i386的包和arm架構的包lipo在一起使用。如:

如此便將模擬器和裝置的靜態庫檔案合併成一個檔案輸出了。

上海有家公司有過一個成功上架的案例,但我沒有在這方面做過測試,至於能不能過審,還需要驗證。

dylib載入呼叫

基於上面的分析,在exec()時,系統核心把應用對映到新的地址空間,每次起始位置都是隨機的。然後使用dyld 載入 dylib 檔案(動態連結庫),dyld 在應用程式中執行的工作就是載入應用依賴的所有動態連結庫,準備好執行所需的一切,它擁有和應用一樣的許可權。

載入 Dylib時,先從主執行檔案的 header 中獲取需要載入的所依賴動態庫的列表,從中找到每個 dylib,然後開啟檔案讀取檔案起始位置,確保它是 Mach-O 檔案(針對不同執行時可執行檔案的檔案型別)。然後找到程式碼簽名並將其註冊到核心。

應用所依賴的 dylib 檔案可能會再依賴其他 dylib,因此動態庫列表是一個遞迴依賴的集合。一般應用會載入 100 到 400 個 dylib 檔案,但大部分都是系統 dylib,它們會被預先計算和快取起來,載入速度很快。但載入內嵌(embedded)的 dylib 檔案很佔時間,所以儘可能把多個內嵌 dylib 合併成一個來載入,或者使用 static archive。

在載入所有的動態連結庫之後,它們只是處在相互獨立的狀態,程式碼簽名使得我們不能修改指令,那樣就不能讓一個 dylib 呼叫另一個 dylib。通過fix-up可以將它們結合起來,dyld 所做的事情就是修正(fix-up)指標和資料。Fix-up 有兩種型別,rebasing(在映象內部調整指標的指向) 和 binding(將指標指向映象外部的內容)。

因為 dylib 之間有依賴關係,所以 動態庫 中的好多操作都是沿著依賴鏈遞迴操作的,Rebasing 和 Binding 分別對應著 recursiveRebase() 和 recursiveBind() 這兩個方法。因為是遞迴,所以會自底向上地分別呼叫 doRebase() 和 doBind() 方法,這樣被依賴的 dylib 總是先於依賴它的 dylib 執行 Rebasing 和 Binding。

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

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

OC呼叫

C++ 會為靜態建立的物件生成初始化器,與靜態語言不同,OC基於Runtime機制可以用類的名字來例項化一個類的物件。Runtime 維護了一張對映類名與類的全域性表,當載入一個 dylib 時,其定義的所有的類都需要被註冊到這個全域性表中。ObjC 在載入時可以通過 fix-up 在動態類中改變例項變數的偏移量,利用這個技術可以在不改變dylib的情況下新增另一個 dylib 中類的方法,而非常見的通過定義類別(Category)的方式改變一個類的方法。

主執行檔案和相關的 dylib的依賴關係構成了一張巨大的有向圖,執行初始化器先載入葉子節點,然後逐步向上載入中間節點,直至最後載入根節點。這種載入順序確保了安全性,載入某個 dylib 前,其所依賴的其餘 dylib 檔案肯定已經被預先載入。最後 dyld 會呼叫 main() 函式。main() 會呼叫 UIApplicationMain(),程式啟動。

程式啟動邏輯

使用Xcode開啟一個專案,很容易會發現一個檔案--main.m檔案,此處就是應用的入口了。程式啟動時,先執行main函式,main函式是ios程式的入口點,內部會呼叫UIApplicationMain函式,UIApplicationMain裡會建立一個UIApplication物件 ,然後建立UIApplication的delegate物件 —–(您的)AppDelegate ,開啟一個訊息迴圈(main runloop),每當監聽到對應的系統事件時,就會通知AppDelegate。

UIApplication物件是應用程式的象徵,每一個應用都有自己的UIApplication物件,而且是單例的。通過[UIApplication sharedApplication]可以獲得這個單例物件,一個iOS程式啟動後建立的第一個物件就是UIApplication物件, 利用UIApplication物件,能進行一些應用級別的操作。

UIApplicationMain函式實現如下:

第一個參數列示引數的個數,第二個參數列示裝載函式的陣列,第三個引數,是UIApplication類名或其子類名,若是nil,則預設使用UIApplication類名。第四個引數是協議UIApplicationDelegate的例項化物件名,這個物件就是UIApplication物件監聽到系統變化的時候通知其執行的相應方法。

啟動完畢會呼叫 didFinishLaunching方法,並在這個方法中建立UIWindow,設定AppDelegate的window屬性,並設定UIWindow的根控制器。如果有storyboard,會根據info.plist中找到應用程式的入口storyboard並載入箭頭所指的控制器,顯示視窗。storyboard和xib最大的不同在於storyboard是基於試圖控制器的,而非檢視或視窗。展示之前會將新增rootViewController的view到UIWindow上面(在這一步才會建立控制器的view)

每個應用程式至少有一個UIWindow,這window負責管理和協調應用程式的螢幕顯示,rootViewController的view將會作為UIWindow的首檢視。

131170656-da5d4ac5d9c90e37
未使用storyboard的啟動

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

1.main 函式

2.UIApplicationMain

  • 建立UIApplication物件
  • 建立UIApplication的delegate物件
  • delegate物件開始處理(監聽)系統事件(沒有storyboard)
  • 程式啟動完畢的時候, 就會呼叫代理的application:didFinishLaunchingWithOptions:方法
  • 在application:didFinishLaunchingWithOptions:中建立UIWindow
  • 建立和設定UIWindow的rootViewController
  • 顯示視窗

3.根據Info.plist獲得最主要storyboard的檔名,載入最主要的storyboard(有storyboard)

  • 建立UIWindow
  • 建立和設定UIWindow的rootViewController
  • 顯示視窗

AppDelegate的代理方法

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

ViewController中的載入順序
1.loadView
2.viewDidLoad
3.load
4.initialize
5.viewWillAppear
6.viewWillLayoutSubviews
7.viewDidLayoutSubviews
8.viewDidAppear

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

一些方法的使用時機

應用程式啟動就會呼叫的方法,在這個方法裡寫的程式碼最先呼叫。

用到本類時才呼叫,這個方法裡一般設定導航控制器的主題等,如果在後面的方法設定導航欄主題就太遲了!

這個方法裡面會建立UIWindow,設定根控制器並展現,比如某些應用程式要載入授權頁面也是在這加,也可以設定觀察者,監聽到通知切換根控制器等。

在使用IB的時候才會涉及到此方法的使用,當.nib檔案被載入的時候,會傳送一個awakeFromNib的訊息到.nib檔案中的每個物件,每個物件都可以定義自己的awakeFromNib函式來響應這個訊息,執行一些必要的操作。在這個方法裡設定view的背景等一系列普通操作。

建立檢視的層次結構,在沒有建立控制器的view的情況下不能直接寫 self.view 因為self.view的底層是:

這麼寫會直接造成死迴圈。

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

檢視將要佈局子檢視,蘋果建議的設定介面佈局屬性的方法,這個方法和viewWillAppear裡,系統的底層都是沒有寫任何程式碼的,也就是說這裡面不寫super 也是可以的。

在這個方法裡一般設定子控制元件的frame。

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

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

啟動分析

應用啟動時,會播放一個啟動動畫。iPhone上是400ms,iPad上是500ms。如果應用啟動過慢,使用者就會放棄使用,甚至永遠都不再回來。為了防止一個應用佔用過多的系統資源,開發iOS的蘋果工程師門設計了一個“看門狗”的機制。在不同的場景下,“看門狗”會監測應用的效能。如果超出了該場景所規定的執行間,“看門狗”就會強制終結這個應用的程式。

iOS App啟動時會連結並載入Framework和static lib,執行UIKit初始化,然後進入應用程式回撥,執行Core Animation transaction等。每個Framework都會增加啟動時間和佔用的記憶體,不要連結不必要的Framework,必要的Framework不要標記為Optional。避免建立全域性的C++物件。

初始化UIKit時字型、狀態列、user defaults、Main.storyboard會被初始化。User defaults本質上是一個plist檔案,儲存的資料是同時被反序列化的,不要在user defaults裡面儲存圖片等大資料。

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

建立網路連線前需要做域名解析,如果閘道器出現問題,dns解析不正常時,dns的超時時間是應用控制不了的。在程式設計時要考慮這些問題,如果程式啟動時有網路連線,應儘快的結束啟動過程,網路訪問通過執行緒解決,而不阻塞主執行緒的執行。

相關文章