OC對象本質(zhì)探索
相信大家都聽說過OC對象的本質(zhì)
其實就是 結(jié)構(gòu)體
逊拍,但是大多數(shù)開發(fā)者不太清楚它的底層實現(xiàn),接下來我們就探索一下:
Clang
-
clang
是一個由Apple主導編寫尺借,基于LLVM
的C/C++/OC
的編譯器
- 主要是用于
底層編譯
,將一些文件
輸出成c++
文件精拟,例如main.m
輸出成main.cpp
燎斩,其目的是為了更好的觀察底層的一些結(jié)構(gòu)及實現(xiàn)的邏輯,方便理解底層原理
蜂绎。
接下來我們對OC對象本質(zhì)的探索就需要使用Clang編譯器
栅表。
-
首先我們創(chuàng)建一個Mac Comman Line Tool 空工程
- 在
main.m
中自定義一個類`LPerson ,并添加一個屬性name
@interface LPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LPerson
@end
- 接下來便需要使用
Clang
將main.m
編譯成main.cpp
,命令如下,這里我們使用第一種:
//1、將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp
//2师枣、將 ViewController.m 編譯成 ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m
//以下兩種方式是通過指定架構(gòu)模式的命令行怪瓶,使用xcode工具 xcrun
//3、模擬器文件編譯
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
//4践美、真機文件編譯
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp
xcode
安裝的時候順帶安裝了xcrun
命令洗贰,xcrun
命令在clang
的基礎上進行了 一些封裝找岖,要更好用一些
使用第一種這里可能會報錯'UIKit/UIKit.h' file not found
,這時改用第二種命令就可以了
- 打開編譯好的
main.cpp
,找到LPerson
的定義哆姻,發(fā)現(xiàn)LPerson
在底層會被編譯成struct 結(jié)構(gòu)體
.
由此可見LPerson對象在C++底層是結(jié)構(gòu)體;
- 由上面的LPerson結(jié)構(gòu)體中我們還發(fā)現(xiàn),其中還有個屬性
NSObject_IMPL
,從代碼中我們可以看出,它就是isa
,是繼承自NSObject
宣增,屬于偽繼承
,偽繼承的方式是直接將NSObject
結(jié)構(gòu)體定義為LGPerson
中的第一個屬性
矛缨,意味著LGPerson 擁有
NSObject中的
所有成員變量`爹脾。
從上面源碼也可以看出對象就是這樣一個
結(jié)構(gòu)體
,且被typedef為id
箕昭。在Foundation層id
就表示一個對象 灵妨。此時便能證明,OC對象的本質(zhì)
其實就是結(jié)構(gòu)體
!
- 從上面源碼中我們也可以看到,
LGPerson
中的第一個屬性NSObject_IVARS
等效于 NSObject中的isa
,是否記得我們用lldb
命令po
一個對象(LPerson
)的時候的返回的是<LGPerson: 0x101404a30>
,這里便是它的isa
.
關(guān)于isa
通過上面的分析我們可以了解到,在對象結(jié)構(gòu)體內(nèi),有一個Class
類型的 isa
變量落竹,他是一個指向該對象類型的指針
,在面向?qū)ο缶幊讨忻诨簦瑢ο笫怯深悇?chuàng)建的,對象可以通過isa 變量
找到自己所屬的類述召。
Class探索
下面是Class
的源碼:
從Class
源碼中我們可以看到它的結(jié)構(gòu),類里面有個 super_class
朱转,指向了類的父類
; 同時類也有一個 isa
指針积暖,那類的 isa 指針指向了哪里呢藤为?
對象是按照
類
所定義的各個屬性和方法生產(chǎn)
的,類 作為對象的模板夺刑,也可看成是對象
缅疟。正如工廠里面的模子也是要專門制作模子的機器生產(chǎn)。元類 (meta class)
就是設計遍愿、管理類(class)
的模板存淫。對象是 類 的實例,類是 元類 的實例沼填。
所以類的isa
指針指向了 元類桅咆。
如果按照這個規(guī)則,聰明的你應該想到了問題了: 元類
也是對象坞笙,元類對象中也有isa
轧邪,那么元類的 isa
又指向哪里呢?總不能指向元元類吧……這樣是無窮無盡的羞海。
Objective-C語言的設計者已經(jīng)考慮到了這個問題忌愚,所有元類的isa
都指向 根元類(meta Root Class)
。關(guān)于實例對象却邓、類硕糊、元類之間的關(guān)系,蘋果官方給了一張圖,非常清晰的表明了三者的關(guān)系简十。
- 實線是
super_class
指針檬某,虛線是isa
指針。Root class
(class) 通常是NSObject
螟蝙,NSObject 是沒有超類的恢恼,所以Root class
(class)的superclass
指向nil
。- 每個
Class
都有一個isa
指針指向唯一的Meta class
- Root class(meta)的 superclass 指向 Root class(class)胰默,也就是 NSObject场斑,形成一個回路。
- 每個 Meta class 的 isa 指針都指向 Root class(meta)牵署。
一個對象 可以通過isa
找到類漏隐,根據(jù)類的isa
和 super_class
找到 元類
與 父類
,進而直到 根元類
和 根類
奴迅,所以 對于最開始的例子LPerson *p = [[LPerson alloc] init]; ``LPerson
可以調(diào)用NSObject
的方法青责,在這中間isa
起到至關(guān)重要的作用。
由此我們可以得出結(jié)論:
-
Object-C 是基于類的對象系統(tǒng)取具。每一個對象都是一些類的實例脖隶;這個對象的
isa
指針指向它所屬的類。- 該類描述這個 對象的數(shù)據(jù)信息 :內(nèi)存分配大小(allocation size)和實例變量的類型(ivar types )與布局(layout)暇检;
- 也描述了 對象的行為 :它能夠響應的選擇器(selectors)和它實現(xiàn)的實例方法(instance methods)产阱。
每個 Object-C 類也是一個對象,它的
isa
指針指向元類占哟,元類是關(guān)于類對象的描述,就像類是普通實例對象的描述一樣酿矢。一個
元類
是根元類的實例
榨乎;根元類是它自身的實例
。isa
指針鏈以一個環(huán)結(jié)束:實例指向類-指向元類-指向根元類-到自身瘫筐。元類的 isa 指針并不重要蜜暑,因為在現(xiàn)實世界中,沒人會向元類對象發(fā)送消息策肝。
isa_t
isa_t
最初出現(xiàn)在之前的alloc探索中,通過alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone-->initInstanceIsa
方法路徑肛捍,查找到initIsa
從源碼中可以看出,isa_t
是通過聯(lián)合體(union)
定義的
isa_t
類型使用聯(lián)合體
的原因也是基于內(nèi)存優(yōu)化
的考慮,這里的內(nèi)存優(yōu)化是指在isa指針中通過char + 位域
(即二進制中每一位均可表示不同的信息)的原理實現(xiàn)之众。通常來說拙毫,isa指針
占用的內(nèi)存大小是8字節(jié),即64位棺禾,已經(jīng)足夠存儲很多的信息了缀蹄,這樣可以極大的節(jié)省內(nèi)存,以提高性能
從isa_t的定義中可以看出:
-
提供了兩個成員,
cls
和bits
缺前,由聯(lián)合體的定義所知蛀醉,這兩個成員是互斥的,也就意味著衅码,當初始化isa指針時拯刁,有兩種初始化方式通過
cls
初始化,bits無默認值
通過
bits
初始化逝段,cls有默認值
還提供了一個結(jié)構(gòu)體定義的位域垛玻,用于存儲類信息及其他信息,結(jié)構(gòu)體的成員ISA_BITFIELD惹恃,這是一個宏定義夭谤,有兩個版本 arm64(對應ios 移動端) 和 x86_64(對應macOS),以下是它們的一些宏定義巫糙,如下圖所示
-
nonpointer
有兩個值朗儒,表示自定義的類等,占1位-
0
:純isa指針
-
1
:不只是類對象地址
参淹,isa中包含了類信息
醉锄、對象的引用計數(shù)
等
-
has_assoc
表示關(guān)聯(lián)對象標志位
,占1
位0
:沒有關(guān)聯(lián)
對象1
:存在關(guān)聯(lián)
對象-
has_cxx_dtor
表示該對象是否有C++/OC的析構(gòu)器
(類似于dealloc
)浙值,占1位- 如果
有析構(gòu)函數(shù)
恳不,則需要做析構(gòu)邏輯
- 如果
沒有
,則可以更快的釋放對象
- 如果
-
shiftclx
表示存儲類的指針的值
(類的地址)开呐, 即類信息-
arm64
中占33
位烟勋,開啟指針優(yōu)化的情況下,在arm64架構(gòu)中有33位用來存儲類指針 -
x86_64
中占44
位
-
magic
用于調(diào)試器判斷當前對象是真的對象
還是沒有初始化的空間
筐付,占6
位-
weakly_refrenced
是 指對象是否被指向
或者曾經(jīng)指向一個ARC的弱變量
- 沒有弱引用的對象可以更快釋放
deallocating
標志對象是是否正在釋放
內(nèi)存has_sidetable_rc
表示 當對象引用計數(shù)大于10
時卵惦,則需要借用該變量存儲進位
-
extra_rc
(額外的引用計數(shù)) --- 表示該對象的引用計數(shù)值,實際上是引用計數(shù)值減1- 如果對象的引用計數(shù)為10瓦戚,那么extra_rc為9
isa指針 位域算法
這里是macOS
環(huán)境沮尿,所以是x86_64
-
通過main中的
LGPerson
斷點 -->initInstanceIsa
-->initIsa
--> 走到else中的 isa初始化
,執(zhí)行lldb
命令:p newisa
,得到newisa
的詳細信息
-
然后繼續(xù)往下執(zhí)行较解,走到
newisa.bits = ISA_MAGIC_VALUE
;下一行畜疾,表示為isa
的bits
成員賦值,重新執(zhí)行lldb
命令p newisa
印衔,得到的結(jié)果如下
我們發(fā)現(xiàn)啡捶,newsize的信息發(fā)生了變化:
- 其中
magic
是59
是由于將isa
指針地址轉(zhuǎn)換為二進制
,從47
(因為前面有4個位域奸焙,共占用47位届慈,地址是從0開始)位開始讀取6
位徒溪,再轉(zhuǎn)換為十進制
,如下圖所示
聯(lián)合體(union)
構(gòu)造數(shù)據(jù)類型的方式有以下兩種:
-
結(jié)構(gòu)體
(struct
) -
聯(lián)合體
(union
金顿,也稱為共用體
)
結(jié)構(gòu)體
結(jié)構(gòu)體
是指把不同的數(shù)據(jù)組合成一個整體
臊泌,其變量是共存
的,變量不管是否使用
揍拆,都會分配內(nèi)存
渠概。
缺點:所有屬性都分配內(nèi)存,比較
浪費內(nèi)存
嫂拴,假設有4個int成員播揪,一共分配了16字節(jié)(128位)
的內(nèi)存,但是在使用時筒狠,你只使用了4字節(jié)(32位)
猪狈,剩余的12字節(jié)(96位)
就是屬于內(nèi)存的浪費優(yōu)點:存儲
容量較大
,包容性強
辩恼,且成員之間不會相互影響
聯(lián)合體
聯(lián)合體
也是由不同的數(shù)據(jù)類型組成
雇庙,但其變量是互斥
的,所有的成員共占一段內(nèi)存
灶伊。而且共用體采用了內(nèi)存覆蓋技術(shù)
疆前,同一時刻只能保存一個成員的值
,如果對新的成員賦值
聘萨,就會將原來成員的值覆蓋掉
缺點:
包容性弱
優(yōu)點:所有成員
共用一段內(nèi)存
竹椒,使內(nèi)存的使用更為精細靈活,同時也節(jié)省了內(nèi)存空間
兩者的區(qū)別
-
內(nèi)存占用情況
- 結(jié)構(gòu)體的
各個成員會占用不同的內(nèi)存
米辐,互相之間沒有影響
- 共用體的
所有成員占用同一段內(nèi)存
胸完,修改一個成員會影響
其余所有成員
- 結(jié)構(gòu)體的
-
內(nèi)存分配大小
- 結(jié)構(gòu)體內(nèi)存
>=
所有成員占用的內(nèi)存總和
(成員之間可能會有縫隙) -
共用體
占用的內(nèi)存
等于最大的成員
占用的內(nèi)存
- 結(jié)構(gòu)體內(nèi)存