一、虛擬內(nèi)存和物理內(nèi)存
進(jìn)程如果能直接訪問物理內(nèi)存無疑是很不安全的构罗,為了解決內(nèi)存安全滥比,現(xiàn)在的計(jì)算機(jī)和操作系統(tǒng)在物理內(nèi)存的基礎(chǔ)上又建立了一層虛擬內(nèi)存。虛擬內(nèi)存和物理內(nèi)存這里不做贅述祷肯。我們主要通過原理來找到優(yōu)化App的方案。
1. 虛擬內(nèi)存
實(shí)際上我們平時(shí)所看到的進(jìn)程中可以直接訪問的連續(xù)內(nèi)存空間0x000000 ~ 0xffffff
疗隶,只是一個(gè)虛擬地址佑笋,需要通過一張映射表映射后才可以獲取到真實(shí)的物理地址。并不是所有的虛擬內(nèi)存都會分配物理內(nèi)存斑鼻,只有那些實(shí)際使用的虛擬內(nèi)存才分配物理內(nèi)存蒋纬,并且分配后的物理內(nèi)存,是通過內(nèi)存映射來管理的坚弱。
2. 虛擬內(nèi)存分頁
剛剛提到虛擬內(nèi)存和物理內(nèi)存通過映射表進(jìn)行映射颠锉,但是這個(gè)映射并不可能是一一對應(yīng)的,那樣就太過浪費(fèi)內(nèi)存了史汗。為了解決效率問題琼掠,實(shí)際上真實(shí)物理內(nèi)存是分頁的。而映射表同樣是以頁為單位的停撞。換句話說瓷蛙,映射表只會映射到某一頁,并不會映射到具體每一個(gè)地址。
Mac OS 、linux內(nèi)存 4kb一頁愕宋,iOS是16kb一頁」谔遥可以使用
pagesize
命令,在終端直接查看道宅。4096字節(jié)=4千字節(jié)食听。
- 0 和 1 代表當(dāng)前地址有沒有在物理內(nèi)存中。
- 從上圖我們也可以看出污茵,進(jìn)程的虛擬地址是連續(xù)的樱报,但是實(shí)際物理內(nèi)存地址并不是連續(xù)的,而是由若干完整的內(nèi)存分頁組成泞当。
- 當(dāng)應(yīng)用被加載到物理內(nèi)存中時(shí) 迹蛤,并不會將整個(gè)應(yīng)用加載到物理內(nèi)存中。只會放用到的那一部分。也就是
懶加載
盗飒,換句話說就是應(yīng)用使用多少嚷量,實(shí)際物理內(nèi)存就分配多少。
二逆趣、Page Fault
1. Page Fault產(chǎn)生原因
- 當(dāng)應(yīng)用訪問到某個(gè)地址蝶溶,映射表中為
0
,也就是說它并沒有被加載到物理內(nèi)存中時(shí)汗贫,系統(tǒng)就會立刻阻塞整個(gè)進(jìn)程身坐,觸發(fā)一個(gè)缺頁中斷
秸脱,即Page Fault
落包。- 當(dāng)一個(gè)
缺頁中斷
被觸發(fā),操作系統(tǒng)會從磁盤中重新讀取這頁數(shù)據(jù)到物理內(nèi)存上摊唇,然后將映射表中虛擬內(nèi)存指向?qū)?yīng)物理內(nèi)存咐蝇。 如果當(dāng)前內(nèi)存已滿,操作系統(tǒng)會通過置換頁算法找一頁數(shù)據(jù)進(jìn)行覆蓋巷查。這也是為什么開再多的應(yīng)用也不會崩掉有序,但是之前開的應(yīng)用再打開,就會重新啟動的根本原因岛请。
2. Page Fault影響
內(nèi)存分頁觸發(fā)中斷異常 Page Fault 后旭寿,會阻塞進(jìn)程,這是會對性能產(chǎn)生影響的崇败。并且在 iOS 系統(tǒng)的生產(chǎn)環(huán)境應(yīng)用盅称,在發(fā)生缺頁中斷
進(jìn)行重新加載時(shí) ,iOS 系統(tǒng)還會對其做一次簽名驗(yàn)證后室,因此 iOS 生產(chǎn)環(huán)境的 Page Fault
所產(chǎn)生的耗時(shí)要更多缩膝。
對用戶而言,使用App時(shí)第一個(gè)直接體驗(yàn)就是啟動 App 時(shí)間岸霹,而啟動時(shí)期會有大量的類
疾层、分類
、三方
等等需要加載和執(zhí)行贡避,此時(shí)大量Page Fault
所產(chǎn)生的的耗時(shí)往往是不能小覷的痛黎。
抖音團(tuán)隊(duì)分享的一個(gè)
Page Fault
,開銷在0.6 ~ 0.8ms
刮吧。實(shí)際測試發(fā)現(xiàn)不同頁會有所不同 , 也跟 cpu 負(fù)荷狀態(tài)有關(guān)舅逸,在0.1 ~ 1.0 ms
之間。
二進(jìn)制重排
這個(gè)方案最早也是 抖音團(tuán)隊(duì) 分享的皇筛。
三琉历、二進(jìn)制重排
1. 二進(jìn)制重排原理
函數(shù)編譯在mach-O
中的位置是根據(jù)ld ( Xcode 的鏈接器)
的編譯順序并非調(diào)用順序來的,因此很可能這兩個(gè)函數(shù)分布在不同的內(nèi)存頁上。
- 如上圖旗笔,編譯順序是
method1
彪置、method2
... 。啟動時(shí)page1與page2都需要從無到有加載到物理內(nèi)存中蝇恶,所以會觸發(fā)兩次Page Fault
拳魁。二進(jìn)制重排
的做法就是將method1
與method4
放到一個(gè)內(nèi)存頁中,那么啟動時(shí)則只需要加載一次 page 即可撮弧,也就是只觸發(fā)一次Page Fault
潘懊。- 在實(shí)際項(xiàng)目中,我們可以將啟動時(shí)需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁中 ) 以盡可能減少
Page Fault
贿衍,進(jìn)而減少啟動耗時(shí)授舟。
2. 二進(jìn)制重排操作
蘋果已經(jīng)給我們提供了這個(gè)機(jī)制,實(shí)際上二進(jìn)制重排就是對即將生成的可執(zhí)行文件重新排列贸辈,這個(gè)操作發(fā)生在鏈接階段释树。
2.1 Order File
Xcode用的鏈接器叫做
ld
,ld
有一個(gè)參數(shù)叫做Order File
擎淤,我們可以通過這個(gè)參數(shù)配置一個(gè) 后綴名 為order
的文件路徑奢啥。在這個(gè)xxx.order
文件中,將需要的符號按順序?qū)懺诶锩孀炻#?dāng)工程build
的時(shí)候桩盲,Xcode會讀取這個(gè)文件,打的二進(jìn)制包就會按照這個(gè)文件中的符號順序進(jìn)行生成對應(yīng)的mach-O
席吴。
2.2 Linkmap 查看二進(jìn)制文件布局
Linkmap
是iOS編譯過程的中間產(chǎn)物赌结,記錄了二進(jìn)制文件的布局,開啟步驟如下:
2.2.1 修改Write Link Map File
為 YES抢腐,然后clean項(xiàng)目并重新編譯
-
Products -> show in finder
姑曙,上上層文件夾,然后找到一個(gè)xxx-LinkMap-normal-arm64.txt
的txt文件
-
這個(gè)文件的
# Symbols:
部分存儲了所有符號的順序迈倍,前面的 .o 等內(nèi)容忽略伤靠,Address
就是實(shí)際的物理地址,可用Mach-O工具
查看
-
我們發(fā)現(xiàn)符號順序是按照
Compile Sources
的文件順序來排列的
當(dāng)我們調(diào)整Compile Sources
中的文件順序后啼染,會發(fā)現(xiàn)符號順序也有了變化宴合。
2.3 二進(jìn)制重排原理
我們二進(jìn)制重排并非只是修改符號地址,而是利用符號順序迹鹅,重新排列整個(gè)代碼在文件的偏移地址卦洽,將啟動需要加載的方法地址放到前面內(nèi)存頁中,以此達(dá)到減少page fault
的次數(shù)從而實(shí)現(xiàn)時(shí)間上的優(yōu)化斜棚。
3. 獲取App啟動時(shí)調(diào)用的所有方法(使用編譯插樁)
備注:Clang插樁實(shí)際上就是一個(gè)代碼覆蓋工具 Clang插樁官網(wǎng)地址
要真正的實(shí)現(xiàn)二進(jìn)制重排阀蒂,我們需要拿到啟動時(shí)的所有方法该窗、函數(shù)等符號,并保存其順序蚤霞,然后寫入xxx.order
文件來實(shí)現(xiàn)二進(jìn)制重排酗失,獲取的方案使用 Clang編譯插樁
。
3.1 在Build Settings
中Other C Flags
添加編譯配置-fsanitize-coverage=func,trace-pc-guard
昧绣。
3.2 添加完編譯配置后规肴,會發(fā)現(xiàn)編譯報(bào)錯(cuò),如下:
3.3 添加Clang函數(shù)
#import "DZHomeViewController.h"
#import <dlfcn.h> // 動態(tài)庫的顯式調(diào)用
#import <libkern/OSAtomic.h> //
/*
考慮到插樁方法會調(diào)用很多次夜畴,使用鎖會影響性能拖刃,所以使用蘋果底層的`原子隊(duì)列`,其內(nèi)部實(shí)際上是一個(gè)鏈表贪绘,遵循先進(jìn)先出
**/
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
// 定義符號結(jié)構(gòu)體
typedef struct {
void *pc;
void *next;
} PCNode;
@interface DZHomeViewController ()
@end
@implementation DZHomeViewController
void(^blockTest)(void) = ^(void) {
};
+ (void)load {
}
+ (void)initialize {
}
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [self deziTest];
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
PCNode *node = OSAtomicDequeue(&symbolList, offsetof(PCNode, 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__]];
//將數(shù)組變成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fontResources.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
- (void)deziTest {
blockTest();
}
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) {
/* 精確定位 哪里開始 到哪里結(jié)束! 在這里面做判斷寫條件!*/
void *PC = __builtin_return_address(0);
DeziNode *node = malloc(sizeof(DeziNode));
*node = (DeziNode){PC,NULL};
//進(jìn)入
OSAtomicEnqueue(&symbolList, node, offsetof(DeziNode, next));
Dl_info info; // 動態(tài)鏈接庫時(shí) 通過傳遞指針給Mach-O頭部Mach-O header兑牡,引用一個(gè)Dl_info結(jié)構(gòu)體
dladdr(PC, &info);
printf("----------------------------------------\nfname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
info.dli_fname,
info.dli_fbase,
info.dli_sname,
info.dli_saddr);
}
@end
- dl_info結(jié)構(gòu)體
typedef struct dl_info {
const char *dli_fname; /* 共享對象的路徑名 */
void *dli_fbase; /* 共享對象的基本地址 */
const char *dli_sname; /* 最近的符號的名稱 */
void *dli_saddr; /* 最近的符號地址 */
} Dl_info;
3.4 匯編斷點(diǎn)調(diào)試
-
首先打開匯編調(diào)試
-
在方法中加斷點(diǎn)
-
調(diào)試結(jié)果
-
結(jié)論
- 由匯編斷點(diǎn)調(diào)試可以發(fā)現(xiàn)在所有的方法函數(shù)里邊插入這個(gè)方法
__sanitizer_cov_trace_pc_guard
,因此每次執(zhí)行方法都會先執(zhí)行插樁方法兔簇。- 所以在編譯時(shí)刻发绢,Clang插樁會靜態(tài)加入?yún)R編指令硬耍,做到全局AOP垄琐,Hook一切方法。
3.5 使用__sanitizer_cov_trace_pc_guard
-
斷點(diǎn)打印發(fā)現(xiàn)
PC
就是方法地址
void *PC = __builtin_return_address(0);
通過這個(gè)函數(shù)经柴,拿到當(dāng)前函數(shù)__sanitizer_cov_trace_pc_guard
的下一個(gè)函數(shù)地址狸窘,也就是程序中的真實(shí)調(diào)用方法。
3.6 通過原子隊(duì)列存取方法
-
插樁時(shí)存
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
/* 定位插樁方法的下一個(gè)方法坯认,也就是程序中的真實(shí)調(diào)用方法 */
void *PC = __builtin_return_address(0);
PCNode *node = malloc(sizeof(PCNode));
*node = (PCNode){PC,NULL};
// 進(jìn)入 &symbolList鏈表表頭翻擒,node節(jié)點(diǎn)數(shù)據(jù),offsetof(PCNode, next) 下一個(gè)成員在鏈表中的偏移地址
OSAtomicEnqueue(&symbolList, node, offsetof(PCNode, next));
}
-
通過touchesBegan方法手動取出原子隊(duì)列所存方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray <NSString *> *symbolNames = [NSMutableArray array];
while (YES) {
// &symbolList鏈表表頭牛哺,
PCNode *node = OSAtomicDequeue(&symbolList, offsetof(PCNode, 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];
}
// 由于先進(jìn)先出的特性陋气,所以要取反
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__]];
// 將數(shù)組變成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fontResources.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
-
將存入本地的
fontResources.order
文件取出,放在工程里
-
配置工程的Order File文件
-
二進(jìn)制重排到此結(jié)束引润,對比前后
xxx-LinkMap-normal-arm64.txt
文件巩趁,我們會發(fā)現(xiàn)啟動時(shí)調(diào)用的方法,已經(jīng)被排到前邊去了
四淳附、使用 System Trace
來檢驗(yàn)二進(jìn)制重排結(jié)果
1. 那么如何衡量頁的加載時(shí)間呢议慰?這里就用到了Instruments中的System Trace工具。
首先奴曙,重新啟動設(shè)備(冷啟動)别凹。?+I打開Instruments,選擇System Trace工具洽糟。
點(diǎn)擊錄制?后炉菲,出現(xiàn)第一個(gè)頁面堕战,馬上停止?。過濾只顯示Main Thread相關(guān)拍霜,選擇Summary: Virtual Memory践啄。
- File Backed Page In次數(shù)就是觸發(fā)Page Fault的次數(shù)了。
- Page Cache Hit就是頁緩存命中的次數(shù)了沉御。
由于獲取Page Fault影響因素很多屿讽,導(dǎo)致每次獲取存在較大波動。只能在盡量保證同一的環(huán)境下吠裆,多次采樣取平均值的來大致估算數(shù)據(jù)伐谈。此處不進(jìn)行贅述。