iOS 14 蘋果對 Objective-C Runtime 的優化

做者:Damien,iOS 開發者。目前就任於攜程。html

Session:developer.apple.com/wwdc20/1016…緩存

概述

Objective-C 是一門古老的語言,誕生於 1984 年,跟隨 Apple 一路浮沉,見證了喬布斯建立了 NeXT,也見證了喬布斯重回 Apple 重創輝煌,它用它特立獨行的語法,堆砌了 UIKit,AppKit, Foundation 等一個個基石,時間來到 2020 年,面對洶涌的"後浪" Swift,"老前輩" Objective-C 也在發揮着本身的餘熱,即便面對愈來愈多陣地失守,惟有「老兵不死,只會慢慢凋亡"才能體現的悲壯。今年,Apple 給 Objective-C Runtime 帶來了新的優化,接下來,讓咱們深刻理解這些變化。安全

類數據結構變化

首先咱們先來了解一下二進制類在磁盤中的表示 bash

首先是類對象自己,包含最常訪問的信息:指向元類,超類和方法緩存的指針,在類結構之中有指向包含更多數據的結構體 class_ro_t的指針,包含了類的名稱,方法,協議,實例變量等等編譯期肯定的信息。其中 ro 表示 read only 的意思。

當類被 Runtime 加載以後,類的結構會發生一些變化,在瞭解這些變化以前,咱們須要知道2個概念: **Clean Memory:**加載後不會發生更改的內存塊,class_ro_t屬於Clean Memory,由於它是隻讀的。 **Dirty Memory:**運行時會進行更改的內存塊,類一旦被加載,就會變成Dirty Memory,例如,咱們能夠在 Runtime 給類動態的添加方法。數據結構

這裏要明確,Dirty MemoryClean Memory要昂貴得多。由於它須要更多的內存信息,而且只要進程正在運行,就必須保留它。對於咱們來講,越多的Clean Memory顯然是更好的,由於它能夠節約更多的內存。咱們能夠經過分離出永不更改的數據部分,將大多數類數據保留爲Clean Memory,如何怎麼作的呢? 在介紹優化方法以前,咱們先來看一下,在類加載以後,類的結構會變成如何呢? app

在類加載到 Runtime 中後會被分配用於讀取/寫入數據的結構體 class_rw_t

Tips:class_ro_t是隻讀的,存放的是編譯期間就肯定的字段信息;而class_rw_t是在 runtime 時才建立的,它會先將class_ro_t的內容拷貝一份,再將類的分類的屬性、方法、協議等信息添加進去,之因此要這麼設計是由於 Objective-C 是動態語言,你能夠在運行時更改它們方法,屬性等,而且分類能夠在不改變類設計的前提下,將新方法添加到類中。dom

事實證實,class_rw_t會佔用比class_ro_t佔用更多的內存,在 iPhone 中,咱們在系統測量了大約 30MB 的這些class_rw_t結構。應該如何優化這些內存呢?經過測量實際設備上的使用狀況,咱們發現大約 10% 的類實際會存在動態的更改行爲,如動態添加方法,使用 Category 方法等。所以,咱們能能夠把這部分動態的部分提取出來,咱們稱之爲class_rw_ext_t,因此,結構會變成這個樣子。 ide

通過拆分,能夠把 90% 的類優化爲 Clean Memory,在系統層面,取得效果是節省了大約 14MB 的內存,使內存可用於更有效的用途。

Tips:heap xxxxx | egrep 'class_rw|COUNT’ 你可使用此命令來查看 class_rw_t 消耗的內存。xxxx能夠替換爲須要測量的 App 名稱。如:heap Mail | egrep 'class_rw|COUNT’\'查看 Mail 應用的使用狀況。函數

相對方法地址

如今,咱們來看看 Runtime 的第二處的變化,方法地址的優化。 每一個類都包含一個方法列表,以便 Runtime 能夠查找和消息發送。結構大概以下圖所示: 佈局

方法包含了3部分的內容:

  • Selector:方法名稱或選擇器。選擇器是字符串,可是它們是惟一的
  • 方法類型編碼:方法類型編碼標識(詳情能夠查看參考連接)
  • IMP:方法實現的函數指針

在 64 位系統中,它們佔用了 24 字節的空間

瞭解了方法的結構以後,咱們來看下進程中內存的簡化視圖

這是一個 64 位的地址空間,其中各類塊分別表示了棧,堆以及各類庫。咱們把焦點放在 AppKit 庫中的init方法。

如圖所示,圖中的3個地址分別爲方法的 3 個部分的表示的絕對地址,咱們知道,庫的地址取決於動態連接庫加載以後的位置,ASLR(Address space layout randomization 地址空間佈局隨機化)的存在,動態連接器須要修正真實的指針地址,這也是一種代價。因爲方法實現地址不會脫離當前庫的地址範圍的特性存在,因此實際上,方法列表並不須要使用 64 位的尋址範圍空間。他們只須要可以在本身的庫地址中查找引用函數地址便可,這些函數將始終在附近。因此咱們可使用 32 位相對偏移來代替絕對 64 位地址。

如今咱們地址將變成這樣

這麼作有幾個優勢:

  1. 不管將庫加載到內存中的任何位置,偏移量始終是相同的,所以從加載後不須要進行修正指針地址。
  2. 它們能夠保存在只讀存儲器中,這會更加的安全。
  3. 使用 32 位偏移量在 64 位平臺上所需的內存量減小了一半。在 iPhone 中咱們能夠節省約 40MB 的內存大小。

優化後,指針所需的內存佔用量能夠減小一半。

相對方法地址會引起另一個問題,那就是在Method Swizzling如何處理呢?衆所皆知,Method Swizzling替換的是 2 個方法函數指針指向,方法函數實現能夠在任意地方實現,使用了相對偏移地址了以後,這樣就沒法工做了。 針對Method Swizzling咱們使用全局映射表來解決這個問題,在映射表中維護Swizzles方法對應的實現函數指針地址。因爲Method Swizzling的操做並不常見,因此這個表不會變得很大,新的Method Swizzling機制以下圖。

Tagged Pointer 格式的變化

接下來咱們會深刻了解 Tagged Pointer 在 ARM CPU 下的格式變化 首先,讓咱們先來了解下 Tagged Pointer 是什麼 **Tagged Pointer:**一種特殊標記的對象,Tagged Pointer 經過在其最後一個 bit 位設置爲特殊標記位,而且把數據直接保存在指針自己中。Tagged Pointer 是一個"僞"對象,使用 Tagged Pointer 有 3 倍的訪問速度提高,100 倍的建立、銷燬速度提高。

Tips:Advances in Objective-C

在咱們查看對象指針時,在 64 位系統中,咱們會看到 16 進制地址如0x00000001003041e0,咱們把它轉換爲二進制表示以下圖

在 64 位系統中,咱們有 64 位能夠表示一個對象指針,可是咱們一般沒有真正使用到全部這些位,因爲內存對齊要求的存在,低位始終爲0,對象必須始終位於指針大小倍數的地址中。高位也始終爲0。實際上咱們只是用中間這一部分的位。
所以,咱們能夠把最低位設置爲 1,表示這個對象是一個 Tagged Pointer 對象。設置爲 0 則表示爲正常的對象
在設置爲 1 表示爲 Tagged Pointer 對象以後,在最低位以後的 3 位,咱們給他賦予類型意義,因爲只有 3 位,因此它能夠表示 7 種數據類型

OBJC_TAG_NSAtom            = 0, 
OBJC_TAG_1                 = 1, 
OBJC_TAG_NSString          = 2, 
OBJC_TAG_NSNumber          = 3, 
OBJC_TAG_NSIndexPath       = 4, 
OBJC_TAG_NSManagedObjectID = 5, 
OBJC_TAG_NSDate            = 6, 
OBJC_TAG_7                 = 7
複製代碼

在剩餘的字段中,咱們能夠賦予他所包含的數據。在 Intel 中,咱們 Tagged Pointer 對象的表示以下

OBJC_TAG_7類型的 Tagged Pointer 是個例外,它能夠將接下來後 8 位做爲它的擴展類型字段,基於此咱們能夠多支持 256 中類型的 Tagged Pointer,如 UIColors 或 NSIndexSets 之類的對象。

上文中,咱們介紹的是在 Intel 中 Tagged Pointer 的表示,在 ARM64 中,咱們狀況有些變化。

咱們使用最高位表明 Tagged Pointer 標識位,最低位 3 位標識 Tagged Pointer 的類型,接下去的位來表示包含的數據(可能包含擴展類型字段),爲何咱們使用高位指示 ARM上 的 Tagged Pointer,而不是像 Intel 同樣使用低位標記?

它實際是對 objc_msgSend 的微小優化。咱們但願 msgSend 中最經常使用的路徑儘量快。最經常使用的路徑表示普通對象指針。咱們有兩種不常見的狀況:Tagged Pointer 指針和 nil。事實證實,當咱們使用最高位時,能夠經過一次比較來檢查二者。與分別檢查 nil 和 Tagged Pointer 指針相比,這會爲 msgSend 中的節省了條件分支。

總結

在 2020 年中,Apple 針對 Objective-C 作了三項優化

  • 類數據結構變化:節約了系統更多的內存。
  • 相對方法地址:節約了內存,而且提升了性能。
  • Tagged Pointer 格式的變化:提升了 msgSend 性能。

經過優化,但願你們能夠享受 iPhone 更好,更快的使用體驗。

Tips: 類結構的數據變動會在最新的 Runtime 版本中體現,實測 MacOS 10.5.5 中已經存在。 相對方法地址的優化在 Xcode developmentTarget > 14 時會自動進行處理。 Tagged Pointer 的變化則會在 iOS 14, MacOS Big Sur, iPadOS 14 上生效。

參考連接

TypeEncodeing

Lets build Tagged Pointers

Advances in Objective-C

限時福利

這篇文章的內容來自於 《WWDC20 內參》。在這裏給你們推薦一下這個專欄。

「WWDC 內參」系列是由老司機週報、知識小集合以及 SwiftGG 幾個技術組織發起的。已經作了幾年了,口碑一直不錯。主要是針對每一年的 WWDC 的內容,作一次精選,並號召一羣一線互聯網的 iOS 開發者,結合本身的實際開發經驗、蘋果文檔和視頻內容作二次創做。

今年一共有 213 個 Session 的內容。《WWDC20 內參》挑選了其中的 135 個 Session,短短兩週,已經創做了 83 篇文章。目前正在限時優惠銷售,只須要 9.9 元,十分優惠。

看了文章還不過癮的朋友,抓緊訂閱 《WWDC20 內參》 xiaozhuanlan.com/wwdc20 繼續閱讀把~

關注咱們

咱們開通了公衆號「老司機技術週報」,每期發佈時公衆號(LSJCoiding)會推送消息,歡迎關注。

相關文章
相關標籤/搜索