iOS 14以前,在磁盤中一個Class
大概長這樣:緩存
這個類對象包含了最經常使用的信息:指向元類、父類、以及方法的緩存。它還有一個指針指向更多的額外信息class_ro_t
,其中 ro表示read only 。這部分信息是隻讀的,其中包含了類名、方法、協議、實例變量和屬性等信息。Swift類和Objective-C類均使用這個結構。安全
當類第一次從磁盤被加載到內存的時候,剛開始就是長這樣的。但類一旦被使用,就會產生一些變化。bash
爲了理解以後發生了什麼,首先咱們須要理解什麼是 Clean Memory 和 Dirty Memory 。markdown
class_ro_t
就是 Clean Memory ,由於它是隻讀的。Dirty Memory 比 Clean Memory 代價更昂貴,由於在進程運行的整個過程當中,都須要被保留; Clean Memory 則能夠爲其餘事情滕出空間,由於當咱們須要時,系統老是能夠很容易地從磁盤中從新加載它。數據結構
macOS能夠經過內存交換來解決內存不足的問題,但iOS不支持這個技術,因此 Dirty Memory 的代價會更昂貴。 Dirty Memory 就是爲何類結構被分爲了這兩個部分的緣由。固然,若是咱們能夠擁有更多的 Clean Memory ,固然是更好的。把不會改變的數據分離出來,咱們就可讓大部分的類數據保持爲 Clean Memory 。app
一旦類被使用,運行時會分配額外的空間來存儲這部分數據,即class_rw_t
,其中 rw表示read write 。這個結構體中,咱們只存儲運行時產生的數據。ide
因此後兩個不經常使用的部分,咱們又能夠拆分出來:函數
這樣就把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
結構大小減半,那麼用就是咱們節約的內存。僅僅郵件就節約了大約15%的內存,經過這個優化,整個系統會減小大量 Dirty Memory 。
若是原來的代碼直接訪問class_rw_t
結構,因爲結構內存佈局發生了變化,可能產生崩潰。蘋果推薦使用運行時API,這樣底層的細節會由他們處理。
每一個類都有一個方法列表。當你寫了一個方法,這個方法就會加入到方法列表中。運行時會用這些列表來解析發送給對象的消息。
每一個方法包含3個部分的信息。
init
。@16@0:8
。這些信息都是指針,在64位的系統上會佔用24字節。
咱們的方法列表是存在於鏡像中的,而鏡像的加載位置可能在內存的任何地方,這取決於動態連接器的選擇。也就是說,連接器須要解析鏡像中的指針,修復它們指向內存真實的的位置。這部分會產生額外的消耗。
又因爲鏡像中的方法都是固定的,不會跑到其餘鏡像中去。其實咱們不須要64位尋址的指針,只須要32位便可。
這樣作有幾個好處:
咱們但願保持這部分數據是隻讀的,但若是咱們使用了 Method Swizzling 呢?
蘋果會在一個全局表中映射交換的實現。因爲交換並非很是常見的操做,因此這個全局表也不會特別大。
此外,在之前的實現中,進行方法交換會致使整個分頁Page
變成 Dirty Memory 。即僅僅一個交換,就可能形成數千字節的 Dirty Memory ,這是很不划算的。
若是咱們的代碼中直接處理了這些底層細節,但沒有處理好的話,可能會形成1個64位的指針去讀取2個32位的指針值。這是沒有意義的,會形成崩潰。一樣,蘋果推薦使用運行時API,這樣底層的細節會由他們處理。
首先,什麼是標記指針 Tagged Pointer ?
這個指針中,其實只使用了中間高亮部分來表示一個真實的對象指針。
因爲字節對齊的緣由,低位老是0;因爲咱們不會真正用到全部64進行尋址,因此高位也有一部分老是0。
Intel處理器
低位爲0表示真實的指針,1表示標記指針。
前面的3個比特是tag號,表示其類型。例如3表示NSNumber
,6表示NSDate
。
tag號爲7時表示一種擴展的tag,會使用額外的8比特表示類型,但有意義的數據長度更短,例如UIColor
或NSIndexSet
。
通常狀況下,只有蘋果能夠添加標記指針的類型。 但若是你是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,這樣底層的細節會由他們處理。
iOS14以後蘋果爲咱們帶來了3項運行時優化:
蘋果推薦使用運行時API,這樣底層的細節會由他們處理。
若是以爲本文對你有所幫助,給我點個贊吧~