iOS開發優化的起步之啓動優化

前言

作爲開發人員,啓動是App給用戶的第一印象,對用戶體驗至關重要。任何開發的APP的業務迭代迅速,如果放任不管,啓動速度會一點點劣化。爲此iOS客戶端團隊做了大量優化工作,除了傳統的修改業務代碼方式,我們還做了些開拓性的探索,首先我們需要考慮的是,應用啓動分爲2種情況:

冷啓動:指 app 被後臺殺死後,在這個狀態打開 app,這種啓動方式叫做冷啓動,根據測試結果並非是殺掉進程後直接啓動,需要先開啓5-10個應用後,然後在啓動應用,纔會完全冷啓動

熱啓動:指 app 沒有被後臺殺死,仍然在後臺運行,通常我們再次去打開這個 app,這種啓動方式叫熱啓動。

而作爲開發人員我們主要要做的啓動優化,主要分爲2種情況:

  • 2.mian函數之後優化

  • 1.main函數之前優化

1.main函數之後:

main函數之後,其實大部分來源於我們的業務代碼,這裏提供一個檢測代碼中花費時間的Demo,可以自行檢測添加代碼,進行添加

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [[BLStopwatch sharedStopwatch] start];
    int a = 0;
    for (int i = 0; i < 10000000; i++) {
        a++;
    }
    [[BLStopwatch sharedStopwatch] splitWithDescription:@"didFinishLaunchingWithOptions"];
    
    

    return YES;
}
- (void)viewDidLoad {
    [super viewDidLoad];
    //刷新時間:
       [[BLStopwatch sharedStopwatch] refreshMedianTime];
       
       int a = 0;
       for (int i = 0; i < 10000000; i++) {
           a++;
       };
       [[BLStopwatch sharedStopwatch] splitWithDescription:@"viewDidLoad"];
    // Do any additional setup after loading the view.
}
-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    //刷新時間:
       [[BLStopwatch sharedStopwatch] refreshMedianTime];
       
       int a = 0;
       for (int i = 0; i < 10000000; i++) {
           a++;
       };
       [[BLStopwatch sharedStopwatch] splitWithDescription:@"viewDidAppear"];
    [[BLStopwatch sharedStopwatch] stopAndPresentResultsThenReset];
    
    
}

檢測結果:

Main階段優化:

  1. 能懶加載的儘量使用懶加載

  2. 儘可能發揮CPU的價值,儘量使用多線程進行加載;

  3. 啓動界面,儘可能的避免使用storyboard,或Xib(這寫都是需要轉換爲代碼的,需要耗費時間)

  4. NSUserDefaults實際上是在Library文件夾下會生產一個plist文件,如果文件太大的話一次能讀取到內存中可能很耗時,這個影響需要評估,如果耗時很大的話需要拆分(需考慮老版本覆蓋安裝兼容問題) 

  5. 每次用NSLog方式打印會隱式的創建一個Calendar,因此需要刪減啓動時各業務方打的log,或者僅僅針對內測版輸出log

 

2.main函數之前

想要查看一下main函數之前我們應用都做了些什麼,我們只需要在在 eidt scheme  -->Run  --> Arguments -->Environment Variables 添加環境變量 DYLD_PRINT_STATISTICS;

添加完成環境變量後,我們可以運行應用,可以監控main之前的運行時間:

這個是應用啓動後時間,由於模擬器之前有安裝過應用,所以這次屬於熱啓動,我們將進程殺死後,再進行檢測

我們對其中的名詞進行解釋一下:

  • dylib load time          : 動態庫加載時間,儘量使用系統庫,外部庫儘量不要超過6個(蘋果建議)

  • rebase/binding          :就是 macho +ASLR 進行內部地址偏移+ 外部符號綁定(例如NSLog 爲系統Founditon框架中的方法就需要外部符號綁定)

  • Objc setuptime          :OC 類註冊時間 ,優化方案:減少oc類,2萬個類會增加800mm這裏優化性較小

  • initializer time            : 加載包含load方法的類時間(非懶加載類)   儘量減少load 可修改爲initalizer中+ 單利方式進行加

  • slowest intializers     :最慢的

  • libsystem.B.dylb       : 系統庫

  • libglInterpose.dylb   :  調試相關,release會去掉的

  • marsbridgentwork    : 系統庫

  • zhitongti                     : 主進程

由此我們可以看出優化方案:

  1. 這裏在main函數之前我們能做的東西就比較少了,首先我們儘量減少動態庫(能合併的,可以儘量合併,不要超過6個),

  2. 儘量減少load 可修改爲initalizer中+ 單利方式進行加載

  3. swift比oc效率高,可以嘗試用swift替換OC,老工程就不建議了

  4. 減少oc類,2萬個類會增加800mm,這裏的優化時間比較小,但是可以考慮將棄用類(每個工程總有一些迭代,但是已經棄用未刪除的代碼)找工具刪除

  5. 這裏還有一個很牛逼的優化二進制重排,在鏈接階段(編譯期間)有這麼個優化

2.1二進制重排

二進制重排,首先需要了解幾個概念:虛擬內存,物理內存;

  • 物理內存:真實的硬件設備(內存條)

  • 虛擬內存:利用磁盤空間虛擬出的一塊邏輯內存,用作虛擬內存的磁盤空間被稱爲交換空間(Swap Space)。(爲了滿足物理內存的不足而提出的策略)

在很久以前,還沒有虛擬內存概念的時候,程序尋址用的都是物理地址,取決於CPU的地址線條數,32位平臺的話 2^32也就是4G 。且每次開啓一個進程都給4G的物理內存。很顯然你內存小一點,這很快就分配完了,於是沒有得到分配資源的進程就只能等待,而且還有一個問題,就是我當前的應用如果使用物理內存,直接給當前地址+偏移量,有可能會訪問到別的進程,這樣也導致進程之間的不安全;

而後引入了虛擬內存極大解決了這方面的問題,一個進程運行時都會得到4G的虛擬內存。這個虛擬內存你可以認爲,每個進程都認爲自己擁有4G的空間,這只是每個進程認爲的,但是實際上,在虛擬內存對應的物理內存上,可能只對應的一點點的物理內存,實際用了多少內存,就會對應多少物理內存,引入虛擬內存主要就是解決物理內存分配,以及進程之間的安全問題;

2.1.1虛擬內存工作原理:

進程得到的這4G虛擬內存是一個連續的地址空間(這也只是進程認爲),而實際上,它通常是被分隔成多個物理內存碎片,還有一部分存儲在外部磁盤存儲器上,在需要時進行數據交換。

進程開始要訪問一個地址,它可能會經歷下面的過程:

  1. 每次我要訪問地址空間上的某一個地址,都需要把地址翻譯爲實際物理內存地址
  2. 所有進程共享這整一塊物理內存,每個進程只把自己目前需要的虛擬地址空間映射到物理內存上
  3. 進程需要知道哪些地址空間上的數據在物理內存上,哪些不在(可能這部分存儲在磁盤上),還有在物理內存上的哪裏,這就需要通過頁表來記錄
  4. 頁表的每一個表項分兩部分,第一部分記錄此頁是否在物理內存上,第二部分記錄物理內存頁的地址(如果在的話)
  5. 當進程訪問某個虛擬地址的時候,就會先去看頁表,如果發現對應的數據不在物理內存上,就會發生缺頁異常
  6. 缺頁異常的處理過程,操作系統立即阻塞該進程,並將硬盤裏對應的頁換入內存,然後使該進程就緒,如果內存已經滿了,沒有空地方了,那就找一個頁覆蓋,至於具體覆蓋的哪個頁,就需要看操作系統的頁面置換算法是怎麼設計的了。

關於虛擬內存與物理內存關係可以用下圖進行描述

頁表的工作原理如下圖所示:

  1. 我們的cpu想訪問虛擬地址所在的虛擬頁(VP3),根據頁表,找出頁表中第三條的值.判斷有效位。 如果有效位爲1,DRMA緩存命中,根據物理頁號,找到物理頁當中的內容,返回。
  2. 若有效位爲0,參數缺頁(Page fault)異常,調用內核缺頁異常處理程序。內核通過頁面置換算法選擇一個頁面作爲被覆蓋的頁面,將該頁的內容刷新到磁盤空間當中。然後把VP3映射的磁盤文件緩存到該物理頁上面。然後頁表中第三條,有效位變成1,第二部分存儲上了可以對應物理內存頁的地址的內容。
  3. 缺頁異常處理完畢後,返回中斷前的指令,重新執行,此時緩存命中,執行1。
  4. 將找到的內容映射到告訴緩存當中,CPU從告訴緩存中獲取該值,結束。

由此我們可看到當有缺頁異常(Page fault )時,就會稍微耗時,這裏我們就可以看到優化點,我們可以可以看一下符號重排一下就可以了;

xcode缺頁異常(Page fault )檢測

在我們xcode中就有檢查工具,在xcode中的instruments中有工具System Trace 中就可以進行調試

啓動這個工具之後我們到自己啓動也後,停止,然後進行分析如下操作:

這裏我們可以看到File backed Page In 就是一共有多少頁,這裏可以看到2583頁;

我們來進行一次熱啓動,直接重啓一下,看看情況:

我們可以看到熱啓動後,一共就只有116頁,這我們就可以看出,實際上熱啓動和冷啓動差距還是很大的,這樣我們就可以看出系統在啓動時需要的分頁會很多,特別是冷啓動,啓動中有大量的缺頁中斷,這就會導致耗時,我們先看一下怎樣監測頁碼;

3蘋果支持二進制排序

首先二進制重排Xcode給我們提供了一種方法,xcode中使用的鏈接器爲LD,ld中有個文件叫做order_file,如果有一個order文件,將符號順序寫入在order文件,Xcode就會將按照order中的順序進行排列;

我們可以看下蘋果官方給我們提供的源碼來查看一下:

其中order文件中內容,這個文件內容就是函數,方法的符號;

我們看下官方給的源碼中xcode如何配置這個order文件的

我們可以看到不管是在debug($(SRCROOT)/libobjc.order)還是在release($(SDKROOT)/AppleInternal/OrderFiles/libobjc.order)環境蘋果都指定了一個order文件

蘋果指定了這個文件後,他們編譯出來的二進制文件,就會按照這個裏面的符號進行排序

3.1詳解二進制排序

     這個Demo是詳解二進制的

那麼我們自己新建個工程來嘗試一下二進制重排,新建工程,其中添加一些方法,首先我們要知道ld鏈接文件的順序還和下面這裏配置有關

這裏的順序的,所以我們需要注意這裏的列順序,例如我當前在ViewController都寫個load方法,方法內輸出控制器名稱

我們可以看到先執行了ViewController 的Load方法,如果修改了鏈接順序,就會有改變,這個大家可以自行嘗試;

接下來我們看下如何獲取到我們項目的符號。Xcode中有設置可以獲取到

1.進入build Settings 找到Linking  write Link Map File 配置爲yes

2.編譯一下工程,我們可以得到編譯文件

3.show in forder並找到上級目錄Intermediates.noindex

4.進入文件夾,找到對應的txt文件

其中BinaryRearrangement-LinkMap-normal-x86_64.txt這個文件就是我們的包含符號順序的文件,打開文件進行查看

可以看出這裏的連接順序,和我們剛纔的文件順序是一致的,這裏我們需要知道的是,鏈接文件實際鏈接的是將.m文件轉換後的.o文件,繼續查看下面sections:

再往下就是重點了symoblo

symoblo有幾個關鍵詞

  • Address 爲macho中的實際代碼地址:
  • Size:代碼佔用的內存
  • File:文件的序號
  • Name:函數的方法名稱,其中的C語言方法前面會加上下劃線

這裏就是我們的符號順序,這裏我們可以看出,函數的排序和我們實際的順序是不一樣的,我們對代碼進行分析,實際順序這幾個函數應該是:

  1. +[ViewController load]
  2. +[AppDelegate load]
  3. _main
  4.  +[AppDelegate load]

然而我們看到實際文件中,和我們分析的順序是不一樣的,這就是我們要優化的點,也是我們進行二進制重排的原因,那麼我們現在嘗試重排二進制,

1.首先編輯文件alan.order內容爲:

2.放到根目錄,並進行xcode配置

3.再次編譯,查看符號文件

4.我們可以看到自己寫入在order文件中的順序已經正常加載,未寫入的,也會加載進來,並且如果你寫了一個沒有的方法,蘋果也不會加載(這裏做了容錯)

二進制重排難點

這樣我們就看到了二進制重排其實也很簡單,其實二進制重排難點主要在於,我們怎麼樣找到這些順序的符號,並生成order文件,這裏我們可以看下抖音研發實踐團隊這篇文章;根據抖音團隊這篇文章我們來繼續查看一下使用Clang插裝方式進行,首先看先Clang官網文章

 

根據Clang官網進行配置這裏-fsanitize-coverage=trace-pc-guard

這裏有幾個坑點:

fsanitize-coverage=trace-pc-guard直接這麼配置,while循環會一直走添加方法需要修改爲:-fsanitize-coverage=func,trace-pc-guard

添加完成後我們bulid 工程,會出現報錯

這個錯誤很容易解決,根據Clang官網加上示例代碼就可以了

extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  // If you set *guard to 0 this code will not be called again for this edge.
  // Now you can get the PC and do whatever you want:
  //   store it somewhere or symbolize it and print right away.
  // The values of `*guard` are as you set them in
  // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  // and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

還會報錯我們將 extern ’C‘ 再次進行編譯將打印的註釋掉就可以編譯正常了

這樣就實現了靜態插裝:

在所有的函數,block 方法內部直接添加一個一行代碼調用這個函數__sanitizer_cov_trace_pc_guard,而且是第一個方法就會調用這個方法,那麼我們只需要對這個函數進行處理並將調用方法進行組裝生成order文件,再加上上一步的操作,就可以實現完全的二進制重排了。

獲取並生成order文件Demo

這裏再說一個坑點,就是如果有OC工程中和swift混編的,插裝需要再添加一個配置文件纔可以將swift文件進行hook住,           -sanitize-coverage=func -sanitize=undefined 工程中有swift才能找到這個配置

這裏輸出一下我的符號順序, 

我們可以看到有一些是我們可以看到的方法,紅色框內部就是swift編譯後的符號,也是可以正常被HOOK到。

接下來我們看一下使用二進制重排和爲使用二進制重排的符號順序:

未使用二進制重排

使用二進制重排後的符號順序:

通過這種方式我們可以達到一定的優化效果;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章