iOS作業系統-- App啟動流程分析與優化

Neo_joke發表於2018-12-11

背景知識:

  • 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函式

iOS作業系統-- App啟動流程分析與優化

iOS作業系統-- App啟動流程分析與優化

基本流程:

App 開始啟動後,系統首先載入可執行檔案(自身 App 的所有 .o 檔案的集合),然後載入動態連結器 dyld,dyld 是一個專門用來載入動態連結庫的庫。 執行從 dyld 開始,dyld 從可執行檔案的依賴開始,遞迴載入所有的依賴動態連結庫。 動態連結庫包括:iOS 中用到的所有系統 framework,載入 OC runtime 方法的 libobjc,系統級別的 libSystem,例如 libdispatch(GCD) 和 libsystem_blocks (Block)。

dyld載入動態庫

動態連結庫的載入過程主要由dyld來完成,dyld是蘋果的動態連結器。

iOS作業系統-- App啟動流程分析與優化

  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系統,在此階段,有相當多的優化工作可以做

iOS作業系統-- App啟動流程分析與優化
這過程包括:

  1. 所有型別的定義和註冊,Objective-C的類不是編譯器決定的,是執行時動態載入到全域性表中的
  2. 非脆弱的ivars變數抵消更新,修改例項變數的記憶體地址偏移問題
  3. 分類替換並新增到方法列表中,將分類中的方法載入到方法列表中
  4. 確認選擇器全域性唯一

Initializers 階段

在Runtime系統載入以後,開始進行初始化

iOS作業系統-- App啟動流程分析與優化

  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的連結工作 關於冷啟動和熱啟動的不同如下:

iOS作業系統-- App啟動流程分析與優化

main階段

iOS作業系統-- App啟動流程分析與優化
從上圖可以得到,影響main階段的啟動時間因素是:

  1. AppDelegate代理的載入生命週期回撥
  2. Application Window的佈局、繪製和載入
  3. RootViewController的載入 優化點:
  4. 壓縮和減小啟動圖片
  5. 儘量不使用storyboard或者是nib來佈局rootViewController
  6. 在didFinishLaunchingWithOptions階段,儘可能減少阻塞程式碼的執行,可以利用多執行緒進行載入邏輯的處理,注意多執行緒對主執行緒同步阻塞可能造成的黑屏問題
  7. 將非同步需求的初始化邏輯進行非同步載入

相關文章