寫在前面
在iOS之武功秘籍②:OC對象原理-中(內(nèi)存對齊和malloc源碼分析)一文中講了對象中的屬性在內(nèi)存中的排列 -- 內(nèi)存對齊 和malloc
源碼分析,那么接下我們就來分析一下isa
的初始化和指向分析與對象的本質(zhì)
一评矩、對象的本質(zhì)
① Clang的了解
Clang
是?個由Apple
主導(dǎo)編寫拭嫁,基于LLVM
的C/C++/Objective-C
輕量級編譯器.源代碼發(fā)布于LLVM BSD
協(xié)議下.Clang
將?持其普通lambda
表達式肚豺、返回類型的簡化處理以及更好的處理constexpr
關(guān)鍵字筷狼。它與
GNU C語?
規(guī)范?乎完全兼容
(當然翩概,也有部分不兼容的內(nèi)容实幕,
包括編譯命令選項也會有點差異)卸奉,并在此基礎(chǔ)上增加了額外的語法特性,?如C函數(shù)重載
(通過__attribute__((overloadable))
來修飾函數(shù))庐船,其?標(之?)就是超越GCC
.它主要是用于
底層編譯
,將一些OC
文件輸出成C++
文件嘲更,例如main.m
輸出成main.cpp
筐钟,其目的是為了更好的觀察底層的一些結(jié)構(gòu)
及實現(xiàn)
的邏輯,方便理解底層原理
② Clang操作指令
// 把?標?件編譯成c++?件 -- 將 main.m 編譯成 main.cpp
clang -rewrite-objc main.m -o main.cpp
// UIKit報錯問題 -- 將 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/iPhoneSimulator14.0.sdk ViewController.m
// `xcode`安裝的時候順帶安裝了`xcrun`命令赋朦,`xcrun`命令在`clang`的基礎(chǔ)上進?了?些封裝篓冲,要更好??些
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 main?arm64.cpp (?機)
③ 探索對象本質(zhì)
-
構(gòu)建測試代碼
-
通過終端,利用
clang
將main.m
編譯成main.cpp
,在終端輸入以下命令- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
-
打開編譯好的
main-arm64.cpp
宠哄,找TCJPerson
的定義壹将,發(fā)TCJPerson
在底層會被編譯成struct
結(jié)構(gòu)體
通過編譯好的main-arm64.cpp
我們可以看到:
-
NSObject
的底層實現(xiàn)其實就是一個包含一個isa
指針的結(jié)構(gòu)體. -
Class
其實就是一個指針,指向了objc_class
類型的結(jié)構(gòu)體. -
TCJPerson_IMPL
結(jié)構(gòu)體內(nèi)有三個成員變量:-
isa
繼承自父類NSObject
helloName
_name
-
- 對于屬性
name
:底層編譯會生成相應(yīng)的setter
(_I_TCJPerson_setName_
,setter
方法內(nèi)調(diào)用objc_setProperty
方法)毛嫉、getter
(_I_TCJPerson_name
)方法,且?guī)臀覀冝D(zhuǎn)化為_name
- 對于成員變量
helloName
:底層編譯不會生成相應(yīng)的setter
诽俯、getter
方法,且沒有轉(zhuǎn)化為_helloName
通過上述分析,理解了OC
對象的本質(zhì) -- 結(jié)構(gòu)體承粤,但是看到NSObject
的定義暴区,會產(chǎn)生一個疑問:為什么isa
的類型是Class
?
- 在iOS之武功秘籍①:OC對象原理-上(alloc & init & new)文章中,提及過
alloc
方法的核心之一的initInstanceIsa
方法,通過查看這個方法的源碼實現(xiàn)辛臊,我們發(fā)現(xiàn)仙粱,在初始化isa
指針時,是通過isa_t
類型初始化的 - 而在
NSObject
定義中isa
的類型是Class
彻舰,其根本原因是由于isa
對外反饋的是類信息伐割,為了讓開發(fā)人員更加清晰明確候味,需要在isa
返回時做了一個類型強制轉(zhuǎn)換,類似于swift
中的as
的強轉(zhuǎn).源碼中isa
的強轉(zhuǎn)如下圖所示
④ 探究屬性get隔心、set方法
通過上文的分析我們知道:對于屬性name
:底層編譯會生成相應(yīng)的setter
和getter
方法,且?guī)臀覀冝D(zhuǎn)化為_name
成員變量,而對于成員變量helloName
:底層編譯不會生成相應(yīng)的setter
白群、getter
方法,且沒有轉(zhuǎn)化為_helloName
.這其中的setter
方法的實現(xiàn)依賴于runtime
中的objc_setProperty
.
接下來我們來看看objc_setProperty
的底層實現(xiàn)
-
在
objc4
源碼中全局搜索objc_setProperty
,找到objc_setProperty
的源碼實現(xiàn) -
進入
reallySetProperty
的源碼實現(xiàn)济炎,其方法的原理就是新值retain
川抡,舊值release
總結(jié):
通過對objc_setProperty
的底層源碼探索,有以下幾點說明:
objc_setProperty
方法的目的適用于關(guān)聯(lián)上層
的set
方法以及底層
的set
方法须尚,其本質(zhì)就是一個接口這么設(shè)計的原因是崖堤,上層的
set
方法有很多,如果直接調(diào)用底層set
方法耐床,會產(chǎn)生很多的臨時變量密幔,當你想查找一個sel
時,會非常麻煩基于上述原因撩轰,蘋果采用了
適配器設(shè)計模式
(即將底層接口適配為客戶端需要的接口)胯甩,對外提供一個接口,供上層的set
方法使用堪嫂,對內(nèi)調(diào)用底層的set
方法偎箫,使其相互不受影響,即無論上層怎么變皆串,下層都是不變的淹办,或者下層的變化也無法影響上層,主要是達到上下層接口隔離的目的.
下圖是上層恶复、隔離層怜森、底層之間的關(guān)系
- 外部
set
方法: 上層 - 個性化定制層(例如setName、setAge
等) -
objc_setProperty
:接口隔離層 (將外界信息轉(zhuǎn)化為對內(nèi)存地址和值的操作) -
reallySetProperty
:底層實現(xiàn)層 (賦值和內(nèi)存管理)
二谤牡、isa底層原理
在iOS之武功秘籍①:OC對象原理-上(alloc & init & new)與iOS之武功秘籍②:OC對象原理-中(內(nèi)存對齊和malloc源碼分析)中分別分析了alloc
中3核心的前兩個副硅,今天來探索initInstanceIsa
是如何將cls
與isa
關(guān)聯(lián)的.
在此之前,需要先了解什么是聯(lián)合體翅萤,為什么isa
的類型isa_t
是使用聯(lián)合體定義的.那么什么是聯(lián)合體?什么又是位域?
①. 位域
①.1 定義
有些信息在存儲時恐疲,并不需要占用一個完整的字節(jié)套么,而只需占一個或幾個二進制位.例如在存放一個開關(guān)量時流纹,只有0和1兩種狀態(tài),用1位二進位即可.為了節(jié)省存儲空間并使處理簡便违诗,C語言
提供了一種數(shù)據(jù)結(jié)構(gòu)
漱凝,稱為位域
或位段
.
所謂位域
就是把一個字節(jié)中的二進位劃分為幾個不同的區(qū)域,并說明每個區(qū)域的位數(shù).每個域有一個域名诸迟,允許在程序中按域名進行操作——這樣就可以把幾個不同的對象用一個字節(jié)的二進制位域來表示.
①.2 與結(jié)構(gòu)體比較
位域的使用與結(jié)構(gòu)體相仿茸炒,它本身也是結(jié)構(gòu)體的一種.
// 結(jié)構(gòu)體
struct TCJStruct {
// (類型說明符 元素);
char a;
int b;
} TCJStr;
// 位域
struct TCJBitArea {
// (類型說明符 位域名: 位域長度);
char a: 1;
int b: 3;
} TCJBit;
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Struct:%lu——BitArea:%lu", sizeof(TCJStr), sizeof(TCJBit));
}
return 0;
}
輸出Struct:8——BitArea:4
.
②. 聯(lián)合體
②.1 定義
當多個數(shù)據(jù)需要共享內(nèi)存或者多個數(shù)據(jù)每次只取其一時愕乎,可以利用聯(lián)合體(union)
- 聯(lián)合體是一個結(jié)構(gòu)
- 它的所有成員相對于基地址的偏移量都為0
- 此結(jié)構(gòu)空間要大到足夠容納最"寬"的成員
- 各變量是“互斥”的——共用一個內(nèi)存首地址,聯(lián)合變量可被賦予任一成員值,但每次只能賦一種值, 賦入新值則沖去舊值.
②.2 與結(jié)構(gòu)體比較
結(jié)構(gòu)體每個成員依次存儲壁公,聯(lián)合體中所有成員的偏移地址都是0感论,也就是所有成員是疊在一起的,所以在聯(lián)合體中在某一時刻紊册,只有一個成員有效——結(jié)構(gòu)體內(nèi)存大小取決于所有元素杖挣,聯(lián)合體取決于最大那個
②.3 補充知識--位運算符
在計算機語言中春瞬,除了加、減、乘路召、除等這樣的算術(shù)運算符之外還有很多運算符凤覆,這里只為大家簡單講解一下位運算符.
位運算符用來對二進制位進行操作似扔,當然租冠,操作數(shù)只能為整型和字符型數(shù)據(jù)。C
語言中六種位運算符:&
按位與遏片、|
按位或嘹害、^
按位異或、~
非吮便、<<
左移和>>
右移笔呀。
我們依舊引用上面的電燈開關(guān)論,只不過現(xiàn)在我們有兩個開關(guān):開關(guān)A和開關(guān)B髓需,1
代表開凿可,0
代表關(guān).
1)按位與&
有0出0,全1出1.
A | B | & |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
我們可以理解為在按位與運算中,兩個開關(guān)是串聯(lián)的,如果我們想要燈亮,需要兩個開關(guān)都打開燈才會亮,所以是1 & 1 = 1. 如果任意一個開關(guān)沒有打開,燈都不會亮,所以其他運算都是0.
2)按位或 |
有1出1,全0出0.
A | B | I |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 1 |
在按位或運算中,我們可以理解為兩個開關(guān)是并聯(lián)的,即一個開關(guān)開,燈就會亮.只有當兩個開關(guān)都是關(guān)的.燈才不會亮.
3)按位異或^
相同為0,不同為1.
A | B | ^ |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 0 |
4)非 ~
非運算即取反運算,在二進制中 1 變 0 授账,0 變 1。例如110101
進行非運算后為001010
惨驶,即1010
.
5)左移 <<
左移運算就是把<<
左邊的運算數(shù)的各二進位全部左移若干位白热,移動的位數(shù)即<<
右邊的數(shù)的數(shù)值,高位丟棄粗卜,低位補0.
左移n位就是乘以2的n次方.例如:a<<4
是指把a的各二進位向左移動4位.如a=00000011(十進制3)屋确,左移4位后為00110000(十進制48).
6)右移 >>
右移運算就是把>>
左邊的運算數(shù)的各二進位全部右移若干位,>>
右邊的數(shù)指定移動的位數(shù).例如:設(shè) a=15续扔,a>>2 表示把00001111右移為00000011(十進制3).
②.4 位運算符的運用
1)取值
可以利用按位與 &
運算取出指定位的值,具體操作是想取出哪一位的值就將那一位置為1,其它位都為0,然后同原數(shù)據(jù)進行按位與計算,即可取出特定的位.
例:
0000 0011
取出倒數(shù)第三位的值
// 想取出倒數(shù)第三位的值攻臀,就將倒數(shù)第三位的值置為1,其它位為0纱昧,跟原數(shù)據(jù)按位與運算
0000 0011
& 0000 0100
------------
0000 0000 // 得出按位與運算后的結(jié)果刨啸,即可拿到原數(shù)據(jù)中倒數(shù)第三位的值為0
上面的例子中,我們從0000 0011
中取值,則有0000 0011
被稱之為源碼.進行按位與操作設(shè)定的0000 0100
稱之為掩碼.
2)設(shè)值
可以通過按位或 |
運算符將某一位的值設(shè)為1或0.具體操作是:
想將某一位的值置為1的話,那么就將掩碼中對應(yīng)位的值設(shè)為1识脆,掩碼其它位為0设联,將源碼與掩碼進行按位或操作即可.
例: 將
0000 0011
倒數(shù)第三位的值改為1
// 改變倒數(shù)第三位的值善已,就將掩碼倒數(shù)第三位的值置為1,其它位為0离例,跟源碼按位或運算
0000 0011
| 0000 0100
------------
0000 0111 // 即可將源碼中倒數(shù)第三位的值改為1
想將某一位的值置為0的話换团,那么就將掩碼中對應(yīng)位的值設(shè)為0,掩碼其它位為1宫蛆,將源碼與掩碼進行按位或操作即可.
例: 將
0000 0011
倒數(shù)第二位的值改為0
// 改變倒數(shù)第二位的值艘包,就將掩碼倒數(shù)第二位的值置為0,其它位為1耀盗,跟源碼按位或運算
0000 0011
| 1111 1101
------------
0000 0001 // 即可將源碼中倒數(shù)第二位的值改為0
到這里相信大家對位運算符有了一定的了解.
③. 結(jié)構(gòu)體位域與聯(lián)合體的使用
我們來看下面的??:我們聲明一個TCJCar
類,類中有四個BOOL
類型的屬性,分別為front
想虎、back
、left
袍冷、right
,通過這四個屬性來判斷這輛小車的行駛方向.
然后我們來查看一下這個TCJCar
類對象所占據(jù)的內(nèi)存大小:
我們看到,一個TCJCar
類的對象占據(jù)16個字節(jié).其中包括一個isa
指針和四個BOOL
類型的屬性,8+1+1+1+1=12,根據(jù)內(nèi)存對齊原則,所以一個TCJCar
類的對象占16個字節(jié).
我們知道,BOOL
值只有兩種情況:0
或1
磷醋,占據(jù)一個字節(jié)的內(nèi)存空間.而一個字節(jié)的內(nèi)存空間中又有8
個二進制位,并且二進制同樣只有0
或1
胡诗,那么我們完全可以使用1個二進制位來表示一個BOOL
值.也就是說我們上面聲明的四個BOOL
值最終只使用4個二進制位就可以邓线,這樣就節(jié)省了內(nèi)存空間.那我們?nèi)绾螌崿F(xiàn)呢?
想要實現(xiàn)四個BOOL
值存放在一個字節(jié)中煌恢,我們可以通過char
類型的成員變量來實現(xiàn).char
類型占一個字節(jié)內(nèi)存空間骇陈,也就是8個二進制位.可以使用其中最后四個二進制位來存儲4個BOOL
值.
當然我們不能把char
類型寫成屬性,因為一旦寫成屬性,系統(tǒng)會自動幫我們添加成員變量,自動實現(xiàn)set
和get
方法.
@interface TCJCar(){
char _frontBackLeftRight;
}
如果我們賦值_frontBackLeftRight
為1
,即0b 0000 0001
,只使用8個二進制位中的最后4個分別用0
或者1
來代表front
、back
瑰抵、left
你雌、right
的值.那么此時front
、back
二汛、left
婿崭、right
的狀態(tài)為:
我們可以分別聲明front
、back
肴颊、left
氓栈、right
的掩碼,來方便我們進行下一步的位運算取值和賦值:
#define TCJDirectionFrontMask 0b00001000 //此二進制數(shù)對應(yīng)十進制數(shù)為 8
#define TCJDirectionBackMask 0b00000100 //此二進制數(shù)對應(yīng)十進制數(shù)為 4
#define TCJDirectionLeftMask 0b00000010 //此二進制數(shù)對應(yīng)十進制數(shù)為 2
#define TCJDirectionRightMask 0b00000001 //此二進制數(shù)對應(yīng)十進制數(shù)為 1
通過對位運算符的左移<<
和右移>>
的了解,我們可以將上面的代碼優(yōu)化成:
#define TCJDirectionFrontMask (1 << 3)
#define TCJDirectionBackMask (1 << 2)
#define TCJDirectionLeftMask (1 << 1)
#define TCJDirectionRightMask (1 << 0)
自定義的set
方法如下:
- (void)setFront:(BOOL)front
{
if (front) {// 如果需要將值置為1婿着,將源碼和掩碼進行按位或運算
_frontBackLeftRight |= TCJDirectionFrontMask;
} else {// 如果需要將值置為0 // 將源碼和按位取反后的掩碼進行按位與運算
_frontBackLeftRight &= ~TCJDirectionFrontMask;
}
}
- (void)setBack:(BOOL)back
{
if (back) {
_frontBackLeftRight |= TCJDirectionBackMask;
} else {
_frontBackLeftRight &= ~TCJDirectionBackMask;
}
}
- (void)setLeft:(BOOL)left
{
if (left) {
_frontBackLeftRight |= TCJDirectionLeftMask;
} else {
_frontBackLeftRight &= ~TCJDirectionLeftMask;
}
}
- (void)setRight:(BOOL)right
{
if (right) {
_frontBackLeftRight |= TCJDirectionRightMask;
} else {
_frontBackLeftRight &= ~TCJDirectionRightMask;
}
}
自定義的get
方法如下:
- (BOOL)isFront
{
return !!(_frontBackLeftRight & TCJDirectionFrontMask);
}
- (BOOL)isBack
{
return !!(_frontBackLeftRight & TCJDirectionBackMask);
}
- (BOOL)isLeft
{
return !!(_frontBackLeftRight & TCJDirectionLeftMask);
}
- (BOOL)isRight
{
return !!(_frontBackLeftRight & TCJDirectionRightMask);
}
此處需要注意的是授瘦,代碼中!
為邏輯運算符非,因為_frontBackLeftRight & TCJDirectionFrontMask
代碼執(zhí)行后竟宋,返回的肯定是一個整型數(shù)提完,如當front
為YES
時,說明二進制數(shù)為0b 0000 1000
丘侠,對應(yīng)的十進制數(shù)為8徒欣,那么進行一次邏輯非運算后,!(8)
的值為0
蜗字,對0
再進行一次邏輯非運算!(0)
帚称,結(jié)果就成了1
官研,那么正好跟front
為YES
對應(yīng).所以此處進行兩次邏輯非運算,!!
.
當然,還要實現(xiàn)初始化方法:
- (instancetype)init
{
self = [super init];
if (self) {
_frontBackLeftRight = 0b00001000;
}
return self;
}
通過測試驗證,我們完成了取值和賦值:③.1 使用結(jié)構(gòu)體位域優(yōu)化代碼
我們在上文講到了位域
的概念,那么我們就可以使用結(jié)構(gòu)體位域
來優(yōu)化一下我們的代碼.這樣就不用再額外聲明上面代碼中的掩碼部分了.位域聲明格式是位域名: 位域長度
.
在使用位域
的過程中需要注意以下幾點:
- 如果一個字節(jié)所蚀扯茫空間不夠存放另一位域時戏羽,應(yīng)從下一單元起存放該位域.
- 位域的長度不能大于數(shù)據(jù)類型本身的長度,比如
int
類型就不能超過32位二進位. - 位域可以無位域名楼吃,這時它只用來作填充或調(diào)整位置.無名的位域是不能使用的.
來測試看一下是否正確,這次我們將front
設(shè)為YES
始花、back
設(shè)為NO
、left
設(shè)為NO
孩锡、right
設(shè)為YES
:
依舊能完成賦值和取值.
但是代碼這樣優(yōu)化后我們?nèi)サ袅搜诖a和初始化的代碼,可讀性很差,我們繼續(xù)使用聯(lián)合體進行優(yōu)化:
③.2 使用聯(lián)合體優(yōu)化代碼
我們可以使用比較高效的位運算來進行賦值和取值酷宵,使用union
聯(lián)合體來對數(shù)據(jù)進行存儲。這樣不僅可以增加讀取效率躬窜,還可以增強代碼可讀性.
#import "TCJCar.h"
//#define TCJDirectionFrontMask 0b00001000 //此二進制數(shù)對應(yīng)十進制數(shù)為 8
//#define TCJDirectionBackMask 0b00000100 //此二進制數(shù)對應(yīng)十進制數(shù)為 4
//#define TCJDirectionLeftMask 0b00000010 //此二進制數(shù)對應(yīng)十進制數(shù)為 2
//#define TCJDirectionRightMask 0b00000001 //此二進制數(shù)對應(yīng)十進制數(shù)為 1
#define TCJDirectionFrontMask (1 << 3)
#define TCJDirectionBackMask (1 << 2)
#define TCJDirectionLeftMask (1 << 1)
#define TCJDirectionRightMask (1 << 0)
@interface TCJCar()
{
union{
char bits;
// 結(jié)構(gòu)體僅僅是為了增強代碼可讀性
struct {
char front : 1;
char back : 1;
char left : 1;
char right : 1;
};
}_frontBackLeftRight;
}
@end
@implementation TCJCar
- (instancetype)init
{
self = [super init];
if (self) {
_frontBackLeftRight.bits = 0b00001000;
}
return self;
}
- (void)setFront:(BOOL)front
{
if (front) {
_frontBackLeftRight.bits |= TCJDirectionFrontMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionFrontMask;
}
}
- (BOOL)isFront
{
return !!(_frontBackLeftRight.bits & TCJDirectionFrontMask);
}
- (void)setBack:(BOOL)back
{
if (back) {
_frontBackLeftRight.bits |= TCJDirectionBackMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionBackMask;
}
}
- (BOOL)isBack
{
return !!(_frontBackLeftRight.bits & TCJDirectionBackMask);
}
- (void)setLeft:(BOOL)left
{
if (left) {
_frontBackLeftRight.bits |= TCJDirectionLeftMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionLeftMask;
}
}
- (BOOL)isLeft
{
return !!(_frontBackLeftRight.bits & TCJDirectionLeftMask);
}
- (void)setRight:(BOOL)right
{
if (right) {
_frontBackLeftRight.bits |= TCJDirectionRightMask;
} else {
_frontBackLeftRight.bits &= ~TCJDirectionRightMask;
}
}
- (BOOL)isRight
{
return !!(_frontBackLeftRight.bits & TCJDirectionRightMask);
}
@end
來我們測試看一下是否正確,這次我們依舊將front
設(shè)為YES
浇垦、back
設(shè)為NO
、left
設(shè)為NO
荣挨、right
設(shè)為YES
:
通過結(jié)果我們看到依舊能完成賦值和取值.
這其中_frontBackLeftRight
聯(lián)合體只占用一個字節(jié),因為結(jié)構(gòu)體中front
男韧、back
、left
默垄、right
都只占一位二進制空間,所以結(jié)構(gòu)體只占一個字節(jié),而char
類型的bits
也只占一個字節(jié).他們都在聯(lián)合體中,因此共用一個字節(jié)的內(nèi)存即可.
而且我們在set
此虑、get
方法中的賦值和取值通過使用掩碼進行位運算來增加效率,整體邏輯也就很清晰了.但是如果我們在日常開發(fā)中這樣寫代碼的話,很可能會被同事打死.雖然代碼已經(jīng)很清晰了,但是整體閱讀起來還是很吃力的.我們在這里學(xué)習(xí)了位運算以及聯(lián)合體這些知識,更多的是為了方便我們閱讀OC
底層的代碼.下面我們來回到本文主題,查看一下isa_t
聯(lián)合體的源碼.
④. isa_t聯(lián)合體
通過源碼我們發(fā)現(xiàn)isa
它是一個聯(lián)合體,聯(lián)合體是一個結(jié)構(gòu)占8個字節(jié),它的特性就是共用內(nèi)存,或者說是互斥,比如說如果cls
賦值了就不在對bits
進行賦值.在isa_t
聯(lián)合體內(nèi)使用宏ISA_BITFIELD
定義了位域,我們進入位域內(nèi)查看源碼:
我們看到,在內(nèi)部分別定義了arm64
位架構(gòu)和x86_64
架構(gòu)的掩碼和位域.我們只分析arm64
為架構(gòu)下的部分內(nèi)容(真機環(huán)境下).
可以清楚的看到ISA_BITFIELD
位域的內(nèi)容以及掩碼ISA_MASK
的值:0x0000000ffffffff8ULL
.我們重點看一下uintptr_t shiftcls : 33;
,在shiftcls
中存儲著類對象和元類對象的內(nèi)存地址信息,我們上文講到,對象的isa
指針需要同ISA_MASK
經(jīng)過一次按位與運算才能得出真正的類對象地址.那么我們將ISA_MASK
的值0x0000000ffffffff8ULL
轉(zhuǎn)化為二進制數(shù)分析一下:
從圖中可以看到ISA_MASK
的值轉(zhuǎn)化為二進制中有33位都為1,上文講到按位與運算是可以取出這33位中的值.那么就說明同ISA_MASK
進行按位與運算就可以取出類對象和元類對象的內(nèi)存地址信息了.
不同架構(gòu)下isa
所占內(nèi)存均為8字節(jié)
——64位
口锭,但內(nèi)部分布有所不同朦前,arm64
架構(gòu)isa
內(nèi)部成員分布如下圖
nonpointer
:表示是否對isa
指針開啟指針優(yōu)化 ——0
:純isa指針
;1
:不止是類對象地址
鹃操,isa
中包含了類信息
韭寸、對象的引用計數(shù)
等has_assoc
:關(guān)聯(lián)對象標志位
,0
沒有荆隘,1
存在has_cxx_dtor
:該對象是否有C++
或者Objc
的析構(gòu)器恩伺,如果有析構(gòu)函數(shù),則需要做析構(gòu)邏輯, 如果沒有臭胜,則可以更快的釋放對象shiftcls
:存儲類指針的值(類的地址),即類癞尚、元類對象的內(nèi)存地址信息.在開啟指針優(yōu)化的情況下耸三,在 arm64 架構(gòu)中有 33 位用來存儲類指針magic
:用于調(diào)試器判斷當前對象是真的對象
還是沒有初始化的空間
weakly_referenced
:對象是否被指向
或者曾經(jīng)指向一個 ARC 的弱變量
,沒有弱引用的對象可以更快釋放deallocating
:標志對象是否正在釋放
內(nèi)存has_sidetable_rc
:當對象引用技術(shù)大于 10
時浇揩,則需要借用該變量存儲進位
extra_rc
:當表示該對象的引用計數(shù)值
仪壮,實際上是引用計數(shù)值減 1, 例如胳徽,如果對象的引用計數(shù)為 10
积锅,那么extra_rc
為 9爽彤。如果引用計數(shù)大于 10
, 則需要使用到下面的has_sidetable_rc
上面所說的當對象
引用技術(shù)大于 10
時,那是一個例如, 不是具體的10.
至此我們已經(jīng)對isa
指針有了新的認識缚陷,arm64
架構(gòu)之后适篙,isa
指針不單單只存儲了類對象和元類對象的內(nèi)存地址,而是使用聯(lián)合體的方式存儲了更多信息箫爷,其中shiftcls
存儲了類對象和元類對象的內(nèi)存地址嚷节,需要同ISA_MASK
進行按位與 &
運算才可以取出其內(nèi)存地址值.
⑤. isa原理探索
⑤.1 isa初始化
在之前的iOS之武功秘籍①:OC對象原理-上(alloc & init & new)一文中輕描淡寫的提了一句obj->initInstanceIsa(cls, hasCxxDtor)
—— 只知道內(nèi)部調(diào)用initIsa(cls, true, hasCxxDtor)
初始化isa
,并沒有對isa
進行細說.
⑤.2 initIsa分析
-
isa_t newisa(0)
相當于初始化isa
這個東西虎锚,newisa.
相當于給isa
賦值屬性. -
SUPPORT_INDEXED_ISA
適用于WatchOS
,isa
作為聯(lián)合體具有互斥性硫痰,而cls
、bits
是isa
的元素窜护,所以當!nonpointer=true
時對cls
進行賦值操作效斑,為false
是對bits
進行賦值操作(反正都是一家人,共用一塊內(nèi)存地址).
⑤.3 驗證isa指針 位域(0-64)
根據(jù)前文提及的0-64
位域柱徙,可以在這里通過initIsa
方法證明isa
指針中有這些位域(目前是處于macOS缓屠,所以使用的是x86_64).
- 首先通過
main
中的TCJPerson
斷點 -->initInstanceIsa
-->initIsa
-->isa_t newisa(0)
完成isa
初始化. - 執(zhí)行
LLDB
指令:p newisa
,得到newisa
的詳細信息 - 繼續(xù)往下執(zhí)行,走到
newisa.bits = ISA_MAGIC_VALUE;
下一行坐搔,表示為isa
的bits
成員賦值藏研,重新執(zhí)行LLDB
命令p newisa
,得到的結(jié)果如下
通過與前一個newsize
的信息對比概行,發(fā)現(xiàn)isa
指針中有一些變化蠢挡,如下圖所示
- 其中
magic
是59是由于將isa
指針地址轉(zhuǎn)換為二進制,從47(因為前面有4個位域凳忙,共占用47位业踏,地址是從0開始)位開始讀取6位,再轉(zhuǎn)換為十進制涧卵,如下圖所示
⑥. isa與類的關(guān)聯(lián)
cls
與 isa
關(guān)聯(lián)原理
就是isa
指針中的shiftcls
位域中存儲了類信息
勤家,其中initInstanceIsa
的過程是將 calloc
返回的指針 和當前的 類cls
關(guān)聯(lián)起來,有以下幾種驗證方式:
- 【方式一】通過
initIsa
方法中的newisa.setClass(cls, this);
方法里面的shiftcls = (uintptr_t)newCls >> 3
驗證 - 【方式二】通過
isa
指針地址與ISA_MSAK
的值&
來驗證 - 【方式三】通過
runtime
的方法object_getClass
驗證 - 【方式四】通過
位運算
驗證
方式一:通過 initIsa 方法
-
運行至
newisa.setClass(cls, this);
方法中shiftcls = (uintptr_t)newCls >> 3;
前一步柳恐,其中shiftcls
存儲當前類的值信息
- 此時查看
cls
伐脖,是TCJPerson
類 -
shiftcls
賦值的邏輯是將TCJPerson
進行編碼后,右移3位
- 此時查看
-
執(zhí)行
LLDB
命令p (uintptr_t)cls
乐设,結(jié)果為(uintptr_t) $2 = 4295000336
,再右移三位
讼庇,有以下兩種方式(任選其一),將得到536875042
存儲到newisa
的shiftcls
中p (uintptr_t)cls >> 3
- 通過上一步的結(jié)果
$2
近尚,執(zhí)行LLDB
命令p $2 >> 3
-
繼續(xù)執(zhí)行程序到
isa = newisa;
部分蠕啄,此時執(zhí)行p newisa
與bits
賦值結(jié)果的對比,bits
的位域中有兩處變化
-
cls
由默認值,變成了TCJPerson
歼跟,將isa
與cls
完美關(guān)聯(lián) -
shiftcls
由0
變成了536875042
所以isa
中通過初始化后的成員的值變化過程和媳,如下圖所示
為什么在shiftcls賦值時需要類型強轉(zhuǎn)?
因為內(nèi)存
的存儲不能存儲字符串
哈街,機器碼
只能識別 0 留瞳、1
這兩種數(shù)字,所以需要將其轉(zhuǎn)換為uintptr_t
數(shù)據(jù)類型叹卷,這樣shiftcls
中存儲的類信息
才能被機器碼理解
撼港, 其中uintptr_t
是long
類型.
為什么需要右移3位?
主要是由于shiftcls
處于isa
指針地址的中間
部分骤竹,前面還有3個
位域帝牡,為了不影響前面的3個
位域的數(shù)據(jù),需要右移
將其抹零
.
方式二:通過 isa & ISA_MSAK
方式三:通過 object_getClass
通過查看object_getClass
的源碼實現(xiàn)蒙揣,同樣可以驗證isa
與類關(guān)聯(lián)的原理靶溜,有以下幾步:
-
main.m
中導(dǎo)入#import <objc/runtime.h>
- 通過
runtime
的api
,即object_getClass
函數(shù)獲取類信息
object_getClass(<#id _Nullable obj#>)
-
查看
object_getClass
函數(shù) 源碼的實現(xiàn) -
點擊進入
object_getClass
底層實現(xiàn) -
進入
getIsa
的源碼實現(xiàn) -
點擊
ISA()
懒震,進入源碼罩息,在點擊getDecodedClass
-
接著點擊
getClass
這與方式二中的原理是一致的,獲得當前的類信息,從這里也可以得出 cls 與 isa 已經(jīng)完美關(guān)聯(lián)
方式四:通過位運算
-
回到
_class_createInstanceFromZone
方法.通過x/4gx obj
得到obj
的存儲信息个扰,當前類的信息存儲在isa
指針中瓷炮,且isa
中的shiftcls
此時占44位
(因為處于macOS
環(huán)境) -
想要讀取中間的
44位
類信息,就需要經(jīng)過位運算
递宅,將右邊3位
娘香,和左邊除去44位以外的部分
都抹零
,其相對位置是不變的.其位運算過程如圖所示办龄,其中shiftcls即為需要讀取的類信息- 將
isa
地址右移3
位:p/x 0x011d800100008111 >> 3
烘绽,得到0x0023b00020001022
- 在將得到的
0x0023b00020001022``左移20
位:p/x 0x0023b00020001022 << 20
,得到0x0002000102200000
- 為什么是
左移20
位?因為先右移了3位
俐填,相當于向右偏移了3位
安接,而左邊
需要抹零
的位數(shù)有17位
,所以一共需要移動20位
- 將得到的
0x0002000041d00000
再右移17位
:p/x 0x0002000102200000 >> 17
得到新的0x0000000100008110
- 將
-
獲取
cls
的地址 與 上面的進行驗證 :p/x cls
也得出0x0000000100008110
英融,所以由此可以證明cls
與isa
是關(guān)聯(lián)的.
三盏檐、isa走位分析
③.1 類在內(nèi)存中只會存在一份
我們都知道對象可以創(chuàng)建多個,那么類是否也可以創(chuàng)建多個呢? 答案是一個.怎么驗證它呢? 來我們看下面代碼及打印結(jié)果:通過運行結(jié)果證明了類在內(nèi)存中只會存在一份.
③.2.1 通過對象/類查看isa走向
類
其實和實例對象
一樣,都是由上級實例化出來的——類的上級叫做元類
.
我們先用p/x
打印類的內(nèi)存地址驶悟,再用x/4gx
打印內(nèi)存結(jié)構(gòu)取到對應(yīng)的isa
胡野,再用& ISA_MASK
進行偏移得到isa
指向的上級(等同于object_getClass
)依次循環(huán).
①打印TCJPerson類
取得isa
②由TCJPerson類
進行偏移得到TCJPerson元類
指針,打印TCJPerson元類
取得isa
③由TCJPerson元類
進行偏移得到NSObject根元類
指針撩银,打印NSObject根元類
取得isa
④由NSObject根元類
進行偏移得到NSObject根元類
本身指針
⑤打印NSObject根類
取得isa
⑥由NSObject根類
進行偏移得到NSObject根元類
指針
結(jié)論:
①實例對象-> 類對象 -> 元類 -> 根元類 -> 根元類(本身)
②NSObject(根類) -> 根元類 -> 根元類(本身)
③指向根元類的isa都是一樣的
③.2.2 通過NSObject查看isa走向
因為是NSObject
(根類)它的元類就是根元類
——輸出可得根元類指向自己
③.2.3 證明類给涕、元類是系統(tǒng)創(chuàng)建的
①運行時偽證法
在main
之前TCJPerson類
和TCJPerson元類
已經(jīng)存在在內(nèi)存中豺憔,不過此時程序已經(jīng)在運行了额获,并沒有什么說服力.
②查看MachO文件法
編譯項目后,使用MachoView打開程序二進制可執(zhí)行文件查看:
結(jié)論:
- 對象是程序員(猿)根據(jù)類實例化來的
- 類是代碼編寫的够庙,內(nèi)存中只有一份,是系統(tǒng)創(chuàng)建的
- 元類是系統(tǒng)編譯時抄邀,系統(tǒng)編譯器創(chuàng)建的耘眨,便于方法的編譯
③.3 isa走位圖
我們對上圖進行總結(jié)一波:圖中實線是 super_class
指針,它代表著繼承鏈的關(guān)系.虛線是isa
指針.
isa走位(虛線):實例對象-> 類對象 -> 元類 -> 根元類 -> 根元類(本身)
繼承關(guān)系(實線):NSObject父類為nil,根元類的父類為NSObject
1.Root class (class)
其實就是NSObject
境肾,NSObject
是沒有超類的剔难,所以Root class(class)
的superclass
指向nil
(NSObject
父類是nil
).
2.每個Class
都有一個isa指針指向唯一的Meta class
.
3.Root class(meta)
的superclass
指向Root class(class)
,也就是NSObject
奥喻,形成一個回路.這說明Root class(meta)
是繼承至Root class(class)
(根元類的父類是NSObject
).
4.每個Meta class
的isa
指針都指向Root class (meta)
-
instance
對象的isa
指向class
對象 -
class
對象的isa
指向meta-class
對象 -
meta-class
對象的isa
指向基類的meta-class
對象
寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.