<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          App 啟動(dòng)優(yōu)化

          共 11951字,需瀏覽 24分鐘

           ·

          2021-06-11 10:32

          ????關(guān)注后回復(fù) “進(jìn)群” ,拉你進(jìn)程序員交流群????


          作者丨Potato_土豆

          轉(zhuǎn)自丨掘金

          鏈接:

          https://juejin.cn/post/6965495023924330504#heading-4


          App的啟動(dòng)過程

          App的啟動(dòng)一般是指從用戶點(diǎn)擊App開始到AppDelegatedidFinishLaunching方法執(zhí)行完成為止,一般又將啟動(dòng)分為冷啟動(dòng)和熱啟動(dòng)。

          • 冷啟動(dòng)
            冷啟動(dòng): 是指App啟動(dòng)前它的進(jìn)程不在系統(tǒng)里,需要系統(tǒng)分配一個(gè)進(jìn)程給它啟動(dòng)的情況,這是一次完成的啟動(dòng)(一般啟動(dòng)優(yōu)化都是優(yōu)化冷啟動(dòng)的過程)
          • 熱啟動(dòng)

            熱啟動(dòng): 是指App在冷啟動(dòng)后將App退后臺(tái),App的進(jìn)程還在系統(tǒng)里,內(nèi)存中海油App的數(shù)據(jù)的情況下,再次啟動(dòng)App的過程,這個(gè)過程做的事情也非常少

          App啟動(dòng)優(yōu)化

          上文也說了一般啟動(dòng)優(yōu)化主要優(yōu)化的是冷啟動(dòng)的過程,熱啟動(dòng)做的事情也非常少。所以這里只講解冷啟動(dòng)過程的優(yōu)化。冷啟動(dòng)過程又被分為main函數(shù)執(zhí)行之前和main函數(shù)執(zhí)行之后
          • main函數(shù)執(zhí)行之前

            操作系統(tǒng)加載App可執(zhí)行文件到內(nèi)存,執(zhí)行一系列的加載&鏈接工作,可以通過添加添加環(huán)境變量DYLD_PRINT_STATISTICS來查看main函數(shù)執(zhí)行之前都做了什么,同時(shí)也可以看出對(duì)應(yīng)消耗的時(shí)間

               不難發(fā)現(xiàn)main函數(shù)執(zhí)行之前主要做了以下幾種事情

          • 動(dòng)態(tài)庫的加載
            對(duì)應(yīng)的是`dylib loading time`可以發(fā)現(xiàn)加載時(shí)間為48.41毫秒  
            優(yōu)化建議 :
            這里主要的優(yōu)化建議是減少動(dòng)態(tài)庫的加載,蘋果公司建議更少的使用動(dòng)態(tài)庫,并且建議動(dòng)態(tài)庫的數(shù)量較多的時(shí)候,盡量將多個(gè)動(dòng)態(tài)庫合并,數(shù)量上蘋果公司最多支持6個(gè)非系統(tǒng)動(dòng)態(tài)庫的合并

          • 偏移修正和符號(hào)綁定
            對(duì)應(yīng)的是rebase/binding time,耗時(shí)9.18毫秒

            • 偏移修正
              任何App生成的二進(jìn)制文件中的方法、函數(shù)都會(huì)有個(gè)地址,而這個(gè)地址是相對(duì)于當(dāng)前二進(jìn)制文件中的偏移地址,但是到了運(yùn)行時(shí)系統(tǒng)會(huì)隨機(jī)生成一個(gè)數(shù)值添加到二進(jìn)制文件的頭部(ASLR安全機(jī)制下文中會(huì)有講解),所以此時(shí)函數(shù)、方法的地址就是 隨機(jī)分配的數(shù)值+偏移地址 這個(gè)過程就是偏移修正

            • 符號(hào)綁定
              動(dòng)態(tài)庫不像是靜態(tài)庫,靜態(tài)庫實(shí)在編譯時(shí)期就將對(duì)應(yīng)使用到的代碼一起打包生成了mach-o文件,所以此時(shí)使用到的靜態(tài)庫的方法、函數(shù)其實(shí)就和自定義的方法、函數(shù)差不多了,能夠直接獲取到對(duì)應(yīng)的地址,
              但是動(dòng)態(tài)庫在編譯階段是不會(huì)被打包進(jìn)
              mach-o文件的,但是此時(shí)又用到了動(dòng)態(tài)庫中的方法,例如用到了NSLog方法,此時(shí)就會(huì)生成一個(gè)!NSLog 符號(hào)此時(shí)這個(gè)符號(hào)會(huì)隨機(jī)指向一個(gè)地址,
              當(dāng)運(yùn)行時(shí),此時(shí)動(dòng)態(tài)庫被加載到內(nèi)存,此時(shí)就可以拿到動(dòng)態(tài)庫對(duì)應(yīng)的方法、函數(shù)的地址,所以此時(shí)就需要將
              !NSLog這個(gè)符號(hào)綁定到相應(yīng)的地址上去(dyld做的),這個(gè)過程就叫做符號(hào)綁定

            • 類的注冊(cè)
              對(duì)應(yīng)的是ObjC setup time,耗時(shí)10.86毫秒
              優(yōu)化建議
              刪除啟動(dòng)后不會(huì)去使用的類

            • 執(zhí)行l(wèi)oad和構(gòu)造函數(shù)
              對(duì)應(yīng)的是initializer time,耗時(shí)110.79毫秒
              優(yōu)化建議
              減少使用load方法相應(yīng)的可以將load中的實(shí)現(xiàn)放在+initialize()方法中去,應(yīng)為一般一個(gè)load方法的執(zhí)行需要耗時(shí)4毫秒,而且如果類中實(shí)現(xiàn)了load那么相對(duì)應(yīng)類的加載就要提前到read_image方法中去執(zhí)行,如果沒有實(shí)現(xiàn)load類的加載則會(huì)方法第一次發(fā)送消息的時(shí)候加載,


          • main函數(shù)執(zhí)行之后

            這個(gè)階段主要是指main函數(shù)執(zhí)行開始到首屏渲染完成方法執(zhí)行完畢。這個(gè)階段主要做的工作包括:

            • 第三方SDK初始化
            • 自定義工具類初始化
            • 首屏數(shù)據(jù)的加載
            • 首屏渲染的一些計(jì)算
              這個(gè)地方的優(yōu)化建議主要有一下幾點(diǎn)
            1. 只處理首屏渲染相關(guān)的任務(wù),其他非首屏的業(yè)務(wù)例如初始化、注冊(cè)監(jiān)聽、配置文件的讀取等等都放在首頁渲染完成之后去做,當(dāng)然也可以開辟一個(gè)線程去處理這些事情。盡量不要占用主線程
            2. 自己的業(yè)務(wù)邏輯的優(yōu)化,已經(jīng)廢棄的不需要用的邏輯代碼、方法、函數(shù)都刪除掉,減少每個(gè)流程的耗時(shí)
            3. 啟動(dòng)時(shí)期的頁面盡量避免使用xib、storyboard(中間會(huì)有個(gè)轉(zhuǎn)換的過程也是需要耗時(shí)的)UI的主框架盡量使用純代碼

          二進(jìn)制重排基礎(chǔ)知識(shí)

          上文主要是針對(duì)特定的階段做一些優(yōu)化處理,除了刪除的優(yōu)化方案還有一種優(yōu)化,就是二進(jìn)制重排,在講解二進(jìn)制重排之前先將幾個(gè)概念性的東西:

          1. 物理內(nèi)存

            就是運(yùn)行內(nèi)存,是指計(jì)算機(jī)上安裝的內(nèi)存,通俗的將其實(shí)就是內(nèi)存條的大小。
            早期的操作系統(tǒng)沒有虛擬內(nèi)存,程序?qū)ぶ酚玫亩际俏锢淼刂罚詻]啟動(dòng)一個(gè)程序開辟一個(gè)進(jìn)程都要相應(yīng)的分配一段物理內(nèi)存給這個(gè)程序,這就造成了如下幾個(gè)問題:

            1. 當(dāng)物理內(nèi)存被分配完成的時(shí)候此時(shí)其他程序就不能再被加載到內(nèi)存(也就是不能運(yùn)行),此時(shí)就需要等待其他程序退出釋放內(nèi)存,此時(shí)才能運(yùn)行新的程序
            2. 程序指令都是在物理內(nèi)存上操作的,那么我這個(gè)進(jìn)程就可以修改其他進(jìn)程的數(shù)據(jù),甚至?xí)薷膬?nèi)核地址空間的數(shù)據(jù)
              針對(duì)以上的問題也就引出了虛擬內(nèi)存
          2. 虛擬內(nèi)存

            指的是把硬盤中的一部分空間用來當(dāng)做內(nèi)存使用
            進(jìn)程和物理內(nèi)存之間增加一個(gè)中間層,這個(gè)中間層就是所謂的虛擬內(nèi)存,主要用于解決當(dāng)多個(gè)進(jìn)程同時(shí)存在時(shí),對(duì)物理內(nèi)存的管理。提高了CPU的利用率,使多個(gè)進(jìn)程可以同時(shí)、按需加載。

            所以虛擬內(nèi)存其本質(zhì)就是一張?zhí)摂M地址和物理地址對(duì)應(yīng)關(guān)系的映射表.每個(gè)進(jìn)程都有一個(gè)獨(dú)立的虛擬內(nèi)存,其地址都是從0開始,大小是4G固定的。
            進(jìn)程開始要訪問一個(gè)地址,它可能會(huì)經(jīng)歷下面的過程:

          1. 每次我要訪問地址空間上的某一個(gè)地址,但是進(jìn)程間是無法互相訪問的,保證了進(jìn)程間數(shù)據(jù)的安全(一個(gè)進(jìn)程只能訪問給定的這篇虛擬內(nèi)存的地址)。都需要把地址翻譯為實(shí)際物理內(nèi)存地址
          2. 所有進(jìn)程共享這整一塊物理內(nèi)存,每個(gè)進(jìn)程只把自己目前需要的虛擬地址空間映射到物理內(nèi)存上
          3. 每個(gè)虛擬內(nèi)存會(huì)劃分一個(gè)一個(gè)頁存儲(chǔ)(頁的大小在iOS中是16K,其他的是4K),進(jìn)程需要知道哪些地址空間上的數(shù)據(jù)在物理內(nèi)存上,哪些不在(可能這部分存儲(chǔ)在磁盤上),還有在物理內(nèi)存上的哪里,這就需要通過頁表來記錄
          4. 頁表的每一個(gè)表項(xiàng)分兩部分,第一部分記錄此頁是否在物理內(nèi)存上,第二部分記錄物理內(nèi)存頁的地址(如果在的話)
          5. 當(dāng)進(jìn)程訪問某個(gè)虛擬地址的時(shí)候,就會(huì)先去看頁表,如果發(fā)現(xiàn)對(duì)應(yīng)的數(shù)據(jù)不在物理內(nèi)存上,就會(huì)發(fā)生缺頁異常
          6. 缺頁異常的處理過程,操作系統(tǒng)立即阻塞該進(jìn)程,并將硬盤里對(duì)應(yīng)的頁換入內(nèi)存,然后使該進(jìn)程就緒,如果內(nèi)存已經(jīng)滿了,沒有空地方了,那就找一個(gè)頁覆蓋,至于具體覆蓋的哪個(gè)頁,就需要看操作系統(tǒng)的頁面置換算法是怎么設(shè)計(jì)的了。

          如下圖所示,虛擬內(nèi)存與物理內(nèi)存間的關(guān)系
          如果物理內(nèi)存被占滿,此時(shí)又有新的頁需要被加載進(jìn)來,此時(shí)新頁就會(huì)吧長時(shí)間沒有使用的頁覆蓋掉

          3.  ASLR

          應(yīng)為虛擬內(nèi)存的起始地址與大小都是固定的,這意味著,當(dāng)我們?cè)L問時(shí),其數(shù)據(jù)的地址也是固定的,這會(huì)導(dǎo)致我們的數(shù)據(jù)非常容易被破解,
          為了解決這個(gè)問題,所以蘋果為了解決這個(gè)問題,在iOS4.3開始引入了ASLR技術(shù),其實(shí)現(xiàn)原理就是在虛擬內(nèi)存的頭部隨機(jī)加上一塊地址,這樣每次啟動(dòng)時(shí)虛擬地址的其實(shí)址就不一樣,所以在程序啟動(dòng)的時(shí)候需要做偏移修正。

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

          從上文的知識(shí)中可以知道,ios程序在加載到虛擬內(nèi)存的時(shí)候會(huì)被分成很多很多頁,如果此時(shí)訪問的虛擬地址的一個(gè)page,對(duì)應(yīng)的物理地址不存在,則會(huì)缺頁異常,此時(shí)會(huì)阻塞進(jìn)程將這一頁加載到物理內(nèi)存然后在訪問。

          這里可以通過instrumentsSystem Trace來查看你的項(xiàng)目的缺頁異常的數(shù)量如下: 步驟:先點(diǎn)擊啟動(dòng)->首頁加載完成后暫停->然后找到你的項(xiàng)目找到主線程 發(fā)現(xiàn)啟動(dòng)之前有兩百多個(gè)缺頁異常,此時(shí)我們?cè)倏错?xiàng)目在編譯時(shí)期的默認(rèn)排列順序,此時(shí)我們寫一個(gè)簡單的demo如下圖:就是寫了幾個(gè)簡單的方法,然后項(xiàng)目中選擇Build-setting搜索link map然后配置此時(shí)會(huì)發(fā)現(xiàn)對(duì)應(yīng)配置的文件夾中生成了對(duì)應(yīng)的link-map文件,發(fā)現(xiàn)方法、函數(shù)等都是按照在文件中的實(shí)現(xiàn)順序來的,而文件的順序是按照comple source中的順序來的如圖:這種情況就造成了每個(gè)頁有可能只有一個(gè)方法是有用的,其他方法、函數(shù)等都不是在啟動(dòng)階段調(diào)用的,這就造成了在啟動(dòng)時(shí)期缺頁異常的數(shù)量會(huì)很多,也就造成了啟動(dòng)時(shí)間變長的情況。
          這也就是需要進(jìn)行二進(jìn)制重排的原因

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

          上文分析了二進(jìn)制重排的原因,就是應(yīng)為頁中空間的浪費(fèi)沒有充分利用每個(gè)頁的空間造成缺頁異常數(shù)量增多,二進(jìn)制重排的原理其實(shí)就是將啟動(dòng)階段用到的方法、函數(shù)全部排在最前面,這樣就能充分利用每個(gè)頁的空間,與此同時(shí)也降低了缺頁異常的數(shù)量。如下圖所示:
          明顯減少了一大半的缺頁異常的數(shù)量

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

          通過上面的原理分析可以知道,如果做二進(jìn)制重排只需要改變編譯時(shí)期方法、函數(shù)等的排列順序就行。其本質(zhì)就是就是對(duì)啟動(dòng)加載的符號(hào)進(jìn)行重新排列

          • 修改排列順序的方法
            Xcode是用的鏈接器叫做ld,ld有一個(gè)參數(shù)叫Order File , 我們可以通過這個(gè)參數(shù)配置一個(gè) order文件的路徑 .
            我們可以通過在Build Settings -> Order File配置一個(gè)后綴為order的文件路徑。在這個(gè)order文件中,將所需要的符號(hào)按照順序?qū)懺诶锩妫陧?xiàng)目編譯時(shí),會(huì)按照這個(gè)文件的順序進(jìn)行加載,以此來達(dá)到我們的優(yōu)化,所以二進(jìn)制重排的關(guān)鍵點(diǎn)在于Order File文件的生成
          • 獲取Order File文件的方法
            1.  如果項(xiàng)目不大的情況下自己也可以根據(jù)項(xiàng)目自己找到啟動(dòng)階段要運(yùn)行的方法、函數(shù),自己編寫Order File文件。
            2.  hook objc_msgSend,但是由于objc_msgSend的參數(shù)是可變的,需要通過匯編獲取,使用門檻比較高。而且也只能拿到OC和swift中@objc后的方法
            3.  靜態(tài)掃描:掃描 Mach-O 特定段和節(jié)里面所存儲(chǔ)的符號(hào)以及函數(shù)數(shù)據(jù)
            4.  Clang插樁:即批量hook,可以實(shí)現(xiàn)100%符號(hào)覆蓋,即完全獲取swift、OC、C、block函數(shù)
          • Clang插樁
            llvm內(nèi)置了一個(gè)簡單的代碼覆蓋率檢測(SanitizerCoverage)。它在函數(shù)級(jí)、基本塊級(jí)和邊緣級(jí)插入對(duì)用戶定義函數(shù)的調(diào)用,相應(yīng)文檔
            具體步驟:
          1. 配置開啟SanitizerCoverage,在build setting中搜索Other C Flags,如下圖
            如果是OC項(xiàng)目則添加-fsanitize-coverage=func,trace-pc-guard,如果是swift項(xiàng)目則添加-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 是一個(gè)哨兵,告訴我們是第幾個(gè)被調(diào)用的
              // 這個(gè)地方 是過濾掉了load方法,所以這里需要注釋掉
              if (!*guard) return;
                /*
                 - 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);
              char PcDescr[1024];
              printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
             }

          主要的方法在于__sanitizer_cov_trace_pc_guard方法,在這里我們可以取到對(duì)應(yīng)方法的地址,為什么方法執(zhí)行之前會(huì)先調(diào)用__sanitizer_cov_trace_pc_guard方法呢,可通過斷點(diǎn)調(diào)試查看,在一個(gè)方法或者函數(shù)的起始處大斷點(diǎn),再看匯編代碼如下圖:發(fā)現(xiàn)在方法執(zhí)行之前插入了__sanitizer_cov_trace_pc_guard方法,所有的函數(shù)執(zhí)行都會(huì)限制性__sanitizer_cov_trace_pc_guard方法,在block前面也打個(gè)斷點(diǎn)發(fā)現(xiàn)block執(zhí)行前也會(huì)被插入__sanitizer_cov_trace_pc_guard方法,繼續(xù)查看swift-oc混編是swift方法是否會(huì)被hook 也會(huì)被hook,所以也驗(yàn)證了clang插樁的方法能覆蓋所有方法、函數(shù)。

          3.  獲取符號(hào) 上述hook方法中我們知道可以拿到當(dāng)前方法或者函數(shù)的地址,拿到地址之后我們可以通過dladdr方法去除對(duì)應(yīng)方法或者函數(shù)的信息具體代碼如下圖:發(fā)現(xiàn)dli_sname就是我們想要的符號(hào),接下來的操作主要就是把這些符號(hào)存儲(chǔ)下來然后生成order然后工程再配置對(duì)應(yīng)的Order file就算完成了。

          4.  輸出order文件
          上文中已經(jīng)可以拿到符號(hào)了,最后的工作就是輸出order文件。
          具體思路:我們可以在__sanitizer_cov_trace_pc_guard將函數(shù)地址信息存儲(chǔ)下來然后給app添加一個(gè)點(diǎn)擊屏幕的監(jiān)聽事件,等到首屏加載完畢說明啟動(dòng)完成所有所需要加載的方法也就加載完成,此時(shí)我們?cè)僭谶@個(gè)方法遍歷地址信息,輸出符號(hào)。
          我這里借用的鏈表存儲(chǔ),所以先要建立一個(gè)節(jié)點(diǎn)如下圖: 然后再通過OSQueueHead創(chuàng)建原子隊(duì)列,其目的是保證讀寫安全。
          通過OSAtomicEnqueue方法將node入隊(duì),通過鏈表的next指針可以訪問下一個(gè)符號(hào)此刻地址的儲(chǔ)存完成下一步就是讀取寫入order文件:具體代碼如下

                -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
                {
              //定義數(shù)組
              NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];

              while (YES) {//一次循環(huán)!也會(huì)被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];
              }
              //反向數(shù)組
              NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];

              //創(chuàng)建一個(gè)新數(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:@"tudou.order"];
              //文件內(nèi)容
              NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
              [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
                }

          運(yùn)行完成發(fā)現(xiàn)生成了order文件5.  Xcode配置order文件 如圖配置文件 6.  查看二進(jìn)制重排結(jié)果 最后同樣的查看生成的link map文件:
          沒有二進(jìn)制重排之前: 發(fā)現(xiàn)是按照文件按照方法的順序來的。
          二進(jìn)制重排之后:
          發(fā)現(xiàn)此時(shí)就是按照我們的order文件的順序來的

          -End-

          最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

          點(diǎn)擊??卡片,關(guān)注后回復(fù)【面試題】即可獲取

          在看點(diǎn)這里好文分享給更多人↓↓

          瀏覽 52
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产女人被躁到高潮的AV | 午夜欧美 | 中国美女一级特黄大片视频软件 | 亚洲成人黄色在线 | 大鸡吧插嫩逼视频网站免费 |