1.冷啟動
1.1 什么是冷啟動?
冷啟動是指記憶體中不包含該應用程式相關的資料,必須要從磁盤載入到記憶體中的啟動程序,
注意:重新打開 APP, 不一定就是冷啟動,
- 當記憶體不足,APP被系統自動殺死后,再啟動就是冷啟動,
- 如果在重新打開 APP 之前,APP 的相關資料還存盤在記憶體中,這時再打開 APP,就是熱啟動
- 冷啟動與熱啟動是由系統決定的,我們無法決定,
- 當然設備重啟以后,第一次打開 APP 的程序,一定是冷啟動,
1.2 如何統計冷啟動耗時?
一般來講,統計 APP 啟動時長,以 main 函式為節點 ,分兩個大階段:
- main 函式之后的代碼,是我們自己寫的,我們可以自行統計進入 main 函式到第一個界面顯示的耗時,
-
- 在 main 函式里列印一下當前的時間,
- 在第一個要顯示的控制器的 viewDidLoad 方法中列印一下當前時間
- 兩個時間的差值,即為main函式后的加載時長,
- main 函式之前,為 pre-main 階段,由于是系統在做事情,這段時間的 耗時,我們沒辦法直接統計,需要查 看系統給我們的反饋,
1.2.1 pre-main階段都做了什么?
接下來看一下專案中的 pre-main 階段的耗時,
- 查看系統給的反饋需要 增加一個環境變數,
- 增加路徑:在 Xcode -> Edit Scheme -> Run -> Arguments -> Environment Variables 中,
- 增加一個環境變數 DYLD_PRINT_STATISTICS:1,
下圖是我專案的加載耗時:
耗時程序分為以下4部分:
- dylib loading time : 是指動態庫加載的耗時,系統的動態庫做過優化,耗時較少, 蘋果官方推薦最多不要超過6個外部動態庫,多余6個,需要考慮合并動態庫,合并動態庫對于啟動時期的優化,非常有效, 像微信的動態庫早期有八九個,現在也優化成6個了,
- rebase/binding
- rebase:是指地址的 偏移修正耗時,
-
- 在編譯生成二進制檔案的時候,每個函式都有一個地址,這個地址是相對于二進制檔案的偏移地址,
- 在啟動時,也就是在二進制檔案在加載到虛擬記憶體的時候,為了安全起見,蘋果有個安全機制(ASLR),會在整個二進制檔案的最前面,隨機加一個偏移值,
- 比如 A 函式,相對于二進制檔案的偏移值是 0x003, 啟動時,整個二進制檔案被分配了一個隨機值0x100, 那么 A 函式在記憶體中的實際地址是 0x003 + 0x100 = 0x103,
- 偏移修正指的就是計算方法在虛擬記憶體中的地址的程序!
- binding: 動態庫的方法系結,是指將方法名字與方法的實作進行系結程序的耗時,
-
- 比如 NSLog 方法,在加載的時候需要先找到Foundation庫,再找到庫里的NSLog的方法的實作,將方法名字和方法實作系結在一起,
- Objc setup time: 注冊所有 OC類 耗時, 類越多耗時越多,有人統計過2萬個自定義的OC的類,大概耗時800毫秒,洗掉不用的類,可以減少耗時,
- initializer time: load方法 和 C++建構式的耗時. 減少重寫load方法,盡量將事情延遲到 main 方法以后,可以減少耗時,
- slowest intializers : 指出了最耗時的幾個庫是下面的6個庫(最后一個是我的專案),
1.2.2 pre-main階段耗時優化方法總結:
- 減少外部動態庫的數量
- 不用的類和方法,洗掉
- 類盡量使用懶加載,也就是盡量不要重寫load方法,
- 啟動時加載的資料使用多執行緒
- 使用純代碼,不用xib storyboard(要額外進行代碼決議轉換和頁面的渲染)
以上方法,都是和自己的專案代碼息息相關的優化方案,不同專案具體是實施動作不一樣,
還有一個優化方法,不管是什么專案,實施動作都一樣 ,對什么專案都有效,那就是二進制重排!
2. 二進制重排
學習二進制重排,首先要知道資料是如何加載到記憶體中的 ,
?記憶體加載機制:資料是如何加載到記憶體中的
我們已經知道資料加載到記憶體的程序,當虛擬記憶體頁還沒有對應的物理記憶體頁時,會出現缺頁例外(PageFault),
當冷啟動時,物理記憶體中是沒有資料的,這時會出現大量的缺頁例外,在iOS生產環境的app,在發生Page Fault進行重新加載時,iOS系統還會對其做一次簽名驗證,因此 iOS 生產環境的 Page Fault 比Debug環境下所產生的耗時更多
這里有沒有優化空間呢?接下來就是優化方案:二進制重排!
在了解二進制重排之前,再了解下在專案編譯生成二進制檔案的時候,類及其內部方法實作的排列順序是什么樣的呢?
2.2.1 二進制檔案中方法實作排序是什么樣的?
- 在 viewController 中,先隨便寫幾個方法,
- 再看下源檔案的編譯順序
接下來查看 Link Map檔案查看符號順序, 查看方式:
- 打開link map
****
- 編譯生成link map 檔案
- 找到link map 檔案
- 專案目錄中,生成的 app 右鍵,show in Finder
- 找到 app 的上上級目錄
- 進入Intermediates.noindex -> TraceDemo.build -> Debug-iphonesimulator -> TraceDemo.build -> TraceDemo-LinkMap-normal-x86_64.txt
- 打開link map 檔案,找到自己的類及方法的名字
5.我們可以直觀的看出 link map中符號的順序,類是以源檔案的編譯順序,從上到下按序排列,方法名是以類中方法的書寫順序,由上到下排序,
2.2.2 為什么需要二進制重排?
從原始碼的執行順序上看,應該是 load -> test2 -> viewDidLoad -> test1.
但是二進制檔案中符號的順序是方法從上到下的書寫順序,沒有按照呼叫順序去排列,
在冷啟動分頁加載二進制檔案時,發現很多頁中都有啟動時需要用到的方法,那么即使頁里面也存在啟動時不需要的方法,但是由于記憶體是分頁管理的,要加載就要整頁加載,這樣就導致了大量不需要在 pre-main 階段執行的方法,也會被加載到記憶體中,增加了啟動的耗時,
\
例如,啟動需要加載100個頁,每個頁可以包含20個方法,但是每個頁里只有2個方法是啟動時后用到的,這樣實際上啟動時必須要的方法是2 * 100 = 200個,如果將這200個方法緊挨著放在一起,那么只需要2頁,比100個頁,減少了98頁,這樣耗時就會大大降低,
2.2.3 如何進行二進制重排?
1. 二進制重排的方法
在專案編譯生成二進制檔案的時候,找到啟動時需要的方法,并且將它們放在一起 重新排序,這就是二進制重排,
兩個關鍵點: 找到啟動時需要方法 & 方法 的重排序
2.方法的重排序:
重排序其實很簡單,xcode已經為我們提供了這個機制,它使用的聯結器叫做 ld, ld有一個引數叫做Order File, 我們可以通過配置order檔案,來使編譯時生成的二進制的檔案的Link Map種的符號順序,按照我們指定的順序排列生成,而且 libobjc 實際上也做了二進制重排 ,
【第一步】在專案根目錄下建一個xxx.order的檔案,里面寫上按照自己想排列的順序,寫上方法或者函式的名字,(如果寫了一個不存在的符號,也不會報錯,會被自動過濾掉~)
【第二步】在 Build Settings 搜索order file 的檔案,將專案根目錄創建的檔案,設定上去,
【第三步】重新編譯,查看 Link Map 檔案的順序,果然,按照我們指定的順序排列啦!
3. 靜態插樁 - 找到冷啟動時的所有方法
接下來,需要做的就是寫入 order 檔案里的符號了,我們不可能手寫上所有的啟動時需要的執行的符號,這里的所有符號包括,呼叫的方法、函式、C++構造方法、swift方法、block,
這里使用 LLVM 內置的簡單代碼覆寫率檢測工具(SanitizerCoverage),它在邊緣、 函式、基本塊 級別上插入對用戶定義函式的呼叫,
edge(默認):檢測邊緣(所有的指令跳轉都會被插入對用戶定義函式的呼叫, 如回圈、分支判斷、方法函式等),bb:檢測基本塊,func:僅將檢測每個 功能的輸入塊(這個就是我們要重排序的符號),
按照檔案,
- 【第1步】搜索并設定 Other C Flags/ Other C++ Flags 為 -fsanitize-coverage=func,trace-pc-guard (這里要用func, 不能用默認的edge, 不然會造成死回圈),
- 如果有swift ,需要設定 Other Swift Flags 設定為 **** -sanitize-coverage=func -sanitize=undefined
- 【第2步】編譯器將插入對模塊建構式的呼叫,所以我們要實作這個方法:
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
通過列印start, stop 地址的內容,從 start 地址開始,到 stop 地址的前4位,存盤的是 uint32 的 1-19的數字,
我們可以從這個函式中知道, 當前專案中自定義的功能輸入塊的數量,
- 【第3步】編譯器會在生成二進制檔案的時候,在每個func呼叫之初,插入以下代碼:
__sanitizer_cov_trace_pc_guard(&guard_variable)
也就是說,每個方法在執行的時候,都會呼叫上面這個方法, 接下來:
-
-
- 我們要實作這個方法,并在這個方法里,獲取到本方法結束后要回傳的地址
-
// 獲取到本方法結束后,要回傳的地址去,這個地址包含在被hook的方法內部,但不是被hook 的方法的首地址
void *PC = __builtin_return_address(0);
-
-
- 并將地址保存一個系統的原子佇列( ( 底層實際上是個堆疊結構 , 利用佇列結構 + 原子性來保證順序 ))中,使用原子佇列,是為了防止多執行緒資源搶奪,原子佇列的存值方法如下:
-
// 將結構體存入到原子佇列中,
// offsetof(type,member) 回傳結構體中成員的偏移值,由于指標PC是8位元組,所以這里回傳8位元組,
// 詳見下圖
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
每個 SYNode 首地址都距離上一個偏移 PC 所占的位元組數,這樣做的妙處就是,每個 SYNode 的 next 的地址,恰巧就是下一個結構體的地址,這樣方便獲取佇列里面的所有資料,
- 【第4步】我們在點擊螢屏的事件中
-
- 把存盤到原子佇列中的地址遍歷出來,
- 根據地址獲取當前地址所在的方法的名稱并存入陣列中,
typedef struct dl_info {
const char *dli_fname; /* 所在檔案 */
void *dli_fbase; /* 檔案地址 */
const char *dli_sname; /* 符號名稱 */
void *dli_saddr; /* 函式起始地址 */
} Dl_info;
//這個函式能通過函式內部地址找到函式符號
int dladdr(const void *, Dl_info *);
-
- 由于原子佇列是堆疊結構,先進后出,所以我們需要將陣列倒序排列
- 由于方法可能會被多次呼叫,我們需要去重
- 再將最后我們當前點擊螢屏的方法洗掉掉
- 將方法名字的陣列,轉成字串,寫到沙盒檔案中
完整代碼如下:
//
// ViewController.m
// TraceDemo
//
// Created by hank on 2020/3/16.
// Copyright ? 2020 hank. All rights reserved.
//
#import "ViewController.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
#import "TraceDemo-Swift.h"
@interface ViewController ()
@end
@implementation ViewController
+(void)initialize
{
}
void(^block1)(void) = ^(void) {
};
void test(){
block1();
}
+(void)load
{
}
- (void)viewDidLoad {
[super viewDidLoad];
[SwiftTest swiftTestLoad];
test();
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, 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];
[symbolNames addObject:symbolName];
}
//取反
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 * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"demo.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
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.
}
//原子佇列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結構體
typedef struct {
void *pc;
void *next;
}SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// 會導致load 方法被return
// if (!*guard) return;
// 獲取到本方法結束后,要回傳的地址去,這個地址包含在被hook的方法內部,但不是被hook 的方法的首地址
void *PC = __builtin_return_address(0);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//進入
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
@end
2.2.4 如何驗證二進制重排的效果?
1.查看缺頁例外數量Page Fualt:
- 查看一下專案的缺頁例外數量,注意需要卸載 APP 或者重啟手機,來保證這個APP完全沒有被加載到記憶體中,因為如果物理記憶體中有該APP的資料,
- 打開 Instrument -> System Trace
3.選擇真機、專案、點擊啟動,當第一個頁面顯示出來后,點擊停止,
- xcode 12搜索main thread, 選擇Virtual Memory,File Backed Page in 就是缺頁例外的數量
優化前:專案的缺頁遺產數量是427
優化后:
優化前:專案的缺頁遺產數量是286
減少了啟動時大概40%的缺頁例外~
3.自動更新order 檔案
隨著代碼迭代,order檔案需要更新,每次手動更新很麻煩,所以需要自動更新,
brew install ios-deploy
APP_ORDER_DIR=appOrderDir
APP_ORDER=./$APP_ORDER_DIR/Documents/app.order
mkdir $APP_ORDER_DIR
ios-deploy --download=/Documents --bundle_id $PRODUCT_BUNDLE_IDENTIFIER --to ./$APP_ORDER_DIR
if [ -e $APP_ORDER ] ;then
cp -f $APP_ORDER ./Resource/app.order
fi
rm -r $APP_ORDER_DIR
【補充xcode13】查看缺頁例外的方式
選擇真機、專案、點擊啟動,當第一個頁面顯示出來后,點擊停止,
青山不改,綠水常流,謝謝大家!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/502408.html
標籤:其他
上一篇:圖論基礎
