iOS啟動優(yōu)化

虛擬內(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)存與物理內(nèi)存聯(lián)系(圖片來自網(wǎng)絡(luò))
頁表工作原理(圖片來走網(wǎng)絡(luò))

內(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

System Trace

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

第一個頁面出來后點擊停止

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

未重排二進(jìn)制時,參查看具體Page Fault與Duration

注意:此處1958就是冷啟動情況下Page Fault次數(shù),367.57就是耗時

4、可以通過設(shè)置Write Link Map File來輸出加載順序

Write Link Map File設(shè)置
加載順序

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

重排二進(jìn)制時,參查看具體Page Fault與Duration

注意:此處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)化

order file添加order文件

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

注意:替換其中的中文為實際的地址

優(yōu)化后加載順序

通過對比兩次加載順序(上面兩幅黑色背景圖),可知打的二進(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

other c flags

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

-sanitize-coverage=func

-sanitize=undefined

other Swift Flags配置

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

配置Order File


經(jīng)過二進(jìn)制重排,啟動速度可提升15%左右

另外:Clang插樁只需要使用一次,所以獲取到.order后,直接刪除上面代碼即可

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容