啟動優(yōu)化之二進(jìn)制重排

一、虛擬內(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)存映射來管理的坚弱。

虛擬內(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é)食听。

  1. 0 和 1 代表當(dāng)前地址有沒有在物理內(nèi)存中。
  2. 從上圖我們也可以看出污茵,進(jìn)程的虛擬地址是連續(xù)的樱报,但是實(shí)際物理內(nèi)存地址并不是連續(xù)的,而是由若干完整的內(nèi)存分頁組成泞当。
  3. 當(dāng)應(yīng)用被加載到物理內(nèi)存中時(shí) 迹蛤,并不會將整個(gè)應(yīng)用加載到物理內(nèi)存中。只會放用到的那一部分。也就是懶加載盗飒,換句話說就是應(yīng)用使用多少嚷量,實(shí)際物理內(nèi)存就分配多少。

二逆趣、Page Fault

1. Page Fault產(chǎn)生原因

  1. 當(dāng)應(yīng)用訪問到某個(gè)地址蝶溶,映射表中為 0,也就是說它并沒有被加載到物理內(nèi)存中時(shí)汗贫,系統(tǒng)就會立刻阻塞整個(gè)進(jìn)程身坐,觸發(fā)一個(gè)缺頁中斷秸脱,即 Page Fault落包。
  2. 當(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)存頁上。

  1. 如上圖旗笔,編譯順序是method1彪置、method2... 。啟動時(shí)page1page2都需要從無到有加載到物理內(nèi)存中蝇恶,所以會觸發(fā)兩次Page Fault拳魁。
  2. 二進(jìn)制重排的做法就是將method1method4放到一個(gè)內(nèi)存頁中,那么啟動時(shí)則只需要加載一次 page 即可撮弧,也就是只觸發(fā)一次Page Fault潘懊。
  3. 在實(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用的鏈接器叫做 ldld有一個(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 SettingsOther 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)被排到前邊去了

二進(jìn)制重排前
二進(jìn)制重排后

四淳附、使用 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)行贅述。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末试疙,一起剝皮案震驚了整個(gè)濱河市诵棵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌祝旷,老刑警劉巖履澳,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異怀跛,居然都是意外死亡距贷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進(jìn)店門吻谋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來忠蝗,“玉大人,你說我怎么就攤上這事漓拾「笞睿” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵骇两,是天一觀的道長速种。 經(jīng)常有香客問我,道長低千,這世上最難降的妖魔是什么配阵? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮栋操,結(jié)果婚禮上闸餐,老公的妹妹穿的比我還像新娘。我一直安慰自己矾芙,他們只是感情好舍沙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著剔宪,像睡著了一般拂铡。 火紅的嫁衣襯著肌膚如雪壹无。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天感帅,我揣著相機(jī)與錄音斗锭,去河邊找鬼。 笑死失球,一個(gè)胖子當(dāng)著我的面吹牛岖是,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播实苞,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼豺撑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了黔牵?” 一聲冷哼從身側(cè)響起聪轿,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎猾浦,沒想到半個(gè)月后陆错,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡金赦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年音瓷,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片素邪。...
    茶點(diǎn)故事閱讀 39,711評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡外莲,死狀恐怖猪半,靈堂內(nèi)的尸體忽然破棺而出兔朦,到底是詐尸還是另有隱情,我是刑警寧澤磨确,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布沽甥,位于F島的核電站,受9級特大地震影響乏奥,放射性物質(zhì)發(fā)生泄漏摆舟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一邓了、第九天 我趴在偏房一處隱蔽的房頂上張望恨诱。 院中可真熱鬧,春花似錦骗炉、人聲如沸照宝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽厕鹃。三九已至兢仰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間剂碴,已是汗流浹背把将。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留忆矛,地道東北人察蹲。 一個(gè)月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像催训,于是被迫代替她去往敵國和親递览。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評論 2 353

推薦閱讀更多精彩內(nèi)容