
一、前言
去年 2020 年的 WWDC 大會(huì)時(shí),因?yàn)槲矣⒄Z(yǔ)也不太好,我就邊看錄播邊用谷歌翻譯著,記錄了一下這次會(huì)議的一些跟我們開(kāi)發(fā)者有關(guān)的變化點(diǎn)。一直沒(méi)有整理發(fā)出來(lái),這次想著把這個(gè)記錄發(fā)一下,不然 2021 的 WWDC 大會(huì)都要開(kāi)始了。
變化點(diǎn)主要有三點(diǎn),這篇主要講一下第一點(diǎn),“數(shù)據(jù)結(jié)構(gòu)的變化”(Class data structures changes)
二、數(shù)據(jù)結(jié)構(gòu)的變化
1、"clean memory" 和 "dirty memory"
先理解兩個(gè)名詞,"clean memory" 和 "dirty memory",字面意思就是 “干凈內(nèi)存” 和 “臟內(nèi)存”。
clean memory是指加載后不會(huì)發(fā)生改變的內(nèi)存。
class_ro_t 就屬于clean memory, 因?yàn)樗侵蛔x的(ro 代表 readonly)dirty memory是指在進(jìn)程運(yùn)行時(shí)會(huì)發(fā)生更改的內(nèi)存,class_rw_t 是 dirty memory(rw 代表 read write)
在 objc4750 的源碼中我們可以看到 class_rw_t 的結(jié)構(gòu),如下圖:

class_ro_t 的結(jié)構(gòu),如下圖:

類結(jié)構(gòu)一經(jīng)使用就會(huì)變成 dirty memory,因?yàn)檫\(yùn)行時(shí)會(huì)向它寫入新的數(shù)據(jù),例如創(chuàng)建一個(gè)新的方法緩存并從類中指向它。
dirty memory 比 clean memory 要昂貴得多,只要進(jìn)程在運(yùn)行,它就必須一直存在。另一方面 clean memory 可以進(jìn)行移除,從而節(jié)省更多的內(nèi)存空間。因?yàn)槿绻阈枰?clean memory,系統(tǒng)可以從磁盤中重新加載。macOS 可以選擇換出 dirty memory,但因?yàn)?iOS 不使用 swap,所以 dirty memory 在 iOS 中代很大。

dirty memory 是這個(gè)類數(shù)據(jù)被分成兩部分的原因,可以保持清潔的數(shù)據(jù)越多越好,通過(guò)分離出那些永遠(yuǎn)不會(huì)更改的數(shù)據(jù),可以把大部分的類數(shù)據(jù)存儲(chǔ)為 clean memory。雖然這些數(shù)據(jù)足以讓我們開(kāi)始,但運(yùn)行時(shí)需要追蹤每個(gè)類的更多信息,所以當(dāng)一個(gè)類首次被使用,運(yùn)行時(shí)會(huì)為它分配額外的存儲(chǔ)容量,這個(gè)運(yùn)行時(shí)分配的存儲(chǔ)容量是 class_rw_t,用于讀取-編寫數(shù)據(jù)。在這個(gè)數(shù)據(jù)結(jié)構(gòu)中,我們存儲(chǔ)了只有在運(yùn)行時(shí)才會(huì)生成的新信息。

例如,所有的類都會(huì)鏈接成一個(gè)樹(shù)狀結(jié)構(gòu),這是通過(guò) First Subclass 和 Next Sibling Class 指針實(shí)現(xiàn)的。這允許運(yùn)行時(shí)遍歷當(dāng)前使用的所有類,這對(duì)于使方法緩存無(wú)效非常有用。
但為什么方法和屬性也在只讀數(shù)據(jù)中時(shí),我們這里還要有方法和屬性呢?

因?yàn)樗麄兛梢栽谶\(yùn)行時(shí)進(jìn)行更改。當(dāng) category 被加載時(shí),它可以向類中添加新的方法,而且程序員可以使用運(yùn)行時(shí) API 動(dòng)態(tài)地添加它們。因?yàn)?class_ro_t 是只讀的,所以我們需要在 class_rw_t 中追蹤這些東西。
現(xiàn)在,結(jié)果是這樣做會(huì)占用相當(dāng)多的內(nèi)存。在任何給定的設(shè)備中都有許多類在使用,我們?cè)?iPhone 上的整個(gè)系統(tǒng)中測(cè)量了大約 30 兆字節(jié)這些 class_rw_t 結(jié)構(gòu),那么我們?nèi)绾慰s小這些結(jié)構(gòu)呢?記住,我們?cè)谧x取-編寫部分需要這些東西,因?yàn)樗鼈兛梢栽谶\(yùn)行時(shí)進(jìn)行更改。但是通過(guò)檢查實(shí)際設(shè)備上的使用情況,大約只有 10% 的類真正地更改了它們的方法。而且只有 Swift 類會(huì)使用這個(gè) demangled name 字段,并且 Swift 類并不需要這一字段,除非有東西詢問(wèn)它們的 Object-C 名稱時(shí)才需要。

2、變化 —— 拆出了 class_rw_ext_t
所以,我們可以拆掉那些平時(shí)不用的部分,這將 class_rw_t 的大小減少了一半。拆出來(lái)的部分叫做 “class_rw_ext_t”,如下圖所示:

這個(gè) “class_rw_ext_t” 我們可以在最新的源碼 objc4-818.2 中看到:
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
對(duì)于那些確實(shí)需要額外信息的類,我們可以分配這些擴(kuò)展記錄中的一個(gè),并把它滑到類中供其使用。

大約 90% 的類從來(lái)不需要這些擴(kuò)展數(shù)據(jù),這在系統(tǒng)范圍內(nèi)可節(jié)省大約14MB 的內(nèi)存,這些內(nèi)存現(xiàn)在可用于更有效的途徑,比如存儲(chǔ)你的 app 的數(shù)據(jù)。
因此,實(shí)際上你可以在你的 Mac 上看到這一變化帶來(lái)的影響,這只需要在終端上運(yùn)行一些簡(jiǎn)單的命令,現(xiàn)在讓我們一起來(lái)看一下。
打開(kāi) MacBook 的終端,運(yùn)行一個(gè)在任何 Mac上都可用的命令,叫做“heap”,它允許你檢查正在運(yùn)行的進(jìn)程所使用的堆內(nèi)存。我將在 Mac 中的 Mail app 上運(yùn)行它,首先先打開(kāi) “郵件” 程序,

現(xiàn)在,如果我運(yùn)行 heap Mail 命令,它會(huì)輸出數(shù)千行,顯示通過(guò)郵件進(jìn)行的每個(gè)堆分配,所以我只要 grep 它為我們今天一直在討論的 class_rw_t 類型,我還需要查詢標(biāo)頭
heap Mail | egrep ‘class_rw|COUNT’
從返回的結(jié)果中,可以看到,我們?cè)卩]件 app 中使用了大概 9000 個(gè)這樣的 class_rw_t 類型,但其中只有大約十分之一,900 多一點(diǎn)實(shí)際上需要使用這一擴(kuò)展信息。所以,我們可以很容易地計(jì)算出通過(guò)這個(gè)改變所節(jié)省的內(nèi)存,這是大小減半的類型。

所以,如果我們從這個(gè)數(shù)字 “152320” 中減去我們必須分配給擴(kuò)展類型的內(nèi)存量 ”21600”,我們可以看到我們節(jié)省了大約 1 兆字節(jié)數(shù)據(jù)的四分之一,這還只是對(duì)于郵件 app 而言。如果我們?cè)谙到y(tǒng)范圍內(nèi)進(jìn)行擴(kuò)展,對(duì) dirty memory 而言,這是真正的節(jié)省內(nèi)存。
3、要用官方 API
現(xiàn)在,很多從類中獲取數(shù)據(jù)的代碼,必須同時(shí)處理那些有擴(kuò)展數(shù)據(jù)和沒(méi)有擴(kuò)展數(shù)據(jù)的類,當(dāng)然運(yùn)行時(shí)會(huì)為你處理這一切,并且從外部看,一切都像往常一樣工作,只是使用更少的內(nèi)存。之所以會(huì)這樣,是因?yàn)樽x取這些結(jié)構(gòu)的代碼,都在運(yùn)行時(shí)內(nèi)并且還會(huì)同時(shí)進(jìn)行更新。堅(jiān)持使用這些 API 真的很重要,因?yàn)槿魏卧噲D直接訪問(wèn)這些數(shù)據(jù)結(jié)構(gòu)的代碼都將在今年的 OS 版本中停止工作,因?yàn)闁|西已經(jīng)發(fā)生了改變,而且該代碼不知道新的布局。我們看到,一些真實(shí)的代碼由于這些變化而崩潰,除了你自己的代碼之外,還要注意那些外部依賴性,你可能正把他們帶入到你的 app 中,它們可能會(huì)在你沒(méi)有意識(shí)到的情況下挖掘這些數(shù)據(jù)結(jié)構(gòu)。這些結(jié)構(gòu)中的所有信息都可通過(guò)官方API獲得。
有一些函數(shù),如 class_getName 和 class_getSuperclass,當(dāng)你使用這些 API 訪問(wèn)信息時(shí),你知道,無(wú)論我們?cè)诤笈_(tái)進(jìn)行什么更改,他們都將繼續(xù)工作。所有的 API 都可以在 Objective-C 運(yùn)行時(shí)說(shuō)明文檔中找到
class_getName
class_getSuperclass
class_copyMethodList
而這個(gè)文檔在 developer.apple.com 中。
接下來(lái),讓我們更深入地了解一下這些類的數(shù)據(jù)結(jié)構(gòu),并看看另一個(gè)變化,Relative method lists 相對(duì)方法列表。
下一章我會(huì)繼續(xù)總結(jié),敬請(qǐng)期待!
以上的我寫的內(nèi)容是根據(jù):2020 WWDC 大會(huì)視頻 總結(jié)記錄的,想看原視頻的可以前去觀看。
轉(zhuǎn)載請(qǐng)備注原文出處,不得用于商業(yè)傳播——凡幾多