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


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

2、選擇真機(jī),工程,啟動(dòng),首個(gè)頁面加載出來點(diǎn)擊停止(冷啟動(dòng))

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

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


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

注意:此處3135是Page fault次數(shù),21.65就是對(duì)應(yīng)的耗時(shí)
二進(jìn)制重排具體步驟:
在進(jìn)行重排前,需要了解幾個(gè)名次
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單元的路徑和文件編號(hào)
2、Sections?記錄Mach-O每個(gè)Segment/section的地址范圍
3、Symbols?按順序記錄每個(gè)符號(hào)的地址范圍(如上面黑色圖)
ld
Xcode 是用的鏈接器叫做ld,ld有一個(gè)參數(shù)叫Order File, 我們可以通過這個(gè)參數(shù)配置一個(gè)order文件的路徑(如下圖),在這個(gè)order文件中,將所需要的符號(hào)按照順序?qū)懺诶锩?,在?xiàng)目編譯時(shí),會(huì)按照這個(gè)文件的順序進(jìn)行加載,以此來達(dá)到我們的優(yōu)化

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

通過對(duì)比兩次加載順序(上面兩幅黑色背景圖),可知打的二進(jìn)制包對(duì)應(yīng)的mach-O是按照.order中的順序進(jìn)行加載的
如果項(xiàng)目小,可以很輕易的找到從啟動(dòng)到首頁加載出現(xiàn)之間所調(diào)用的所有方法,如果項(xiàng)目很大,那么這些文件的查找將是一個(gè)十分費(fèi)力的事情;那么該如何查找呢?
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、在首頁添加兩個(gè)方法
添加方法之前需要先定義兩個(gè)結(jié)構(gòu)用來方便存儲(chǔ)和讀取
引入相關(guān)庫:
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h> //調(diào)用動(dòng)態(tài)鏈接庫用的
#import <libkern/OSAtomic.h> //原子隊(duì)列
//定義原子隊(duì)列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定義符號(hào)結(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)?
因?yàn)樘砑恿薕ther C Flags后,會(huì)自動(dòng)找這兩
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:并不是最后一個(gè)符號(hào)的地址,而是整個(gè)符號(hào)表的最后一個(gè)地址,最后一個(gè)符號(hào)的地址=stop-4(因?yàn)槭菑母叩刂吠偷刂纷x取的,且stop是一個(gè)無符號(hào)int類型,占4個(gè)字節(jié))。stop存儲(chǔ)的值是符號(hào)的
? ????? //函數(shù)、方法、block都能拿到
????? for(uint32_t*x = start; x < stop; x++)
? ? ????*x = ++N;
}
/// 全面hook方法、函數(shù)、以及block調(diào)用,用于捕捉符號(hào),是在多線程進(jìn)行的,這個(gè)方法中只存儲(chǔ)pc,以鏈表的形式
/// @param guard? 是一個(gè)哨兵,告訴我們是第幾個(gè)被調(diào)用的
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//? if (!*guard) return; //這行代碼會(huì)把load 方法return掉
? ? /*
?? ? char PcDescr[1024];
?? ? printf("guard: %p %x PC %s \n"guard,*guard,PcDescr)
?? ? */
? ? //PC是當(dāng)前函數(shù)返回到上一個(gè)調(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};
? ? //加入隊(duì)列
? ? //符號(hào)的訪問不是通過下標(biāo)訪問,是通過鏈表的next指針,所以需要借用offsetof(結(jié)構(gòu)體類型,下一個(gè)的地址即next)
? ? OSAtomicEnqueue(&symbolList, node,offsetof(SYNode,next));//鏈表數(shù)據(jù)結(jié)構(gòu)
}
3、獲取所有符號(hào)并寫入文件
while循環(huán)從隊(duì)列中取出符號(hào),處理非OC方法的前綴,存到數(shù)組中
3.1數(shù)組取反,因?yàn)槿腙?duì)存儲(chǔ)的順序是反序的
3.2數(shù)組去重,并移除本身方法的符號(hào)
3.3將數(shù)組中的符號(hào)轉(zhuǎn)成字符串并寫入到lvjianxiong.order文件中
- (void)getSymbolFile{
? ? //定義數(shù)組
? ? NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
? ? while (YES) {//一次循環(huán)!也會(huì)被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)一次會(huì)被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)建一個(gè)新數(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)用獲取符號(hào)文件
-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{
? ? [self getSymbolFile];
}
此處也可以放入到其他地方,方便的地方即可,只是為了方便獲取符號(hào)文件,一般來說,是第一個(gè)渲染的界面
5、拷貝文件,放入指定位置,并配置路徑
一般將該文件放入主項(xiàng)目路徑下,并在Build Settings --> Order File中配置./lvjianxiong.order

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