我是如何讓微博綠洲的啓動速度提高30%的

綠洲iOS研發工程師,綠洲ID:收納箱KeepFit。html

0. 序言

啓動是App給用戶的第一印象,對用戶體驗相當重要。試想一個App須要啓動5s以上,你還想用它麼?node

最初的工程確定是沒有這些問題的,但隨着業務需求不斷豐富,代碼愈來愈多。若是聽任無論的話,啓動時間會不斷上漲,最後讓人沒法接受。swift

本文從優化原理出發,介紹了我是如何經過修改庫的類型和Clang插樁找到啓動所需符號,而後修改編譯參數完成二進制文件的從新排布提高應用的啓動速度的。數組

下面咱們先上結論:緩存

  • 優化前:sass

    Total pre-main time: 1.2 seconds (100.0%)
             dylib loading time: 567.72 milliseconds (45.5%)
            rebase/binding time: 105.14 milliseconds (8.4%)
                ObjC setup time:  40.01 milliseconds (3.2%)
               initializer time: 532.47 milliseconds (42.7%)
               slowest intializers :
                 libSystem.B.dylib :   4.70 milliseconds (0.3%)
              libglInterpose.dylib : 295.89 milliseconds (23.7%)
                      AFNetworking :  48.75 milliseconds (3.9%)
                             Oasis : 285.94 milliseconds (22.9%)
    複製代碼
  • 優化後安全

    Total pre-main time: 822.34 milliseconds (100.0%)
             dylib loading time: 196.71 milliseconds (23.9%)
            rebase/binding time: 104.95 milliseconds (12.7%)
                ObjC setup time:  31.14 milliseconds (3.7%)
               initializer time: 489.53 milliseconds (59.5%)
               slowest intializers :
                 libSystem.B.dylib :   4.65 milliseconds (0.5%)
              libglInterpose.dylib : 230.19 milliseconds (27.9%)
                      AFNetworking :  41.60 milliseconds (5.0%)
                             Oasis : 335.84 milliseconds (40.8%)
    複製代碼

經過staticlib優化二進制重排兩項技術,我成功將綠洲的pre-main時間從1.2s降到了大約0.82s,提高了大約31.6%ruby

兩臺手機都是iPhone 11 Pro,右邊是優化後的效果。(原諒我右邊點開還慢了一點😂)app

1. 動態庫轉靜態庫

蘋果建議將應用程序的總啓動時間設定在400毫秒如下,而且咱們必須在20秒以內完成啓動,不然系統會殺死咱們的應用程序。咱們能夠儘可能優化應用main函數到didFinishLaunchingWithOptions的時間,但如何調試在調用代碼以前發生的啓動速度慢的狀況呢?框架

1.1 Pre-main時間的查看

在系統執行應用程序的main函數並調用應用程序委託函數(applicationWillFinishLaunching)以前,會發生不少事情。咱們能夠將DYLD_PRINT_STATISTICS環境變量添加到項目scheme中。

DYLD_PRINT_STATISTICS

運行一下,咱們能夠看到控制檯的輸出:

Total pre-main time: 1.2 seconds (100.0%)
         dylib loading time: 567.72 milliseconds (45.5%)
        rebase/binding time: 105.14 milliseconds (8.4%)
            ObjC setup time:  40.01 milliseconds (3.2%)
           initializer time: 532.47 milliseconds (42.7%)
           slowest intializers :
             libSystem.B.dylib :   4.70 milliseconds (0.3%)
          libglInterpose.dylib : 295.89 milliseconds (23.7%)
                  AFNetworking :  48.75 milliseconds (3.9%)
                         Oasis : 285.94 milliseconds (22.9%)
複製代碼

這是我使用iPhone 11 Pro的運行結果。這裏只是講解各個部分的做用,不討論如何優化和對比,不用深究這個時間。

注意:若是你要測試應用的最慢啓動時間,記得使用你支持的最慢的設備來進行測試。

輸出顯示系統調用應用程序main時所用的總時間,而後是主要步驟的分解。

WWDC 2016 Session 406優化應用程序啓動時間詳細介紹了每一個步驟以及改進時間的提示,如下是簡要的總結說明:

  • dylib loading time 動態加載程序查找並讀取應用程序使用的依賴動態庫。每一個庫自己均可能有依賴項。雖然蘋果系統框架的加載是高度優化的,但加載嵌入式框架可能會很耗時。爲了加快動態庫的加載速度,蘋果建議您使用更少的動態庫,或者考慮合併它們。
    • 建議的目標是六個額外的(非系統)框架
  • Rebase/binding time 修正調整鏡像內的指針(從新調整)和設置指向鏡像外符號的指針(綁定)。爲了加快從新定位/綁定時間,咱們須要更少的指針修復。
    • 若是有大量(大的是20000)Objective-C類、選擇器和類別的應用程序能夠增長800ms的啓動時間。
    • 若是應用程序使用C++代碼,那麼使用更少的虛擬函數。
    • 使用Swift結構體一般也更快。
  • ObjC setup time Objective-C運行時須要進行設置類、類別和選擇器註冊。咱們對從新定位綁定時間所作的任何改進也將優化這個設置時間。
  • initializer time 運行初始化程序。若是使用了Objective-C的 +load 方法,請將其替換爲 +initialize 方法。

在系統調用main以後,main將依次調用UIApplicationMain和應用程序委託方法。

1.2 動態庫與靜態庫加載的耗時

1.2.1 加載動態庫耗時

咱們先來看看工程裏面有多少動態庫:

  1. 在項目的Product文件夾找到咱們的工程.app文件,右鍵選擇Show in Finder
  2. 來到相應目錄後右鍵選擇顯示包內容
  3. 找到Frameworks文件夾,打開。
  4. 項目是純Swift編寫,下面都是系統Swift庫,咱們無法優化,能夠無論。

Product中Frameworks文件夾

能夠看到咱們的項目中有了36個動態庫,下面是pre-main的總時間:

Total pre-main time: 1.2 seconds (100.0%)
         dylib loading time: 567.72 milliseconds (45.5%)
        rebase/binding time: 105.14 milliseconds (8.4%)
            ObjC setup time:  40.01 milliseconds (3.2%)
           initializer time: 532.47 milliseconds (42.7%)
           slowest intializers :
             libSystem.B.dylib :   4.70 milliseconds (0.3%)
          libglInterpose.dylib : 295.89 milliseconds (23.7%)
                  AFNetworking :  48.75 milliseconds (3.9%)
                         Oasis : 285.94 milliseconds (22.9%)
複製代碼

1.2.2 使用靜態庫耗時

在Pod的工程中,選擇咱們使用的庫,而後點擊Build Settings,搜索或者找到Mach-O Type設置,修改Mach-O TypeStatic Library

staticlib

按照上面的步驟,把咱們的動態庫的Mach-O Type都改爲靜態庫,⇧+⌘+K執行一次Clean Build Folder,而後從新構建一次。

Product中Frameworks文件夾

這裏還保留了3個動態庫,是由於Objective-C沒有命名空間,有符號衝突,就保留了下來。下面是pre-main的總時間:

Total pre-main time: 877.84 milliseconds (100.0%)
         dylib loading time: 220.07 milliseconds (25.0%)
        rebase/binding time: 112.29 milliseconds (12.7%)
            ObjC setup time:  30.78 milliseconds (3.5%)
           initializer time: 514.70 milliseconds (58.6%)
           slowest intializers :
             libSystem.B.dylib :   4.33 milliseconds (0.4%)
          libglInterpose.dylib : 253.44 milliseconds (28.8%)
                  AFNetworking :  37.08 milliseconds (4.2%)
                        OCLibs :  61.75 milliseconds (7.0%)
                         Oasis : 246.28 milliseconds (28.0%)
複製代碼

能夠看到,經過修改Mach-O Type從動態庫改成靜態庫,dylib loading time獲得了很大的提高,而其餘部分的耗時變化不大。總時間從1.2s降到了大約0.9s,優化了大約0.3s的啓動時間。

1.2.3 遇到的坑

可是若是隻改Mach-O Type的話,Archive以後在Organizer中嘗試Validate App會報錯:

  • Found an unexpected Mach-O header code: 0x72613c21

0x72613c21

其實這裏是CocoaPods的一個配置問題,CocoaPods會在項目中的Build Phases添加一個 [CP] Embed Pods Frameworks 執行腳本。

"${PODS_ROOT}/Target Support Files/Pods-項目名/Pods-項目名-frameworks.sh"
複製代碼

咱們在執行pod install後會生成一個Pods-項目名-frameworks.sh的腳本文件。因爲咱們是手動修改的Mach-O Type類型,這個腳本中的install_framework仍然會執行,因此咱們要把轉換成靜態庫的這些庫從Pods-項目名-frameworks.sh文件中刪除。

AFNetworking爲例,須要從文件中刪除:

install_framework "${BUILT_PRODUCTS_DIR}/AFNetworking/AFNetworking.framework"
複製代碼

固然你也能夠寫一個ruby腳本在使用CocoaPodspost_install進行處理。

  1. 把相關的庫轉成靜態的。

    target.build_configurations.each do |config|
        config.build_settings['MACH_O_TYPE'] = 'staticlib'
    end
    複製代碼
  2. 讀取Pods-項目名-frameworks.sh文件,刪除相關的字符串。

    regex = /install_framework.*\/#{pod_name}\.framework\"/
    pod_frameworks_content.gsub!(regex, "")
    複製代碼

2. 二進制重排

2.1 App啓動

進程若是能直接訪問物理內存無疑是很不安全的,因此操做系統在物理內存的上又創建了一層虛擬內存。蘋果在這個基礎上還有 ASLR(Address Space Layout Randomization) 技術的保護,不過不是此次的重點。

iOS系統中虛擬內存到物理內存的映射都是以頁爲最小單位的。當進程訪問一個虛擬內存Page而對應的物理內存卻不存在時,就會出現Page Fault缺頁中斷,而後加載這一頁。雖然自己這個處理速度是很快的,可是在一個App的啓動過程當中可能出現上千(甚至更多)次Page Fault,這個時間積累起來會比較明顯了。

iOS系統中一頁是16KB。

咱們常說的啓動是指點擊App到第一頁顯示爲止,包含pre-mainmaindidFinishLaunchingWithOptions結束的整個時間。maindidFinishLaunchingWithOptions結束,這個部分是咱們能夠控制的,已經有不少文章講解應該怎麼優化了,不是本文的重點。這裏講的二進制重排主要是針對如何減小Page Fault的優化。

另外,還有兩個重要的概念:冷啓動熱啓動。可能有些同窗認爲殺掉再重啓App就是冷啓動了,實際上是不對的。

  • 冷啓動

    程序徹底退出,之間加載的分頁數據被其餘進程所使用覆蓋以後,或者重啓設備、第一次安裝,纔算是冷啓動。

  • 熱啓動

    程序殺掉以後,立刻又從新啓動。這個時候相應的物理內存中仍然保留以前加載過的分頁數據,能夠進行重用,不須要所有從新加載。因此熱啓動的速度比較快。

後面會利用Instruments工具System Trace更直觀地比較這兩種啓動。

2.2 二進制重排相關概念

2.2.1 二進制重排的意義

程序默認狀況下是順序執行的。

順序加載

若是啓動須要使用的方法分別在2頁Page1Page2中(method1method3),爲了執行相應的代碼,系統就必須進行兩個Page Fault

重排

若是咱們對方法進行從新排列,讓method1method3在一個Page,那麼就能夠較少一次Page Fault

那麼怎麼衡量重排效果並驗證呢?

  • 查看Page Fault次數是否減小。
  • 查看編譯過程的中間產物LinkMap文件進行確認。

2.2.2 System Trace

那麼如何衡量頁的加載時間呢?這裏就用到了Instruments中的System Trace工具。

首先,從新啓動設備(冷啓動)。⌘+I打開Instruments,選擇System Trace工具。

點擊錄製⏺後,出現第一個頁面,立刻中止⏹。過濾只顯示Main Thread相關,選擇Summary: Virtual Memory

  • File Backed Page In次數就是觸發Page Fault的次數了。
  • Page Cache Hit就是頁緩存命中的次數了。

冷啓動

下面咱們看看熱啓動的狀況。殺掉App,接着直接從新執行一遍以前的操做(不重啓):

熱啓動

對比冷啓動和熱啓動的File Backed Page In次數,能夠看到熱啓動狀況下,觸發的Page Fault的次數就變得很小了。

2.2.3 啓動順序

2.2.3.1 文件順序

Build PhasesCompile Sources列表順序決定了文件執行的順序(能夠調整)。若是不進行重排,文件的順序決定了方法、函數的執行順序。

Compile Sources

咱們在ViewControllerAppDelegate中加入如下代碼,並執行。

+ (void)load {
    NSLog(@"%s", __FUNCTION__);
}

//輸出
2020-04-23 22:56:13.551729+0800 BinaryOptimization[59505:5477304] +[ViewController load]
2020-04-23 22:56:13.553714+0800 BinaryOptimization[59505:5477304] +[AppDelegate load]
複製代碼

咱們調整Compile Sources中這兩個類的順序,而後再執行。

交換後

2020-04-23 23:00:08.248118+0800 BinaryOptimization[59581:5482198] +[AppDelegate load]
2020-04-23 23:00:08.249015+0800 BinaryOptimization[59581:5482198] +[ViewController load]
複製代碼

能夠看到,隨着Compile Sources中的文件順序的修改,+load方法的執行順序也發生了改變。

2.2.3.2 符號表順序

Build Settings中修改Write Link Map FileYES編譯後會生成一個Link Map符號表txt文件。

執行⌘ + B構建後,選擇Product中的App,在Finder中打開,選擇Intermediates.noindex文件夾,

Intermediates.noindex

找到LinkMap文件,這裏是BinaryOptimization-LinkMap-normal-arm64.txt

image.png

打開文件以後來到第一部分的最後。

LinkMap

咱們能夠看到這個順序和咱們Compile Sources中的順序是一致的。接下來的部分:

# Sections:
# Address	Size    	Segment	Section
0x100005ECC	0x0000065C	__TEXT	__text
0x100006528	0x0000009C	__TEXT	__stubs
0x1000065C4	0x000000B4	__TEXT	__stub_helper
0x100006678	0x000000BE	__TEXT	__cstring
0x100006736	0x00000D2B	__TEXT	__objc_methname
0x100007461	0x00000070	__TEXT	__objc_classname
0x1000074D1	0x00000ADA	__TEXT	__objc_methtype
0x100007FAC	0x00000054	__TEXT	__unwind_info
0x100008000	0x00000008	__DATA_CONST	__got
0x100008008	0x00000040	__DATA_CONST	__cfstring
0x100008048	0x00000018	__DATA_CONST	__objc_classlist
0x100008060	0x00000010	__DATA_CONST	__objc_nlclslist
0x100008070	0x00000020	__DATA_CONST	__objc_protolist
0x100008090	0x00000008	__DATA_CONST	__objc_imageinfo
0x10000C000	0x00000068	__DATA	__la_symbol_ptr
0x10000C068	0x00001348	__DATA	__objc_const
0x10000D3B0	0x00000018	__DATA	__objc_selrefs
0x10000D3C8	0x00000010	__DATA	__objc_classrefs
0x10000D3D8	0x00000008	__DATA	__objc_superrefs
0x10000D3E0	0x00000004	__DATA	__objc_ivar
0x10000D3E8	0x000000F0	__DATA	__objc_data
0x10000D4D8	0x00000188	__DATA	__data
複製代碼

這個是Mach-O的一些信息,不是此次的重點。接在這部分以後的符號纔是,因爲比較多,我只截取了部分。

# Symbols:
# Address	  Size    	  File  Name
0x100005ECC	0x0000003C	[  1] +[AppDelegate load]
0x100005F08	0x00000088	[  1] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100005F90	0x00000108	[  1] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x100006098	0x00000080	[  1] -[AppDelegate application:didDiscardSceneSessions:]
0x100006118	0x0000003C	[  2] +[ViewController load]
0x100006154	0x0000004C	[  2] -[ViewController viewDidLoad]
0x1000061A0	0x000000A0	[  3] _main
0x100006240	0x000000B4	[  4] -[SceneDelegate scene:willConnectToSession:options:]
0x1000062F4	0x0000004C	[  4] -[SceneDelegate sceneDidDisconnect:]
0x100006340	0x0000004C	[  4] -[SceneDelegate sceneDidBecomeActive:]
0x10000638C	0x0000004C	[  4] -[SceneDelegate sceneWillResignActive:]
0x1000063D8	0x0000004C	[  4] -[SceneDelegate sceneWillEnterForeground:]
0x100006424	0x0000004C	[  4] -[SceneDelegate sceneDidEnterBackground:]
0x100006470	0x0000002C	[  4] -[SceneDelegate window]
0x10000649C	0x00000048	[  4] -[SceneDelegate setWindow:]
0x1000064E4	0x00000044	[  4] -[SceneDelegate .cxx_destruct]
0x100006528	0x0000000C	[  5] _NSLog
0x100006534	0x0000000C	[  5] _NSStringFromClass
0x100006540	0x0000000C	[  7] _UIApplicationMain
0x10000654C	0x0000000C	[  6] _objc_alloc
0x100006558	0x0000000C	[  6] _objc_autoreleasePoolPop
0x100006564	0x0000000C	[  6] _objc_autoreleasePoolPush
...
複製代碼

能夠看到,總體的順序和Compile Sources的中的順序是同樣的,而且方法是按照文件中方法的順序進行連接的。AppDelegate中的方法添加完後,纔是ViewController中的方法,以此類推。

  • Address 表示文件中方法的地址。
  • Size 表示方法的大小。
  • File 表示在第幾個文件中。
  • Name 表示方法名。

2.2.4 二進制重排初體驗

在項目根目錄建立一個order文件。

touch BinaryOptimization.order
複製代碼

而後在Build Settings中找到Order File,填入./BinaryOptimization.order

Order File

BinaryOptimization.order文件中填入:

+[ViewController load]
+[AppDelegate load]
_main
-[ViewController someMethod]
複製代碼

而後執行⌘ + B構建。

image.png

能夠看到Link Map中的最上面幾個方法和咱們在BinaryOptimization.order文件中設置的方法順序一致!

Xcode的鏈接器ld還忽略掉了不存在的方法 -[ViewController someMethod]

若是提供了link選項 -order_file_statistics,會以warning的形式把這些沒找到的符號打印在日誌裏。

2.3 二進制重排實戰

要真正的實現二進制重排,咱們須要拿到啓動的全部方法、函數等符號,並保存其順序,而後寫入order文件,實現二進制重排。

抖音有一篇文章抖音研發實踐:基於二進制文件重排的解決方案 APP啓動速度提高超15%,可是文章中也提到了瓶頸:

基於靜態掃描+運行時trace的方案仍然存在少許瓶頸:

  • initialize hook不到
  • 部分block hook不到
  • C++經過寄存器的間接函數調用靜態掃描不出來

目前的重排方案可以覆蓋到80%~90%的符號,將來咱們會嘗試編譯期插樁等方案來進行100%的符號覆蓋,讓重排達到最優效果。

同時也給出瞭解決方案編譯期插樁

2.3.1 Clang插樁

其實就是一個代碼覆蓋工具,更多信息能夠查看官網

Build SettingsOther C Flags添加-fsanitize-coverage=trace-pc-guard配置,編譯的話會報錯。

Undefined symbol: ___sanitizer_cov_trace_pc_guard_init
Undefined symbol: ___sanitizer_cov_trace_pc_guard
複製代碼

查看官網會須要咱們添加一個兩個函數:

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

// This callback is inserted by the compiler as a module constructor
// into every DSO. 'start' and 'stop' correspond to the
// beginning and end of the section with the guards for the entire
// binary (executable or DSO). The callback will be called at least
// once per DSO and may be called multiple times with the same parameters.
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);
}
複製代碼

咱們把代碼添加到ViewController.m中,咱們不須要 extern "C" 因此能夠刪掉, __sanitizer_symbolize_pc() 還會報錯,不重要先註釋了而後繼續。

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

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

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
// void *PC = __builtin_return_address(0);
  char PcDescr[1024];
// __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
複製代碼

函數 __sanitizer_cov_trace_pc_guard_init統計了方法的個數。運行後,咱們能夠看到:

INIT: 0x104bed670 0x104bed6b0

(lldb) x 0x104bed670
0x104bed670: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x104bed680: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb) x 0x104bed6b0-0x4
0x104bed6ac: 10 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00  ................
0x104bed6bc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
複製代碼

讀取內存以後,咱們能夠看到一個相似計數器的東西。最後一個打印的是結束位置,按顯示是4位4位的,因此向前移動4位,打印出來的應該就是最後一位。

根據小端模式,10 00 00 00對應的是00 00 00 10即16。咱們在ViewController中添加一些方法:

void(^block)(void) = ^(void){
    
};

void test() {
    block();
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    test();
}
複製代碼

再打印一次:

(lldb) x 0x10426d6dc-0x4
0x10426d6d8: 13
複製代碼

能夠看到增長了3(block是匿名函數),計數器統計了函數/方法的個數,這裏添加了三個,索引增長了3。

咱們再點擊一下屏幕:

guard: 0x1007196ac 8 PC 
guard: 0x1007196a8 7 PC 
guard: 0x1007196a4 6 PC Hq
複製代碼

咱們發現,每點擊一次屏幕就有3個打印。咱們在touchesBegan:touches withEvent:開頭設置一個點斷,並開啓彙編顯示(菜單欄DebugDebug WorkflowAlways Show Disassembly)。

斷點彙編

若是咱們查看其餘函數也會發現彙編代碼中有相似的顯示。

也就是說Clang插樁就是在彙編代碼中插入了 __sanitizer_cov_trace_pc_guard函數的調用。

拿到了所有的符號以後須要保存,可是不能用數組,由於有可能會有在子線程執行的,因此用數組會有線程問題 。這裏咱們使用原子隊列:

#import <libkern/OSAtomic.h>
#import <dlfcn.h>

/* 原子隊列特色 一、先進後出 二、線程安全 三、只能保存結構體 */
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

// 符號結構體鏈表
typedef struct {
    void *pc;
    void *next;
} SymbolNode;

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;  // Duplicate the guard check.
    
    // 函數執行前會將下一個要執行的函數地址保存到寄存器中
    // 這裏是拿到函數的返回地址
    void *PC = __builtin_return_address(0);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC, NULL};
    // 入隊
    OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
    
    // 如下是一些打印,只是看一下,實際中能夠註釋
    // dlopen 經過動態庫拿到句柄 經過句柄拿到函數的內存地址
    // dladdr 經過函數內存地址拿到函數
    typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object 函數的路徑 */
        void            *dli_fbase;     /* Base address of shared object 函數的地址 */
        const char      *dli_sname;     /* Name of nearest symbol 函數符號 */
        void            *dli_saddr;     /* Address of nearest symbol 函數起始地址 */
    } Dl_info;
    Dl_info info;
    dladdr(PC, &info);
    printf("fnam:%s \n fbase:%p \n sname:%s \n saddr:%p \n",
           info.dli_fname,
           info.dli_fbase,
           info.dli_sname,
           info.dli_saddr);
}
複製代碼

運行後這裏咱們能夠看到不少打印,只取一條來講明,很明顯其中sname就是咱們須要的符號名了。

fnam:/private/var/containers/Bundle/Application/3EAE3817-0EF7-4892-BC55-368CC504A568/BinaryOptimization.app/BinaryOptimization 
 fbase:0x100938000 
 sname:+[AppDelegate load] 
 saddr:0x10093d81c 
複製代碼

下面咱們經過點擊屏幕導出所須要的符號,須要注意的是C函數和Swift方法前面須要加下劃線。(這裏點能夠在前面提到的LinkMap文件中確認)

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSMutableArray <NSString *>* symbolNames = [NSMutableArray array];
    while (YES) {
        SymbolNode * node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
        if (node == NULL) {
            break;
        }
        Dl_info info;
        dladdr(node->pc, &info);
        
        NSString * name = @(info.dli_sname);
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["]; //OC方法不處理
        NSString * symbolName = isObjc? name : [@"_" stringByAppendingString:name]; //c函數、swift方法前面帶下劃線
        [symbolNames addObject:symbolName];
        printf("%s \n",info.dli_sname);
    }
    
    NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
    NSMutableArray<NSString*>* funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    while (name = [emt nextObject]) {
        if (![funcs containsObject:name]) {
            [funcs addObject:name];
        }
    }
    // 刪掉當前方法,由於這個點擊方法不是啓動須要的
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"BinaryOptimization.order"];
    NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    // 在路徑上建立文件
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    
    NSLog(@"%@",filePath);
}
複製代碼

這時若是你直接點擊屏幕,有個巨坑,會看到控制檯一直在輸出,出現了死循環:

-[ViewController touchesBegan:withEvent:] 
-[ViewController touchesBegan:withEvent:] 
...
複製代碼

咱們在while裏面設置一個斷點:

image.png

發現 __sanitizer_cov_trace_pc_guard竟然有10個,這個地方會觸發 __sanitizer_cov_trace_pc_guard中的入隊,這裏又進行出隊,最後就死循環了。

解決辦法:

Build SettingsOther C Flags添加func配置,即-fsanitize-coverage=func,trace-pc-guard

官網對func的參數的解釋:只檢測每一個函數的入口。

再次運行點擊屏幕就不會有問題了。

2.3.2 從真機上獲取order文件

咱們把order文件存在了真機上的tmp文件夾中,要怎麼拿到呢?

WindowDevices And Simulators(快捷鍵⇧+⌘+2)中:

獲取真機文件

2.3.3 Swift

Swift也能夠重排麼?固然能夠!

咱們在項目中添加一個Swift類,而後在viewDidLoad調用一下:

class SwiftTest: NSObject {
    @objc class public func swiftTestLoad(){
        print("swiftTest");
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [SwiftTest swiftTestLoad];
}
複製代碼

Build SettingOther Swift Flags設置:

-sanitize-coverage=func
-sanitize=undefined
複製代碼

運行後點擊一下屏幕,查看控制檯:

-[ViewController touchesBegan:withEvent:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate sceneDidBecomeActive:] 
-[SceneDelegate sceneWillEnterForeground:] 
// 下面這4個就是Swift的
$ss5print_9separator10terminatoryypd_S2StFfA1_ 
$ss5print_9separator10terminatoryypd_S2StFfA0_ 
$s18BinaryOptimization9SwiftTestC05swiftD4LoadyyFZ 
$s18BinaryOptimization9SwiftTestC05swiftD4LoadyyFZTo 
-[ViewController viewDidLoad] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate scene:willConnectToSession:options:] 
-[SceneDelegate window] 
-[SceneDelegate window] 
-[SceneDelegate setWindow:] 
-[SceneDelegate window] 
-[AppDelegate application:didFinishLaunchingWithOptions:] 
main 
2020-04-24 13:08:43.923191+0800 BinaryOptimization[459:65420] /private/var/mobile/Containers/Data/Application/DA2EC6F0-93C9-45A0-9D95-C21883E0532C/tmp/BinaryOptimization.order
複製代碼

全部處理完以後,最後須要Write Link Map File改成NO,把Other C Flags/Other Swift Flags的配置刪除掉。

由於這個配置會在咱們代碼中自動插入跳轉執行 __sanitizer_cov_trace_pc_guard。重排完就不須要了,須要去除掉。 同時把ViewController中的 __sanitizer_cov_trace_pc_guard也要去除掉。

2.3.4 二進制重排先後的對比

在項目中進行實踐並測試以後:

  • 進行二進制重排前,File Backed Page In(Page Fault Count)發生了2569次,耗時298ms

Page Fault Count

  • 進行二進制重排後,File Backed Page In(Page Fault Count)發生了2311次,耗時248ms

Page Fault Count

能夠看到,通過二進制重排減小了Page Fault的次數,總時間從298ms降到了大約248ms,優化了大約50ms的啓動時間。

3. 總結

  1. 經過將動態庫轉爲靜態庫,咱們優化了dylib loading time
    • 蘋果官方建議爲6個如下,這裏咱們由於符號衝突,只保留了3個動態庫。
  2. 經過二進制重排,讓啓動須要的方法排列更緊湊,減小了Page Fault的次數。
    • 獲取符號表時,採用Clang插樁能夠直接hook到Objective-C方法、Swift方法、C函數、Block,能夠不用區別對待。相比於抖音以前提出的方案確實簡單不少,門檻也要低一些。

重要:

有朋友問到Pod中的三方庫可否加入order文件中,答案是能夠的!

文中的二進制重排實踐過程,考慮了三方庫的啓動時須要的符號。文章裏面沒有特別說明,但原理是同樣的。

相關文章
相關標籤/搜索