isa
的做用 , 實際數據結構 , 其中不一樣二進制位存儲內容說明 , 包括 isa
優化 , 是否爲 TaggedPoint
.isa
的指向 , 以及 SuperClass
的指向探索 .
isa
是咱們能把底層知識點串聯起來最爲關鍵的一條引線 . 經過本篇文章探索 , 對於對象的本質有更深層次的理解 .面試
Objective-C
是一門面向對象的編程語言。每一個對象都是其 類 的實例 , 被稱爲實例對象 . 每個對象都有一個名爲 isa
的指針,指向該對象的類。編程
新建 Command Line
工程 , 新建一個類 LBObject
, 寫一個屬性一個成員變量 , clang
編譯.bash
clang -rewrite-objc main.m -o main.cpp
複製代碼
打開 main.cpp
. 咱們看到以下 :數據結構
typedef struct objc_object LBPerson;
typedef struct {} _objc_exc_LBPerson;
#endif
extern "C" unsigned long OBJC_IVAR_$_LBPerson$_name;
struct LBPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *name;
NSString *_name;
};
struct NSObject_IMPL {
Class isa;
};
複製代碼
能夠看到 , 類其實就是一個包含 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
}
複製代碼
今後能夠得知 , 類也是一個對象 , 咱們稱之爲類對象 .ide
源碼以下 :函數
typedef struct objc_class *Class;
/// A pointer to an instance of a class.
typedef struct objc_object *id;
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
/**/
}
/// Represents an instance of a class.
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
/*...*/
}
複製代碼
是否是有點傻傻分不清了 . 梳理一下 :oop
其指示關係以下圖 .post
圖片引用自 : iOS 內存管理在最初的時候 , isa
其實就是一個指針 , 起到指向的做用 , 將對象 , 類 , 以及元類鏈接起來 , 後來蘋果針對其進行了優化 , 採用 聯合體 + 位域 的方式來節省內存 與存儲更多內容 .
先來看下 對象的 getIsa
方法 :
#if SUPPORT_TAGGED_POINTERS
inline Class objc_object::getIsa()
{
if (!isTaggedPointer()) return ISA();
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
複製代碼
這裏引出一個概念 , TaggedPointer
.
64
位機器也就是 iPhone 5S
時 , 指針對象佔用 8
字節內存 .NSNumber
對象 , 也會佔用 8
字節內存 , 而 32
位機器下佔用 4
字節 .64
位機器下時 , 會形成很大空間浪費 .所以 , 爲了節省內存和提升執行效率,蘋果提出了
Tagged Pointer
的概念。對於64
位程序,引入Tagged Pointer
後,相關邏輯能減小一半的內存佔用,以及 3 倍的訪問速度提高,100 倍的建立、銷燬速度提高。
爲了存儲和訪問一個 NSNumber
對象,咱們須要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期 。這些都給程序增長了額外的邏輯,形成運行效率上的損失 。
因爲
NSNumber
、NSDate
一類的變量自己的值須要佔用的內存大小經常不須要 8 個字節,拿整數來講,4 個字節所能表示的有符號整數就能夠達到 20 多億 (注:2^31=2147483648,另外 1 位做爲符號位),對於絕大多數狀況都是能夠處理的。
所以蘋果將一個對象的指針拆成兩部分,一部分直接保存數據,另外一部分做爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。因此,引入了 Tagged Pointer
對象以後 , 對象的指針其實再也不是傳統意義上的指針 .
Tagged Pointer
特色:
Tagged Pointer
專門用來存儲小的對象,例如 NSNumber
和 NSDate
Tagged Pointer
指針的值再也不是地址了,而是真正的值。因此,實際上它再也不是一個對象了,它只是一個披着對象皮的普通變量而已。因此,它的內存並不存儲在堆中,也不須要 malloc
和 free
objc_msgSend
能識別 Tagged Pointer
,好比 NSNumber
的 intValue
方法,直接從指針提取數據 )Tag
+ Data
,也就是將數據直接存儲在了指針中 .那麼回到 getIsa
方法中 , 當爲對象類型時 , 很明顯是非 isTaggedPointer
. 直接來到 ISA() ;
#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
}
複製代碼
這裏又看到一個 NONPOINTER_ISA
與 INDEXED_ISA
. INDEXED_ISA
源碼以下 :
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
複製代碼
也就是說 64 位機器下爲 1 . 那麼咱們來講一說 NONPOINTER_ISA
.
咱們已經知道對象的 isa
指針,是用來代表對象所屬的類類型。 可是若是isa指針僅表示類型的話,對內存顯然也是一個極大的浪費。
因而,就像
Tagged Pointer
同樣,對於isa
指針,蘋果一樣進行了優化。isa
指針表示的內容變得更爲豐富,除了代表對象屬於哪一個類以外,還附加了引用計數extra_rc
,是否有被weak
引用標誌位weakly_referenced
,是否有附加對象標誌位has_assoc
等信息 , 使用的就是咱們剛剛提到的 聯合體 + 位域 的數據結構 .
而 nonpointer
就是是否進行優化的標識 , 優化以後的聯合體中存儲了是否優化的標識在其中的 struct
的第一個二進制位中 . ( 下面咱們會仔細講述 ) .
那麼接下來 , 終於進入到 isa_t
的結構了 .
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
數據結構其實爲 isa_t
, 是一個聯合體 ( 或者叫共用體 ,union
) .
其中 ISA_BITFIELD
宏定義在不一樣架構下表示以下 :
# if __arm64__
# 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
# elif __x86_64__
# 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
複製代碼
首先看到 isa_t
是一個聯合體的數據結構 , 聯合體意味着公用內存 , 也就是說 isa
其實總共仍是佔用 8 個字節內存 , 共 64 個二進制位 .
而上述不一樣架構的宏定義中定義的位域就是 64 個二進制位中 , 每一個位置存儲的是什麼內容 .
- 因爲聯合體的特性 ,
cls
,bits
以及struct
都是 8 字節內存 , 也就是說他們在內存中是徹底重疊的 .- 實際上在
runtime
中,任何對struct
的操做和獲取某些值,如extra_rc
,實際上都是經過對bits
作位運算實現的。bits
和struct
的關係能夠看作 :bits
向外提供了操做struct
的接口,而struct
自己則說明了bits
中各個二進制位的定義。
以獲取有無關聯對象來舉例 :
能夠直接使用
isa.has_assoc
, 也就是點語法直接訪問bits
中第二個二進制位中的數據 . ( arm 64 架構中 )
所以 , bits
與 struct
的關係理解清楚之後 , 咱們 isa
其實就有兩種狀況 , cls
或者是 bits
, 也就是咱們剛剛所提到的 nonPointer_isa
與否 , 兩種狀況完美驗證 . 以下圖 :
arm64
架構下 ,
ISA_BITFIELD
. 咱們來看看每一個字段都存儲了什麼內容 , 以便更深入的理解對象的本質 .
# 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
複製代碼
-成員- | 位 | 含義 |
---|---|---|
nonpointer | 1bit | 標誌位 - 1 ( 奇數 )表示開啓了isa優化,0 ( 偶數 ) 表示沒有啓用isa優化 |
has_assoc | 1bit | 標誌位 - 代表對象是否有關聯對象。沒有關聯對象的對象釋放的更快 , 關聯對象能夠參考 Category底層原理 |
has_cxx_dtor | 1bit | 標誌位 - 代表對象是否有C++或ARC析構函數。沒有析構函數的對象釋放的更快 , 參考 OC 對象的建立流程 中有詳細敘述對象釋放完整流程 |
shiftcls | 33bit | 存儲類指針的值。開啓指針優化的狀況下,在 arm64 架構中有 33 位用來存儲類指針。 |
magic | 6bit | 用於調試器判斷當前對象是真的對象仍是沒有初始化的空間 , 固定爲 0x1a |
weakly_referenced | 1bit | 標誌位 - 用於表示該對象是否被別ARC對象弱引用或者引用過。沒有被弱引用的對象釋放的更快 |
deallocating | 1bit | 標誌位 - 用於表示該對象是否正在被釋放 |
has_sidetable_rc | 1bit | 標誌位 - 用於標識是否當前的引用計數過大 ( 大於 10 ) ,沒法在 isa 中存儲,則須要借用sidetable來存儲 |
extra_rc | 19bit | 其實是對象的引用計數減 1 . 好比,一個 object 對象的引用計數爲7,則此時 extra_rc 的值爲 6 |
以上就是 arm64
架構下 isa 每個位置所存儲的內容 , x86
架構下存儲數據不變 , 只是佔據位有所不一樣 , 就不重複講述了 .
那咱們接下來以 extra_rc
爲例來探索一下其存儲和獲取的過程 .
首先再來看一下這張圖 , 咱們如何能拿到 isa_t
NSObject * obj = [[NSOibect alloc] init];
複製代碼
分析過程 :
- 1️⃣ : 那麼
obj
就是一個NSObject *
類型 . 而NSObject
是一個Class isa
, 也就是objc_class *
- 2️⃣ : 換句話說 ,
obj == objc_class **
. 而objc_class
繼承於objc_object
. 也就是說objc_class
的首地址其實就是objc_object
- 3️⃣ : 因爲
objc_object
內部就是一個isa_t
. 所以obj == objc_class **
能夠替換成obj == objc_class **
- 4️⃣ : 當
isa
開啓優化時 , 也就是說isa
再也不是一個指針 , 而是咱們以前講的聯合體 . 所以改成obj == isa_t *
綜上 , 咱們獲得結論 , obj
就是一個 指向 isa_t
的指針 .
接下來咱們來實際演練一遍 .
注意:
arm64
下 isa_t
中後 19 位爲 extra_rc
.extra_rc
實際存儲爲引用計數減 1 .@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *obj =[NSObject alloc];
NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj);
}
複製代碼
注意使用真機 , 使用 arm64
架構下的 bits
.
實際上 obj
對象此時引用計數爲 1 , 預期一致 .
接下來修改代碼以下 :
@interface ViewController ()
@property(nonatomic, strong) NSObject *obj1;
@property(nonatomic, strong) NSObject *obj2;
@property(nonatomic, weak) NSObject *weakRefObj;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *obj =[NSObject alloc];
_obj1 = obj;
NSObject *tempObj = obj;
NSLog(@"isa_t = %p", *(void **)(__bridge void*)obj);
}
複製代碼
引用計數顯示爲 2 , 實際爲 3 - 1 = 2
. 符合預期 .
最後添加代碼以下 :
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
NSLog(@"3. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
_obj2 = _obj1;
NSLog(@"4. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
_weakRefObj = _obj1;
NSLog(@"5. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
NSObject *attachObj = [[NSObject alloc] init];
objc_setAssociatedObject(_obj1, "attachKey", attachObj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
NSLog(@"6. obj isa_t = %p", *(void **)(__bridge void*)_obj1);
}
複製代碼
分別在 NSLog
斷點打印並查看 isa_t
結果以下 .
引用計數 , 弱引用標識 , 關聯對象標識 均符合預期 .
最後 , 來看下實際 isa
指向內容 ( 在 isa_t
3 - 36 位中存儲爲 shiftcls
指針 , 至關於 isa
未優化時的 NONPOINTER_ISA
指針 )
isa 在 lldb 中 , 獲取 isa_t 中某一段位置的數據 , 直接經過 宏定義中提供好的 mask 能夠快速獲取指定位置存儲的值 .
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
複製代碼
經過以上探索 , 咱們很清楚 runtime
中 isa
的具體結構 . 那麼接下來 , 咱們來探索一下 isa
的指向 , 這個問題也已是面試常客的地位了 .
在探索 isa
指向以前 , 咱們須要知道這個問題 :
class
、objc_getClass
、object_getclass
方法有什麼區別 ?
( 這個問題來自 阿里、字節:一套高效的iOS面試題 這篇文章 , 恰好在寫探索 isa
文章 , 就一塊兒來解答一下 ) .
先上源碼 .
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
複製代碼
Class object_getClass(id obj) {
if (obj) return obj->getIsa();
else return Nil;
}
Class objc_getClass(const char *aClassName) {
if (!aClassName) return Nil;
// NO unconnected, YES class handler
return look_up_class(aClassName, NO, YES);
}
複製代碼
getIsa
方法上面粘貼過 , 咱們就不沾了 . 其實就是獲取 isa
指向 .
1️⃣、 class
方法 .
isa
指向也就是類對象 ( 也就是 - class
) .+ class
) .2️⃣、object_getClass
方法.
object_getClass
其實就是獲取 isa
指向 .
也就是說 當須要獲取元類時 , 則須要使用類對象調用 object_getClass
.
寫法以下 :
void test(){
NSObject *obj =[NSObject alloc];
// NSObject類
Class class = object_getClass(obj);
// NSObject元類
Class metaClass = object_getClass(class);
}
複製代碼
3️⃣、objc_getClass
方法
寫法以下 :
Class objcClass = objc_getClass("NSObject");
複製代碼
瞭解了這幾個函數之後 , 咱們就開始探索 isa
的指向了 .
提示 : ( 在探索前 , 請對 OC類對象/實例對象/元類 有詳細瞭解 )
isa 走位流程圖 , 圖片引用自官方文檔
新建一個 LBSuperClass
類繼承於 NSObject
, 一個 LBSubClass
繼承於 LBSuperClass
, 建立代碼以下 .
NSObject * object = [NSObject alloc];
LBSuperClass * superClass = [LBSuperClass alloc];
LBSubClass * subClass = [LBSubClass alloc];
複製代碼
三個對象建立完加斷點 , 運行代碼.
提示 :
lldb
命令
x/4g
表明打印對象首地址開始連續 4 個 8 字節內存地址內容 .X/5g
,x/6g
以此類推 , 就免去由於小端模式x
打印必須從右往左讀的問題 .
p/t
、p/o
、p/d
、p/x
分別表明二進制、八進制、十進制和十六進制打印 .經過前面探索 , 咱們知道對象首地址前 8 個字節 , 其實就是
isa
.要使用真機跑 , 模擬器不開啓
isa
優化 .
調試結果以下 :
結論 1️⃣ : 實例對象的 isa
指向類對象 .
結論 2️⃣ : 類對象的 isa
指向元類對象 . ( 注意 , 元類對象的地址與類對象不一樣 )
結論 3️⃣ : 根元類 ( NSObject 的元類 ) 指向本身 .
結論 4️⃣ : 元類的 isa
指向根源類 .
提示 :
對象中第 8 - 16 位置存儲的是 superClass
指針地址 .
struct objc_class : objc_object {
// Class ISA; 繼承與父類 objc_object
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
}
複製代碼
結論 1️⃣ : 子類繼承父類 ( 好像等於沒說 , 忽略.. ) .
結論 2️⃣ : NSObject
的元類 ( 根源類 ) 的父類指向 NSObject
類對象 .
完美驗證了上述 isa
流程指示圖 .
isa
是鏈接實例對象 , 類與元類的重要橋樑 .isa
在 64 位機器開始引入TaggedPointer
, 與isa
的優化 (NONPOINTER_ISA
) , 使用聯合體 + 位域的模式 , 來存儲更多內容 , 取值方式也變成使用掩碼 mask 位運算獲取真實cls
.isa
指向 :
- 實例對象的
isa
指向類對象 .- 類對象的
isa
指向元類對象 . ( 注意 , 元類對象的地址與類對象不一樣 , 名稱相同 ) .- 根元類 (
NSObject
的元類 ) 指向本身 .- 元類的
isa
指向根源類 .superClass
指向 :
- 子類
superClass
指向父類 .- 根源類
superClass
指向NSObject
類 .
至此 , isa
的相關知識點咱們已經探索完畢了 . 後續繼續更新類的結構探索 , KVC
, KVO
, RunLoop
等底層探索 , 敬請關注 .