標籤:base 方便 它的 c函數 tle 個數 分配 apt 將不
程式啟動之前從exec()開始
main()函數是整個程式的入口,在程式啟動之前,系統會調用exec()函數。在Unix中exec和system的不同在於,system是用shell來調用程式,相當於fork+exec+waitpid,fork 函數建立子進程後通常都會調用 exec 函數來執行一個新程式;而exec是直接讓你的程式代替原來的程式運行。
system 是在單獨的進程中執行命令,完了還會回到你的程式中。而exec函數是直接在你的進程中執行新的程式,新的程式會把你的程式覆蓋,除非調用出錯,否則你再也回不到exec後面的代碼,也就是當前的程式變成了exec調用的那個程式了。
UNIX 提供了 6 種不同的 exec 函數供我們使用。
#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[]);
通過分析我們發現,含有 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架構,其餘的架構層次都是一樣的。
Core OS是用FreeBSD和Mach所改寫的一個名叫Darwin的開放原始碼作業系統, 是開源、符合POSIX標準的一個Unix核心。這一層包含並提供了整個iPhone OS的一些基礎功能,比如:硬體驅動, 記憶體管理,程式管理,線程管理(POSIX),檔案系統,網路(BSD Socket),以及標準輸入輸出等等,所有這些功能都會通過C語言的API來提供。
核心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。
動態庫的一個重要特性就是 隨插即用 性,我們可以選擇在需要的時候再載入動態庫。如果不希望在軟體一啟動就載入動態庫,需要將
Targets-->Build Phases-->Link Binary With Libraries
中 *.framework 對應的Status由預設的 Required 改成 Optional ;或者將 xx.framework 從 Link Binary With Libraries 列表中刪除。
可以使用dlopen載入動態庫,動態庫中真正的可執行代碼為 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來載入動態庫,實現代碼如下:
- (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);}}
可以為動態庫的載入和移除添加監聽回調,github上有一個完整的範例程式碼,從中可以發現,一個工程軟體啟動的時候會載入多達一百二十多個動態庫,即使是一個空白的項目。
但是,需要注意的一點是,不要在初始化方法中調用 dlopen(),對效能有影響。因為 dyld 在 App 開始前運行,由於此時是單線程運行所以系統會取消加鎖,但 dlopen() 開啟了多線程,系統不得不加鎖,這就嚴重影響了效能,還可能會造成死結以及產生未知的後果。所以也不要在初始化器中建立線程。
據說,iOS現在可以使用自訂的動態庫,低版本的需要手動的使用dlopen()載入。動態庫上架會有一些審核的規則,如不要把x86/i386的包和arm架構的包lipo在一起使用。如:
lipo –create Release-iphoneos/libiphone.a Debig-iphonesimulator/libiphone.a –output libiphone.a
如此便將模擬器和裝置的靜態庫檔案合并成一個檔案輸出了。
上海有家公司有過一個成功上架的案例,但我沒有在這方面做過測試,至於能不能過審,還需要驗證。
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。
int main(int argc, char * argv[]) {@autoreleasepool {return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}}
UIApplication對象是應用程式的象徵,每一個應用都有自己的UIApplication對象,而且是單例的。通過[UIApplication sharedApplication]可以獲得這個單例對象,一個iOS程式啟動後建立的第一個對象就是UIApplication對象, 利用UIApplication對象,能進行一些應用層級的操作。
UIApplicationMain函數實現如下:
int UIApplicationMain{ int argc, char *argv[], NSString *principalClassName, NSString * delegateClassName}
第一個參數表示參數的個數,第二個參數表示裝載函數的數組,第三個參數,是UIApplication類名或其子類名,若是nil,則預設使用UIApplication類名。第四個參數是協議UIApplicationDelegate的執行個體化對象名,這個對象就是UIApplication對象監聽到系統變化的時候通知其執行的相應方法。
啟動完畢會調用 didFinishLaunching方法,並在這個方法中建立UIWindow,設定AppDelegate的window屬性,並設定UIWindow的根控制器。如果有storyboard,會根據info.plist中找到應用程式的入口storyboard並載入箭頭所指的控制器,顯示視窗。storyboard和xib最大的不同在於storyboard是基於試圖控制器的,而非視圖或視窗。展示之前會將添加rootViewController的view到UIWindow上面(在這一步才會建立控制器的view)
[window addSubview: window.rootViewControler.view];
每個應用程式至少有一個UIWindow,這window負責管理和協調應用程式的螢幕顯示,rootViewController的view將會作為UIWindow的首視圖。
未使用storyboard的啟動
程式啟動的完整過程如下:
1.main 函數
2.UIApplicationMain
建立UIApplication對象
建立UIApplication的delegate對象
delegate對象開始處理(監聽)系統事件(沒有storyboard)
程式啟動完畢的時候, 就會調用代理的application:didFinishLaunchingWithOptions:方法
在application:didFinishLaunchingWithOptions:中建立UIWindow
建立和設定UIWindow的rootViewController
顯示視窗
3.根據Info.plist獲得最主要storyboard的檔案名稱,載入最主要的storyboard(有storyboard)
AppDelegate的代理方法
//app啟動完畢後就會調用- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{}//app程式失去焦點就會調用 - (void)applicationWillResignActive:(UIApplication *)application{}//app進入背景時候調用, 一般在這裡儲存應用的資料(遊戲資料,比如暫停遊戲)- (void)applicationDidEnterBackground:(UIApplication *)application{}//app程式程式從後台回到前台就會調用- (void)applicationWillEnterForeground:(UIApplication *)application{}//app程式擷取焦點就會調用- (void)applicationDidBecomeActive:(UIApplication *)application{ }// 記憶體警告,可能要終止程式,清除不需要再使用的記憶體- (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;
這個方法裡面會建立UIWindow,設定根控制器並展現,比如某些應用程式要載入授權頁面也是在這加,也可以設定觀察者,監聽到通知切換根控制器等。
- (void)awakeFromNib;
在使用IB的時候才會涉及到此方法的使用,當.nib檔案被載入的時候,會發送一個awakeFromNib的訊息到.nib檔案中的每個對象,每個對象都可以定義自己的awakeFromNib函數來響應這個訊息,執行一些必要的操作。在這個方法裡設定view的背景等一系列普通操作。
- (void)loadView;
建立視圖的階層,在沒有建立控制器的view的情況下不能直接寫 self.view 因為self.view的底層是:
if(_view == nil){_view = [self loadView]}
這麼寫會直接造成死迴圈。
如果重寫這個loadView方法裡面什麼都不寫,會顯示黑屏。
- (void)viewWillLayoutSubviews;
視圖將要布局子視圖,蘋果建議的設定介面布局屬性的方法,這個方法和viewWillAppear裡,系統的底層都是沒有寫任何代碼的,也就是說這裡面不寫super 也是可以的。
- (void)layoutSubviews;
在這個方法裡一般設定子控制項的frame。
- (void)drawRect:(CGRect)rect;
UI控制項都是畫上去的,在這一步就是把所有的東西畫上去。drawRect方法只能在載入時調用一次,如果後面還需要調用,比如下載進度的圓弧,需要一直刷幀,就要使用setNeedsDisplay來定時多次調用本方法。
- (void)applicationDidBecomeActive:(UIApplication *)application;
這是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的逾時時間是應用控制不了的。在程式設計時要考慮這些問題,如果程式啟動時有網路連接,應儘快的結束啟動過程,網路訪問通過線程解決,而不阻塞主線程的運行。
文/吳白(簡書作者)
原文連結:http://www.jianshu.com/p/231b1cebf477
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。
iOS App從點擊到啟動