App啓動優化

  • App的啓動過程

    App的啓動通常是指從用戶點擊App開始到AppDelegatedidFinishLaunching方法執行完成爲止,通常又將啓動分爲冷啓動和熱啓動。html

    • 冷啓動
      冷啓動: 是指App啓動前它的進程不在系統裏,須要系統分配一個進程給它啓動的狀況,這是一次完成的啓動(通常啓動優化都是優化冷啓動的過程)
    • 熱啓動
      熱啓動: 是指App在冷啓動後將App退後臺,App的進程還在系統裏,內存中海油App的數據的狀況下,再次啓動App的過程,這個過程作的事情也很是少
  • App啓動優化

    上文也說了通常啓動優化主要優化的是冷啓動的過程,熱啓動作的事情也很是少。因此這裏只講解冷啓動過程的優化。冷啓動過程又被分爲main函數執行以前和main函數執行以後node

    • main函數執行以前
      操做系統加載App可執行文件到內存,執行一系列的加載&連接工做,能夠經過添加添加環境變量DYLD_PRINT_STATISTICS來查看main函數執行以前都作了什麼,同時也能夠看出對應消耗的時間 image.png 不難發現main函數執行以前主要作了如下幾種事情
      • 動態庫的加載
        對應的是dylib loading time能夠發現加載時間爲48.41毫秒
        優化建議 :
        這裏主要的優化建議是減小動態庫的加載,蘋果公司建議更少的使用動態庫,而且建議動態庫的數量較多的時候,儘可能將多個動態庫合併,數量上蘋果公司最多支持6個非系統動態庫的合併
      • 偏移修正和符號綁定
        對應的是rebase/binding time,耗時9.18毫秒
        • 偏移修正
          任何App生成的二進制文件中的方法、函數都會有個地址,而這個地址是相對於當前二進制文件中的偏移地址,可是到了運行時系統會隨機生成一個數值添加到二進制文件的頭部(ASLR安全機制下文中會有講解),因此此時函數、方法的地址就是 隨機分配的數值+偏移地址 這個過程就是偏移修正
        • 符號綁定
          動態庫不像是靜態庫,靜態庫實在編譯時期就將對應使用到的代碼一塊兒打包生成了mach-o文件,因此此時使用到的靜態庫的方法、函數其實就和自定義的方法、函數差很少了,可以直接獲取到對應的地址,可是動態庫在編譯階段是不會被打包進mach-o文件的,可是此時又用到了動態庫中的方法,例如用到了NSLog方法,此時就會生成一個!NSLog 符號此時這個符號會隨機指向一個地址,當運行時,此時動態庫被加載到內存,此時就能夠拿到動態庫對應的方法、函數的地址,因此此時就須要將!NSLog這個符號綁定到相應的地址上去(dyld作的),這個過程就叫作符號綁定
      • 類的註冊
        對應的是ObjC setup time,耗時10.86毫秒
        優化建議
        刪除啓動後不會去使用的類
      • 執行load和構造函數
        對應的是initializer time,耗時110.79毫秒
        優化建議
        減小使用load方法相應的能夠將load中的實現放在+initialize()方法中去,應爲通常一個load方法的執行須要耗時4毫秒,並且若是類中實現了load那麼相對應類的加載就要提早到read_image方法中去執行,若是沒有實現load類的加載則會方法第一次發送消息的時候加載,
    • main函數執行以後
      這個階段主要是指main函數執行開始到首屏渲染完成方法執行完畢。 這個階段主要作的工做包括:
      • 第三方SDK初始化
      • 自定義工具類初始化
      • 首屏數據的加載
      • 首屏渲染的一些計算
      這個地方的優化建議主要有一下幾點
      1. 只處理首屏渲染相關的任務,其餘非首屏的業務例如初始化、註冊監聽、配置文件的讀取等等都放在首頁渲染完成以後去作,固然也能夠開闢一個線程去處理這些事情。儘可能不要佔用主線程
      2. 本身的業務邏輯的優化,已經廢棄的不須要用的邏輯代碼、方法、函數都刪除掉,減小每一個流程的耗時
      3. 啓動時期的頁面儘可能避免使用xib、storyboard(中間會有個轉換的過程也是須要耗時的)UI的主框架儘可能使用純代碼
  • 二進制重排基礎知識

    上文主要是針對特定的階段作一些優化處理,除了刪除的優化方案還有一種優化,就是二進制重排,在講解二進制重排以前先將幾個概念性的東西:ios

    1. 物理內存
      就是運行內存,是指計算機上安裝的內存,通俗的將其實就是內存條的大小。
      早期的操做系統沒有虛擬內存,程序尋址用的都是物理地址,因此沒啓動一個程序開闢一個進程都要相應的分配一段物理內存給這個程序,這就形成了以下幾個問題:
      1. 當物理內存被分配完成的時候此時其餘程序就不能再被加載到內存(也就是不能運行),此時就須要等待其餘程序退出釋放內存,此時才能運行新的程序
      2. 程序指令都是在物理內存上操做的,那麼我這個進程就能夠修改其餘進程的數據,甚至會修改內核地址空間的數據
      針對以上的問題也就引出了虛擬內存
    2. 虛擬內存
      指的是把硬盤中的一部分空間用來當作內存使用
      進程和物理內存之間增長一箇中間層,這個中間層就是所謂的虛擬內存,主要用於解決當多個進程同時存在時,對物理內存的管理。提升了CPU的利用率,使多個進程能夠同時、按需加載。因此虛擬內存其本質就是一張虛擬地址和物理地址對應關係的映射表.每一個進程都有一個獨立的虛擬內存,其地址都是從0開始,大小是4G固定的。
      進程開始要訪問一個地址,它可能會經歷下面的過程:
      1. 每次我要訪問地址空間上的某一個地址,可是進程間是沒法互相訪問的,保證了進程間數據的安全(一個進程只能訪問給定的這篇虛擬內存的地址)。都須要把地址翻譯爲實際物理內存地址
      2. 全部進程共享這整一塊物理內存,每一個進程只把本身目前須要的虛擬地址空間映射到物理內存上
      3. 每一個虛擬內存會劃分一個一個頁存儲(頁的大小在iOS中是16K,其餘的是4K),進程須要知道哪些地址空間上的數據在物理內存上,哪些不在(可能這部分存儲在磁盤上),還有在物理內存上的哪裏,這就須要經過頁表來記錄
      4. 頁表的每個表項分兩部分,第一部分記錄此頁是否在物理內存上,第二部分記錄物理內存頁的地址(若是在的話)
      5. 當進程訪問某個虛擬地址的時候,就會先去看頁表,若是發現對應的數據不在物理內存上,就會發生缺頁異常
      6. 缺頁異常的處理過程,操做系統當即阻塞該進程,並將硬盤裏對應的頁換入內存,而後使該進程就緒,若是內存已經滿了,沒有空地方了,那就找一個頁覆蓋,至於具體覆蓋的哪一個頁,就須要看操做系統的頁面置換算法是怎麼設計的了。
      以下圖所示,虛擬內存與物理內存間的關係 未命名文件(34).jpg 若是物理內存被佔滿,此時又有新的頁須要被加載進來,此時新頁就會吧長時間沒有使用的頁覆蓋掉
    3. ASLR
      應爲虛擬內存的起始地址與大小都是固定的,這意味着,當咱們訪問時,其數據的地址也是固定的,這會致使咱們的數據很是容易被破解,爲了解決這個問題,因此蘋果爲了解決這個問題,在iOS4.3開始引入了ASLR技術,其實現原理就是在虛擬內存的頭部隨機加上一塊地址,這樣每次啓動時虛擬地址的其實址就不同,因此在程序啓動的時候須要作偏移修正。
  • 二進制重排緣由

    從上文的知識中能夠知道,ios程序在加載到虛擬內存的時候會被分紅不少不少頁,若是此時訪問的虛擬地址的一個page,對應的物理地址不存在,則會缺頁異常,此時會阻塞進程將這一頁加載到物理內存而後在訪問。這裏能夠經過instrumentsSystem Trace來查看你的項目的缺頁異常的數量以下: image.png 步驟:先點擊啓動->首頁加載完成後暫停->而後找到你的項目找到主線程 image.png發現啓動以前有兩百多個缺頁異常,此時咱們再看項目在編譯時期的默認排列順序,此時咱們寫一個簡單的demo以下圖: image.png就是寫了幾個簡單的方法,而後項目中選擇Build-setting搜索link map而後配置image.png此時會發現對應配置的文件夾中生成了對應的link-map文件,image.png發現方法、函數等都是按照在文件中的實現順序來的,而文件的順序是按照comple source中的順序來的如圖: image.png這種狀況就形成了每一個頁有可能只有一個方法是有用的,其餘方法、函數等都不是在啓動階段調用的,這就形成了在啓動時期缺頁異常的數量會不少,也就形成了啓動時間變長的狀況。這也就是須要進行二進制重排的緣由算法

  • 二進制重排原理

    上文分析了二進制重排的緣由,就是應爲頁中空間的浪費沒有充分利用每一個頁的空間形成缺頁異常數量增多,二進制重排的原理其實就是將啓動階段用到的方法、函數所有排在最前面,這樣就能充分利用每一個頁的空間,與此同時也下降了缺頁異常的數量。以下圖所示: 未命名文件(35).jpg
    明顯減小了一大半的缺頁異常的數量swift

  • 二進制重排實踐

    經過上面的原理分析能夠知道,若是作二進制重排只須要改變編譯時期方法、函數等的排列順序就行。其本質就是就是對啓動加載的符號進行從新排列數組

    • 修改排列順序的方法
      Xcode是用的連接器叫作ld,ld有一個參數叫Order File , 咱們能夠經過這個參數配置一個 order文件的路徑 .
      咱們能夠經過在Build Settings -> Order File配置一個後綴爲order的文件路徑。在這個order文件中,將所須要的符號按照順序寫在裏面,在項目編譯時,會按照這個文件的順序進行加載,以此來達到咱們的優化,因此二進制重排的關鍵點在於Order File文件的生成
    • 獲取Order File文件的方法
      1. 若是項目不大的狀況下本身也能夠根據項目本身找到啓動階段要運行的方法、函數,本身編寫Order File文件。
      2. hook objc_msgSend,可是因爲objc_msgSend的參數是可變的,須要經過彙編獲取,使用門檻比較高。並且也只能拿到OC和swift中@objc後的方法
      3. 靜態掃描:掃描 Mach-O 特定段和節裏面所存儲的符號以及函數數據
      4. Clang插樁:即批量hook,能夠實現100%符號覆蓋,即徹底獲取swift、OC、C、block函數
    • Clang插樁
      llvm內置了一個簡單的代碼覆蓋率檢測(SanitizerCoverage)。它在函數級、基本塊級和邊緣級插入對用戶定義函數的調用,相應文檔
      具體步驟:
      1. 配置開啓SanitizerCoverage,在build setting中搜索Other C Flags,以下圖image.png若是是OC項目則添加-fsanitize-coverage=func,trace-pc-guard,若是是swift項目則添加-sanitize-coverage=func-sanitize=undefined
      2. 添加hook方法
        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) {
             //guard 是一個哨兵,告訴咱們是第幾個被調用的
             // 這個地方 是過濾掉了load方法,因此這裏須要註釋掉
             if (!*guard) return;
               /*
                - PC 當前函數返回上一個調用的地址
                - 0 當前這個函數地址,即當前函數的返回地址
                - 1 當前函數調用者的地址,即上一個函數的返回地址
               */
             void *PC = __builtin_return_address(0);
             char PcDescr[1024];
             printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
           }
        
        複製代碼
        主要的方法在於__sanitizer_cov_trace_pc_guard方法,在這裏咱們能夠取到對應方法的地址,爲何方法執行以前會先調用__sanitizer_cov_trace_pc_guard方法呢,可經過斷點調試查看,在一個方法或者函數的起始處大斷點,再看彙編代碼以下圖: image.png image.png 發如今方法執行以前插入了__sanitizer_cov_trace_pc_guard方法,全部的函數執行都會限制性__sanitizer_cov_trace_pc_guard方法,在block前面也打個斷點發現 image.pngblock執行前也會被插入__sanitizer_cov_trace_pc_guard方法,繼續查看swift-oc混編是swift方法是否會被hook image.png image.png也會被hook,因此也驗證了clang插樁的方法能覆蓋全部方法、函數。
      3. 獲取符號 上述hook方法中咱們知道能夠拿到當前方法或者函數的地址,拿到地址以後咱們能夠經過dladdr方法去除對應方法或者函數的信息具體代碼以下圖: image.png image.png image.png 發現dli_sname就是咱們想要的符號,接下來的操做主要就是把這些符號存儲下來而後生成order而後工程再配置對應的Order file就算完成了。
      4. 輸出order文件
        上文中已經能夠拿到符號了,最後的工做就是輸出order文件。
        具體思路:咱們能夠在__sanitizer_cov_trace_pc_guard將函數地址信息存儲下來而後給app添加一個點擊屏幕的監聽事件,等到首屏加載完畢說明啓動完成全部所須要加載的方法也就加載完成,此時咱們再在這個方法遍歷地址信息,輸出符號。
        我這裏借用的鏈表存儲,因此先要創建一個節點以下圖: image.png 而後再經過OSQueueHead建立原子隊列,其目的是保證讀寫安全。
        image.png 經過OSAtomicEnqueue方法將node入隊,經過鏈表的next指針能夠訪問下一個符號 image.png 此刻地址的儲存完成下一步就是讀取寫入order文件:具體代碼以下
        -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
        {
            //定義數組
            NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
        
            while (YES) {//一次循環!也會被HOOK一次!!
               SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        
                if (node == NULL) {
                    break;
                }
                Dl_info info = {0};
                dladdr(node->pc, &info);
        //        printf("%s \n",info.dli_sname);
                NSString * name = @(info.dli_sname);
                free(node);
        
                BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
                //須要注意若是不是OC方法須要添加下劃線
                NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
                [symbolNames addObject:symbolName];
            }
            //反向數組
            NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
        
            //建立一個新數組
            NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
            NSString * name;
            //去重!
            while (name = [enumerator nextObject]) {
                if (![funcs containsObject:name]) {//數組中不包含name
                    [funcs addObject:name];
                }
            }
            [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
            //數組轉成字符串
            NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
            //字符串寫入文件
            //文件路徑
            NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tudou.order"];
            //文件內容
            NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
            [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        }
        複製代碼
        運行完成發現生成了order文件 image.png
      5. Xcode配置order文件 如圖配置文件 image.png
      6. 查看二進制重排結果 最後一樣的查看生成的link map文件:
        沒有二進制重排以前: image.png 發現是按照文件按照方法的順序來的。
        二進制重排以後:
        image.png 發現此時就是按照咱們的order文件的順序來的
相關文章
相關標籤/搜索