虛擬內(nèi)存&物理內(nèi)存
在計算機早期,數(shù)據(jù)的訪問都是通過物理地址訪問的,即進(jìn)程直接對應(yīng)到具體的物理內(nèi)存;
這種方式有兩個問題
一、內(nèi)存數(shù)據(jù)的安全問題(可以通過已知地址+偏移量來獲取到內(nèi)存中數(shù)據(jù))
二、內(nèi)存不夠用
針對問題,分別有不同的解決方案
內(nèi)存不夠用:虛擬內(nèi)存
在進(jìn)程和物理內(nèi)存之間增加一個中間層,這個中間層就是所謂的虛擬內(nèi)存,主要用于解決當(dāng)多個進(jìn)程同時存在時,對物理內(nèi)存的管理。提高了CPU的利用率,使多個進(jìn)程可以同時、按需加載。所以虛擬內(nèi)存其本質(zhì)就是一張?zhí)摂M地址和物理地址對應(yīng)關(guān)系的映射表
1、每個進(jìn)程都有一個獨立的虛擬內(nèi)存,其地址都是從0開始,大小是4G固定的,每個虛擬內(nèi)存又會劃分為一個一個的頁表(頁表的大小在iOS中是16K,其他的是4K),每次加載都是以頁表為單位加載的,進(jìn)程間是無法互相訪問的,保證了進(jìn)程間數(shù)據(jù)的安全性;頁表的每一個表項分兩部分,第一部分記錄此頁是否在物理內(nèi)存上,第二部分記錄物理內(nèi)存頁的地址(如果在的話)
2、一個進(jìn)程中,只有部分功能是活躍的,所以只需要將進(jìn)程中活躍的部分放入物理內(nèi)存,避免物理內(nèi)存的浪費(優(yōu)化空間)
3、當(dāng)CPU需要訪問數(shù)據(jù)時,首先是訪問虛擬內(nèi)存,然后通過虛擬內(nèi)存去尋址,即把地址翻譯為實際物理內(nèi)存地址,然后對相應(yīng)的物理地址進(jìn)行訪問
4、如果在訪問時,虛擬地址的內(nèi)容未加載到物理內(nèi)存,會發(fā)生缺頁異常(PageFault),缺頁異常的處理過程,操作系統(tǒng)立即阻塞該進(jìn)程,并將硬盤里對應(yīng)的頁換入內(nèi)存,然后使該進(jìn)程就緒,如果內(nèi)存已經(jīng)滿了,沒有空地方了,那就找一個頁覆蓋,至于具體覆蓋的哪個頁,就需要看操作系統(tǒng)的頁面置換算法是怎么設(shè)計的了


內(nèi)存數(shù)據(jù)的安全問題:ASLR
虛擬內(nèi)存的起始地址與大小都是固定的,這意味著,當(dāng)訪問時,其數(shù)據(jù)的地址也是固定的,這會導(dǎo)致內(nèi)存的數(shù)據(jù)非常容易被破解,為了解決這個問題,所以蘋果為了解決這個問題,在iOS4.3開始引入了ASLR技術(shù)
ASLR的概念:(Address Space Layout Randomization )地址空間配置隨機加載,是一種針對緩沖區(qū)溢出的安全保護(hù)技術(shù),通過對堆、棧、共享庫映射等線性區(qū)布局的隨機化,通過增加攻擊者預(yù)測目的地址的難度,防止攻擊者直接定位攻擊代碼位置,達(dá)到阻止溢出攻擊的目的的一種技術(shù)
其目的的通過利用隨機方式配置數(shù)據(jù)地址空間,使某些敏感數(shù)據(jù)(例如APP登錄注冊、支付相關(guān)代碼)配置到一個惡意程序無法事先獲知的地址,令攻擊者難以進(jìn)行攻擊
由于ASLR的存在,導(dǎo)致可執(zhí)行文件和動態(tài)鏈接庫在虛擬內(nèi)存中的加載地址每次啟動都不固定,所以需要在編譯時來修復(fù)鏡像中的資源指針,來指向正確的地址。即正確的內(nèi)存地址 = ASLR地址 + 偏移值
優(yōu)化方案
優(yōu)化方案可以根據(jù)pre-main以及main函數(shù)階段的優(yōu)化(本章暫時先不討論)
接下來著重介紹pre-main階段的一種優(yōu)化方案:二進(jìn)制重排
二進(jìn)制重排原理:
在虛擬內(nèi)存部分(上面第4點),已知,當(dāng)進(jìn)程訪問一個虛擬內(nèi)存,而對應(yīng)的物理內(nèi)存不存在時,會觸發(fā)缺頁中斷(Page Fault),因此阻塞進(jìn)程。此時就需要先加載數(shù)據(jù)到物理內(nèi)存,然后再繼續(xù)訪問。這個對性能是有一定影響的
基于Page Fault,App在冷啟動過程中,會有大量的類、分類、三方等需要加載和執(zhí)行,此時的產(chǎn)生的Page Fault所帶來的的耗時是很大的??聪聢D
1、打開Instruments-->System Trace

2、選擇真機,工程,啟動,首個頁面加載出來點擊停止(冷啟動)

3、查看Main Thread 下的Summary: Virtual Memory

注意:此處1958就是冷啟動情況下Page Fault次數(shù),367.57就是耗時
4、可以通過設(shè)置Write Link Map File來輸出加載順序


從上面的Page Fault的次數(shù)以及加載順序,可以發(fā)現(xiàn)其實導(dǎo)致Page Fault次數(shù)過多的根本原因是啟動時刻需要調(diào)用的方法,處于不同的Page導(dǎo)致的。因此,我們的優(yōu)化思路就是:將所有啟動時刻需要調(diào)用的方法,排列在一起,即放在一個頁中,來減少Page Fault。這就是二進(jìn)制重排的核心原理

注意:此處3135是Page fault次數(shù),21.65就是對應(yīng)的耗時
二進(jìn)制重排具體步驟:
在進(jìn)行重排前,需要了解幾個名次
Link Map
Link Map是iOS編譯過程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局,需要在Xcode的Build Settings里開啟Write Link Map File,Link Map主要包含三部分
Link Map主要包含三部分
1、object Files?生成二進(jìn)制用到的link單元的路徑和文件編號
2、Sections?記錄Mach-O每個Segment/section的地址范圍
3、Symbols?按順序記錄每個符號的地址范圍(如上面黑色圖)
ld
Xcode 是用的鏈接器叫做ld,ld有一個參數(shù)叫Order File, 我們可以通過這個參數(shù)配置一個order文件的路徑(如下圖),在這個order文件中,將所需要的符號按照順序?qū)懺诶锩?,在項目編譯時,會按照這個文件的順序進(jìn)行加載,以此來達(dá)到我們的優(yōu)化

1、在.order文件中 ,需要將從啟動到首頁展示出來的符號按順序?qū)懺诶锩?/p>
2、當(dāng)工程 build 的時候 , Xcode 會讀取這個文件 , 打的二進(jìn)制包就會按照這個文件中的符號順序進(jìn)行生成對應(yīng)的mach-O.
可以通過輸出Link Map File來對比重排前后文件的順序
Link Map File查找路徑:
/Users/用戶名/Library/Developer/Xcode/DerivedData/App名稱/Build/Intermediates.noindex/App名稱.build/Debug-iphoneos/App名稱.build/App名稱-LinkMap-normal-arm64.txt
注意:替換其中的中文為實際的地址

通過對比兩次加載順序(上面兩幅黑色背景圖),可知打的二進(jìn)制包對應(yīng)的mach-O是按照.order中的順序進(jìn)行加載的
如果項目小,可以很輕易的找到從啟動到首頁加載出現(xiàn)之間所調(diào)用的所有方法,如果項目很大,那么這些文件的查找將是一個十分費力的事情;那么該如何查找呢?
Clang插樁
具體步驟:
1、配置
在TARGETS-->Build Settings --> Other C Flags 添加:-fsanitize-coverage=func,trace-pc-guard

如果有Swift,那么還需要在TARGETS-->Build Settings --> Other Swift Flags 添加:
-sanitize-coverage=func
-sanitize=undefined

2、在首頁添加兩個方法
添加方法之前需要先定義兩個結(jié)構(gòu)用來方便存儲和讀取
引入相關(guān)庫:
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h> //調(diào)用動態(tài)鏈接庫用的
#import <libkern/OSAtomic.h> //原子隊列
//定義原子隊列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體
typedef struct{
? ? void*pc;//
? ? void*next;
} SYNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t*stop)
void __sanitizer_cov_trace_pc_guard(uint32_t*guard)?
因為添加了Other C Flags后,會自動找這兩
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);
? ????? //start:起始位置
? ????? //stop:并不是最后一個符號的地址,而是整個符號表的最后一個地址,最后一個符號的地址=stop-4(因為是從高地址往低地址讀取的,且stop是一個無符號int類型,占4個字節(jié))。stop存儲的值是符號的
? ????? //函數(shù)、方法、block都能拿到
????? for(uint32_t*x = start; x < stop; x++)
? ? ????*x = ++N;
}
/// 全面hook方法、函數(shù)、以及block調(diào)用,用于捕捉符號,是在多線程進(jìn)行的,這個方法中只存儲pc,以鏈表的形式
/// @param guard? 是一個哨兵,告訴我們是第幾個被調(diào)用的
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//? if (!*guard) return; //這行代碼會把load 方法return掉
? ? /*
?? ? char PcDescr[1024];
?? ? printf("guard: %p %x PC %s \n"guard,*guard,PcDescr)
?? ? */
? ? //PC是當(dāng)前函數(shù)返回到上一個調(diào)用的地址!!? 參數(shù):0代表當(dāng)前函數(shù)返回到哪里 1代表上層函數(shù)返回到哪里去
? ? void *PC = __builtin_return_address(0);
? ? //創(chuàng)建結(jié)構(gòu)體!
? ? SYNode* node =malloc(sizeof(SYNode));
? ? *node = (SYNode){PC,NULL};
? ? //加入隊列
? ? //符號的訪問不是通過下標(biāo)訪問,是通過鏈表的next指針,所以需要借用offsetof(結(jié)構(gòu)體類型,下一個的地址即next)
? ? OSAtomicEnqueue(&symbolList, node,offsetof(SYNode,next));//鏈表數(shù)據(jù)結(jié)構(gòu)
}
3、獲取所有符號并寫入文件
while循環(huán)從隊列中取出符號,處理非OC方法的前綴,存到數(shù)組中
3.1數(shù)組取反,因為入隊存儲的順序是反序的
3.2數(shù)組去重,并移除本身方法的符號
3.3將數(shù)組中的符號轉(zhuǎn)成字符串并寫入到lvjianxiong.order文件中
- (void)getSymbolFile{
? ? //定義數(shù)組
? ? NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
? ? while (YES) {//一次循環(huán)!也會被HOOK一次!!
????? ? ? ? //解決循環(huán)辦法:Other C Flags 添加 func,只有func才被hook
? ? ? ? ????//取出
?? ? ? ????SYNode* node =OSAtomicDequeue(&symbolList,offsetof(SYNode, next));
????? ? ? ??if(node ==NULL) {
? ? ????? ? ? ? break;
? ? ? ? ????}
????? ? ? ? Dl_info info = {0};
? ? ????? ? dladdr(node->pc, &info);//將pc賦值給info
? ? ? ? ????printf("%s \n",info.dli_sname);
????? ? ? ? //重復(fù)的原因是while(YES),即:循環(huán)一次會被hook一次
????? ? ? ? NSString* name =@(info.dli_sname);
? ? ????? ? free(node);
? ? ? ? //
? ? ? ? BOOL?isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
? ? ? ? NSString* symbolName = isObjc ? name : [@"_"stringByAppendingString:name];
? ? ? ? //是否去重??
? ? ? ? [symbolNames addObject:symbolName];
????}
? ? //取出來是反的,所以需要反轉(zhuǎn)數(shù)組
? ? //反向數(shù)組
????//? ? symbolNames = (NSMutableArray*)[[symbolNames reverseObjectEnumerator] allObjects];
? ? NSEnumerator* enumerator = [symbolNames reverseObjectEnumerator];
? ? //創(chuàng)建一個新數(shù)組
? ? NSMutableArray* funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
? ? NSString* name;
? ? //去重!
? ? while(name = [enumerator nextObject]) {
? ? ? ? if(![funcs containsObject:name]) {//數(shù)組中不包含name
? ? ? ? ? ? [funcs addObject:name];
? ? ? ? }
? ? }
? ? //去掉自己
? ? [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
? ? //數(shù)組轉(zhuǎn)成字符串
? ? NSString* funcStr = [funcs componentsJoinedByString:@"\n"];
? ? //字符串寫入文件
? ? //文件路徑
? ? NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lvjianxiong.order"];
? ? //文件內(nèi)容
? ? NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
? ? [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
4、在首頁的touchesBegan方法中調(diào)用獲取符號文件
-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{
? ? [self getSymbolFile];
}
此處也可以放入到其他地方,方便的地方即可,只是為了方便獲取符號文件,一般來說,是第一個渲染的界面
5、拷貝文件,放入指定位置,并配置路徑
一般將該文件放入主項目路徑下,并在Build Settings --> Order File中配置./lvjianxiong.order

經(jīng)過二進(jìn)制重排,啟動速度可提升15%左右
另外:Clang插樁只需要使用一次,所以獲取到.order后,直接刪除上面代碼即可