iOS啟動優化(上)-概念篇

語言: CN / TW / HK

前言

啟動App是給使用者第一印象,如果啟動比較慢,很可能會導致這個使用者流失,那麼啟動時間的優化就顯得尤為重要,本文結合啟動時間從一些基本的概念入手分析

啟動時間

啟動根據main分為兩個階段:main函式之前(pre-main)和main函式之後,在pre-main階段,程式最早能執行的程式碼是+load函式,但load之前還有很多步驟,例如載入動態庫的時間等,這些我們怎麼去檢測呢,這個時候就需要用到DYLD檢測,也就是通過配置環境變數,它會將這個階段相關的耗時反饋給開發者。 - 新建個工程,然後cmd + shift + ,-> Run -> Arguments配置環境變數DYLD_PRINT_STATISTICS,也可以設定DYLD_PRINT_STATISTICS_DETAILS這個會詳細些。本文配置DYLD_PRINT_STATISTICS來講

截圖2021-09-02 10.23.17.png
- 在真機上執行,結果如下:

截圖2021-09-01 11.30.30.png
- 這個代表本次pre-main時間用了338.68毫秒: - dylib loading time:是動態庫載入耗時。系統提供的動態庫已經載入在共享快取空間,已經做了優化,但自定義動態庫並沒有這個優化,蘋果建議建立的動態庫不能超過6個,如果大於這個數就需要做動態庫合併 - rebase/binding time:是重定位/繫結耗時,涉及到動態庫合併 - ObjC setup time:OC註冊類。在做專案時,可以刪除廢棄的(不使用的)類,只要在裡面就會影響啟動時間 - initializer time:執行load建構函式的耗時。 - slowest intializers:最慢的動態庫耗時

下面著重講rebase/binding相關的知識

實體記憶體和虛擬記憶體

實體記憶體

  • 在早期的作業系統,CPU直接從記憶體條讀取資料,這就導致了記憶體不夠用問題,如下圖:

截圖2021-09-02 14.30.02.png
- 應用需要載入時就會直接載入到記憶體條中,然後CPU去記憶體條讀取。當載入的應用多了,再載入新的應用時記憶體就不夠用了,這時候就需要幹掉一些應用然後再去開啟這個新的應用。 - 載入到記憶體條中的應用都是根據記憶體地址去讀,那麼如果通過一些外掛載入到記憶體,然後可以在遍歷記憶體條中地址訪問到其他應用的內容,就導致賬號被盜等安全問題,所以這個方式也是不安全的 - 怎麼解決這些問題呢?工程師們發現載入到記憶體中的應用,大多數之用到一小部分功能,這就導致了資源的浪費,於是就產生了懶載入。懶載入是引用載入到記憶體中時,先載入啟動相關的,後面要用到就再載入到記憶體條。

截圖2021-09-02 15.18.10.png
這樣雖然會減少記憶體的佔用,但是應用接下來的記憶體不知道要分配到哪,這樣就導致程式碼不連續,需要在執行過程不斷計算地址很不方便,而且效率很低,於是工程師們就創造了虛擬表,也就引出了虛擬記憶體

虛擬記憶體

  • 有了虛擬表,應用程式就只讀程式碼,而程式碼計算地址的事情交給CPU和硬體MMUMMU記憶體管理單元,它只做一件事:翻譯地址

截圖2021-09-03 07.10.46.png
- 這樣應用程式的在執行時訪問的記憶體就是連續的,而訪問的記憶體就是虛擬記憶體,虛擬記憶體對應的就是計算好的實體地址。 - 虛擬地址和實體地址不是一個位元組一個位元組對印的,這樣效率就很低。由於現在的應用記憶體是一塊一塊的,乾脆就以塊為單位去對印,單位就是page,此刻就產生了記憶體分頁管理的概念

對映表(記憶體分頁)

  • 頁的大小在不同的作業系統中是不一樣的。在iOS(64位)一頁是16K,在MAC中是4K。在MAC中可以使用環境變數PAGESIZE檢視:

截圖2021-09-03 07.26.07.png

  • 現在記憶體連續的問題得以解決,安全問題也解決了。由於一個應用只訪問自己的虛擬表,而翻譯出來的實體地址也是固定的,所以那些外掛就無法訪問其它應用。記憶體溢位的問題也解決了,因為現在記憶體都是一頁一頁的訪問,所以就不會產生溢位。
  • 當應用載入時,首先啟動需要的程式碼會直接加到記憶體,當要執行新的程式碼時,cpu發現這段程式碼記憶體中沒有就會把程式碼卡住,然後作業系統會將這頁加到實體記憶體中,這個現象就叫做缺頁中斷(pagefault)
  • 作業系統需要執行的一頁程式碼載入到實體記憶體中時,會往空缺處插入。但手機啟動後,實體記憶體就沒有空位了,裡面都放了其它的一些資料,但到底往哪裡加呢,這個由作業系統決定。作業系統提供一個演算法:頁面置換演算法,它會覆蓋掉不那麼活躍的部分。所以有時候多開啟些應用後,再去進入第一次開啟的應用時會重新啟動。
  • 有時候在訪問記憶體時會訪問比App當前大的記憶體,因為虛擬表有8G大小,能訪問的有4G,那麼他會訪問到其它應用嗎?不會,這塊虛擬記憶體是沒有資料,它指向NULL

截圖2021-09-04 23.43.56.png

rebase/binding

  • binding是繫結,當內部檔案訪問外部方法,就要通過內部符號繫結訪問外部,現在的繫結方式都是懶載入

  • rebase是重定位。由於虛擬記憶體產生後,每次記憶體都是從0開始,這個時候相應的安全就不存在了,所以作業系統就出現了一個新技術ASLR:讓每次生成的虛擬頁面,不要從0開始,從隨機的值開始,應用的每次啟動的起始位置都是隨機的。此時行數的位置就成了ASLR+OFFSET,也就是rebase重定向

  • 一頁的缺頁中斷是感知不到的,時間是毫秒級別,基本感知不到。但同時有大量的缺頁中斷,這個時候使用者就能感知到了,冷啟動時就會產生大量的缺頁異常。如下面例子:

  • 重簽名一個微信7.0.8ipa,然後跑到自己的工程。執行起來後Cmd+i開啟instruments,然後選擇System Trace,搜尋Main Thread,之後選擇Virtual Memory

截圖2021-09-05 10.17.14.png

  • File Backed Page In可以看到缺頁中斷的個數以及所用的時間:

截圖2021-09-05 10.19.44.png

  • 通過觀察發現消耗的總時間中,PageFault佔絕大部份 截圖2021-09-05 10.24.07.png
  • 再啟動一次發現耗時要少的多,此時應用的實體記憶體還沒被覆蓋,所以啟動會時間會少很多
  • 冷啟動:應用在實體記憶體中沒有佔用時的啟動是冷啟動。例如第一次開啟App,或者APP被殺死後,一段時間過後再開啟,都是冷啟動
  • 熱啟動:應用編譯在實體記憶體中的記憶體還存在的啟動就是熱啟動,例如短時間APP從後臺返回,APP殺死後立即開啟,此時它的實體記憶體還存在,此時就是熱啟動。

  • 如果減少PageFault的個數,就會達到優化的目的,具體操作下篇文章再進行講解