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

iOS開發2019-07-06 04:02:48

黑客技術
點擊右側關注,瞭解黑客的世界!

Linux編程
點擊右側關注,免費入門到精通!

程序員嚴選
甄選正品好物,程序員生活指南!


https://mp.weixin.qq.com/s/n74RlGsySEy0mKGl0J-dJw


背景知識:


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





基本流程:


App 開始啟動後,系統首先加載可執行文件(自身 App 的所有 .o 文件的集合),然後加載動態鏈接器 dyld,dyld 是一個專門用來加載動態鏈接庫的庫。執行從 dyld 開始,dyld 從可執行文件的依賴開始,遞歸加載所有的依賴動態鏈接庫。


動態鏈接庫包括:iOS 中用到的所有系統 framework,加載 OC runtime 方法的 libobjc,系統級別的 libSystem,例如 libdispatch(GCD) 和 libsystem_blocks (Block)。


dyld加載動態庫


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



  1. 系統先讀取App的可執行文件(Mach-O文件)裏的mach-o essay-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 鏡像文件


官方文檔:
https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/MachOOverview.htm


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};


減少靜態變量的使用


  1. 減少符號表的導出
    通過設置-exported_symbols_list或-unexported_symbols_lis來限制符號表的導出,從而減少dyld的工作量

  2. 去除沒有使用的動態庫依賴,明確所依賴的frameworks是require還是optional,optional會動態進行額外檢查

  3. 刪除沒有用的方法

  4. 減少+load函數的實現,並減少在其中操作的邏輯

  5. 對某些經常調用的代碼進行二進制化,生成靜態庫,多使用靜態庫代替動態庫,將多個靜態庫框架,集中製作成靜態framework,從而能夠減少dyld的鏈接工作
    關於冷啟動和熱啟動的不同如下:


main階段



從上圖可以得到,影響main階段的啟動時間因素是:


  1. AppDelegate代理的加載生命週期回調

  2. Application Window的佈局、繪製和加載

  3. RootViewController的加載
    優化點:

  4. 壓縮和減小啟動圖片

  5. 儘量不使用storyboard或者是nib來佈局rootViewController

  6. 在didFinishLaunchingWithOptions階段,儘可能減少阻塞代碼的執行,可以利用多線程進行加載邏輯的處理,注意多線程對主線程同步阻塞可能造成的黑屏問題

  7. 將非同步需求的初始化邏輯進行異步加載


 推薦↓↓↓ 

👉16個技術公眾號】都在這裏!

涵蓋:程序員大咖、源碼共讀、程序員共讀、數據結構與算法、黑客技術和網絡安全、大數據科技、編程前端、Java、Python、Web編程開發、Android、iOS開發、Linux、數據庫研發、幽默程序員等。

萬水千山總是情,點個 “在看” 行不行
https://hk.wxwenku.com/d/201078290