綠洲iOS研發工程師,綠洲ID:收納箱KeepFit。html
啓動是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
蘋果建議將應用程序的總啓動時間設定在400毫秒如下,而且咱們必須在20秒以內完成啓動,不然系統會殺死咱們的應用程序。咱們能夠儘可能優化應用main函數到didFinishLaunchingWithOptions的時間,但如何調試在調用代碼以前發生的啓動速度慢的狀況呢?框架
在系統執行應用程序的main函數並調用應用程序委託函數(applicationWillFinishLaunching)以前,會發生不少事情。咱們能夠將DYLD_PRINT_STATISTICS環境變量添加到項目scheme中。
運行一下,咱們能夠看到控制檯的輸出:
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優化應用程序啓動時間詳細介紹了每一個步驟以及改進時間的提示,如下是簡要的總結說明:
在系統調用main以後,main將依次調用UIApplicationMain和應用程序委託方法。
咱們先來看看工程裏面有多少動態庫:
Product
文件夾找到咱們的工程.app
文件,右鍵選擇Show in Finder。能夠看到咱們的項目中有了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%)
複製代碼
在Pod的工程中,選擇咱們使用的庫,而後點擊Build Settings,搜索或者找到Mach-O Type設置,修改Mach-O Type爲Static Library。
按照上面的步驟,把咱們的動態庫的Mach-O Type都改爲靜態庫,⇧+⌘+K執行一次Clean Build Folder,而後從新構建一次。
這裏還保留了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的啓動時間。
可是若是隻改Mach-O Type的話,Archive以後在Organizer中嘗試Validate App會報錯:
其實這裏是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
腳本在使用CocoaPods的post_install進行處理。
把相關的庫轉成靜態的。
target.build_configurations.each do |config|
config.build_settings['MACH_O_TYPE'] = 'staticlib'
end
複製代碼
讀取Pods-項目名-frameworks.sh
文件,刪除相關的字符串。
regex = /install_framework.*\/#{pod_name}\.framework\"/
pod_frameworks_content.gsub!(regex, "")
複製代碼
進程若是能直接訪問物理內存無疑是很不安全的,因此操做系統在物理內存的上又創建了一層虛擬內存。蘋果在這個基礎上還有 ASLR(Address Space Layout Randomization) 技術的保護,不過不是此次的重點。
iOS系統中虛擬內存到物理內存的映射都是以頁爲最小單位的。當進程訪問一個虛擬內存Page而對應的物理內存卻不存在時,就會出現Page Fault缺頁中斷,而後加載這一頁。雖然自己這個處理速度是很快的,可是在一個App的啓動過程當中可能出現上千(甚至更多)次Page Fault,這個時間積累起來會比較明顯了。
iOS系統中一頁是16KB。
咱們常說的啓動是指點擊App到第一頁顯示爲止,包含pre-main、main到didFinishLaunchingWithOptions結束的整個時間。main到didFinishLaunchingWithOptions結束,這個部分是咱們能夠控制的,已經有不少文章講解應該怎麼優化了,不是本文的重點。這裏講的二進制重排主要是針對如何減小Page Fault的優化。
另外,還有兩個重要的概念:冷啓動、熱啓動。可能有些同窗認爲殺掉再重啓App就是冷啓動了,實際上是不對的。
冷啓動
程序徹底退出,之間加載的分頁數據被其餘進程所使用覆蓋以後,或者重啓設備、第一次安裝,纔算是冷啓動。
熱啓動
程序殺掉以後,立刻又從新啓動。這個時候相應的物理內存中仍然保留以前加載過的分頁數據,能夠進行重用,不須要所有從新加載。因此熱啓動的速度比較快。
後面會利用Instruments工具System Trace更直觀地比較這兩種啓動。
程序默認狀況下是順序執行的。
若是啓動須要使用的方法分別在2頁Page1和Page2中(method1和method3),爲了執行相應的代碼,系統就必須進行兩個Page Fault。
若是咱們對方法進行從新排列,讓method1和method3在一個Page,那麼就能夠較少一次Page Fault。
那麼怎麼衡量重排效果並驗證呢?
那麼如何衡量頁的加載時間呢?這裏就用到了Instruments中的System Trace工具。
首先,從新啓動設備(冷啓動)。⌘+I打開Instruments,選擇System Trace工具。
點擊錄製⏺後,出現第一個頁面,立刻中止⏹。過濾只顯示Main Thread相關,選擇Summary: Virtual Memory。
下面咱們看看熱啓動的狀況。殺掉App,接着直接從新執行一遍以前的操做(不重啓):
對比冷啓動和熱啓動的File Backed Page In次數,能夠看到熱啓動狀況下,觸發的Page Fault的次數就變得很小了。
Build Phases中Compile Sources列表順序決定了文件執行的順序(能夠調整)。若是不進行重排,文件的順序決定了方法、函數的執行順序。
咱們在ViewController和AppDelegate中加入如下代碼,並執行。
+ (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方法的執行順序也發生了改變。
Build Settings中修改Write Link Map File爲YES
編譯後會生成一個Link Map符號表txt
文件。
執行⌘ + B構建後,選擇Product中的App,在Finder中打開,選擇Intermediates.noindex文件夾,
找到LinkMap文件,這裏是BinaryOptimization-LinkMap-normal-arm64.txt。
打開文件以後來到第一部分的最後。
咱們能夠看到這個順序和咱們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
表示方法名。在項目根目錄建立一個order文件。
touch BinaryOptimization.order
複製代碼
而後在Build Settings中找到Order File,填入./BinaryOptimization.order
。
在BinaryOptimization.order
文件中填入:
+[ViewController load]
+[AppDelegate load]
_main
-[ViewController someMethod]
複製代碼
而後執行⌘ + B構建。
能夠看到Link Map中的最上面幾個方法和咱們在BinaryOptimization.order文件中設置的方法順序一致!
Xcode的鏈接器ld還忽略掉了不存在的方法 -[ViewController someMethod]。
若是提供了link選項 -order_file_statistics,會以warning的形式把這些沒找到的符號打印在日誌裏。
要真正的實現二進制重排,咱們須要拿到啓動的全部方法、函數等符號,並保存其順序,而後寫入order
文件,實現二進制重排。
抖音有一篇文章抖音研發實踐:基於二進制文件重排的解決方案 APP啓動速度提高超15%,可是文章中也提到了瓶頸:
基於靜態掃描+運行時trace的方案仍然存在少許瓶頸:
- initialize hook不到
- 部分block hook不到
- C++經過寄存器的間接函數調用靜態掃描不出來
目前的重排方案可以覆蓋到80%~90%的符號,將來咱們會嘗試編譯期插樁等方案來進行100%的符號覆蓋,讓重排達到最優效果。
同時也給出瞭解決方案編譯期插樁。
其實就是一個代碼覆蓋工具,更多信息能夠查看官網。
Build Settings中 Other 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:開頭設置一個點斷,並開啓彙編顯示(菜單欄Debug→Debug Workflow→Always 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裏面設置一個斷點:
發現 __sanitizer_cov_trace_pc_guard竟然有10個,這個地方會觸發 __sanitizer_cov_trace_pc_guard中的入隊,這裏又進行出隊,最後就死循環了。
解決辦法:
Build Settings中Other C Flags添加func
配置,即-fsanitize-coverage=func,trace-pc-guard
。
官網對func的參數的解釋:只檢測每一個函數的入口。
再次運行點擊屏幕就不會有問題了。
咱們把order
文件存在了真機上的tmp
文件夾中,要怎麼拿到呢?
在Window→Devices And Simulators(快捷鍵⇧+⌘+2)中:
Swift也能夠重排麼?固然能夠!
咱們在項目中添加一個Swift類,而後在viewDidLoad
調用一下:
class SwiftTest: NSObject {
@objc class public func swiftTestLoad(){
print("swiftTest");
}
}
- (void)viewDidLoad {
[super viewDidLoad];
[SwiftTest swiftTestLoad];
}
複製代碼
Build Setting中Other 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也要去除掉。
在項目中進行實踐並測試以後:
能夠看到,通過二進制重排減小了Page Fault的次數,總時間從298ms降到了大約248ms,優化了大約50ms的啓動時間。
重要:
有朋友問到Pod中的三方庫可否加入order文件中,答案是能夠的!
文中的二進制重排實踐過程,考慮了三方庫的啓動時須要的符號。文章裏面沒有特別說明,但原理是同樣的。