你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> iOS App從點擊到啟動

iOS App從點擊到啟動

編輯:IOS開發基礎

程序啟動之前

從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框架,其余的架構層次都是一樣的。

1170656-247c6478b7e43c22.jpg

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

1170656-273f4ba893a40054.png

核心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的首視圖。

1170656-da5d4ac5d9c90e37.jpg

未使用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的代理方法

//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的超時時間是應用控制不了的。在程序設計時要考慮這些問題,如果程序啟動時有網絡連接,應盡快的結束啟動過程,網絡訪問通過線程解決,而不阻塞主線程的運行。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved