iOS之深入解析对象isa的底层原理
生活随笔
收集整理的這篇文章主要介紹了
iOS之深入解析对象isa的底层原理
小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
對(duì)象本質(zhì)
一、NSObject 本質(zhì)
OC代碼的底層實(shí)現(xiàn)實(shí)質(zhì)是 C/C++代碼 ,繼而編譯成匯編代碼,最終變成機(jī)器語(yǔ)言。
① clang C/C++ 編譯器
- Clang 是?個(gè) C 語(yǔ)?、C++、Objective-C 語(yǔ)?的 輕量級(jí)編譯器 ,源代碼發(fā)布于 BSD 協(xié)議下。
- Clang 將?持其 普通lambda表達(dá)式 ,返回類(lèi)型的簡(jiǎn)化處理以及更好的 處理constexpr關(guān)鍵字 。
- Clang 是?個(gè)由 Apple 主導(dǎo)編寫(xiě),基于 LLVM的C/C++/Objective-C編譯器 。
- 2013年4?,Clang 已經(jīng)全??持 C++11標(biāo)準(zhǔn) ,并開(kāi)始實(shí)現(xiàn) C++1y特性 (也就是 C++14,這是 C++ 的下?個(gè)?更新版本)。Clang 將?持其 普通lambda表達(dá)式 ,返回類(lèi)型的簡(jiǎn)化處理以及更好的處理 constexpr關(guān)鍵字 。
- Clang 是?個(gè) C++ 編寫(xiě),基于 LLVM 發(fā)布于 LLVM BSD 許可證下的 C/C++/Objective-C/Objective-C++ 編譯器。它與 GNU C 語(yǔ)?規(guī)范?乎完全兼容(當(dāng)然也有部分不兼容的內(nèi)容,包括編譯命令選項(xiàng)也會(huì)有點(diǎn)差異),并在此基礎(chǔ)上增加了額外的語(yǔ)法特性,?如 C 函數(shù)重載(通過(guò) attribute((overloadable) )來(lái)修飾函數(shù)),其?標(biāo)(之?)就是超越 GCC 。
- clang -rewrite-objc main.m -o main.cpp 把?標(biāo)?件編譯成c++?件。
- UIKit報(bào)錯(cuò)問(wèn)題:Xcode安裝的時(shí)候順帶安裝了 xcrun 命令, xcrun 命令在 clang 的基礎(chǔ)上進(jìn)?了?些封裝。
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o
main-arm64.cpp (模擬器) - xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o mainarm64.cpp (?機(jī))
② 運(yùn)用 clang 將目標(biāo)文件編譯成 cpp(C++文件)
- 在main.m中添加以下代碼:
- 打開(kāi)終端,進(jìn)入main.m所在的文件夾,通過(guò) clang rewirte-objc main.m -o main.cpp 或 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 命令,生成cpp文件。
- 然后回到main.m的文件中,打開(kāi)cpp文件,搜索“YDWHandsomeBoy”,即可看到,對(duì)象YDWHandsomeBoy在底層被編譯成了一個(gè)結(jié)構(gòu)體:
- 可以看到上面的c++代碼中有個(gè)NSObject_IMPL NSObject_IVARS這個(gè)東西,然后搜索NSObject_IMPL可以發(fā)現(xiàn):
- 由此可以得出:
- NSObject的底層實(shí)現(xiàn)實(shí)質(zhì)是一個(gè) 結(jié)構(gòu)體 ,而結(jié)構(gòu)體中的成員 isa是Class類(lèi)型 ;
- 通過(guò)源碼 typedef struct objc_class *Class 可知它是一個(gè)指針,在64為環(huán)境下指針占8個(gè)字節(jié),而在32位機(jī)下是占4個(gè)字節(jié),因此該結(jié)構(gòu)體占8個(gè)字節(jié)(因?yàn)樵摻Y(jié)構(gòu)體只有一個(gè)成員)。
二、NSObject 對(duì)象內(nèi)存
- 初始化一個(gè)NSObject并打印:
- 可知NSObject對(duì)象占16個(gè)字節(jié)。那么與上文中NSObject結(jié)構(gòu)體中占8個(gè)字節(jié)是否沖突?再次打印:
- 不難發(fā)現(xiàn):獲取NSObject類(lèi)的實(shí)例對(duì)象的成員變量所占用的(內(nèi)存對(duì)齊之后)大小,顯示確實(shí)為8個(gè)字節(jié)。
- 在objc的源碼中找到 class_getInstanceSize方法,發(fā)現(xiàn)它返回的是 cls->alignedInstanceSize() ,對(duì)它的描述為Class’s ivar size rounded up to a pointer-size boundary意指 返回成員變量占據(jù)的大小 。因此創(chuàng)建一個(gè)NSObject對(duì)象需要分配16個(gè)字節(jié),只是真正利用的只有8個(gè)字節(jié),即isa這個(gè)成員的大小。
- 查看allocWithZone的源碼發(fā)現(xiàn)它最底層的創(chuàng)建實(shí)例的方法實(shí)際上是調(diào)用了C語(yǔ)言的 calloc方法 ,在該方法中,規(guī)定若 分配的字節(jié)不滿(mǎn)16將把它分配為16個(gè)字節(jié) 。
三、若一個(gè)YDWHandsomeBoy類(lèi)繼承自NSObject類(lèi),那么YDWHandsomeBoy類(lèi)的對(duì)象占多少內(nèi)存?
- 新建YDWHandsomeBoy類(lèi),添加成員變量,在main中實(shí)現(xiàn)以下代碼:
- 通過(guò)以上代碼可以看出:
- 若一個(gè)類(lèi)繼承自另一個(gè)類(lèi),則它的底層會(huì) 將父類(lèi)的成員變量放在結(jié)構(gòu)體的最前面,此后依次放置本類(lèi)的成員變量 。
- 而從之前的分析可知,NSObject_IMPL的本質(zhì)就是一個(gè) 裝有成員變量isa的結(jié)構(gòu)體 ,因此,YDWHandsomeBoy類(lèi)對(duì)象所占的內(nèi)存為isa的內(nèi)存8加上YDWHandsomeBoy類(lèi)成員變量所占的空間,若不滿(mǎn)16個(gè)字節(jié),會(huì)強(qiáng)制分配到16個(gè)字節(jié)。
- 由于 內(nèi)存對(duì)齊 的規(guī)定,結(jié)構(gòu)體的最終大小必須是 最大成員的倍數(shù) 。
isa
一、isa 簡(jiǎn)介
- alloc初始化時(shí)不僅 創(chuàng)建對(duì)象并且分配內(nèi)存 ,同時(shí) 初始化 isa 指針屬性。
- Objective-C 對(duì)象在底層本質(zhì)上是 結(jié)構(gòu)體 ,所有的對(duì)象里面都會(huì)包含有一個(gè) isa ,isa 的定義是一個(gè) 聯(lián)合體 isa_t ,isa_t 包含了 當(dāng)前對(duì)象指向類(lèi)的信息 。
- isa 是一個(gè) 聯(lián)合體,而這其實(shí)是從內(nèi)存管理層面來(lái)設(shè)計(jì)的,因?yàn)槁?lián)合體是 所有成員共享一個(gè)內(nèi)存,聯(lián)合體 內(nèi)存的大小取決于內(nèi)部成員內(nèi)存大小最大的那個(gè)元素 。
- 對(duì)于 isa 指針來(lái)說(shuō),就不用額外聲明很多的屬性,直接在 內(nèi)部的 ISA_BITFIELD 保存信息 。
- 由于聯(lián)合體 屬性間互斥 ,所以 cls 和 bits 在 isa 初始化流程時(shí)是在 兩個(gè)分支 中被賦值的。
- isa_t 是一個(gè)聯(lián)合體,聯(lián)合體的特性就是 內(nèi)部所有的成員共用一塊內(nèi)存地址空間 ,也就是說(shuō)isa_t、cls、bits會(huì)共用同一塊內(nèi)存地址空間,這塊 內(nèi)存地址空間大小取決于最大長(zhǎng)度內(nèi)部成員的大小 ,即64位8字節(jié),由此可以知道isa 的所占的內(nèi)存空間大小為8字節(jié)。isa_t聯(lián)合體如下:
二、isa 結(jié)構(gòu)
isa 作為一個(gè)聯(lián)合體,有一個(gè)結(jié)構(gòu)體屬性為 ISA_BITFIELD ,其大小為 8 個(gè)字節(jié),也就是 64 位?;赺_arm64__ 和 x86 64 架構(gòu)如下:
# if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL # define ISA_BITFIELD \uintptr_t nonpointer : 1; \uintptr_t has_assoc : 1; \uintptr_t has_cxx_dtor : 1; \uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \uintptr_t magic : 6; \uintptr_t weakly_referenced : 1; \uintptr_t deallocating : 1; \uintptr_t has_sidetable_rc : 1; \uintptr_t extra_rc : 19 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18)# elif __x86_64__ # define ISA_MASK 0x00007ffffffffff8ULL # define ISA_MAGIC_MASK 0x001f800000000001ULL # define ISA_MAGIC_VALUE 0x001d800000000001ULL # define ISA_BITFIELD \uintptr_t nonpointer : 1; \uintptr_t has_assoc : 1; \uintptr_t has_cxx_dtor : 1; \uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \uintptr_t magic : 6; \uintptr_t weakly_referenced : 1; \uintptr_t deallocating : 1; \uintptr_t has_sidetable_rc : 1; \uintptr_t extra_rc : 8 # define RC_ONE (1ULL<<56) # define RC_HALF (1ULL<<7)# else # error unknown architecture for packed isa # endif- union-isa_t 存儲(chǔ)分配如下:
- nonpointer: 表示是否對(duì) isa 指針 開(kāi)啟指針優(yōu)化 :
0: 純 isa 指針;
1: 不止是類(lèi)對(duì)象地址, isa 中包含了類(lèi)信息、對(duì)象的引用計(jì)數(shù)等。 - has_assoc: 關(guān)聯(lián)對(duì)象標(biāo)志位 ,0 沒(méi)有,1 存在。
- has_cxx_dtor: 該對(duì)象 是否有 C++ 或者 Objc 的析構(gòu)器 ,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯,如果沒(méi)有,則可以更快的釋放對(duì)象。
- shiftcls: 存儲(chǔ)類(lèi)指針的值 ,開(kāi)啟指針優(yōu)化的情況下,在 arm64 架構(gòu)中有 33 位用來(lái)存儲(chǔ)類(lèi)指針。
- magic: 用于 調(diào)試器判斷當(dāng)前對(duì)象是真的對(duì)象還是沒(méi)有初始化的空間 。
- weakly_referenced: 標(biāo)志對(duì)象是否被指向或者曾經(jīng) 指向一個(gè) ARC 的弱變量 ,沒(méi)有弱引用的對(duì)象可以更快釋放。
- deallocating: 標(biāo)志對(duì)象 是否正在釋放內(nèi)存 。
- has_sidetable_rc: 當(dāng)對(duì)象引用技術(shù)大于 10 時(shí),則需要借用該變量 存儲(chǔ)進(jìn)位 。
- extra_rc: 當(dāng)表示該 對(duì)象的引用計(jì)數(shù)值 ,實(shí)際上是引用計(jì)數(shù)值減 1。 例如,如果對(duì)象的引用計(jì)數(shù)為 10,那么 extra_rc 為 9;如果引用計(jì)數(shù)大于 10, 則需要使用到has_sidetable_rc。
三、isa 初始化
① isa 源碼實(shí)現(xiàn)
- 在objc的源碼中有isa的初始化方法:
- 由于nonpointer傳入的是true,SUPPORT_INDEXED_ISA定義為0,所以可以對(duì)這段代碼簡(jiǎn)化一下:
② isa 初始化數(shù)據(jù)
- 可以看到對(duì)bits的賦值ISA_MAGIC_VALUE = 0x001d800000000001ULL,將此轉(zhuǎn)為二進(jìn)制,在結(jié)合isa_t的結(jié)構(gòu)得出如下的isa_t的初始數(shù)據(jù)圖:
- 對(duì) isa 賦值ISA_MAGIC_VALUE初始化實(shí)際上只是設(shè)置了indexed和magic兩部分的數(shù)據(jù):
- indexed表示 isa_t 的類(lèi)型 :0表示 raw isa,也就是沒(méi)有結(jié)構(gòu)體的部分,訪問(wèn)對(duì)象的 isa 會(huì)直接返回一個(gè)指向 cls 的指針,也就是在 iPhone 遷移到 64 位系統(tǒng)之前時(shí) isa 的類(lèi)型;1則表示當(dāng)前 isa 不是指針,但是其中也有 cls 的信息,只是其中關(guān)于類(lèi)的指針都是保存在 shiftcls 中。
- magic 用于 調(diào)試器判斷當(dāng)前對(duì)象是否有初始化空間 。
- 在設(shè)置indexed和magic的值后會(huì)對(duì)has_cxx_dtor進(jìn)行設(shè)值。has_cxx_dtor表示該對(duì)象是否有 C++ 或者 Objc 的析構(gòu)器 ,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯,如果沒(méi)有,則可以更快的釋放對(duì)象。
- 將當(dāng)前對(duì)象的類(lèi)指針存放在 shiftcls 中:
- 對(duì)cls的地址右移動(dòng)3位的目的是 為了減少內(nèi)存的消耗 ,因?yàn)轭?lèi)的指針需要按照8字節(jié)對(duì)齊,也就是說(shuō)類(lèi)的指針的大小必定是8的倍數(shù),其二進(jìn)制后三位為0 ,右移三位抹除后面的3位0并不會(huì)產(chǎn)生影響。
③ isa的初始化流程示意
四、isa 關(guān)聯(lián)對(duì)象和類(lèi)
isa 是對(duì)象中的第一個(gè)屬性,這是在繼承的時(shí)候發(fā)生的,要早于對(duì)象的成員變量,屬性列表,方法列表以及所遵循的協(xié)議列表。在 alloc 底層,有一個(gè)方法叫做 initIsa ,這個(gè)方法的作用就是 初始化 isa 聯(lián)合體位域 。上文中我們已經(jīng)看到了這個(gè)方法:
newisa.shiftcls = (uintptr_t)cls >> 3;① cls 存儲(chǔ)到 isa
- isa 剛初始化時(shí),還沒(méi)有被賦值,bits 全為空值,p newisa 如下:
- 繼續(xù)向下執(zhí)行,當(dāng)斷點(diǎn)執(zhí)行到如下位置的時(shí)候,bits 會(huì)被賦上默認(rèn)值(nonpointer = 1,magic = 59),繼續(xù)p newisa如下:
- 為什么 magic = 59 呢?其實(shí),通過(guò)計(jì)算器可以轉(zhuǎn)換算出:0x001d800000000001 = 59,上面我們已經(jīng) po 0x001d800000000001ULL 了,可以看到這個(gè)默認(rèn)值。
- 繼續(xù)輸出 bits 二進(jìn)制、輸出 cls 指針二進(jìn)制可以看到:在未設(shè)置 shiftcls 時(shí),bits 從右到左 [3, 46] 位都是0。如下:
- 為什么要右移三位?在 Objective-C 中,類(lèi)的指針是按照字節(jié)(8 bits)對(duì)齊的,也就是說(shuō)類(lèi)指針地址轉(zhuǎn)化成十進(jìn)制后,都是8的倍數(shù),也就是說(shuō),類(lèi)指針地址轉(zhuǎn)化成二進(jìn)制后,后三位都是0。既然是沒(méi)有意義的0,那么在存儲(chǔ)時(shí)就可以省略,用節(jié)省下來(lái)的空間存儲(chǔ)一些其他信息。
- 當(dāng) bits 被賦值之后,如下:
- 可以看到,現(xiàn)在的 bits 的 [3, 46] 位正好是之前 cls 指針右移三位的內(nèi)容。
② isa 關(guān)聯(lián)對(duì)象和類(lèi)
- 通過(guò) LLDB 進(jìn)行調(diào)試打印,就可以知道一個(gè)對(duì)象的 isa 會(huì)關(guān)聯(lián)到這個(gè)對(duì)象所屬的類(lèi):
- LLDB 調(diào)試的時(shí)候左移右移操作其實(shí)很好理解,先觀察 isa 的 ISA_BITFIELD 位域的結(jié)構(gòu):ISA_BITFIELD 的前 3 位是 nonpointer、has_assoc、has_cxx_dtor ,中間 44 位是 shiftcls ,后面 17 位是剩余的內(nèi)容,同時(shí)因?yàn)?iOS 是 小端模式 ,那么就需要去掉右邊的 3 位和左邊的 17位,所以就會(huì)采用 >> 3 << 3 然后 << 13 >> 13 的操作。
五、isa 走位分析
① class object(類(lèi)對(duì)象)/ metaclass(元類(lèi))
- Object-C的對(duì)象其本質(zhì)就是結(jié)構(gòu)體,前面也分析了每一個(gè)對(duì)象都會(huì)有一個(gè)isa。同時(shí)類(lèi)的本質(zhì)也是一個(gè)結(jié)構(gòu)體,而且是繼承自objc_object的。
- 在objc_class中也有isa:
- class_isMetaClass用于判斷Class對(duì)象是否為元類(lèi),object_getClass用于獲取對(duì)象的isa指針指向的對(duì)象。
- 我們知道:對(duì)象可以創(chuàng)建多個(gè),但是類(lèi)是否可以創(chuàng)建多個(gè)呢?其實(shí)答案是否定的,類(lèi)在內(nèi)存中只會(huì)存在一份。
- 通過(guò) LLDB 調(diào)試打印,其實(shí)可以發(fā)現(xiàn):類(lèi)的內(nèi)存結(jié)構(gòu)里面的第一個(gè)結(jié)構(gòu)打印出來(lái)還是 Boy,那么是不是就意味著 對(duì)象 ->類(lèi)->類(lèi) 這樣的死循環(huán)呢?這里的第二個(gè)類(lèi)其實(shí)是 元類(lèi),是由系統(tǒng)創(chuàng)建的,這個(gè)元類(lèi)無(wú)法被我們實(shí)例化。
- 一個(gè)實(shí)例對(duì)象通過(guò)class方法獲取的Class就是它的isa指針指向的類(lèi)對(duì)象,而類(lèi)對(duì)象不是元類(lèi),類(lèi)對(duì)象的isa指針指向的對(duì)象是元類(lèi)。關(guān)系如下:
② isa 走位
- 官方的經(jīng)典 isa 走位圖:
- 實(shí)例對(duì)象的isa指向的是類(lèi);
- 類(lèi)的isa指向的元類(lèi);
- 元類(lèi)指向根元類(lèi);
- 根元類(lèi)指向自己;
- NSObject的父類(lèi)是nil,根元類(lèi)的父類(lèi)是NSObject。
- LLDB 調(diào)試打印:
六、對(duì)象的本質(zhì) isa
- OC 對(duì)象的本質(zhì)就是一個(gè)結(jié)構(gòu)體,在 libObjc 源碼的 objc-private.h 源文件中可以看到:
- 對(duì)于對(duì)象所屬的類(lèi)來(lái)說(shuō),也可以在 objc-runtime-new.h 源文件中找到(即 objc_class 內(nèi)存中第一個(gè)位置是 isa,第二個(gè)位置是 superclass):
- 對(duì)象在底層其實(shí)是一個(gè)結(jié)構(gòu)體 objc_object ,而Class 在底層也是一個(gè)結(jié)構(gòu)體 objc_class 。
總結(jié)
以上是生活随笔為你收集整理的iOS之深入解析对象isa的底层原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: iOS之深入解析YYImage图片处理的
- 下一篇: iOS之深入解析AFNetworking