IOS優(yōu)化:啟動時間

原創(chuàng):知識點總結(jié)性文章
創(chuàng)作不易,請珍惜瘾晃,之后會持續(xù)更新贷痪,不斷完善
個人比較喜歡做筆記和寫總結(jié),畢竟好記性不如爛筆頭哈哈蹦误,這些文章記錄了我的IOS成長歷程劫拢,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉(zhuǎn),大家可通過command + F 輸入目錄標(biāo)題后迅速尋找到你所需要的內(nèi)容

目錄

  • 一强胰、檢測啟動時間
  • 二舱沧、為什么需要二進制重排
  • 三、二進制重排原理
  • 四哪廓、調(diào)試 Page Fault
  • 五狗唉、order文件
  • 六、靜態(tài)插樁代碼覆蓋工具的機制和原理
  • 七涡真、寫入order文件
  • 八分俯、驗證插樁前后差距
  • 九、全文總結(jié)
  • 十哆料、摒棄解釋缸剪,直接使用

一、檢測啟動時間

冷啟動:指APP被后臺kill后重新啟動APP东亦,這種啟動方式叫做冷啟動杏节。
熱啟動:APP的狀態(tài)由running切換為suspend,APP 沒有被kill仍然在后臺運行典阵。再次把APP切換到前臺奋渔,這種啟動方式叫熱啟動。

啟動時間的劃分可以把main()函數(shù)作為關(guān)鍵點分割成兩塊壮啊。t1階段蚣常,main()之前的處理所需時間世吨,稱為pre-main。t2階段,main()main()之后處理所需時間俱尼。t2階段耗時的主要是業(yè)務(wù)代碼推薦 BLStopwatch抖韩,這個工具可以打點統(tǒng)計業(yè)務(wù)耗時 本部分優(yōu)化根據(jù)各自業(yè)務(wù)需求自行處理缤弦。

通過添加環(huán)境變量可以獲取到pre-main階段的時間谢鹊。Xcode 中提供了測量 pre-main的時間 檢測。Edit scheme -> Run -> Auguments 添加環(huán)境變量 DYLD_PRINT_STATISTICS拓萌,value設(shè)為YES岁钓。

啟動以后可以看到啟動時長:

加載dylib

分析每個dylib(大部分是系統(tǒng)的),找到其Mach-O文件,打開并讀取驗證有效性甜紫;找到代碼簽名注冊到內(nèi)核降宅,最后對dylib的每個segment調(diào)用mmap()

rebase/bind

dylib加載完成之后囚霸,它們處于相互獨立的狀態(tài),需要綁定起來激才。Rebase將鏡像讀入內(nèi)存拓型,修正鏡像內(nèi)部的指針,性能消耗主要在IO瘸恼。Bind是查詢符號表劣挫,設(shè)置指向鏡像外部的指針,性能消耗主要在CPU計算东帅。

Objc setup

runtime會維護一張類名與類的方法列表的全局表压固。讀取所有類,將類對象其注冊到這個全局表中(class registration)靠闭。讀取所有分類帐我,把分類加載到類對象中(category registration)。檢查selector的唯一性(selector uniquing)愧膀。

initalizer time

這部分其實就是load方法的耗時拦键。

優(yōu)化思路

移除不需要用到的動態(tài)庫,盡量使用系統(tǒng)庫檩淋,且蘋果建議數(shù)量控制在 6 個以下
移除不需要用到的類芬为;合并功能類似的類和擴展;經(jīng)測試 20000 個類會增加約 800毫秒
盡量進行懶加載蟀悦,盡量避免在load()方法里執(zhí)行操作媚朦,把操作推遲到initialize()方法


二、為什么需要二進制重排

當(dāng)我們向操作系統(tǒng)申請內(nèi)存時日戈,操作系統(tǒng)并不是直接分配給我們物理內(nèi)存询张,而是只標(biāo)記當(dāng)前進程擁有該段內(nèi)存,當(dāng)真正使用這段內(nèi)存時才會分配涎拉。這種延遲分配物理內(nèi)存的方式就通過 page fault 機制來實現(xiàn)的瑞侮。

當(dāng)我們訪問一個內(nèi)存地址時,如果該地址非法鼓拧,或者我們對其沒有訪問權(quán)限半火,或者該地址對應(yīng)的物理內(nèi)存還未分配, cpu 都會生成一個 page fault 季俩,進而執(zhí)行操作系統(tǒng)的 page fault handler钮糖。如果是因為還未分配物理內(nèi)存,操作系統(tǒng)會立即分配物理內(nèi)存給當(dāng)前進程,然后重試產(chǎn)生這個page fault的內(nèi)存訪問指令店归。

程序加載時阎抒,不可能一下全部加載到內(nèi)存,類似懶加載消痛。這樣就導(dǎo)致訪問虛擬內(nèi)存的某一頁時且叁,沒有和真正的物理內(nèi)存進行映射,導(dǎo)致page fault秩伞。每次page fault都會阻塞進程逞带,耗費5ms左右的時間。

對用戶而言纱新,使用App時第一個直接體驗就是啟動 App 時間展氓,而啟動時期會有大量的類、分類脸爱、三方等等需要加載和執(zhí)行遇汞,此時多個Page Fault所產(chǎn)生的的耗時往往是不能小覷的,下面我們就通過二進制重排來優(yōu)化啟動耗時簿废。


三空入、二進制重排原理

程序啟動時,會加載必要的page捏鱼,為了減少page fault的數(shù)量执庐,加快啟動速度,可以把必要的代碼盡量合并到一個page中导梆。iOS是ld鏈接器轨淌,可以通過order文件進行二進制重排達(dá)到這個目的。

假設(shè)在啟動時期我們需要調(diào)用兩個函數(shù) method1method4看尼,函數(shù)編譯在 mach-O 中的位置是根據(jù) ld ( Xcode 的鏈接器) 的編譯順序并非調(diào)用順序來的递鹉,因此很可能這兩個函數(shù)分布在不同的內(nèi)存頁上。如下圖藏斩,那么啟動時躏结,page1page2 都需要從無到有加載到物理內(nèi)存中,從而觸發(fā)兩次Page Fault狰域。二進制重排的做法就是將 method1method4 放到一個內(nèi)存頁中媳拴,那么啟動時則只需要加載一次 page 即可,也就是只觸發(fā)一次 Page Fault兆览。在實際項目中屈溉,我們可以將啟動時需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁中 ) 以盡可能減少 Page Fault,進而減少啟動耗時抬探。


四子巾、調(diào)試 Page Fault

最好是卸載App,重新安裝,調(diào)試第一次啟動的效果线梗。如果多次啟動調(diào)試椰于,你會發(fā)現(xiàn)count的波動范圍很大。所以如果想獲取準(zhǔn)確的數(shù)據(jù)仪搔,最好重新安裝App或者打開多個App之后瘾婿,再來調(diào)試。這是因為內(nèi)存管理機制僻造,殺掉進程時憋他,他所占用的物理內(nèi)存空間,如果沒有被覆蓋使用髓削,那么這部分內(nèi)存有很大可能一直存在。重新打開镀娶,內(nèi)存就不需要全部初始化立膛。所以 冷熱啟動的界定不能以是否后臺殺死來簡單判斷。

首先Xcode跑對應(yīng)項目到手機上梯码,然后每次殺后臺宝泵,重啟,打開Xcode選中 Open Developer ToolInstruments面板轩娶,選擇System Trace儿奶,選擇真機設(shè)備,點擊運行按鈕鳄抒,等待首頁出現(xiàn)點擊?停止闯捎,等待分析完成。刪除過濾條件许溅,直接輸入main瓤鼻,找到項目的target箭頭展開,點擊Main thread贤重,根據(jù)圖中選擇Virtual Memory 查看 File Backed Page In次數(shù)茬祷。

  1. 打開Instruments,選擇System Trace并蝗。
  1. 選擇真機祭犯,選擇工程,選擇啟動滚停,當(dāng)頁面加載出來的時候沃粗,停止。

這里面File Backed Page In就是page fault的次數(shù)铐刘。當(dāng)我們把APP殺死后里面再啟動陪每,結(jié)果發(fā)現(xiàn)File Backed Page In這個值變得很小,說明APP就算殺死后,在啟動不是冷啟動檩禾,還是有一部數(shù)據(jù)在系統(tǒng)的緩存中挂签。如何才是真正的冷啟動呢,我們可以把APP殺掉后啟動多個手機里面的APP盼产,然后再啟動APP饵婆,發(fā)現(xiàn)File Backed Page In又變得很大。二進制重排是在鏈接階段生成的戏售,重排之后生成可執(zhí)行文件侨核,所以我們只能在編譯階段來優(yōu)化,而無法對已生成的ipa進行優(yōu)化灌灾。


五搓译、order文件

前面說了這么多,那么具體該怎么操作呢锋喜?蘋果其實已經(jīng)給我們提供了這個機制些己。實際上 二進制重排就是對即將生成的可執(zhí)行文件重新排列,即它發(fā)生在鏈接階段嘿般。首先段标,Xcode 用的鏈接器叫做 ldld 有一個參數(shù)叫 Order File炉奴,我們可以通過這個參數(shù)配置一個后綴名為order的文件路徑逼庞。在這個 order 文件中,將你需要的符號按順序?qū)懺诶锩嬲案稀.?dāng)工程 build 的時候赛糟,Xcode 會讀取這個文件,打的二進制包就會按照這個文件中的符號順序進行生成對應(yīng)的 mach-O共耍。

我們可以在XCode配置二進制重排虑灰,首先我們要確定符號的順序,才能知道怎么重排痹兜,XCode使用的鏈接器叫做ld穆咐,ld有個參數(shù)叫order_file,我們可以將文件的路徑告訴XCode字旭,在order_file文件中把符號的順序?qū)戇M去对湃,XCode編譯的時候就會按照文件中的符號順序打包成二進制可執(zhí)行文件。

我們可以在蘋果的objc4-750源碼中找到這種文件遗淳。

可以參考一下libObjc 項目拍柒,它已經(jīng)使用了二進制重排進行優(yōu)化。這些都是ios應(yīng)用啟動加載過程中熟悉的方法屈暗。order 文件里符號寫錯了或不存在會不會有問題拆讯?ld 會忽略這些符號脂男,如果提供了link 選項-order_file_statistics,他們會以warning的形式把這些沒找到的符號打印在日志里种呐。會不會影響上架宰翅?不會,order文件只是重新排列了所生成的mach-O(可執(zhí)行文件) 中函數(shù)表與符號表的順序爽室。打開后是下面這種格式:

里面全是函數(shù)符號汁讼,我們打開項目,在build setting 里面搜索order file阔墩,發(fā)現(xiàn)這里面指定了order的文件路徑嘿架,因為一旦在這里指定了order file的路徑,XCode就會在編譯的時候按照文件里面寫進去的順序啸箫。

我們現(xiàn)在寫一個Demo耸彪,AppDelegate添加如下方法。

+ (void)test111 {
    NSLog(@"test111");
}

+ (void)test222 {
    NSLog(@"test222");
}

+ (void)test333 {
    NSLog(@"test333");
}

然后編譯忘苛,如何查看整個項目的符號順序呢搜囱,我們到Build Settings搜索Link MapLink Map就是我們鏈接的符號表柑土,我們把它改成YES,這樣編譯的時候就會把鏈接的符號表給我們寫出來绊汹。

command + R我們運行下稽屏,然后在Products里面的.app文件,在我們Intermediates.noindex-->項目名.build--->Debug-iphoneos-->項目名.build--->項目名-LinkMap-normal-x86_64.txt西乖,這個文件里面就有鏈接的符號順序表狐榔。

我們在項目中用touch創(chuàng)建test.order文件,修改方法順序获雕。

然后在Build setting里面搜下order file在后面將該文件地址添加進去薄腻。

這樣Xcode在編譯時候就會按照order文件中的符號順序鏈接代碼了,我們編譯一下届案,再看一下LinkMap-normal-x86_64.txt文件庵楷。

我們發(fā)現(xiàn)是按照order的符號順序來的,而且如果order里面寫了項目中不存在的方法符號楣颠,XCode會自動過濾掉尽纽,不存在影響。我們二進制重排并非只是修改符號地址 , 而是利用符號順序 , 重新排列整個代碼在文件的偏移地址 , 將啟動需要加載的方法地址放到前面內(nèi)存頁中 , 以此達(dá)到減少 page fault 的次數(shù)從而實現(xiàn)時間上的優(yōu)化 , 一定要清楚這一點童漩。

項目實戰(zhàn)

1弄贿、新建一個項目,添加方法

2矫膨、修改配置差凹,編譯期奔,找到xxx.txt文件

3、新建一個order文件:touch binary.order危尿,加入幾個方法

-[ViewController test3]
-[ViewController test2]
-[ViewController test1]

4呐萌、修改Order File配置為:$(SRCROOT)/Binary/binary.order 或 ./Binary/binary.order

5、clean脚线,編譯搁胆,再次查看xxx.txt文件。

可以看到邮绿,我們所寫的這三個方法已經(jīng)被放到最前面了渠旁,也就是說,這三個方法被放到了距離 mach-O 中首地址偏移量最小位置船逮。假設(shè)這三個方法原本在不同的三頁顾腊,那么意味著我們已經(jīng)優(yōu)化掉了兩個 Page Fault。到這里挖胃,離啟動優(yōu)化就只差一步了杂靶,如何獲取啟動運行的函數(shù)?我們采用clang 插樁的技術(shù)方案酱鸭,這樣完全拿到 swift吗垮、oc、c凹髓、block 全部函數(shù)烁登。


六、靜態(tài)插樁代碼覆蓋工具的機制和原理

簡單來說 SanitizerCoverage 是 Clang 內(nèi)置的一個代碼覆蓋工具蔚舀。它把一系列以 __sanitizer_cov_trace_pc_ 為前綴的函數(shù)調(diào)用插入到用戶定義的函數(shù)里饵沧,借此實現(xiàn)了全局 AOP 的大殺器。其覆蓋之廣赌躺,包含 Swift/Objective-C/C/C++ 等語言狼牺,Method/Function/Block 全支持。

開啟SanitizerCoverage 的方法是:在build settings 里的 Other C Flags 中添加 -fsanitize-coverage=func,trace-pc-guard礼患。如果含有 Swift 代碼的話是钥,還需要在 Other Swift Flags 中加入 -sanitize-coverage=func-sanitize=undefined。所有鏈接到 App 中的二進制都需要開啟 SanitizerCoverage讶泰,這樣才能完全覆蓋到所有調(diào)用咏瑟。通過llvm 插樁的確定 order_file 的方案,需要使用源碼重新打包痪署。如果項目全是已經(jīng)編譯好的二進制模塊码泞,使用該方案效果不佳。

騰訊大神寫了個工具 AppOrderFiles狼犯。CocoaPods 接入余寥,程序啟動完成函數(shù)一行調(diào)用领铐,生成 Order File。全在 GitHub 里了:github.com/yulingtianx…

AppOrderFiles(^(NSString *orderFilePath) {
    NSLog(@"OrderFilePath:%@", orderFilePath);
});

添加編譯設(shè)置宋舷。直接搜索 Other C Flags 來到 Apple Clang - Custom Compiler Flags 中添加配置:-fsanitize-coverage=trace-pc-guard绪撵。通過這種方式適合純 OC 工程獲取符號。由于 swift 的編譯器前端是自己的 swift 編譯前端程序祝蝠,因此配置稍有不同音诈。搜索 Other Swift Flags,添加兩條配置即可:-sanitize-coverage=func绎狭、 -sanitize=undefined细溅。swift類同樣可以通過這個方式獲取。

cocoapod 工程引入的庫儡嘶,會產(chǎn)生多 target喇聊,我們在主target添加的配置是不會生效的,我們需要針對需要的target做對應(yīng)的設(shè)置蹦狂。對于直接手動導(dǎo)入到工程里的 sdk誓篱,不管是靜態(tài)庫 .a 還是動態(tài)庫,會默認(rèn)使用主工程的設(shè)置凯楔,也就是可以拿到符號的窜骄。

按照上面配置完成以后,在代碼任意地方實現(xiàn)如下兩個方法摆屯。這樣所有的方法調(diào)用后啊研,都會調(diào)用一次__sanitizer_cov_trace_pc_guard方法。在每個函數(shù)調(diào)用的第一句實際代碼鸥拧,會被添加進去了一個 bl指令, 調(diào)用到__sanitizer_cov_trace_pc_guard 這個函數(shù)中來 削解。

bl是匯編跳轉(zhuǎn)指令富弦,即調(diào)用方法。靜態(tài)插樁實際上是在編譯期氛驮,在每一個函數(shù)內(nèi)部第一行代碼處腕柜,添加 hook 代碼 ( 即我們添加的__sanitizer_cov_trace_pc_guard 函數(shù) ) ,實現(xiàn)全局的方法 hook矫废,即AOP效果盏缤。

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

七、寫入order文件

寫入文件時有許多需要注意的地方蓖扑,即坑點唉铜。考慮到這個方法會來特別多次律杠,使用鎖會影響性能潭流,這里使用蘋果底層的原子隊列 ( 底層實際上是個棧結(jié)構(gòu)竞惋,利用隊列結(jié)構(gòu) + 原子性來保證順序 ) 來實現(xiàn)。上述這種clang插樁的方式灰嫉,會在while循環(huán)中同樣插入hook代碼拆宛。通過匯編會查看到while循環(huán),會被多次靜態(tài)加入 __sanitizer_cov_trace_pc_guard調(diào)用讼撒,導(dǎo)致死循環(huán)浑厚。解決方式是將 Other C Flags 修改為如下:-fsanitize-coverage=func,trace-pc-guardfunc表示僅hook函數(shù)時調(diào)用根盒。

引入頭文件
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
pragma mark - 獲取order文件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    while (YES) {
        //offsetof 就是針對某個結(jié)構(gòu)體找到某個屬性相對這個結(jié)構(gòu)體的偏移量
        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:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        
        //去重
        if (![symbolNames containsObject:symbolName]) {
            [symbolNames addObject:symbolName];
        }
    }

    //取反
    NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
    NSLog(@"%@",symbolAry);
    
    //將結(jié)果寫入到文件
    NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"binary.order"];
    NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
    BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
    if (result) {
        NSLog(@"%@",filePath);
    }else{
        NSLog(@"文件寫入出錯");
    }
    
}
//原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
typedef struct{
    void * pc;
    void * next;
}SymbolNode;
pragma mark - 靜態(tài)插樁代碼
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);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    
    //入隊
    // offsetof 用在這里是為了入隊添加下一個節(jié)點找到 前一個節(jié)點next指針的位置
    OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}

最后運行钳幅,下載.order文件到本地,就可以愉快的玩耍了郑象。這樣order文件就被寫入沙盒中贡这。我們可以把這個文件導(dǎo)出,加入到我們工程中厂榛。生成的order文件如下圖所示:


八盖矫、驗證插樁前后差距

重排前后LinkMap文件對比,如下圖所示:

插樁前監(jiān)控數(shù)據(jù):

插樁后監(jiān)控數(shù)據(jù):

本次測試設(shè)備是iPhone xs max击奶,優(yōu)化結(jié)果:627.51-169.06=458.45ms辈双。

使用方法

每次準(zhǔn)備發(fā)版前,使用MedLinkerOptimize這個target柜砾,運行App湃望,點擊首頁的"重排"按鈕,然后點擊"生成order文件”按鈕(此時比較耗時痰驱、耐心等待证芭!),用新生成的order文件替換項目中的舊order文件担映。


九废士、全文總結(jié)

我們可以看到圖中項目的Page Fault數(shù)量并不多,這是因為當(dāng)前項目是一個demo蝇完,代碼和文件都極少官硝。當(dāng)代碼多起來的話,Page Fault的 數(shù)量和加載耗時都會隨著代碼增加而增加短蜕。二進制重排 可以很好優(yōu)化這個問題氢架,其中心思想是重新排列 方法符號的順序, 使啟動的相關(guān)方法排在最前面從而減少啟動Page Falut的數(shù)量朋魔。我們先來看看原來的符號順序岖研,這需要用到 鏈接映射文件 Link Map FileLink Map File 里可以看到方法符號的排序警检。知道了原來的符號排序缎玫,開發(fā)者怎么去設(shè)置自己想要的順序呢硬纤?Xcode提供了排列符號的設(shè)置給開發(fā)者,設(shè)置 order_file 即可赃磨。蘋果也一直身體力行筝家,objc 源碼就采用了二進制重排優(yōu)化。雖然知道了可以通過設(shè)置 .order文件調(diào)整符號的位置邻辉,但是并不知道怎么編寫 order_file 溪王。下載objc-750源碼(源碼下載地址),查看其order_file值骇。打開libobjc.order莹菱,原來只需要填寫符號即可。全手寫一定是不可取的吱瘩,想實現(xiàn)自動化就要解決下列問題:保證不遺漏方法道伟。保證方法符號正確。保證方法符號順序正確使碾。抖音團隊使用的是 靜態(tài)掃描+運行時trace的方案蜜徽, 能夠覆蓋到80%~90%的符號。但是上述的方法也存在性能瓶頸票摇。initialize hook不到拘鞋。部分block hook不到。C++通過寄存器的間接函數(shù)調(diào)用靜態(tài)掃描不出來矢门。為了解決這個瓶頸盆色,我打算嘗試一下在文末提到的編譯期插樁。顧名思義祟剔,編譯插樁就是在代碼編譯期間修改已有的代碼或者生成新代碼隔躲。編譯期時,在每一個函數(shù)內(nèi)部二進制源數(shù)據(jù)添加 hook 代碼來實現(xiàn)全局hook效果物延。

說白了我們要跟蹤到 每個方法的執(zhí)行蹭越,從而獲取到啟動時 方法執(zhí)行的順序,然后再按照這個順序去編寫order file教届。跟蹤的具體實現(xiàn)會用到clang 的 SanitizerCoverage,這是什么東西驾霜?案训?LLVM 具有內(nèi)置的簡單代碼覆蓋率檢測工具(SanitizerCoverage)它可以在函數(shù),塊粪糙、邊緣級別插入用戶定義函數(shù)并提供回調(diào)强霎。通過看守者跟蹤 (Tracing PCs with guards

文檔是個好東西~里面就有 example。


十蓉冈、摒棄解釋城舞,直接使用

1轩触、使用二進制重排

二進制重排原理

嘗試這樣一種場景,在應(yīng)用啟動過程中家夺,調(diào)用到的方法處在不同的內(nèi)存分頁上脱柱,那么在啟動過程中就會不停地觸發(fā)缺頁中斷,導(dǎo)致進程阻塞拉馋,從而引起啟動時間變長榨为。而如果可以嘗試將啟動過程所需要的方法盡可能集中在較少的分頁上,通過減少缺頁中斷的觸發(fā)次數(shù)煌茴,就可以將啟動時間縮短随闺。這就二進制重排優(yōu)化啟動的基本原理。

如何判斷缺頁中斷耗時

既然需要進行優(yōu)化蔓腐,那就需要有個衡量的標(biāo)準(zhǔn)矩乐,來進行優(yōu)化前后的對比來查看優(yōu)化是否達(dá)到預(yù)期效果。這里主要有兩種方式回论。

使用Instruments工具

在Xcode中可以使用Instruments工具中的System Trace工具來查看在應(yīng)用啟動階段中缺頁中斷的觸發(fā)次數(shù).為了能夠更加真實的反應(yīng)數(shù)據(jù) , 最好是將應(yīng)用殺掉重新安裝 , 因為冷熱啟動的界定其實由于進程的原因并不一定后臺殺掉應(yīng)用重新打開就是冷啟動散罕。

  1. 打開 Instruments , 選擇 System Trace透葛。
  1. 選擇真機芭届,選擇工程,點擊啟動眠饮,當(dāng)首個頁面加載出來點擊停止暑劝。
  1. 等待分析完成,查看缺頁次數(shù)萨蚕。這就是第一次安裝時候的page fault次數(shù)與耗時靶草。
設(shè)置Xcode調(diào)試參數(shù)

通過Xcode啟動參數(shù)設(shè)置可以查看到啟動過程的耗時,從側(cè)面做一個驗證岳遥。打開項目在Xcode中奕翔,通過Edit Scheme->Arguments(或者快捷鍵組合cmd+shift+,)設(shè)置打印加載數(shù)據(jù)分析參數(shù)來查看啟動參數(shù)。這樣就可以在啟動之后查看到啟動加載相關(guān)操作的耗時浩蓉。

如何查看自己工程的符號順序

不得不說派继,Xcode是開發(fā)神器,你需要的功能它幾乎都有捻艳。Link Map 是編譯期間產(chǎn)生的產(chǎn)物 驾窟,(ld的讀取二進制文件順序默認(rèn)是按照Compile Sources - GUI里的順序 ) ,它記錄了二進制文件的布局认轨,這時候就可以通過設(shè)置Write Link Map File來查看绅络。

然后運行項目,就會在Products的同級目錄中找到關(guān)于項目的一個.txt文件,這里就保存了在編譯期間的二進制分布信息恩急。這個符號順序明顯是按照 Compile Sources 的文件順序來排列的杉畜。

在這個.txt文件中可以看到符號的加載順序:

如何改變二進制符號的加載順序

在Xcode中可以通過設(shè)置Order File來人為干預(yù)編譯期間的符號加載順序。隨便定義一個symbols.order文件:

-[ViewController clipView]
_CGRectMake
-[ViewController pan:]

在Xcode編譯配置中設(shè)置symbols.order的路徑:

清理項目編譯衷恭,重新編譯此叠,然后查看編譯生成的.txt文件,就會發(fā)現(xiàn)設(shè)置的order文件確實改變了文件的編譯順序匾荆。

獲取啟動期間加載的所有符號

基于以上的實踐基礎(chǔ)拌蜘,可以使用clang靜態(tài)插樁來獲取啟動期間調(diào)用到的函數(shù)符號。

編寫order文件

獲取到啟動期間的所有符號之后對符號進行調(diào)整順序去重之后牙丽,寫入.order文件用以改變編譯期間的二進制布局简卧,達(dá)到減少觸發(fā)缺頁中斷,縮短啟動時間的目的烤芦。


2举娩、clang靜態(tài)插樁方式

靜態(tài)插樁作用

通過靜態(tài)插樁,可以查看項目中的代碼執(zhí)行情況构罗,進而為項目優(yōu)化提供依據(jù)铜涉。

  • 重排二進制文件:可以根據(jù)啟動時調(diào)用的方法,存儲在.order文件中遂唧,認(rèn)為干預(yù)二進制文件的生成芙代,優(yōu)化啟動速度;
  • 刪除無用代碼:可以根據(jù)項目中方法的執(zhí)行情況盖彭,查看方法的覆蓋率纹烹,將沒有使用到的方法進行刪除,減少二進制文件的大姓俦摺铺呵;
  • 跟蹤方法調(diào)用順序:可以將調(diào)用的符號進行保存來查看應(yīng)用中方法的調(diào)用順序,跟蹤異常隧熙。
步驟一:添加 Build Setting 設(shè)置
Target -> Build Setting -> Custom Complier Flas ->

Other C Flags 添加:
-fsanitize-coverage=func,trace-pc-guard


Other Swift Flags 添加:
-sanitize-coverage=func
-sanitize=undefined

代碼插樁是指根據(jù)一定的策略在代碼中插入樁點來統(tǒng)計代碼覆蓋的技術(shù)手段片挂,一般可以分為三個粒度:

  • 函數(shù)(function):按照函數(shù)為單位進行插樁;
  • 基本塊(basic block):按照代碼執(zhí)行單元進行分組的執(zhí)行單元贞盯,單元內(nèi)部的代碼執(zhí)行次數(shù)一定是相同的音念;
  • 邊界(Edge):按照代碼執(zhí)行路徑進行插樁。

針對iOS來說躏敢,clang支持以上粒度的插樁方式闷愤。這里先介紹一些函數(shù)粒度的插樁實現(xiàn)。Clang 是一個高度模塊化開發(fā)的輕量級編譯器父丰。可以通過設(shè)置Clang的編譯參數(shù)實現(xiàn)靜態(tài)插樁。在Xcode->Build Settings中搜索Other C Flags然后在其中添加

// 基本塊覆蓋可以使用參數(shù):  -fsanitize-coverage=bb,trace-pc-guard
// 邊緣覆蓋可以使用參數(shù):  -fsanitize-coverage=edge,trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard
步驟二:添加代碼

添加以下兩個函數(shù)到啟動最早的那個 ViewController 即可蛾扇。

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

//初始化原子隊列
static OSQueueHead list = OS_ATOMIC_QUEUE_INIT;
//定義節(jié)點結(jié)構(gòu)體
typedef struct {
    void *pc;   //存下獲取到的PC
    void *next; //指向下一個節(jié)點
} Node;

哨兵初始化函數(shù)攘烛,其中[*start,*end)表示了哨兵的標(biāo)志,這里可以理解為每個哨兵guard是一個指針镀首,保存了一個uint32_t的整形數(shù)據(jù)來作為自己的標(biāo)記坟漱。

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

當(dāng)每個函數(shù)開始調(diào)用時會被插入該回調(diào),所以在方法調(diào)用開始就會執(zhí)行該回調(diào)更哄。函數(shù)中guard就是__sanitizer_cov_trace_pc_guard_init[start, end)區(qū)間中一個芋齿。

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
     // 在這里可以嘗試獲取到執(zhí)行函數(shù)的信息
     void *PC = __builtin_return_address(0);
     Node *node = malloc(sizeof(Node));
     *node = (Node){PC, NULL};
     // offsetof() 計算出列尾,OSAtomicEnqueue() 把 node 加入 list 尾巴
     OSAtomicEnqueue(&list, node, offsetof(Node, next));
}

然后運行項目會發(fā)現(xiàn)成翩,回調(diào)正常執(zhí)行那么問題來了觅捆,如何在回調(diào)函數(shù)中獲取到當(dāng)前執(zhí)行函數(shù)的信息呢?

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
     NSMutableArray *arr = [NSMutableArray array];
     while(1){
         //有進就有出麻敌,這個方法和 OSAtomicEnqueue() 類比使用
         Node *node = OSAtomicDequeue(&list, offsetof(Node, next));
         //退出機制
         if (node == NULL) {
             break;
         }
         //獲取函數(shù)信息
         Dl_info info;
         dladdr(node->pc, &info);
         NSString *sname = [NSString stringWithCString:info.dli_sname encoding:NSUTF8StringEncoding];
         printf("%s \n", info.dli_sname);
         //處理c函數(shù)及block前綴
         BOOL isObjc = [sname hasPrefix:@"+["] || [sname hasPrefix:@"-["];
         //c函數(shù)及block需要在開頭添加下劃線
         sname = isObjc ? sname: [@"_" stringByAppendingString:sname];
         
         //去重
         if (![arr containsObject:sname]) {
             //因為入棧的時候是從上至下栅炒,取出的時候方向是從下至上,那么就需要倒序术羔,直接插在數(shù)組頭部即可
             [arr insertObject:sname atIndex:0];
         }
     }
       
     //去掉 touchesBegan 方法 啟動的時候不會用到這個
     [arr removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
     //數(shù)組合成字符串
     NSString * funcStr = [arr  componentsJoinedByString:@"\n"];
     //寫入文件
     NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"link.order"];
     NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
     NSLog(@"%@", filePath);
     [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
步驟三:取出 order file
  • 在步驟二的代碼NSLog(@"%@", filePath);斷點
  • 如果頁面無法觸發(fā)點擊赢赊,viewDidLoad里面調(diào)用touchesBegan:withEvent:也可以
  • 運行代碼后記錄link.order 的路徑
  • Finder 前往路徑取出 order file

使用Xcode連接真機,啟動應(yīng)用直至第一個控制器界面加載完成级历,使用快捷鍵cmd+shift+2進入Devices and Simulators界面释移,選擇對應(yīng)應(yīng)用并點擊Download Containers選擇保存路徑下載文件。在下載的.xcappd中右鍵顯示包內(nèi)容寥殖,在AppData->Library->Caches路徑下即可保存的.txt文件玩讳。由于隊列的特性,這里的符號與實際調(diào)用順序是相反的扛禽。這樣就可查看到在應(yīng)用首個控制器顯示之前系統(tǒng)調(diào)用的所有符號锋边,從而為應(yīng)用啟動優(yōu)化奠定基礎(chǔ)。

步驟四:設(shè)置 order file

link.order的路徑放到工程根目錄
Target -> Build Setting -> Linking -> Order File 設(shè)置路徑

步驟五:編譯代碼

把步驟一 order file 的設(shè)置還原
把步驟二添加代碼刪除
clean 以后編譯代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載编曼,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者豆巨。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市掐场,隨后出現(xiàn)的幾起案子往扔,更是在濱河造成了極大的恐慌,老刑警劉巖熊户,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萍膛,死亡現(xiàn)場離奇詭異,居然都是意外死亡嚷堡,警方通過查閱死者的電腦和手機蝗罗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門艇棕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人串塑,你說我怎么就攤上這事沼琉。” “怎么了桩匪?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵打瘪,是天一觀的道長。 經(jīng)常有香客問我傻昙,道長闺骚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任妆档,我火速辦了婚禮僻爽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘过吻。我一直安慰自己进泼,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布纤虽。 她就那樣靜靜地躺著乳绕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪逼纸。 梳的紋絲不亂的頭發(fā)上洋措,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音杰刽,去河邊找鬼菠发。 笑死,一個胖子當(dāng)著我的面吹牛贺嫂,可吹牛的內(nèi)容都是我干的滓鸠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼第喳,長吁一口氣:“原來是場噩夢啊……” “哼糜俗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起曲饱,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤悠抹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扩淀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體楔敌,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年驻谆,在試婚紗的時候發(fā)現(xiàn)自己被綠了卵凑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庆聘。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖勺卢,靈堂內(nèi)的尸體忽然破棺而出掏觉,到底是詐尸還是另有隱情,我是刑警寧澤值漫,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站织盼,受9級特大地震影響杨何,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沥邻,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一危虱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧唐全,春花似錦埃跷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至延届,卻和暖如春剪勿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背方庭。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工厕吉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人械念。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓头朱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親龄减。 傳聞我的和親對象是個殘疾皇子项钮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355