WWDC2020 關(guān)于類(lèi)的一些變化
Class Data structures Changes:
運(yùn)行時(shí)的數(shù)據(jù)變化
類(lèi)對(duì)象本身存放了最常訪問(wèn)的信息辐烂,指向元類(lèi)、超類(lèi)和方法緩存的指針
它還有一個(gè)存放額外信息的class_ro_t
RO代表只讀 存放了類(lèi)名缸榄、方法、協(xié)議和實(shí)例變量的信息 Swift類(lèi)和Objective-c共享這一基礎(chǔ)結(jié)構(gòu)祝拯。每個(gè)Swift也有這些數(shù)據(jù)結(jié)構(gòu)甚带。
clean memory
:指的是加載之后不會(huì)發(fā)生改變的內(nèi)存她肯。 class_ro_t
就屬于clean memory 是只讀的。
dirty memory
:指的是進(jìn)程在運(yùn)行時(shí)會(huì)發(fā)生改變的內(nèi)存鹰贵。類(lèi)的結(jié)構(gòu)一經(jīng)使用會(huì)就變成dirty memory
晴氨,因?yàn)檫\(yùn)行時(shí)會(huì)寫(xiě)入新的數(shù)據(jù)。
dirty memory
要比clean memory
昂貴的多碉输,只要進(jìn)程在運(yùn)行籽前,它就必須一只存在。clean memory
可以進(jìn)行移除從而節(jié)省更多內(nèi)存空間敷钾,因?yàn)槿绻枰?code>clean memory系統(tǒng)就可以從磁盤(pán)中重新加載枝哄。
ios不使用swap dirty memory
在iOS中代價(jià)很大,所以dirty memory
是類(lèi)數(shù)據(jù)被分成兩部分的原因闰非。保持越多的clean memory
越好膘格,通過(guò)分離出那些永不會(huì)改變的數(shù)據(jù),可以吧大部分的數(shù)據(jù)存儲(chǔ)位clean memory
但是運(yùn)行時(shí)需要追蹤每個(gè)類(lèi)的更多信息财松,所以當(dāng)一個(gè)類(lèi)首次被使用運(yùn)行時(shí)會(huì)為它分配額外的存儲(chǔ)容量,這個(gè)存儲(chǔ)容量是class_rw_t
用于讀取-編寫(xiě)數(shù)據(jù)纱控。這個(gè)數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)了只有在運(yùn)行時(shí)才會(huì)生成的新信息辆毡。比如:所有的類(lèi)都會(huì)鏈接成一個(gè)樹(shù)狀結(jié)構(gòu),通過(guò)使用First Subclass
和Next Sibling Class
指針實(shí)現(xiàn)的甜害,這允許運(yùn)行時(shí)遍歷當(dāng)前使用的所有類(lèi)舶掖,這在方法緩存無(wú)效時(shí)非常有用。
為什么方法和屬性在只讀數(shù)據(jù)中時(shí)(ro)尔店,rw中還要有方法和屬性呢眨攘?
因?yàn)樗鼈兛梢栽谶\(yùn)行時(shí)進(jìn)行更改。當(dāng)category被加載時(shí)嚣州,它可以向類(lèi)中添加新的方法鲫售。而且開(kāi)發(fā)人員也可以使用運(yùn)行時(shí)API動(dòng)態(tài)地添加它們。因?yàn)?code>class_ro_t是只讀的该肴,所以需要使用class_rw_t
來(lái)追蹤這些信息情竹。
有了ro還使用rw追中運(yùn)行時(shí)的變化,這樣會(huì)占用很多的內(nèi)存匀哄。蘋(píng)果開(kāi)發(fā)人員在測(cè)試中發(fā)現(xiàn)秦效,大約只有10%的類(lèi)真正的改變了它們的方法。
上圖中涎嚼,Dmangled Name
是Swift使用的字段阱州,這屬于擴(kuò)展字段,除非是需要使用它們的object-c名稱(chēng)時(shí)才需要法梯,因此拆分掉那些平時(shí)不用的部分苔货,這將減少了class_rw_t
一半的大小。對(duì)于確實(shí)需要使用擴(kuò)展的類(lèi),可以統(tǒng)一分配到一個(gè)擴(kuò)展中蒲赂。
蘋(píng)果給出了一個(gè)檢查class_rw
使用的例子:
在Mac終端中使用$heap Mail | egrep class_rw|COUNT
檢測(cè)了一下郵件app中大約9000多個(gè)這樣的class_rw_t
類(lèi)型阱冶,但是大約只有10%左右的類(lèi)需要使用擴(kuò)展信息。
蘋(píng)果推薦堅(jiān)持使用官方給出的API很重要滥嘴,因?yàn)槟悴恢赖讓拥臄?shù)據(jù)結(jié)構(gòu)發(fā)生了怎樣的變化木蹬,但是總保證官方API的正常使用。
相反若皱,那些自己直接訪問(wèn)底層數(shù)據(jù)結(jié)構(gòu)的代碼在更新后將產(chǎn)生崩潰镊叁。因?yàn)榕f的訪問(wèn)方式不適用新的內(nèi)存布局。同時(shí)也需要注意走触,可能有些第三方引入的代碼也會(huì)產(chǎn)生這樣的問(wèn)題晦譬。
官方APIS例如:
class_getName
class_getSuperclass
class_copyMethodList
沒(méi)事多看看官方文檔。
Relative method lists:
相對(duì)方法列表
當(dāng)你在類(lèi)上編寫(xiě)新方法是它會(huì)被添加到列表中互广,運(yùn)行時(shí)使用這些列表來(lái)解析消息發(fā)送敛腌。
每個(gè)方法都包含三個(gè)信息:
- 方法的名稱(chēng) 或者說(shuō)選擇器。選擇器是字符串惫皱,但它們具有唯一性像樊,所以它們可以使用指針相等來(lái)進(jìn)行比較。
- 方法的類(lèi)型編碼旅敷。這是一個(gè)表示參數(shù)和返回類(lèi)型的字符串生棍。它是運(yùn)行時(shí)introspection和消息forwarding所必須的東西
- 指向方法實(shí)現(xiàn)的指針,方法的實(shí)際代碼媳谁。當(dāng)你編寫(xiě)一個(gè)方法時(shí)涂滴,他會(huì)被編譯成一個(gè)c函數(shù),方法列表的entry會(huì)指向該函數(shù)晴音。
由以上來(lái)看每個(gè)方法表?xiàng)l目占用8+8+8 = 24個(gè)字節(jié)柔纵。這屬于clean memory
,它從磁盤(pán)中加載段多,存放在內(nèi)存中首量。
這是要一個(gè)很大的地址空間,它需要64位來(lái)尋址进苍。
這個(gè)地址空間它劃分成了幾個(gè)部分:棧加缘、堆。
可執(zhí)行文件和庫(kù)或二進(jìn)制圖像觉啊,這些都加載到了進(jìn)程中拣宏,用藍(lán)色表示。
我們放大其中一個(gè)二進(jìn)制圖像來(lái)查看:
這個(gè)方法條目指的三個(gè)信息向其二進(jìn)制文件中的位置杠人。告訴我們二進(jìn)制圖像可以加載到內(nèi)存中的任何地方勋乾,這取決于動(dòng)態(tài)鏈接器決定吧它放在哪里宋下。
這意味著 連接器需要將指針解析到圖像中,并在加載時(shí)將其修正為 指向其在內(nèi)存中的實(shí)際位置辑莫。
一個(gè)來(lái)自二進(jìn)制文件的類(lèi)方法條目永遠(yuǎn)只指向該二進(jìn)制文件內(nèi)的方法實(shí)現(xiàn)学歧。
不可能使一個(gè)方法的元數(shù)據(jù)存在于一個(gè)二進(jìn)制文件中,而實(shí)現(xiàn)它的代碼在另外一個(gè)二進(jìn)制文件中各吨。
這意味著 方法列表?xiàng)l目實(shí)際上并不需要能夠引用整個(gè)64位的地址空間枝笨。它只需要能夠引用自己二進(jìn)制中的函數(shù),因?yàn)檫@些實(shí)現(xiàn)函數(shù)總是在附近揭蜒。
因此 無(wú)需使用絕對(duì)的64位地址横浑,可以使用二進(jìn)制中的32位的相對(duì)偏移。(蘋(píng)果說(shuō)今年就是這么改的)
現(xiàn)在一個(gè)方法條目只需要 4+4+4 = 12個(gè)字節(jié)屉更。
蘋(píng)果說(shuō)這么做有幾個(gè)好處:
- 偏移量始終是相同的徙融。不管image在哪里加載到內(nèi)存中,從磁盤(pán)中加載后都不需要進(jìn)行修正瑰谜,所以放在只讀內(nèi)存中更安全欺冀。
- 32位的偏移量已經(jīng)將64位平臺(tái)所需的內(nèi)存量減少了一般。
蘋(píng)果爸爸說(shuō)一臺(tái)iPhone形同范圍內(nèi)測(cè)試約80MB的這些方法萨脑,因?yàn)樗鼈兊某叽鐪p半脚猾,節(jié)省了40MB的內(nèi)存,這樣你的app就有更多的內(nèi)存從而可以讓你的用戶體驗(yàn)更好砚哗。
那么,Swizzling呢砰奕?
二進(jìn)制中的方法列表現(xiàn)在不能引用完整的地址空間如果你swizzle一個(gè)方法蛛芥,他就可以在任何地方實(shí)施。而且剛才說(shuō)過(guò)军援,我們希望保持這些方法列表為只讀仅淑,因而使用了一張全局列表。
這個(gè)全局列表將方法印射到他們被swizzle的實(shí)現(xiàn)上胸哥。
swizzling并不常見(jiàn)涯竟,絕大多數(shù)方法都沒(méi)有被swizzle過(guò)所以這個(gè)表最終不會(huì)變的很大。
蘋(píng)果說(shuō)內(nèi)存每次都是按頁(yè)面來(lái)“弄臟”的空厌,使用舊式的方法列表庐船,swizzle一個(gè)方法,會(huì)“弄臟”它所在的整個(gè)頁(yè)面嘲更,一次swizzle就會(huì)導(dǎo)致產(chǎn)生大量千字節(jié)的dirty memory
筐钟。
有了這個(gè)全局表 我們只需要為了一個(gè)額外的表的條目付出代價(jià)。
這個(gè)改變將會(huì)在一下系統(tǒng)開(kāi)始生效
- macOS Big Sur
- iOS 14
- tvOS 14
- watchOS 7
蘋(píng)果也說(shuō)了 新舊兩種方法列表今后可以同時(shí)兼容 前提是 你使用的是官方提供的APIs
如果是在不匹配的deployment targets上運(yùn)行了自己構(gòu)建的讀取方法赋朦,則可能會(huì)出現(xiàn)運(yùn)行時(shí)讀取方法信息時(shí)的崩潰(舊版64位篓冲,新版32位加偏移量李破,不匹配就會(huì)發(fā)生讀取錯(cuò)誤).
Tagged pointer format changes:
arm64上 tagged pointer變化
什么是Tagged pointer?
首先我們看看一個(gè)普通的64位對(duì)象地址指針:
指針地址一共64位 但是我們只在一個(gè)真正的對(duì)象指針中使用了中間這些位(途中黃色標(biāo)記部分)
由于對(duì)齊要求的存在壹将,低位始終未0嗤攻,對(duì)象必須總是位域指針大小倍數(shù)的一個(gè)地址中
由于地址空間有限,所以高位始終未0诽俯,實(shí)際上是用不到2^64妇菱。
正常對(duì)象指針地址的高位和低位始終未0。
如果這時(shí)候我們選擇一個(gè)始終未0的位置惊畏,把它變?yōu)?恶耽。
那么這時(shí)候我們一看就知道這不是一個(gè)真正的對(duì)象指針。
然后也可以給其他位賦予一些其他意義 稱(chēng)作為 tagged pointer
例如我們可以在其他位中塞入一個(gè)數(shù)值 然后只要教NSNumber
如何讀取這些位颜启,并讓運(yùn)行時(shí)適當(dāng)?shù)奶幚韙agged pointer偷俭,系統(tǒng)的其他部分就可以吧這些東西當(dāng)做對(duì)象指針來(lái)處理,并且永遠(yuǎn)不知道其中的區(qū)別缰盏。
這樣可以節(jié)省我們?yōu)槊恳环N類(lèi)似情況分配一個(gè)小數(shù)字對(duì)象的代價(jià)
順便說(shuō)一下涌萤,這些值實(shí)際上是通過(guò)與進(jìn)程啟動(dòng)是初始化的隨機(jī)值相結(jié)合而被混淆的。
蘋(píng)果爸爸給出intel的完整的taggerd pointers示例
低位設(shè)置為1 表示這是一個(gè)taggerd pointer
,真正的指針這個(gè)位置必須為0.接下來(lái)的3位是標(biāo)簽號(hào)口猜,這表示了taggerd pointer的類(lèi)型负溪。
例如3它表示是一個(gè)NSNumber,6表示是一個(gè)NSDate济炎,最多可以表示8中類(lèi)型川抡。剩下的位是有效負(fù)載,這是特定類(lèi)型可以隨意使用的數(shù)據(jù)须尚。
現(xiàn)在tag=7 有一個(gè)特殊情況崖堤,他表示一個(gè)擴(kuò)展標(biāo)簽,擴(kuò)展標(biāo)簽使用接下來(lái)的8位來(lái)編碼類(lèi)型耐床,這允許多出256個(gè)標(biāo)簽類(lèi)型密幔,代價(jià)是減少了有效負(fù)載。這也使得能夠表達(dá)更多的類(lèi)型標(biāo)簽撩轰。
蘋(píng)果爸爸說(shuō)swift開(kāi)發(fā)人員可以創(chuàng)建自己的tagged pointer
類(lèi)型胯甩。
具有關(guān)聯(lián)值的枚舉,那就是一個(gè)類(lèi)似于tagged pointer
的類(lèi)堪嫂。
swift運(yùn)行時(shí)將枚舉判別器存儲(chǔ)在關(guān)聯(lián)值有效負(fù)載的備用位中偎箫,
而且swift對(duì)值類(lèi)型的使用實(shí)際上使得tagged pointer
變的沒(méi)那么重要了,因?yàn)橹挡辉傩枰耆侵羔槾笮 ?br>
例如Swift UUID類(lèi)型可以使兩個(gè)字并保持內(nèi)聯(lián)溉苛,而不是分配一個(gè)單獨(dú)的對(duì)象镜廉,因?yàn)樗贿m合放在一個(gè)指針里面。
在arm64位中愚战,把最高位設(shè)置為1用來(lái)表示tagged pointer
娇唯,緊接著的3位作為tag齐遵,余下部分作為有效負(fù)載。
為什么在arm64中使用最高位為1而不像Intel一樣使用最低為呢塔插?
實(shí)際上是對(duì)objc_msgsend
的一個(gè)優(yōu)化我們希望objc_msgsend
中最常見(jiàn)的路徑可以盡可能的快梗摇,而最常見(jiàn)的路徑就是一個(gè)普通指針。
我們有兩種不太常見(jiàn)的情況想许,tagged pointer
和 nil
伶授。實(shí)時(shí)證明,當(dāng)使用最高位時(shí)流纹,可以通過(guò)一次比較對(duì)這兩個(gè)值進(jìn)行檢查糜烹。
相比于分開(kāi)檢查nil
和tagged pointer
,這樣就可以給msgsend
中的常見(jiàn)情況節(jié)省了一個(gè)分支條件漱凝。
接下來(lái)的8位用作擴(kuò)展標(biāo)簽疮蹦,然后剩下的作為有效負(fù)載。單這是iOS13以前所使用的格式茸炒。
iOS14之后 把tag移到最后3位愕乎,最高位任然是1 因?yàn)樗鼘?duì)msgsend的優(yōu)惠效果依舊很明顯,如果正在使用擴(kuò)展為壁公,它會(huì)占據(jù)標(biāo)簽位后的高8位感论。
為什么要這么做呢?
來(lái)看看普通指針紊册,我們現(xiàn)有的工具比肄,比如動(dòng)態(tài)鏈接會(huì)忽略指針的前8位。這是由于Top Byte Ignore
的ARM特性囊陡。
把擴(kuò)展標(biāo)簽放在Top Byte Ignore
位薪前。對(duì)于一個(gè)對(duì)齊指針,底部3位總是0关斜,那么為它添加7以將低位設(shè)置為1。7表示這是一個(gè)擴(kuò)展標(biāo)簽铺浇。這意味著我們實(shí)際上可將上面的指針?lè)湃胍粋€(gè)擴(kuò)展標(biāo)簽指針有效負(fù)載中痢畜。
結(jié)果就是一個(gè)tagged pointer
以及其有效負(fù)載中包含一個(gè)正常指針
為什么這樣很有用呢?
它開(kāi)啟了tagged pointer
的能力鳍侣,引用二進(jìn)制文件中的常量數(shù)據(jù)的能力丁稀。例如字符串或其他數(shù)據(jù)結(jié)構(gòu),否則它們將不得不占用dirty memory
倚聚。
這些變化會(huì)在iOS14發(fā)布開(kāi)始使用线衫。并且相關(guān)的直接訪問(wèn)的代碼也會(huì)失效,蘋(píng)果爸爸這里再一次強(qiáng)調(diào)如果使用APIs就不會(huì)出現(xiàn)問(wèn)題惑折,要規(guī)范要規(guī)范要規(guī)范授账。
正確的使用API進(jìn)行類(lèi)型檢查枯跑。
CF類(lèi)型這樣也能。
好了 終于寫(xiě)完了 對(duì)照視頻一邊看一邊敲字 人快虛脫了 先來(lái)一杯82年的冰闊落壓壓驚