對象在alloc的時候有重要三步。
計算需要開辟的內存空間大小
開辟指定大小的空間
將對象與isa指針關聯起來
本文重點分析,如何將對象與isa指針關聯起來
。
OC對象的本質
在探索對象與isa關聯之前,需要了解到底什么是OC對象。那如何探索OC對象呢?
探索前準備
在探索OC對象的本質之前,先了解一個編譯器:Clang
。
Clang
是一個由Apple主導編寫
的,基于LLVM
的C/C++/OC的編譯器
。Clang
主要用于底層編譯,可以將OC文件
轉化為C++文件
,這使得我們可以更好的觀察底層的結構
及實現邏輯
。常用的Clang編譯指令
//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/iPhoneSimulator14.0.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
開始探索
- 自定義一個類LGPerson,為了方便觀察,在自定義類中增加一個屬性name。
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation LGPerson
@end
- 在main函數中定義一個LGPerson對象。
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *person = [LGPerson alloc];
NSLog(@"%@",person);
}
return 0;
}
- 使用clang將main函數編譯成c++文件
clang -rewrite-objc main.m -o main.cpp
- 打開編譯好的main.cpp,找到LGPerson的定義
//NSObject的定義
typedef struct objc_class *Class;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
//NSObject 的底層編譯
struct NSObject_IMPL {
Class isa;
};
//LGPerson的底層編譯
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 等效于 Class isa;
NSString *_name;
};
由上面的代碼可以看到,LGPerson
在底層會被編譯成結構體
。
-
LGPerson_IMPL
中的第一個成員是一個嵌套的結構體NSObject_IMPL
,這個結構體的成員只有一個,那就是isa
。 -
LGPerson_IMPL
中的第二個成員就是定義的屬性name。
總結
對象的本質
是一個含有isa的結構體
。isa
是Class類型
。Class
是一個objc_class結構體
類型的指針
。
探索isa
object_class結構在Object.mm中。定義如下:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
struct objc_object {
private:
isa_t isa;
}
由源碼可知,objc_class
結構體繼承于objc_object
結構體,,而objc_object
中只有一個私有成員isa_t isa
。
接下來我們一項一項分析。
isa_t isa
在arm64之前
,isa
就是一個普通的指針
,只存儲類對象
、元類對象
的指針
,但arm64之后isa做了優化
,采用了聯合體
的形式,這使得8字節
的內存可以存儲更多的內容。
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
#endif
};
其中ISA_BITFIELD
對8字節內存的位域進行了宏定義,方便不同架構下代碼統一。
接下來以arm64架構對isa的位域進行說明
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
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
}
};
-
nonpointer(
bit0
):是否對isa指針開啟指針優化
。- 0 - 純isa指針;
- 1 - 不止是類對象地址,還包含了類信息、對象引用計數等。
-
has_assoc(
bit1
):關聯對象標志位
;- 0 - 沒有關聯對象;
- 1 - 存在關聯對象;
-
has_cxx_dtor(
bit2
):該對象是否有C++或者Objc的析構器,如果有則做析構邏輯,如果沒有則可以更快的釋放對象。- 0 - 沒有C++或者Objc的析構器;
- 1 - 有C++或者Objc的的析構器;
shiftcls(
bit3 - bit35
):存儲類的指針
,開啟指針優化的情況下在arm64架構
下有33位
來存儲類的指針
。magic(
bit36 - bit41
):判斷當前對象是否初始化完成
。調試器用來判斷當前對象是真的對象還是沒有初始化的空間。weakly_referenced(
bit42
):對象被指向或者曾經指向一個 ARC 的弱變量,沒有弱引用的對象可以更快釋放(dealloc的底層代碼有體現)deallocating(
bit43
):標志對象是否正在釋放內存
。has_sidetable_rc(
bit44
):判斷該對象的引用計數
是否過大
,如果過大則需要其他散列表
來進行存儲。extra_rc(
bit45 - bit63
):存放該對象的引用計數值減1后的結果
。對象的引用計數超過 1,會存在這個里面,如果引用計數為 10,extra_rc 的值就為 9。
initIsa
了解了isa的結構之后,接下來看一下是如何將isa與類關聯起來的。
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
}
這個函數主要做三件事:
定義一個newisa結構體,并初始化為全0
向newisa結構體中的各位賦值
把newisa賦值給objc_object的成員變量
這個過程最重要的就是第二步,向newisa結構體中的各位賦值
。
-
newisa.bits = ISA_MAGIC_VALUE;
將newisa.bits
初始化為ISA_MAGIC_VALUE
。
而ISA_MAGIC_VALUE
被宏定義為0x000001a000000001ULL
。轉換為二進制就是
ISA_MAGIC_VALUE
從這個對應著isa_t的64個位域,可以看到這是對nonpointer
和magic
賦值。
newisa.has_cxx_dtor = hasCxxDtor;
對has_cxx_dtor賦值newisa.shiftcls = (uintptr_t)cls >> 3
將cls右移3位
,然后賦值給newisa.shiftcls
。
重點來了,這里為什么要右移3位。
shiftcls為什么是(uintptr_t)cls >> 3
cls是Class類型,定義如下:
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
// 省略 ...
}
再繼續看cache_t
、class_data_bits_t
發現都是結構體,然后看里面的成員變量,大部分都是uintptr_t
類型的,查看定義
typedef unsigned long uintptr_t;
根據內存對齊原則
,可知Class
肯定是8字節對齊
的,同樣的,cls
的指向地址(也既開始地址)肯定是8
的倍數,轉換成二進制后
,低三位
肯定是000
。
再聯想聯合體的說明,共用內存
,可見蘋果設計優化節省內存
的良苦用心。
賦值shiftcls
的時候既沒有改變cls
的值,也最大的優化了內存使用
。
至此,isa與類就關聯起來,接下來就來驗證了。
驗證之前分析
定義一個LGPerson對象
,然后再進入到initIsa函數
中,斷點停下。
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
ASSERT(!isTaggedPointer());
if (!nonpointer) {
isa = isa_t((uintptr_t)cls);
} else {
ASSERT(!DisableNonpointerIsa);
ASSERT(!cls->instancesRequireRawIsa());
isa_t newisa(0);
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
isa = newisa;
}
}
- 當執行
isa_t newisa(0);
時,通過lldb調試輸出newisa的值。
(lldb) p newisa
(isa_t) $1 = {
cls = nil
bits = 0
= {
nonpointer = 0
has_assoc = 0
has_cxx_dtor = 0
shiftcls = 0
magic = 0
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
此時newisa
內容為全為0;
- 當執行完位域賦值后再查看
newisa
的值
(lldb) p newisa
(isa_t) $2 = {
cls = LGPerson
bits = 8303516107965037
= {
nonpointer = 1
has_assoc = 0
has_cxx_dtor = 1
shiftcls = 536875085
magic = 59
weakly_referenced = 0
deallocating = 0
has_sidetable_rc = 0
extra_rc = 0
}
}
此時可以看到,isa
的shiftcls
已經與類關聯起來了。
當isa
的shiftcls
已經與類關聯起來之后,回到obj->initInstanceIsa(cls, hasCxxDtor);
這里。
通過lldb輸出obj的內容
(lldb) x/4gx obj
0x101233f30: 0x001d80010000826d 0x0000000000000000
0x101233f40: 0x0000000000000000 0x0000000000000000
根據我們的分析,對象是結構體,且第一個成員為isa
。此時0x001d80010000826d
就是isa
。
如何驗證該isa就是LGPerson類
呢?
我們前面分析isa的shiftcls就是類
,那如何取出shiftcls
的值呢?
這里仍然是前面的分析,shiftcls
是isa
的bit3 - bit35
。
接下來通過lldb取出isa
的bit3 - bit35
。
//兩種方式取出bit3 - bit35
//1. 通過移位的方式
(lldb) p 0x001d80010000826d >> 3
(long) $14 = 1037939513495629
(lldb) p 1037939513495629 << 30
(long) $15 = 576465233028055040
(lldb) p 576465233028055040 >> 27
(long) $16 = 4295000680
(lldb) po 4295000680
LGPerson
//2. 通過掩碼的方式
(lldb) po 0x001d80010000826d & 0x0000000ffffffff8ULL
LGPerson
果然,此時isa
的bit3 - bit35
存的就是LGPerson類信息。這也驗證了我們之前的分析。
疑問
前面提到newisa.shiftcls = (uintptr_t)cls >> 3;
,即將cls右移了3位
之后再賦值給shiftcls
。那為何取出來的時候直接是Class
而不需要左移3位
呢?
解答
前面有講到,為什么
右移3位
?
答:因為Class是8字節對齊
,cls
的指向地址
(也既開始地址)肯定是8的倍數
,轉換為二進制
后后3位就是0
,出于優化的考慮
,在賦值的時候無需將無用的后3位也賦值過去。賦值之后如何取?
我們來看蘋果是如何取出Class
的。
objc_object::ISA()
{
ASSERT(!isTaggedPointer());
return (Class)(isa.bits & ISA_MASK);
}
這里直接將isa.bits
與掩碼ISA_MASK
進行與操作
后,強轉成Class
類型。
為什么這樣就可以取出Class
呢?
答:因為進行與操作
可以直接將isa.bits
除bit3 - bit35
保留外,其余的位都置0
,此時就相當于bit3 - bit35
為Class
右移3位的值,而bit0 - bit2
為Class
的后三位。這樣就與Class
結構對應了。因此可以通過強轉獲得Class
信息。