WWDC20 iOS14 Runtime優化

1. Class結構體變化

iOS 14以前,在磁盤中一個Class大概長這樣:緩存

這個類對象包含了最經常使用的信息:指向元類、父類、以及方法的緩存。它還有一個指針指向更多的額外信息class_ro_t,其中 ro表示read only 。這部分信息是隻讀的,其中包含了類名、方法、協議、實例變量和屬性等信息。Swift類和Objective-C類均使用這個結構。安全

當類第一次從磁盤被加載到內存的時候,剛開始就是長這樣的。但類一旦被使用,就會產生一些變化。bash

爲了理解以後發生了什麼,首先咱們須要理解什麼是 Clean MemoryDirty Memorymarkdown

  • Clean Memory :被加載後就不會再變化的內存。例如,class_ro_t就是 Clean Memory ,由於它是隻讀的。
  • Dirty Memory :在進程運行時會發生變化的內存。類結構體一旦被使用就是 Dirty Memory ,由於運行時會寫入新的數據,例如它的方法緩存部分。

Dirty MemoryClean Memory 代價更昂貴,由於在進程運行的整個過程當中,都須要被保留; Clean Memory 則能夠爲其餘事情滕出空間,由於當咱們須要時,系統老是能夠很容易地從磁盤中從新加載它。數據結構

macOS能夠經過內存交換來解決內存不足的問題,但iOS不支持這個技術,因此 Dirty Memory 的代價會更昂貴。 Dirty Memory 就是爲何類結構被分爲了這兩個部分的緣由。固然,若是咱們能夠擁有更多的 Clean Memory ,固然是更好的。把不會改變的數據分離出來,咱們就可讓大部分的類數據保持爲 Clean Memoryapp

一旦類被使用,運行時會分配額外的空間來存儲這部分數據,即class_rw_t,其中 rw表示read write 。這個結構體中,咱們只存儲運行時產生的數據。ide

  • First SubclassNext Sibling Class 指針讓運行時能夠遍歷當前使用的全部類。
  • MethodsPropertiesProtocols ,這部分也是能夠在運行時進行修改的。在實踐中發現,其實只有大約10%類的方法會發生變化,因此這部份內存能夠獲得優化,滕出一些空間。
  • Demangled Name 只會被Swift類所使用,並且除非有須要獲取它們的Objective-C名稱,甚至都不會用到。

因此後兩個不經常使用的部分,咱們又能夠拆分出來:函數

這樣就把class_rw_t,拆成了2部分。若是確實有須要,咱們纔會這部分class_rw_ext_t結構分配內存。大約90%的類都不須要這部分額外的數據,系統就能夠節約大概14MB的內存。工具

使用原結構大約須要30MB內存,拆分後能夠節約大概14MB。oop

對macOS Big Sur的郵件App進行測試,發現大約有9千多個類使用了class_rw_t結構,而只有大約10%,即9百多個類使用到了class_rw_ext_t結構。

咱們能夠簡單計算一下,class_rw_t結構大小減半,那麼用1.0-(293120-43392)/293120≈14.8\%就是咱們節約的內存。僅僅郵件就節約了大約15%的內存,經過這個優化,整個系統會減小大量 Dirty Memory

若是原來的代碼直接訪問class_rw_t結構,因爲結構內存佈局發生了變化,可能產生崩潰。蘋果推薦使用運行時API,這樣底層的細節會由他們處理。

2. 相關方法列表變化

每一個類都有一個方法列表。當你寫了一個方法,這個方法就會加入到方法列表中。運行時會用這些列表來解析發送給對象的消息。

每一個方法包含3個部分的信息。

  • 名稱,或者選擇器,例如init
  • 方法參數類型的編碼,例如@16@0:8
  • 方法的IMP,Objective-C方法最終會編譯爲一個C函數。

這些信息都是指針,在64位的系統上會佔用24字節。

咱們的方法列表是存在於鏡像中的,而鏡像的加載位置可能在內存的任何地方,這取決於動態連接器的選擇。也就是說,連接器須要解析鏡像中的指針,修復它們指向內存真實的的位置。這部分會產生額外的消耗。

又因爲鏡像中的方法都是固定的,不會跑到其餘鏡像中去。其實咱們不須要64位尋址的指針,只須要32位便可。

這樣作有幾個好處:

  • 這個偏移量相對鏡像是固定的,與鏡像加載的位置無關,當它們從磁盤加載進來後就不要進行修復了。
  • 由於再也不須要進行修復了,這部分數據就能夠保存在只讀內存(Clean Memory )中,這樣也更安全。
  • 在64位系統中,指針大小從64位的24字節降低到32位的12字節。根據實際測量,方法列表佔用內存大約爲80MB,減半的話就能夠節約40MB內存。

咱們但願保持這部分數據是隻讀的,但若是咱們使用了 Method Swizzling 呢?

蘋果會在一個全局表中映射交換的實現。因爲交換並非很是常見的操做,因此這個全局表也不會特別大。

此外,在之前的實現中,進行方法交換會致使整個分頁Page變成 Dirty Memory 。即僅僅一個交換,就可能形成數千字節的 Dirty Memory ,這是很不划算的。

若是咱們的代碼中直接處理了這些底層細節,但沒有處理好的話,可能會形成1個64位的指針去讀取2個32位的指針值。這是沒有意義的,會形成崩潰。一樣,蘋果推薦使用運行時API,這樣底層的細節會由他們處理。

3. 標記指針結構變化

首先,什麼是標記指針 Tagged Pointer

這個指針中,其實只使用了中間高亮部分來表示一個真實的對象指針。

因爲字節對齊的緣由,低位老是0;因爲咱們不會真正用到全部64進行尋址,因此高位也有一部分老是0。

  • Intel處理器

    低位爲0表示真實的指針,1表示標記指針。

    前面的3個比特是tag號,表示其類型。例如3表示NSNumber,6表示NSDate

    tag號爲7時表示一種擴展的tag,會使用額外的8比特表示類型,但有意義的數據長度更短,例如UIColorNSIndexSet

    通常狀況下,只有蘋果能夠添加標記指針的類型。 但若是你是Swift開發者,則能夠建立本身的標記指針。若是你曾用過有類實例對象關聯值的枚舉,那就像是一個標記指針。

  • ARM64

    • iOS14如下系統

      ARM64中整個反過來了,首位爲1表示標記指針,後面3位表示tag號。

      這個高低位的翻轉主要是由於objc_msgSend的一個小優化。蘋果須要儘量快地處理objc_msgSend的指針,一般是普通指針,標記指針和nil更少見一些。使用一個比較就能夠直接肯定是標記真正或者是nil,更容易進入常見的邏輯中。

      #define likely(x) __builtin_expect(!!(x), 1)
      #define unlikely(x) __builtin_expect(!!(x), 0)
      複製代碼

      一樣,tag號爲7時表示一種擴展的tag,會使用額外的8比特表示類型。

    • iOS14

      iOS 14中tag號被移動到了低位。對於現有的工具,例如動態連接器,對於指針的高8位,ARM的特性 Top Biyte Ignore 會直接被忽略。蘋果把擴展部分放在了 Top Biyte Ignore 生效的部分。對於字節對齊的指針,低3位老是0,恰好放下3位的tag號。最終,帶來的一個有趣的效果就是,一個標記指針的payload中就能夠放下一個普通指針了。這就讓一個標記指針能夠指向一個常量,例如字符串或者其餘可能佔用 Dirty Memory 的數據結構。

      若是項目中有涉及到這部分的代碼,再將來可能產生崩潰。一樣,蘋果推薦使用運行時API,這樣底層的細節會由他們處理。

4. 總結

iOS14以後蘋果爲咱們帶來了3項運行時優化:

  • 更小的類數據結構。
  • 更小的方法列表。
  • 標記指針的變化。

蘋果推薦使用運行時API,這樣底層的細節會由他們處理。

5. 參考


若是以爲本文對你有所幫助,給我點個贊吧~

相關文章
相關標籤/搜索