引:
什么是對象
OC的對象、類主要是基于C\C++的結構體數據結構實現的。OC對象的本質就是結構體。
在探索本質前,我們需要了解一個編輯器:clang
Clang
clang
是一個由Apple主導編寫
,基于LLVM
的C/C++/OC的編輯器
主要是用于
底層編譯
,將一些文件輸出成C++
文件,例如main.m
輸出成main.cpp
,其目的是為了更好的觀察底層
的一些結構
及實現的邏輯
,方便理解底層原理
。
對象的本質
- 在main中定義一個LGPerson類繼承于NSObject
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSString *nickName;
@end
@implementation LGPerson
@end
- 通過終端,利用clang將main.m編譯成main.cpp。有以下4幾種編譯指令,可以根據自己的實際情況來編譯。
//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
//以下兩種方式是通過指定架構模式的命令行,使用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
- 編譯完成之后,打開main.cpp的文件,找到我們定義好的LGPerson類,發現在底層會被編譯成
struct結構體
-
LGPerson_IMPL
中的第一個屬性為NSObject_IMPL
的結構體
,通過代碼發現此結構體就是isa
,是繼承自NSObject,屬于偽繼承
,偽繼承
的方式是直接將NSObject結構體定義為LGPerson中的第一個屬性
,意味著LGPerson 擁有 NSObject中的所有成員變量
。每個類的第一個屬性都是Class isa
-
//NSObject的定義
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
//NSObject通過clang編譯的定義
struct NSObject_IMPL {
Class isa;
};
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_name;
extern "C" unsigned long OBJC_IVAR_$_LGPerson$_nickName;
// LGPerson的底層編譯結果
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
NSString *_nickName;
};
編譯后得到的結果如下圖所示:
問:isa的類型為什么會是Class?
通過之前查找源代碼找到initIsa方法,知道isa是通過isa_t類型初始化的。通過分析獲取isa是通過get方法,于是我們找到了getIsa這個方法,結果如下圖:
#if SUPPORT_NONPOINTER_ISA
inline Class
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
if (isa.nonpointer) {
uintptr_t slot = isa.indexcls;
return classForIndex((unsigned)slot);
}
return (Class)isa.bits;
#else
return (Class)(isa.bits & ISA_MASK);
#endif
}
源碼中,我們可以清楚的知道在isa返回
的時候做了一個類型強制轉換
。
union聯合體位域
結構體 struct
:各成員各自擁有自己額內存,各自使用互不干涉,同時存在的,遵循內存對齊原則。一個struct的總長度等于內部最大成員的整數倍,不足的要補齊
, 結構體(struct
)中所有變量是“共存”的——優點是“有容乃大”, 全面;缺點是struct內存空間的分配是粗放的,不管用不用,全分配。
聯合體union
:各成員共用一塊內存空間
,并且同時只有一個成員
可以得到這塊內存的使用權
(對該內存的讀寫),各變量共用一個內存首地址。因此,聯合體比結構體更加節約內存。一個union變量的總長度至少能容納最大的成員變量,而且要滿足是所有成員變量類型大小的整數倍。不允許對聯合體變量名U2直接賦值或其他操作。聯合體(union
)中是各變量是“互斥”的——缺點就是不夠“包容”; 但優點是內存使用更為精細靈活,也節省了內存空間
有時候為了節省內存占用可以使用的技術
@interface Car : NSObject
@property (nonatomic, assign) BOOL front;
@property (nonatomic, assign) BOOL left;
@property (nonatomic, assign) BOOL back;
@property (nonatomic, assign) BOOL right;
@end
@implementation Car
@end
四個 BOOL屬性占用內存為 4 字節(sizeof(BOOL)= 1), 因為每次只能選擇一個方向,所以有點內存浪費,直接用 1 bit 表示一個方向也是可以的
union direction_t {
char bits; // 1 字節
struct {
char front: 1; // 1 bit
char left: 1; // 1 bit
char back: 1; // 1 bit
char right: 1; // 1 bit
};
};
printf("size of union direction_t = %lu",sizeof(_direction));
//size of union direction_t = 1
isa的類型isa_t
從源碼中,可以看到到isa指針的類型isa_t
的定義,從定義中可以看出是通過聯合體(union)
定義的。
union isa_t {//聯合體
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
//提供了cls和bits,兩者是互斥的關系
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
isa_t
類型使用聯合體的原因
也是基于內存優化的考慮
,這里的內存優化是指在isa指針
中通過char + 位域(即二進制中每一位均可表示不同的信息)
的原理實現。通常來說,isa指針
占用的內存大小8字節
,即64位
,已經足夠存儲很多信息了,這樣可以極大的節省內存,以提高性能
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);///isa初始化
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);///isa初始化
#if SUPPORT_INDEXED_ISA /// !nonpointer的執行流程,即isa 通過cls定義
ASSERT(cls->classArrayIndex() > 0);
newisa.bits = ISA_INDEX_MAGIC_VALUE;
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else ///bits的執行流程
newisa.bits = ?ISA_MAGIC_VALUE;///bits進行賦值為0x001f800000000001ULL
// isa.magic is part of ISA_MAGIC_VALUE
// isa.nonpointer is part of ISA_MAGIC_VALUE
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
// This write must be performed in a single store in some cases
// (for example when realizing a class because other threads
// may simultaneously try to use the class).
// fixme use atomics here to guarantee single-store and to
// guarantee memory order w.r.t. the class index table
// ...but not too atomic because we don't want to hurt instantiation
isa = newisa;
}
}
-
提供了兩個成員,
cls
和bits
,由聯合體的定義所知,這兩個成員是互斥的
,也可以通過上訴的代碼可以看出cls
賦值和bit
是賦值也是互斥的
。也就意味著,當初始化isa
指針時,有兩種初始化方式通過cls初始化,bits無默認值
通過bits初始化,cls有默認值
還提供了一個結構體定義的位域,用于存儲類信息及其他信息,結構體的成員
ISA_BITFIEID
,這是一個宏定義,有兩個版本__arm64__
(對應iOS移動端)和__x86_64__
(對應macOS),以下是它們的一些宏定義如下:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; \/*是否對isa指針開啟指針優化 */
uintptr_t has_assoc : 1; \/*是否有關聯對象*/
uintptr_t has_cxx_dtor : 1; \/*是否有C++相關實現*/
uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \/*存儲類信息*/
uintptr_t magic : 6; \/*調試器判斷對象是真對象還是為初始化空間*/
uintptr_t weakly_referenced : 1; \/*對象是否被指向或者曾經指向一個ARC的弱變量 */
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; \/*是否對isa指針開啟指針優化 */
uintptr_t has_assoc : 1; \/*是否有關聯對象*/
uintptr_t has_cxx_dtor : 1; \/*是否有C++相關實現*/
uintptr_t shiftcls : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \/*存儲類信息*/
uintptr_t magic : 6; \/*調試器判斷對象是真對象還是為初始化空間*/
uintptr_t weakly_referenced : 1; \/*對象是否被指向或者曾經指向一個ARC的弱變量 */
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
nonpointer
有兩個值,表示自定義的類等,占1位。0是純isa指針
,1不只是類對象地址,isa中包含了類信息、對象的引用計數等
。has_assoc
表示關聯對象標志等,占1位。0是沒有關聯對象
,1是存在關聯對象
。has_cxx_dtor
表示該對象是否有C++/OC的析構函數(dealloc),占1位。如果有析構函數,則需要做析構邏輯
,如果沒有,則可以更快的釋放對象
。shiftcls
表示存儲類的指針的值(類地址),即類信息。arm64中占 33位
,開啟指針優化的情況下,在arm64架構中有33
位用來存儲類指針,x86_64中占 44位
magic
用于調試器判斷當前對象是真對象
還是沒有初始化空間
,占6
位weakly_referenced
是指對象是否被指向
或者曾經指向一個ARC的弱變量
。沒有弱引用對象可以更快釋放。deallocating
標志對象是否正在釋放
內存has_sidetable_rc
表示 當對象引用計數大于10時,則需要借用該變量存儲進位extra_rc(額外的引用計數)
--- 表示該對象的引用計數值,實際上是引用計數值減1
。如果對象的引用計數為10,那么extra_rc為9
isa 與 類 的關聯
cls 與 isa
關聯原理就是isa
指針中的shiftcls位域
中存儲了類
信息,其中initInstanceIsa
的過程是將calloc
指針 和當前的類cls
關聯起來,有以下幾種驗證方式:
1、通過initIsa
方法中的newisa.shiftcls = (uintptr_t)cls >> 3
;驗證
- 通過
lldb
打印賦值前后
的newisa
的過程我們發現shiftcls
在賦值的過程中有兩個值發生了變化,cls通過0x001d800000000001變成了LGPerson
,bits中的shiftcls從0變成了536871965
,將isa和cls
關聯了起來。
如結果下圖:
2、通過isa指針地址與ISA_MSAK 的值 & 來驗證
arm64
中,ISA_MASK
宏定義的值為0x0000000ffffffff8ULL
-
x86_64
中,ISA_MASK
宏定義的值為0x00007ffffffffff8ULL
首先知道ISA_MASK宏定義如上,然后回到obj->initInstanceIsa
,通過LLDB
打印結果如下:
isa指針地址& ISA_MASK的結果
3、通過位運算驗證
通過上述的一些源碼分析,我們知道isa中占有的64
位信息,而存儲類信息的shiftcls
是占33位
或者44位
,是從第4位開始
存儲。而我們的源碼是macOS
環境所以此時shiftcls占44位
。
所以我們獲取isa的值時,需要將右邊3位和左邊17位抹零,并且保證其相對位置不變。
然后通過LLDB指令驗證步驟如下圖: