iOS 底層 - isa 的前世此生

一、前言概述

  • 本篇文章首先講述 isa 的做用 , 實際數據結構 , 其中不一樣二進制位存儲內容說明 , 包括 isa 優化 , 是否爲 TaggedPoint .
  • 而後以引用計數爲例實際探索 .
  • 最後講述 isa 的指向 , 以及 SuperClass 的指向探索 .
  • 其中穿插了一些面試題以及涉及到的知識點 .

isa 是咱們能把底層知識點串聯起來最爲關鍵的一條引線 . 經過本篇文章探索 , 對於對象的本質有更深層次的理解 .面試

二、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 其實就是一個指針 , 起到指向的做用 , 將對象 , 類 , 以及元類鏈接起來 , 後來蘋果針對其進行了優化 , 採用 聯合體 + 位域 的方式來節省內存 與存儲更多內容 .

② 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 .

TaggedPointer

參考自 唐巧 - 深刻理解 Tagged Pointer

  • 在開始使用 64 位機器也就是 iPhone 5S 時 , 指針對象佔用 8 字節內存 .
  • 也就是說當咱們存儲基礎數據類型 , 底層封裝成 NSNumber 對象 , 也會佔用 8 字節內存 , 而 32 位機器下佔用 4 字節 .
  • 所以若是沒有額外處理 , 在遷移到 64 位機器下時 , 會形成很大空間浪費 .

所以 , 爲了節省內存和提升執行效率,蘋果提出了 Tagged Pointer 的概念。對於 64 位程序,引入 Tagged Pointer 後,相關邏輯能減小一半的內存佔用,以及 3 倍的訪問速度提高,100 倍的建立、銷燬速度提高。

未引入 Tagged Pointer

爲了存儲和訪問一個 NSNumber 對象,咱們須要在堆上爲其分配內存,另外還要維護它的引用計數,管理它的生命期 。這些都給程序增長了額外的邏輯,形成運行效率上的損失 。

引入 Tagged Pointer

因爲 NSNumberNSDate 一類的變量自己的值須要佔用的內存大小經常不須要 8 個字節,拿整數來講,4 個字節所能表示的有符號整數就能夠達到 20 多億 (注:2^31=2147483648,另外 1 位做爲符號位),對於絕大多數狀況都是能夠處理的。

所以蘋果將一個對象的指針拆成兩部分,一部分直接保存數據,另外一部分做爲特殊標記,表示這是一個特別的指針,不指向任何一個地址。因此,引入了 Tagged Pointer 對象以後 , 對象的指針其實再也不是傳統意義上的指針 .

Tagged Pointer 做用

Tagged Pointer 特色:

  • 1️⃣ : Tagged Pointer 專門用來存儲小的對象,例如 NSNumberNSDate
  • 2️⃣ : Tagged Pointer 指針的值再也不是地址了,而是真正的值。因此,實際上它再也不是一個對象了,它只是一個披着對象皮的普通變量而已。因此,它的內存並不存儲在堆中,也不須要 mallocfree
  • 3️⃣ : 在內存讀取上有着 3 倍的效率,建立時比之前快 106 倍 . ( objc_msgSend 能識別 Tagged Pointer,好比 NSNumberintValue 方法,直接從指針提取數據 )
  • 4️⃣ : 使用 Tagged Pointer 後,指針內存儲的數據變成了 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_ISAINDEXED_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 .

NONPOINTER_ISA

咱們已經知道對象的 isa 指針,是用來代表對象所屬的類類型。 可是若是isa指針僅表示類型的話,對內存顯然也是一個極大的浪費。

因而,就像 Tagged Pointer 同樣,對於 isa 指針,蘋果一樣進行了優化。isa 指針表示的內容變得更爲豐富,除了代表對象屬於哪一個類以外,還附加了引用計數 extra_rc,是否有被 weak 引用標誌位 weakly_referenced,是否有附加對象標誌位 has_assoc 等信息 , 使用的就是咱們剛剛提到的 聯合體 + 位域 的數據結構 .

nonpointer 就是是否進行優化的標識 , 優化以後的聯合體中存儲了是否優化的標識在其中的 struct 的第一個二進制位中 . ( 下面咱們會仔細講述 ) .

那麼接下來 , 終於進入到 isa_t 的結構了 .

isa 內存結構

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_t

從上源碼得知 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 作位運算實現的。
  • bitsstruct 的關係能夠看作 : bits 向外提供了操做 struct 的接口,而 struct 自己則說明了 bits 中各個二進制位的定義。

以獲取有無關聯對象來舉例 :

能夠直接使用 isa.has_assoc , 也就是點語法直接訪問 bits 中第二個二進制位中的數據 . ( arm 64 架構中 )

所以 , bitsstruct 的關係理解清楚之後 , 咱們 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 實戰演練 - 引用計數探索 - 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 的指針 .

接下來咱們來實際演練一遍 .

注意:

  • arm64isa_t 中後 19 位爲 extra_rc .
  • extra_rc 實際存儲爲引用計數減 1 .
  • iOS 小端模式 , 讀取二進制位時從右往左讀 .
@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
複製代碼

經過以上探索 , 咱們很清楚 runtimeisa 的具體結構 . 那麼接下來 , 咱們來探索一下 isa 的指向 , 這個問題也已是面試常客的地位了 .

在探索 isa 指向以前 , 咱們須要知道這個問題 :

③ 面試題 class , objc_getClass 與 object_getclass

classobjc_getClassobject_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 指向探索

isa 走位流程圖 , 圖片引用自官方文檔

代碼準備

新建一個 LBSuperClass 類繼承於 NSObject , 一個 LBSubClass 繼承於 LBSuperClass , 建立代碼以下 .

NSObject * object = [NSObject alloc];
LBSuperClass * superClass = [LBSuperClass alloc];
LBSubClass * subClass = [LBSubClass alloc];
複製代碼

三個對象建立完加斷點 , 運行代碼.

提示 :

  • lldb 命令

    • x/4g 表明打印對象首地址開始連續 48 字節內存地址內容 . X/5g , x/6g 以此類推 , 就免去由於小端模式 x 打印必須從右往左讀的問題 .

    • p/tp/op/dp/x分別表明二進制、八進制、十進制和十六進制打印 .

  • 經過前面探索 , 咱們知道對象首地址前 8 個字節 , 其實就是 isa .

  • 要使用真機跑 , 模擬器不開啓 isa 優化 .

lldb 調試探索

調試結果以下 :

結論 1️⃣ : 實例對象的 isa 指向類對象 .

結論 2️⃣ : 類對象的 isa 指向元類對象 . ( 注意 , 元類對象的地址與類對象不一樣 )

結論 3️⃣ : 根元類 ( NSObject 的元類 ) 指向本身 .

結論 4️⃣ : 元類的 isa 指向根源類 .

⑤ superClass 指向探索

提示 :

對象中第 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 是鏈接實例對象 , 類與元類的重要橋樑 .
  • isa64 位機器開始引入 TaggedPointer , 與 isa 的優化 ( NONPOINTER_ISA ) , 使用聯合體 + 位域的模式 , 來存儲更多內容 , 取值方式也變成使用掩碼 mask 位運算獲取真實 cls .
  • isa 指向 :
    • 實例對象的 isa 指向類對象 .
    • 類對象的 isa 指向元類對象 . ( 注意 , 元類對象的地址與類對象不一樣 , 名稱相同 ) .
    • 根元類 ( NSObject 的元類 ) 指向本身 .
    • 元類的 isa 指向根源類 .
  • superClass 指向 :
    • 子類 superClass 指向父類 .
    • 根源類 superClass 指向 NSObject 類 .

至此 , isa 的相關知識點咱們已經探索完畢了 . 後續繼續更新類的結構探索 , KVC , KVO , RunLoop 等底層探索 , 敬請關注 .

相關文章
相關標籤/搜索