重學OC第二十四篇:啟動優化
文章目錄
前言
啟動分為冷啟動和熱啟動,主要區別是記憶體是否有APP載入的資料,如果所有的資料需從硬碟讀取後載入到記憶體,那就為冷啟動。下面主要是關於冷啟動方面的優化。
一、冷啟動
1.1 效能檢測
APP啟動分兩個階段來測試:
- main函式前(pre-main)
主要是dyld流程部分,包括動態庫載入、類的載入、C++靜態物件處理等。通過在Xcode中新增環境變數DYLD_PRINT_STATISTICS為YES
//模擬器資料不真實,僅參考
Total pre-main time: 126687488.8 seconds (0.0%) //總耗時
dylib loading time: 258.93 milliseconds (0.0%) //dyld載入耗時,官方建議自定義動態庫不超過6個
rebase/binding time: 126687487.6 seconds (0.0%)//重定址和符號繫結耗時
ObjC setup time: 632.00 milliseconds (0.0%)//OC類註冊耗時
initializer time: 366.95 milliseconds (0.0%)//+load、c++構造初始化耗時
slowest intializers : //最慢的初始化器
- main函式後
從main()函式開始至applicationWillFinishLaunching結束,我們統一稱為main()函式之後的部分。利用錨點分析applicationWillFinishLaunching的耗時。
1.2 優化
官方建議的啟動時間要求:
應該在400ms內完成main()函式之前的載入
整體過程耗時不能超過20秒
,否則系統會kill掉程式,App啟動失敗
對於main函式前的優化
- 減少OC類,移除不需要的類
- 減少動態庫,移除不需要的動態庫。可通過動態庫合併來讓動態庫數量不超過6個
- 儘量不要寫__attribute__((constructor))的C函式,也儘量不要用到C++的靜態物件
- 儘量不要使用+load
- 壓縮圖片大小,減少I/O操作量
對於main函式後的優化
- 延遲不必要的頁面、配置等載入
- 耗時操作考慮多執行緒非同步操作
- 使用二進位制重排來減少啟動時硬碟到記憶體的操作次數。
二、二進位制重排
二進位制重排指的是重排編譯階段的程式碼檔案和函式的順序。要了解二進位制重排需瞭解虛擬記憶體,相關知識可參考《深入理解計算機系統》第9章虛擬記憶體中的內容。
2.1 原理
編譯器生成二進位制程式碼時,預設按照連結的.o順序寫檔案,按照.o內部順序寫函式,如果函式跨頁了,就會觸發多次Page Fault,所以把函式排到一個Page裡,只需要一次Page Fault。(iOS中每頁大小為16KB)
2.1.1 PageFault檢測
通過Instruments
中的Systme Trace
,選擇專案,選中main thread,選擇Virtual Memory
,檢視File Backed Page In(PageFault)
和其他的相關次數、時間等。
2.2 重排
要進行重排要看到二進位制資料的順序,如何進行重排。
2.2.1 二進位制符號順序檢視
在Build Settings
中搜尋link Map
,設定Write Link Map File為YES
,然後編譯,在包
檔案中向上兩級
找到Intermediates.noindex,進入xxx.build最後找到xxx-LinkMap-normal-x86_64.txt檔案,開啟找到Symbol可看到按編譯時檔案和檔案內部函式書寫順序排列。
2.2.2 通過.order檔案重排
建立xxx.order檔案,在Build Settings
中搜尋order file
,加入./xxx.order,然後編譯。
那xxx.order檔案中的內容怎麼來,可通過clang插樁hook函式然後儲存到xxx.order中。
2.2.3 Clang插樁
官方介紹: https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs
SanitizerCoverage 是 Clang 內建的一個程式碼覆蓋工具,它把一系列以 _sanitizer_cov_trace_pc 為字首的函式呼叫插入到使用者定義的函式裡來實現全域性 AOP。其覆蓋之廣,包含 Swift/Objective-C/C/C++ 等語言,Method/Function/Block 全支援。
使用步驟:
- Build Settings 搜尋 Other C Flags,新增-fsanitize-coverage=func, trace-pc-guard引數(OC); Other Swift Flags新增-sanitize-coverage=func 和 -sanitize=undefined(swift)
- 在__sanitizer_cov_trace_pc_guard_init和__sanitizer_cov_trace_pc_guard_init方法中進行相關程式碼編寫或直接使用第三方AppOrderFiles
- 在didFinishLaunchingWithOptions中呼叫相應的方法生成xxx.order檔案並進行呼叫順序重排
- 把重排後的xxx.order檔案拷貝到專案的根目錄下,在Build Settings中搜尋order file並加入./xxx.order或者${SRCROOT}/xxx.order
AppOrderFiles原始碼
#import "AppOrderFiles.h"
#import <dlfcn.h> //提供了載入和處理動態連線庫的系統呼叫
#import <libkern/OSAtomicQueue.h>
#import <pthread.h>
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
static BOOL collectFinished = NO;
typedef struct {
void *pc;
void *next;
} PCNode;
// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint32_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);
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (collectFinished || !*guard) {
return;
}
// 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.
*guard = 0;
void *PC = __builtin_return_address(0);
PCNode *node = malloc(sizeof(PCNode));
*node = (PCNode){PC, NULL};
OSAtomicEnqueue(&queue, node, offsetof(PCNode, next));
}
extern void AppOrderFiles(void(^completion)(NSString *orderFilePath)) {
collectFinished = YES;
__sync_synchronize();
NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableArray <NSString *> *functions = [NSMutableArray array];
while (YES) {
PCNode *node = OSAtomicDequeue(&queue, offsetof(PCNode, next)); //出列
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info); //把資訊讀入info中
if (info.dli_sname) {
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["]; //OC方法
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name]; //非OC方法加_字首
[functions addObject:symbolName];
}
}
if (functions.count == 0) {
if (completion) {
completion(nil);
}
return;
}
NSMutableArray<NSString *> *calls = [NSMutableArray arrayWithCapacity:functions.count];
NSEnumerator *enumerator = [functions reverseObjectEnumerator]; //取反
NSString *obj;
while (obj = [enumerator nextObject]) {
if (![calls containsObject:obj]) { //去重
[calls addObject:obj];
}
}
[calls removeObject:functionExclude]; //移除當前方法
NSString *result = [calls componentsJoinedByString:@"\n"];
NSLog(@"Order:\n%@", result);
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"app.order"];
NSData *fileContents = [result dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [[NSFileManager defaultManager]
createFileAtPath:filePath
contents:fileContents
attributes:nil]; //寫入檔案
if (completion) {
completion(success ? filePath : nil);
}
});
}
總結
- 對冷啟動效能的檢測,在Xcode中新增環境變數
DYLD_PRINT_STATISTICS為YES
來列印pre-main階段的耗時 - 建議優化應該
在400ms內完成main()函式之前的載入,整體過程耗時不能超過20秒
- 通過
Instruments
中的Systme Trace
檢視PageFault - 在
Build Settings
中搜尋link Map
,設定Write Link Map File為YES
,然後編譯,在包
檔案中向上兩級
找到Intermediates.noindex,進入xxx.build最後找到xxx-LinkMap-normal-x86_64.txt檔案檢視二進位制符號 - 通過
Clang插樁
最後生成.order檔案
,把重排後的xxx.order檔案拷貝到專案的根目錄
下,在Build Settings
中搜尋order file
並加入${SRCROOT}/xxx.order。
相關文章
- 重學OC第二十六篇:RunLoopOOP
- 重學OC第二十一篇:@synchronized分析synchronized
- 運籌優化(十四)--離散優化的啟發式演算法優化演算法
- Tomcat 第二篇:啟動流程Tomcat
- iOS深思篇 | 啟動時間的度量和優化iOS優化
- iOS啟動優化iOS優化
- App啟動優化APP優化
- Android 效能優化 ---- 啟動優化Android優化
- Activity的啟動過程第二篇
- Oracle優化案例-(三十四)Oracle優化
- App啟動速度優化APP優化
- 第二十四篇:可靠訊號機制
- 高德APP啟動耗時剖析與優化實踐(iOS篇)APP優化iOS
- iOS效能優化 - APP啟動時間優化iOS優化APP
- APP啟動時間優化APP優化
- App啟動頁面優化APP優化
- 美人相機啟動優化優化
- android冷啟動優化Android優化
- Android效能優化筆記(一)——啟動優化Android優化筆記
- Android應用優化之冷啟動優化Android優化
- 面試Tip:Android優化之APP啟動優化面試Android優化APP
- EntityFramework優化:第一次啟動優化Framework優化
- 移動spa商城優化記(一)---首屏優化篇優化
- 十四、Android效能優化之ServiceAndroid優化
- 重學c#系列—— IO流[三十四]C#
- 優化 App 的啟動時間優化APP
- 學習 Webpack5 之路(優化篇)Web優化
- Session重疊問題學習(三)--優化Session優化
- Android效能優化之啟動過程(冷啟動和熱啟動)Android優化
- 【OC梳理】效能檢測及優化彙總優化
- Linux啟動時間優化技巧Linux優化
- IdleHandler,頁面啟動優化神器優化
- Android效能優化之App應用啟動分析與優化Android優化APP
- 效能優化篇優化
- Session重疊問題學習(五)--最優化Session優化
- Session重疊問題學習(四)--再優化Session優化
- hyperf 啟動、重啟、停止、檔案變化監聽命令包
- 視覺 SLAM 十四講-基礎數學篇視覺SLAM