啟動(dòng)優(yōu)化-二進(jìn)制重排與clang插樁

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

啟動(dòng)優(yōu)化-概念與建議

  • 在上一篇啟動(dòng)優(yōu)化的概念中侥涵,我們理解了虛擬內(nèi)存與物理內(nèi)存步氏,在加載APP不活躍的部分時(shí),會(huì)訪問(wèn)虛擬內(nèi)存的page(映射表)集索,而對(duì)應(yīng)的物理內(nèi)存沒(méi)有與映射表與之關(guān)聯(lián)的話轨奄,將會(huì)觸發(fā)缺頁(yè)異常(page fault),這個(gè)時(shí)候就必須將應(yīng)用在虛擬內(nèi)存不活躍的部分在映射表進(jìn)行與物理內(nèi)存的關(guān)聯(lián)再加載應(yīng)用到物理內(nèi)存腕让,可以理解觸發(fā)缺頁(yè)異常會(huì)對(duì)性能有一定的影響性。

  • 一般來(lái)說(shuō)歧斟,App在冷啟動(dòng)的過(guò)程中纯丸,會(huì)有很多的類偏形,分類,第三方需要加載和執(zhí)行觉鼻,也就是會(huì)觸發(fā)最多次的缺頁(yè)異常俊扭,當(dāng)許多的缺頁(yè)異常一起觸發(fā)會(huì)帶來(lái)大量的耗時(shí),如下以WeChat為例滑凉,啟動(dòng)階段觸發(fā)Page Fault的次數(shù)

  • 打開(kāi)instrumentsSystem Trace

  • 點(diǎn)擊啟動(dòng)统扳,為了表現(xiàn)冷啟動(dòng),需要重啟手機(jī)清除緩存數(shù)據(jù)畅姊,
  • 從instruments測(cè)試結(jié)果,可以看到pagefault次數(shù)有3193次吹由,可以看到這個(gè)是非常影響性能的

優(yōu)化思路

測(cè)試

  • 首先我們通過(guò)以下Demo查看方法在編譯時(shí)期的排列順序若未,在viewController中按下列順序定義,以下幾個(gè)方法
   @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 倾鲫。
  • linkmap路徑 (或是上一張圖片的path to Link Map File)粗合。
  • CMD+B編譯demo,依據(jù)對(duì)應(yīng)的路徑查找link map文件,如下所示乌昔,可以發(fā)現(xiàn)類中函數(shù)的加載順序是從上到下的隙疚。
*   而文件順序是根據(jù)Build Phases -> Compile Sources中的順序加載的。

思路

由上面的測(cè)試磕道,可以看到供屉,文件加載以及函數(shù)的調(diào)用的順序會(huì)影響page fault的數(shù)量,有極大的可能溺蕉,在啟動(dòng)時(shí)刻調(diào)用的方法是不同的page的伶丐,所以我們可以將我們?cè)趩?dòng)時(shí)刻調(diào)用的方法排列在同一頁(yè)中,如此一來(lái)就可以減少觸發(fā)page fault的次數(shù)疯特,這也就是二進(jìn)制重排原理哗魂。

  • 注意:在iOS生產(chǎn)環(huán)境的app,在發(fā)生page fault進(jìn)行重新加載時(shí)漓雅,iOS系統(tǒng)還會(huì)對(duì)其做一次簽名驗(yàn)證 录别,因此iOS在生產(chǎn)環(huán)境的page Fault比Debug環(huán)境下所產(chǎn)生的耗時(shí)更多時(shí)間。

實(shí)現(xiàn)二進(jìn)制重排

名詞解釋

Link Map

  • Linkmap是iOS編譯過(guò)程中間的產(chǎn)物邻吞,紀(jì)錄了二進(jìn)制的文件佈局组题,需要再Xcode的Build Settings裡開(kāi)啟Write Link Map File , Link Map 主要包含三個(gè)部分:
    • Object Files生成二進(jìn)制用到的link單元的路徑和文件編號(hào)
    • Sections 紀(jì)錄Mach-O每個(gè)Segment/section的地址範(fàn)圍
    • Symbols按順序紀(jì)錄每個(gè)符號(hào)的地址範(fàn)圍

ld

  • ld是Xcode使用的鏈接器,有一個(gè)參數(shù)order_file吃衅,我們可以通過(guò)在Build Settings -> Order File 配置一個(gè)後綴為order的文件路徑往踢。在這個(gè)order文件中,將所需要的符號(hào)按照順序?qū)懺谘e面徘层,在項(xiàng)目編譯時(shí)峻呕,會(huì)按照這個(gè)文件的順序進(jìn)行加載利职,以此來(lái)達(dá)到我們的優(yōu)化。也就是說(shuō)二進(jìn)制重排的本質(zhì)就是對(duì)啟動(dòng)加載的符號(hào)進(jìn)行重新排列瘦癌。
  • 但是我們要如何獲取我們的函數(shù)呢猪贪?以下有幾個(gè)思路...
    1. hook objc_msgSend:我們知道,函數(shù)的本質(zhì)是發(fā)送消息讯私,在底層都會(huì)來(lái)到objc_msgSend热押,但是由於objc_msgSend的參數(shù)是可變的,需要通過(guò)彙編獲取斤寇,對(duì)開(kāi)發(fā)人員要求較高桶癣,且也只能拿到OC和swift中@objc後的方法。
    2. 靜態(tài)掃描:掃描Mach-O特定段ㄉ和節(jié)裡面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)
    3. Clang插樁:批量hook娘锁,可以實(shí)現(xiàn)100%符號(hào)覆蓋牙寞,最完全獲取swift,OC莫秆,block函數(shù)

Clang 插樁

  • llvm內(nèi)置了一個(gè)簡(jiǎn)單的代碼覆蓋率檢測(cè)(SanitizerCoverage)间雀。他對(duì)於基本塊級(jí)邊緣級(jí)插入對(duì)用戶定義函數(shù)的調(diào)用。接下來(lái)介紹的批量hook镊屎,就需要借助於sanitizerCoverage 惹挟。
  • 關(guān)於clang的插樁覆蓋的官方文檔如下:

Clang 12 documentation

  • 文檔中有詳細(xì)描述即簡(jiǎn)短的Demo演示。

第一步:開(kāi)啟SanitizerCoverage

  • OC項(xiàng)目:Build SettingsOther C Flags 添加 fsanitize-coverage=func,trace-pc-guard
  • Swift項(xiàng)目:須額外在Build SettingsOther Swift Flags 中加入sanitize-coverage=funcsanitize=undefined
  • 所有鏈接到App中的二進(jìn)制都需要開(kāi)啟SanitizerCoverage 缝驳,這樣才能完全覆蓋到所有調(diào)用
  • 也可以通過(guò)podfile 來(lái)配置參數(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

第二步:重寫(xiě)方法连锯,新建一個(gè)OC文件ACOrderFile,重寫(xiě)兩個(gè)方法

  • __sanitizer_cov_trace_pc_guard_init 方法

    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.
    }
    
    
    • 參數(shù)1:start是一個(gè)指針党巾,指向無(wú)符號(hào)int類萎庭,4個(gè)字節(jié),相當(dāng)於一個(gè)數(shù)組的起始位置 齿拂,即符號(hào)的起始位置(是從高位往低位讀)
  • 參數(shù)2:Stop 也是一個(gè)指針驳规,由於取數(shù)據(jù)是從高地址往低地址讀取的,所以如果直接讀取stop的地址並不是stop真正的地址署海,而是讀取到最後的地址吗购,所以讀取stop時(shí),由於stop占4個(gè)字節(jié)砸狞,stop真實(shí)地址 = stop打印的地址-0x4
  • stop內(nèi)存地址中存儲(chǔ)的值表示什麼呢捻勉?我們可以增加一個(gè)方法/C++/屬性的方法(多3個(gè)),發(fā)現(xiàn)其值也會(huì)增加對(duì)應(yīng)的數(shù)刀森,例如我們?cè)黾右粋€(gè)test1方法踱启。可以看到數(shù)據(jù)從1d變成了1e
  • __sanitizer_cov_trace_pc_guard 方法,主要是捕獲所有的啟動(dòng)時(shí)刻的符號(hào)埠偿,將所有符號(hào)入隊(duì)
  • 參數(shù)guard是一個(gè)哨兵透罢,告訴我們是第幾個(gè)被調(diào)用的
  • 符號(hào)的存儲(chǔ)需要借助於鏈表,所以需要定義鏈表節(jié)點(diǎn)ACNode
  • 通過(guò)OSQueueHead創(chuàng)建原子隊(duì)列冠蒋,其目的是保證讀寫(xiě)的安全
  • 通過(guò)OSAtomicEnqueue 方法將node入隊(duì)羽圃,通過(guò)鏈表的next指針可以訪問(wèn)下一個(gè)符號(hào)
       //原子隊(duì)列,其目的是保證寫(xiě)入安全抖剿,線程安全
       static  OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
       //定義符號(hào)結(jié)構(gòu)體朽寞,以鏈表的形式
       typedef struct {
           void *pc;
           void *next;
       }ACNode;

       /*
        - start:起始位置
        - stop:並不是最後一個(gè)符號(hào)地址,而是整個(gè)符號(hào)表的最後一個(gè)地址斩郎,
        最後一個(gè)符號(hào)的地址=stop-4
       (因?yàn)樽x取數(shù)據(jù)是從高地址往低地址讀取的脑融,且stop是一個(gè)無(wú)符號(hào)int類型,占4個(gè)字節(jié))孽拷。
        stop存儲(chǔ)的值是符號(hào)
        */
       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)用脓恕,用於捕捉符號(hào),是多線程進(jìn)行的窿侈,
        這個(gè)方式只儲(chǔ)存pc炼幔,以鏈表的形式

        - guard 是一個(gè)哨兵,告訴我們是第幾個(gè)被調(diào)用的
        */
       void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
           //將load方法過(guò)濾掉了史简,所以需要注釋掉
           //    if (!*guard) return;

           //獲取PC
           /*
            - PC 當(dāng)前函數(shù)返回上一個(gè)調(diào)用的地址
            - 0 當(dāng)前這個(gè)函數(shù)地址乃秀,即當(dāng)前函數(shù)的返回地址
            - 1 當(dāng)前函數(shù)調(diào)用者的地址,即上一個(gè)函數(shù)的返回地址
           */
           void *PC = __builtin_return_address(0);
           //創(chuàng)建node圆兵,並賦值
           ACNode *node = malloc(sizeof(ACNode));
           *node = (ACNode){PC, NULL};

           //加入隊(duì)列
           //符號(hào)的訪問(wèn)不是通過(guò)下標(biāo)訪問(wèn)跺讯,是通過(guò)鏈表的next指針,
           //所以需要借用offsetof(結(jié)構(gòu)體類型殉农,下一個(gè)的地址即next)
           OSAtomicEnqueue(&queue, node, offsetof(ACNode, next));
       }

第三步:獲取所有符號(hào)並寫(xiě)入文件

  • while循環(huán)從隊(duì)列中取出符號(hào)刀脏,處理非OC方法的前綴,存入數(shù)組中
  • 數(shù)組取反超凳,因?yàn)槿腙?duì)儲(chǔ)存的順序是反序的愈污。
  • 數(shù)組去重,並移除本身方法的符號(hào)
  • 將數(shù)組中的符號(hào)轉(zhuǎn)成字符串並寫(xiě)入到AC.oder文件中
  • 另外也可以在第一個(gè)didFinishLaunchingWithOptions根視圖轮傍,獲取符號(hào)(這部分可以由自己決定暂雹,以下範(fàn)例是在touchesBegan方法調(diào)用時(shí)獲取)
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
        //while 循環(huán)取符號(hào)
        while (YES) {
            //出隊(duì)
            ACNode * node = OSAtomicDequeue(&symbolList, offsetof(ACNode, next));
            if (node == NULL) {
                break;
            }
            //取出PC存入info
            Dl_info info;
            dladdr(node->pc, &info);
            //printf("%s \\n", info.dli_sname);
            NSString * name = @(info.dli_sname);
            //判斷是不是OC方法,如果不是需要加下滑線创夜,反之杭跪,則直接存儲(chǔ)
            BOOL  isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
            NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
            [symbolNames addObject:symbolName];
        }
        //取反 (隊(duì)列的存儲(chǔ)是反序的)
        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"];
        //字符串寫(xiě)入文件
        NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"AC.order"];
        NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
        [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        NSLog(@"%@",funcStr);
    }

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

  • Build Settings -> Other C Flags 的如果配置的是-fsanitize-coverage=trace-pc-guard ,在while部分會(huì)出現(xiàn)死循環(huán)(我們?cè)趖ouchBegin方法中調(diào)適)
  • 我們透過(guò)彙編查看,發(fā)現(xiàn)有三個(gè)地方調(diào)用__sanitizer_cov_trace_pc_guard 的調(diào)用
  • 第一次 touchBegin調(diào)用__sanitizer_cov_trace_pc_guard
  • 第二次bl跳轉(zhuǎn)跳轉(zhuǎn)因?yàn)閣hile循環(huán)涧尿,只要跳轉(zhuǎn)就會(huì)被hook系奉,即有b,bl指令现斋,就會(huì)被hook
  • 第三次bl是printf

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

第四步 拷貝文件喜最,放入指定位置 並配置路徑

  • 一般將該文件放入主項(xiàng)目的路徑下,並在Build Settings -> Order File 中配置./AC.order庄蹋,下面是配置前後的對(duì)比
  • 沒(méi)有配置瞬内,照序加載
  • 有配置,依照啟動(dòng)時(shí)時(shí)刻所需(自己配置的.oder文件)加載


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末限书,一起剝皮案震驚了整個(gè)濱河市虫蝶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌倦西,老刑警劉巖能真,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異扰柠,居然都是意外死亡粉铐,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門(mén)卤档,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蝙泼,“玉大人,你說(shuō)我怎么就攤上這事劝枣√捞ぃ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵舔腾,是天一觀的道長(zhǎng)溪胶。 經(jīng)常有香客問(wèn)我,道長(zhǎng)稳诚,這世上最難降的妖魔是什么哗脖? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮采桃,結(jié)果婚禮上懒熙,老公的妹妹穿的比我還像新娘。我一直安慰自己普办,他們只是感情好工扎,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著衔蹲,像睡著了一般肢娘。 火紅的嫁衣襯著肌膚如雪呈础。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,874評(píng)論 1 314
  • 那天橱健,我揣著相機(jī)與錄音而钞,去河邊找鬼。 笑死拘荡,一個(gè)胖子當(dāng)著我的面吹牛臼节,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播珊皿,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼网缝,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蟋定?” 一聲冷哼從身側(cè)響起粉臊,我...
    開(kāi)封第一講書(shū)人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驶兜,沒(méi)想到半個(gè)月后扼仲,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抄淑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年屠凶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肆资。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阅畴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迅耘,到底是詐尸還是另有隱情,我是刑警寧澤监署,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布颤专,位于F島的核電站,受9級(jí)特大地震影響钠乏,放射性物質(zhì)發(fā)生泄漏栖秕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一晓避、第九天 我趴在偏房一處隱蔽的房頂上張望簇捍。 院中可真熱鬧,春花似錦俏拱、人聲如沸暑塑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)事格。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驹愚,已是汗流浹背远搪。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逢捺,地道東北人谁鳍。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像劫瞳,于是被迫代替她去往敵國(guó)和親倘潜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361

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