iOS底層-啟動優(yōu)化(二進(jìn)制重排)

二進(jìn)制重排原理

在虛擬內(nèi)存部分,我們知道撕予,當(dāng)進(jìn)程訪問一個虛擬內(nèi)存page乞封,而對應(yīng)的物理內(nèi)存不存在時做裙,會觸發(fā)缺頁中斷(Page Fault),因此阻塞進(jìn)程肃晚。此時就需要先加載數(shù)據(jù)到物理內(nèi)存锚贱,然后再繼續(xù)訪問。這個對性能是有一定影響的关串。

基于Page Fault拧廊,我們思考杂穷,App在冷啟動過程中,會有大量的類卦绣、分類、三方等需要加載和執(zhí)行飞蚓,此時的產(chǎn)生的Page Fault所帶來的的耗時是很大的滤港。以WeChat為例,我們來看下趴拧,在啟動階段的Page Fault的次數(shù)

  • CMD+i快捷鍵溅漾,選擇System Trace


    image.png
  • 點(diǎn)擊啟動(啟動前需要重啟手機(jī),清除緩存數(shù)據(jù))著榴,第一個界面出來后添履,停掉野芒,按照下圖中操作:


    image.jpg

從圖中可以看出WeChat發(fā)生的PageFault有2800+次庄涡,可想而知,這個是非常影響性能的该押。

  • 然后我們再通過Demo查看方法在編譯時期的排列順序问麸,在ViewController中按下列順序定義以下幾個方法
@implementation ViewController

void test1(){
    printf("1");
}

void test2(){
    printf("2");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    test1();
}

+(void)load{
    printf("3");
    test2();
}
@end
  • 在Build Setting -> Write Link Map File設(shè)置為YES


    image.png
  • CMD+B編譯demo往衷,然后在對應(yīng)的路徑下查找 link map文件,如下所示严卖,可以發(fā)現(xiàn) 類中函數(shù)的加載順序是從上到下的席舍,而文件的順序是根據(jù)Build Phases -> Compile Sources中的順序加載的

image.jpg

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

二進(jìn)制重排原理.png

注意:在iOS生產(chǎn)環(huán)境的app,在發(fā)生Page Fault進(jìn)行重新加載時鲁冯,iOS系統(tǒng)還會對其做一次簽名驗(yàn)證拷沸,因此 iOS 生產(chǎn)環(huán)境的 Page Fault 比Debug環(huán)境下所產(chǎn)生的耗時更多。

二進(jìn)制重排實(shí)踐

下面薯演,我們來進(jìn)行具體的實(shí)踐撞芍,首先理解幾個名詞

Link Map

Linkmap是iOS編譯過程的中間產(chǎn)物,記錄了二進(jìn)制文件的布局跨扮,需要在Xcode的Build Settings里開啟Write Link Map File,Link Map主要包含三部分:

Object Files 生成二進(jìn)制用到的link單元的路徑和文件編號

Sections 記錄Mach-O每個Segment/section的地址范圍

Symbols 按順序記錄每個符號的地址范圍

Id

ld是Xcode使用的鏈接器序无,有一個參數(shù)order_file验毡,我們可以通過在Build Settings -> Order File配置一個后綴為order的文件路徑。在這個order文件中帝嗡,將所需要的符號按照順序?qū)懺诶锩婢ǎ陧?xiàng)目編譯時,會按照這個文件的順序進(jìn)行加載哟玷,以此來達(dá)到我們的優(yōu)化

所以二進(jìn)制重排的本質(zhì)就是對啟動加載的符號進(jìn)行重新排列狮辽。

到目前為止,原理我們基本弄清楚了巢寡,如果項(xiàng)目比較小喉脖,完全可以自定義一個order文件,將方法的順序手動添加抑月,但是如果項(xiàng)目較大树叽,涉及的方法特別多,此時我們?nèi)绾潍@取啟動運(yùn)行的函數(shù)呢谦絮?有以下幾種思路

  • 1题诵、hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息挨稿,在底層都會來到objc_msgSend仇轻,但是由于objc_msgSend的參數(shù)是可變的,需要通過匯編獲取奶甘,對開發(fā)人員要求較高篷店。而且也只能拿到OC 和 swift中@objc 后的方法

  • 2、靜態(tài)掃描:掃描 Mach-O 特定段和節(jié)里面所存儲的符號以及函數(shù)數(shù)據(jù)

  • 3臭家、Clang插樁:即批量hook疲陕,可以實(shí)現(xiàn)100%符號覆蓋,即完全獲取swift钉赁、OC蹄殃、C、block函數(shù)

Clang 插樁

llvm內(nèi)置了一個簡單的代碼覆蓋率檢測(SanitizerCoverage)你踩。它在函數(shù)級诅岩、基本塊級和邊緣級插入對用戶定義函數(shù)的調(diào)用。我們這里的批量hook带膜,就需要借助于SanitizerCoverage吩谦。

關(guān)于 clang 的插樁覆蓋的官方文檔如下 : clang 自帶代碼覆蓋工具 文檔中有詳細(xì)概述,以及簡短Demo演示膝藕。

  • 第一步:配置開啟 SanitizerCoverage

  • OC項(xiàng)目式廷,需要在:在 Build Settings 里的 “Other C Flags” 中添加 -fsanitize-coverage=func,trace-pc-guard

  • 如果是Swift項(xiàng)目,還需要額外在 “Other Swift Flags” 中加入-sanitize-coverage=func 和 -sanitize=undefined

  • 所有鏈接到 App 中的二進(jìn)制都需要開啟 SanitizerCoverage芭挽,這樣才能完全覆蓋到所有調(diào)用滑废。

  • 也可以通過podfile來配置參數(shù)

post_install do |installer|
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      config.build_settings['OTHER_CFLAGS'] = '-fsanitize-coverage=func,trace-pc-guard'
      config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-coverage=func -sanitize=undefined'
    end
  end
end
  • 第二步:重寫方法新建一個OC文件CJLOrderFile蝗肪,重寫兩個方法

  • __sanitizer_cov_trace_pc_guard_init方法

  • 參數(shù)1 start 是一個指針,指向無符號int類型蠕趁,4個字節(jié)薛闪,相當(dāng)于一個數(shù)組的起始位置,即符號的起始位置(是從高位往低位讀

image.jpg
  • 參數(shù)2 stop俺陋,由于數(shù)據(jù)的地址是往下讀的(即從高往低讀逛绵,所以此時獲取的地址并不是stop真正的地址,而是標(biāo)記的最后的地址倔韭,讀取stop時,由于stop占4個字節(jié)瓢对,stop真實(shí)地址 = stop打印的地址-0x4)


    image.jpg

stop內(nèi)存地址中存儲的值表示什么寿酌?在增加一個方法/塊/c++/屬性的方法(多3個),發(fā)現(xiàn)其值也會增加對應(yīng)的數(shù)硕蛹,例如增加一個test1方法

image.jpg
  • __sanitizer_cov_trace_pc_guard方法 醇疼,主要是捕獲所有的啟動時刻的符號,將所有符號入隊(duì)

  • 參數(shù)guard是一個哨兵法焰,告訴我們是第幾個被調(diào)用的

  • 符號的存儲需要借助于鏈表秧荆,所以需要定義鏈表節(jié)點(diǎn)CJLNode,

  • 通過OSQueueHead創(chuàng)建原子隊(duì)列埃仪,其目的是保證讀寫安全

  • 通過OSAtomicEnqueue方法將node入隊(duì)乙濒,通過鏈表的next指針可以訪問下一個符號

//原子隊(duì)列,其目的是保證寫入安全卵蛉,線程安全
static  OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定義符號結(jié)構(gòu)體颁股,以鏈表的形式
typedef struct {
    void *pc;
    void *next;
}CJLNode;

/*
 - start:起始位置
 - stop:并不是最后一個符號的地址,而是整個符號表的最后一個地址傻丝,最后一個符號的地址=stop-4(因?yàn)槭菑母叩刂吠偷刂纷x取的甘有,且stop是一個無符號int類型,占4個字節(jié))葡缰。stop存儲的值是符號的
 */
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
    static uint64_t N;
    if (start == stop || *start) return;
    printf("INIT: %p - %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++) {
        *x = ++N;
    }
    
}

/*
 可以全面hook方法亏掀、函數(shù)、以及block調(diào)用泛释,用于捕捉符號滤愕,是在多線程進(jìn)行的,這個方法中只存儲pc胁澳,以鏈表的形式
 
 - guard 是一個哨兵该互,告訴我們是第幾個被調(diào)用的
 */
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;//將load方法過濾掉了,所以需要注釋掉
    
    //獲取PC
    /*
     - PC 當(dāng)前函數(shù)返回上一個調(diào)用的地址
     - 0 當(dāng)前這個函數(shù)地址韭畸,即當(dāng)前函數(shù)的返回地址
     - 1 當(dāng)前函數(shù)調(diào)用者的地址宇智,即上一個函數(shù)的返回地址
    */
    void *PC = __builtin_return_address(0);
    //創(chuàng)建node蔓搞,并賦值
    CJLNode *node = malloc(sizeof(CJLNode));
    *node = (CJLNode){PC, NULL};
    
    //加入隊(duì)列
    //符號的訪問不是通過下標(biāo)訪問,是通過鏈表的next指針随橘,所以需要借用offsetof(結(jié)構(gòu)體類型喂分,下一個的地址即next)
    OSAtomicEnqueue(&queue, node, offsetof(CJLNode, next));
}
  • 【第三步:獲取所有符號并寫入文件】
    -while循環(huán)從隊(duì)列中取出符號,處理非OC方法的前綴机蔗,存到數(shù)組中
  • 數(shù)組取反蒲祈,因?yàn)槿腙?duì)存儲的順序是反序的
  • 數(shù)組去重,并移除本身方法的符號
  • 將數(shù)組中的符號轉(zhuǎn)成字符串并寫入到cjl.order文件中
extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
    
    collectFinished = YES;
    __sync_synchronize();
    NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //創(chuàng)建符號數(shù)組
        NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
        
        //while循環(huán)取符號
        while (YES) {
            //出隊(duì)
            CJLNode *node = OSAtomicDequeue(&queue, offsetof(CJLNode, next));
            if (node == NULL) break;
            
            //取出PC,存入info
            Dl_info info;
            dladdr(node->pc, &info);
//            printf("%s \n", info.dli_sname);
            
            if (info.dli_sname) {
                //判斷是不是OC方法萝嘁,如果不是梆掸,需要加下劃線存儲,反之牙言,則直接存儲
                NSString *name = @(info.dli_sname);
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
                [symbolNames addObject:symbolName];
            }
           
        }
        
        if (symbolNames.count == 0) {
            if (completion) {
                completion(nil);
            }
            return;
        }
        
        //取反(隊(duì)列的存儲是反序的)
        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:functionExclude];
        
        //將數(shù)組變成字符串
        NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
        NSLog(@"Order:\n%@", funcStr);
        
        //字符串寫入文件
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"cjl.order"];
        NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        if (completion) {
            completion(success ? filePath : nil);
        }
    });
}
  • 【第四步:在didFinishLaunchingWithOptions方法最后調(diào)用】需要注意的是酸钦,這里的調(diào)用位置是由你決定的,一般來說咱枉,是第一個渲染的界面
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    [self test11];
    
    getOrderFile(^(NSString *orderFilePath) {
        NSLog(@"OrderFilePath:%@", orderFilePath);
    });
    
    return YES;
}

- (void)test11{
    
}

此時的cjl.order中只有這三個方法

image.png
  • 【第五步:拷貝文件卑硫,放入指定位置,并配置路徑】一般將該文件放入主項(xiàng)目路徑下蚕断,并在Build Settings -> Order File中配置./cjl.order欢伏,下面是配置前后的對比(上邊是配置前的熟悉怒,下邊是配置后符號順序的)
對比.jpg

注意點(diǎn):避免死循環(huán)

  • Build Settings -> Other C Flags的如果配置的是-fsanitize-coverage=trace-pc-guard亿乳,在while循環(huán)部分會出現(xiàn)死循環(huán)(我們在touchBegin方法中調(diào)試)
image.jpg
  • 我們打開匯編調(diào)試硝拧,發(fā)現(xiàn)有3個__sanitizer_cov_trace_pc_guard的調(diào)用
image.jpg
  • 第一次是bl 是 touchBegin
image.jpg
  • 第三次 bl 是 printf
  • 第二次 bl 是因?yàn)閣hile 循環(huán)。 即 只要是跳轉(zhuǎn)葛假,就會被hook河爹,即有 bl、b的指令桐款,就會被hook
image.jpg

解決方式:將BuildSetting中的other C Flags的-fsanitize-coverage=trace-pc-guard 咸这,改成-fsanitize-coverage=func,trace-pc-guard

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市魔眨,隨后出現(xiàn)的幾起案子媳维,更是在濱河造成了極大的恐慌,老刑警劉巖遏暴,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侄刽,死亡現(xiàn)場離奇詭異,居然都是意外死亡朋凉,警方通過查閱死者的電腦和手機(jī)州丹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人墓毒,你說我怎么就攤上這事吓揪。” “怎么了所计?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵柠辞,是天一觀的道長。 經(jīng)常有香客問我主胧,道長叭首,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任踪栋,我火速辦了婚禮焙格,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘夷都。我一直安慰自己间螟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布损肛。 她就那樣靜靜地躺著,像睡著了一般荣瑟。 火紅的嫁衣襯著肌膚如雪治拿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天笆焰,我揣著相機(jī)與錄音劫谅,去河邊找鬼。 笑死嚷掠,一個胖子當(dāng)著我的面吹牛捏检,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播不皆,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼贯城,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了霹娄?” 一聲冷哼從身側(cè)響起能犯,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎犬耻,沒想到半個月后踩晶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡枕磁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年渡蜻,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡茸苇,死狀恐怖排苍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情税弃,我是刑警寧澤纪岁,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站则果,受9級特大地震影響幔翰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜西壮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一遗增、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧款青,春花似錦做修、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至康震,卻和暖如春燎含,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腿短。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工屏箍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人橘忱。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓赴魁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親钝诚。 傳聞我的和親對象是個殘疾皇子颖御,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

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