iOS操做系統-- App啓動流程分析與優化

背景知識:

  • mach-o文件爲基於Mach核心的操做系統的可執行文件、目標代碼或動態庫,是.out的代替,其提供了更強的擴展性並提高了符號表中信息的訪問速度,
  • 符號表,用於標記源代碼中包括標識符、聲明信息、行號、函數名稱等元素的具體信息,好比說數據類型、做用域以及內存地址,iOS符號表在dSYM文件中
  • 程序構建過程:編譯分三步走,對 源文件進行預處理(processing),處理預編譯指令,生成.i文件,下一步進行編譯,進行詞法分析(lex工具識別詞法規則語義表)、語法分析和語義分析生成.s彙編文件,最後進行彙編,生成二進制目標文件.o。目標文件再進行連接器連接,造成可執行文件.a或mach-o文件。
  • 連接分爲動態連接和靜態連接,靜態連接會將全部目標文件.o所有內容連接到執行文件中,若是另外的執行文件須要其中的功能,也必須所有收錄。動態連接爲了解決這樣的空間浪費問題,只將函數信息連接加入執行文件
  • dyld是加載動態連接庫的庫,該庫在加載可執行文件的時候,遞歸加載所須要的全部動態庫。動態庫包括iOS操做系統的系統framework,oc的runtime系統libobjc,系統級別的庫libSystem,例如libdispatch(GCD)、libsystem_block(Block)

App啓動大體流程

對於一個可執行文件來講,它的加載過程是: 分爲兩大部分:緩存

  1. pre-main 指的是操做系統開始執行一個可執行文件,並完成進程建立、執行文件加載、動態連接、環境配置
  2. main 指的是從加載main函數入口之後,到app delegate完成加載回調的過程

操做系統加載App可執行文件

操做系統加載可執行文件,經過fork(建立一個進程)指令在新的空間內來執行可執行文件,加載依賴的可執行文件(mach-o)文件,定位其內部與外部指針引用,例如字符串與函數,執行聲明爲attribute((constructor))的C函數,加載擴展(Category)中的方法,C++靜態對象加載,調用ObjC的+load函數bash

基本流程:多線程

App 開始啓動後,系統首先加載可執行文件(自身 App 的全部 .o 文件的集合),而後加載動態連接器 dyld,dyld 是一個專門用來加載動態連接庫的庫。 執行從 dyld 開始,dyld 從可執行文件的依賴開始,遞歸加載全部的依賴動態連接庫。 動態連接庫包括:iOS 中用到的全部系統 framework,加載 OC runtime 方法的 libobjc,系統級別的 libSystem,例如 libdispatch(GCD) 和 libsystem_blocks (Block)。app

dyld加載動態庫

動態連接庫的加載過程主要由dyld來完成,dyld是蘋果的動態連接器。框架

  1. 系統先讀取App的可執行文件(Mach-O文件)裏的mach-o headers
  2. dyld去初始化運行環境,從裏面得到動態依賴,開啓緩存策略,加載程序相關依賴庫(其中也包含咱們的可執行文件),並對這些庫進行連接,最後調用每一個依賴庫的初始化方法,在這一步,runtime被初始化。當全部依賴庫的初始化後,輪到最後一位(程序可執行文件)進行初始化。
  3. 檢查和確認符號表的是否存在和正確
  4. Map全部mach-o文件,用來總體統計變量聲明、函數調用等信息
  5. 進行bind操做,對從其餘庫的引用的符號、函數等,進行其內存地址進行修正綁定
  6. 進行rebase操做,對自身庫內部的引用進行修正
  7. 進行runtime系統初始化,會對項目中全部類進行類結構初始化,而後調用全部的load方法。
  8. 最後dyld返回main函數地址,main函數被調用,咱們便來到了熟悉的程序入口。 當加載一個 Mach-O 文件 (一個可執行文件或者一個庫) 時,動態連接器首先會檢查共享緩存看看是否存在其中,若是存在,那麼就直接從共享緩存中拿出來使用。每個進程都把這個共享緩存映射到了本身的地址空間中。這個方法大大優化了 OS X 和 iOS 上程序的啓動時間。

Mach-O 鏡像文件

官方文檔: developer.apple.com/library/arc…異步

Mach-O是OS X中二進制文件的本機可執行格式,是傳送代碼的首選格式。可執行格式肯定二進制文件中的代碼和數據被讀入內存的順序。代碼和數據的排序會影響內存使用和分頁活動,從而直接影響程序的性能。段的大小經過其包含的全部段中的字節數來度量,並向上舍入到下一個虛擬內存頁邊界。 Mach-O二進制文件被組織成segements。每一個segement包含一個或多個部分。每一個部分都有不一樣類型的代碼或數據。segement始終從頁面邊界開始,但section不必定是頁面對齊的。所以,segement終是4096字節或4千字節的倍數,其中4096字節是最小大小。 Mach-O可執行文件的segement和section根據其預期用途命名。segement名稱的約定是使用以雙下劃線開頭的全大寫字母(例如,TEXT); section名稱的約定是使用以雙下劃線開頭的全小寫字母(例如, text)。 Mach-O可執行文件中有幾個可能的segements,但只有兩個與性能有關:__TEXT段和__DATA段。函數

The __TEXT Segment: Read Only __TEXT segment是包含可執行代碼和常量數據的只讀區域。按照慣例,編譯器工具建立具備至少一個只讀__TEXT segment的每一個可執行文件。因爲該段是隻讀的,所以內核能夠將__TEXT segment直接從可執行文件映射到內存中一次。當segment被映射到內存時,它能夠在全部進程之間共享其內容。 (這主要是框架和其餘共享庫的狀況。)只讀屬性還意味着構成__TEXT segment的頁面永遠沒必要保存到後備存儲。若是內核須要釋放物理內存,它能夠丟棄一個或多個__TEXT頁面,並在須要時從磁盤從新讀取它們。 __TEXT segment的主要部分,sections分佈工具

  • __text 已編譯的可執行文件的機器代碼
  • __const 通常的常量數據
  • __cstring 文字字符串常量(源代碼中的引用字符串)
  • __picsymbol_stub 動態連接器(dyld)使用的與位置無關的代碼存根例程

The __DATA Segment: Read/Write __DATA segment 包含可執行文件的很是量變量。該 segement 是可讀寫的,由於它是可寫的,因此對於與庫連接的每一個進程,邏輯上覆制靜態庫或其餘動態共享庫的__DATA段。當內存頁面可讀寫時,內核會使其變爲copy-on-write。此技術能夠作到,動態庫是在內存中共享的,能夠被其餘各個進程訪問,但由於__DATA Segment是可讀可寫的,就會經過某一進程對共享的_DATA Segment有寫操做的時候,再進行單獨的_DATA內存空間複製。 __DATA segment 有許多部分,其中一些僅由動態連接器使用。下面 列出了能夠出如今__DATA segment 中的一些更重要的部分。有關段的完整列表,請參閱Mach-O運行時體系結構。佈局

  • __data 初始化的全局變量(例如int a = 1;或static int a = 1;)。
  • __const 須要重定位的常量數據(例如,char * const p =「foo」;)
  • __bss 未初始化的靜態變量(例如,static int a;)。
  • __common 未初始化的外部全局變量(例如,int a;外部功能塊)。
  • __dyld 佔位符部分,由動態連接器使用。
  • __la_symbol_ptr lazy符號指針。可執行文件調用的每一個未定義函數的符號指針。
  • __nl_symbol_ptr 非lazy符號指針。可執行文件引用的每一個未定義數據符號的符號指針。

Mach-O 性能影響 Mach-O可執行文件的__TEXT segment和__DATA segment的組成與性能有直接關係。優化這些sections的技術和目的是不一樣的。可是,它們的共同目標是:提升內存使用效率。性能

最典型的Mach-O的文件由可執行代碼組成,在__TEXT,__text當中。如__TEXT segment,該__TEXT是隻讀的,並直接映射到可執行文件,因此若是內核須要回收某些__text頁面佔用的物理內存,就沒必要將頁面保存到back store再將其分頁。它只須要釋放內存,並在後面代碼引用的時候從磁盤從新讀回。雖然這比交換內存分頁的成本低,由於這只是一個磁盤訪問,而不是兩個內存分頁的交換 , 但這仍然很損耗性能,特別是若是必須從磁盤從新建立許多頁面。

對於這種狀況的改進,是經過程序從新排序來改進代碼的引用位置,如改進參考位置中所述。該技術將方法和功能組合在一塊兒,具體取決於它們的執行順序,調用頻率以及它們相互調用的頻率。若是__text部分組中的頁面以這種方式邏輯上起做用,則它們不太可能被屢次釋放和讀回。例如,若是將全部啓動時初始化函數放在一個或兩個頁面上,則在發生初始化後沒必要從新建立頁面。

與__TEXT段不一樣,__DATA能夠寫入段,所以段中的頁面__DATA不可共享。框架中的很是量全局變量可能會對性能產生影響,由於與框架連接的每一個進程都會得到這些變量的副本。解決這個問題的主要解決辦法是儘量多的非恆定的全局變量儘量轉移到__TEXT,__const經過宣佈他們部分const。減小共享內存頁面描述了此技術和相關技術。這一般不是應用程序的問題,由於應用程序中的__DATA部分不與其餘應用程序共享。

編譯器將不一樣類型的很是量全局數據存儲在段的不一樣部分中__DATA。這些類型的數據是未初始化的靜態數據和符號與未聲明的「暫定定義」的ANSI C概念一致extern。未初始化的靜態數據位於__bss段的__DATA部分中。暫定的符號在__common 該__DATA部分。

該 ANSI C和 C ++標準指定系統必須將未初始化的靜態變量設置爲零。(未初始化的其餘類型的未初始化數據。)因爲未初始化的靜態變量和臨時定義符號存儲在單獨的部分中,所以系統須要以不一樣方式對待它們。可是當變量位於不一樣的部分時,它們更有可能最終出如今不一樣的內存頁面上,所以能夠單獨進行交換,從而使代碼運行速度變慢。如減小共享內存頁面中所述,這些問題的解決方案是在段的一個部分中合併不是常量全局數據__DATA。

ObjC Runtime

dyld的加載過程會初始化Runtime系統,在此階段,有至關多的優化工做能夠作

這過程包括:

  1. 全部類型的定義和註冊,Objective-C的類不是編譯器決定的,是運行時動態載入到全局表中的
  2. 非脆弱的ivars變量抵消更新,修改實例變量的內存地址偏移問題
  3. 分類替換並添加到方法列表中,將分類中的方法加載到方法列表中
  4. 確認選擇器全局惟一

Initializers 階段

在Runtime系統加載之後,開始進行初始化

  1. Objc的+load()函數
  2. C++的構造函數屬性函數 形如attribute((constructor)) void DoSomeInitializationWork()
  3. 非基本類型的C++靜態全局變量的建立(一般是類或結構體)(non-trivial initializer) 好比一個全局靜態結構體的構建,若是在構造函數中有繁重的工做,那麼會拖慢啓動速度

pre-main階段分析

從上面能夠得出如下幾個結論,影響該階段啓動時間的因素以下:

  1. Mach-O可執行文件的加載和內存從新分配規劃,對於其segment和section進行虛擬內存的分頁管理的調度
  2. dyld動態連接內存中的公共鏡像,在運行時進行檢查共享數據和連接調用
  3. Runtime的初始化,包括class註冊、category加載、變量對齊等
  4. C++靜態對象和全局變量的加載
  5. ObjeC全部load函數的調用加載

優化措施:

  1. 減小ObjC的類膨脹問題,清理沒有使用的類,合併鬆散無用的類
  2. 減小靜態變量的聲明和初始化的分離
static int x;
static short conv_table [128];
//更換爲
static int x = 0;
static short conv_table [128] = {0};
複製代碼

減小靜態變量的使用 3. 減小符號表的導出 經過設置-exported_symbols_list或-unexported_symbols_lis來限制符號表的導出,從而減小dyld的工做量 4. 去除沒有使用的動態庫依賴,明確所依賴的frameworks是require仍是optional,optional會動態進行額外檢查 5. 刪除沒有用的方法 6. 減小+load函數的實現,並減小在其中操做的邏輯 7. 對某些常常調用的代碼進行二進制化,生成靜態庫,多使用靜態庫代替動態庫,將多個靜態庫框架,集中製做成靜態framework,從而可以減小dyld的連接工做 關於冷啓動和熱啓動的不一樣以下:

main階段

從上圖能夠獲得,影響main階段的啓動時間因素是:

  1. AppDelegate代理的加載生命週期回調
  2. Application Window的佈局、繪製和加載
  3. RootViewController的加載 優化點:
  4. 壓縮和減少啓動圖片
  5. 儘可能不使用storyboard或者是nib來佈局rootViewController
  6. 在didFinishLaunchingWithOptions階段,儘量減小阻塞代碼的執行,能夠利用多線程進行加載邏輯的處理,注意多線程對主線程同步阻塞可能形成的黑屏問題
  7. 將非同步需求的初始化邏輯進行異步加載
相關文章
相關標籤/搜索