iOS探索 isa初始化&指向分析

我是好寶寶發表於2020-01-06

歡迎閱讀iOS探索系列(按序閱讀食用效果更加)

寫在前面

在介紹isa之前,先介紹一個位域和結構體的知識點

一、位域

1.定義

有些資訊在儲存時,並不需要佔用一個完整的位元組,而只需佔幾個或一個二進位制位。例如在存放一個開關量時,只有0和1兩種狀態,用1位二進位即可。為了節省儲存空間並使處理簡便,C語言提供了一種資料結構,稱為位域位段

所謂位域就是把一個位元組中的二進位劃分為幾個不同的區域,並說明每個區域的位數。每個域有一個域名,允許在程式中按域名進行操作——這樣就可以把幾個不同的物件用一個位元組的二進位制位域來表示

2.與結構體比較

位域的使用與結構體相仿,它本身也是結構體的一種

// 結構體
struct FXStruct {
    // (型別說明符 元素);
    char a;
    int b;
} FXStr;

// 位域
struct FXBitArea {
    // (型別說明符 位域名: 位域長度);
    char a: 1;
    int b: 3;
} FXBit;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"Struct:%lu——BitArea:%lu", sizeof(FXStr), sizeof(FXBit));
    }
    return 0;
}
複製程式碼

輸出Struct:8——BitArea:4

位域有興趣的可以看下struct中位域的定義

二、聯合體

1.定義

當多個資料需要共享記憶體或者多個資料每次只取其一時,可以利用聯合體(union)

  • 聯合體是一個結構
  • 它的所有成員相對於基地址的偏移量都為0
  • 此結構空間要大到足夠容納最"寬"的成員
  • 各變數是“互斥”的——共用一個記憶體首地址,聯合變數可被賦予任一成員值,但每次只能賦一種值, 賦入新值則衝去舊值

2.與結構體比較

結構體每個成員依次儲存,聯合體中所有成員的偏移地址都是0,也就是所有成員是疊在一起的,所以在聯合體中在某一時刻,只有一個成員有效——結構體記憶體大小取決於所有元素,聯合體取決於最大那個

iOS探索 isa初始化&指向分析

三、isa結構/流程分析

1.isa初始化

在之前的iOS探索 alloc流程中輕描淡寫的提了一句obj->initInstanceIsa(cls, hasCxxDtor)——只知道內部呼叫initIsa(cls, true, hasCxxDtor)初始化isa,並沒有對isa進行細說

2.initIsa分析

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        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
        newisa.bits = ISA_MAGIC_VALUE;
        // 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;
    }
}
複製程式碼

①建立物件跟著斷點不難發現nonpointer為true

if-else跳轉了else流程——SUPPORT_INDEXED_ISA表示isa_t中存放的 Class資訊是Class 的地址,還是一個索引(根據該索引可在類資訊表中查詢該類結構地址)

isa_t newisa(0)相當於初始化isa這個東西,new.相當於給isa賦值屬性

1.SUPPORT_INDEXED_ISA適用於WatchOS 2.isa作為聯合體具有互斥性,而cls、bits是isa的元素,所以當!nonpointer=true時對cls進行賦值操作,為false是對bits進行賦值操作(反正都是一家人,共用一塊記憶體地址)

3.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是個聯合體,擁有兩個初始化方法

isa內部有個Class cls——Classisa有繫結關係——isa指向類的結構

③isa採用聯合體+位域的形式來優化記憶體(ISA_BITFIELD是個位域巨集定義)

先初始化bits決定聯合體的長度,再對聯合體內的位域ISA_BITFIELD進行賦值

聯合體所有屬性共用記憶體,記憶體長度等於其最長成員的長度,使程式碼儲存資料高效率的同時,有較強的可讀性;而位域可以容納更多型別

4.ISA_BITFIELD巨集定義

#if SUPPORT_PACKED_ISA

    // extra_rc must be the MSB-most field (so it matches carry/overflow flags)
    // nonpointer must be the LSB (fixme or get rid of it)
    // shiftcls must occupy the same bits that a real class pointer would
    // bits + RC_ONE is equivalent to extra_rc + 1
    // RC_HALF is the high bit of extra_rc (i.e. half of its range)

    // future expansion:
    // uintptr_t fast_rr : 1;     // no r/r overrides
    // uintptr_t lock : 2;        // lock for atomic property, @synch
    // uintptr_t extraBytes : 1;  // allocated with extra bytes

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   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
#   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;                                         \
      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
#   define RC_ONE   (1ULL<<56)
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

// SUPPORT_PACKED_ISA
#endif
複製程式碼

不同架構下isa所佔記憶體均為8位元組——64位,但內部分佈有所不同,arm64架構isa內部成員分佈如下圖

iOS探索 isa初始化&指向分析
nonpointer:表示是否對isa指標開啟指標優化——0:純isa指標;1:不止是類物件地址,isa 中包含了類資訊、物件的引用計數等

has_assoc:關聯物件標誌位,0沒有,1存在

has_cxx_dtor:該物件是否有 C++ 或者 Objc 的析構器,如果有解構函式,則需要做析構邏輯, 如果沒有,則可以更快的釋放物件

shiftcls:儲存類指標的值,在開啟指標優化的情況下,在 arm64 架構中有 33 位用來儲存類指標

magic:用於偵錯程式判斷當前物件是真的物件還是沒有初始化的空間

weakly_referenced:物件是否被指向或者曾經指向一個 ARC 的弱變數, 沒有弱引用的物件可以更快釋放

deallocating:標誌物件是否正在釋放記憶體

has_sidetable_rc:當物件引用技術大於 10 時,則需要借用該變數儲存進位

extra_rc:當表示該物件的引用計數值,實際上是引用計數值減 1, 例如,如果物件的引用計數為 10,那麼 extra_rc 為 9。如果引用計數大於 10, 則需要使用到下面的 has_sidetable_rc

shiftcls之外瞭解即可

5.shiftcls關聯類

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *p = [[FXPerson alloc] init];
        NSLog(@"%@",p);
    }
    return 0;
}
複製程式碼

父類NSObject結構中可以有個isa屬性

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
複製程式碼

但是記憶體中屬性的位置是會因為優化發生改變的,下面就來證實下記憶體中第一位一定是isa

5.1 反證法(不推薦)

iOS探索 isa初始化&指向分析
假設例項物件第一位記憶體是isa

①列印第一位記憶體

②二進位制列印第一位記憶體的記憶體值

③因為模擬器是x86架構的,由isa位域結構可知,shiftcls前面有3位——右移3位——抹去isa前3位

shiftcls後面有17位——左移17位——抹去isa後17位

⑤因為末尾的0都是我們新增的——右移17位——得到shiftcls

⑥根據newisa.shiftcls = (uintptr_t)cls >> 3;——shiftcls等於class地址右移3位

比對兩組shiftcls二進位制,發現它們二進位制一樣(前面不一樣是因為沒抹掉)

如果你聽得雲裡霧裡的話,請看第二種方法

5.2 通過object_getClass(推薦)

①在<objc/runtime.h>下使用object_getClass方法

#import <objc/runtime.h>

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        FXPerson *p = [[FXPerson alloc] init];
        object_getClass(p);
    }
    return 0;
}
複製程式碼

②跟進object_getClass方法

/***********************************************************************
* object_getClass.
* Locking: None. If you add locking, tell gdb (rdar://7516456).
**********************************************************************/
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
複製程式碼

③跟進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];
    }
}
複製程式碼

④一般isTaggedPointer都為false,跟進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
}
複製程式碼

⑤已知SUPPORT_INDEXED_ISA適用於WatchOS,那麼走return (Class)(isa.bits & ISA_MASK);

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif
複製程式碼

⑥檢驗

列印出isa & mask的值,與class相比較(mask取x86架構)

iOS探索 isa初始化&指向分析

從上述兩種方法都能得出例項物件首地址一定 是isa

6.isa初始化流程圖

iOS探索 isa初始化&指向分析

四、isa走位

1.類在記憶體中只會存在一份

@interface FXPerson : NSObject
@end
@implementation FXPerson
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Class class1 = [FXPerson class];
        Class class2 = [FXPerson alloc].class;
        Class class3 = object_getClass([FXPerson alloc]);
        Class class4 = [FXPerson alloc].class;
        NSLog(@"\n%p\n%p\n%p\n%p",class1,class2,class3,class4);
    }
    return 0;
}
複製程式碼
0x1000024a0
0x1000024a0
0x1000024a0
0x1000024a0
複製程式碼

輸出證明類在記憶體中只會存在一個,而例項物件可以存在多個(自行證明)

2.1 通過物件/類檢視isa走向

其實和例項物件一樣,都是由上級例項化出來的——類的上級叫做元類

我們先用p/x列印類的記憶體地址,再用x/4gx列印記憶體結構取到對應的isa,再用mask進行偏移得到isa指向的上級(等同於object_getClass)依次迴圈

iOS探索 isa初始化&指向分析
①列印FXPerson類取得isa

②由FXPerson類進行偏移得到FXPerson元類指標,列印FXPerson元類取得isa

③由FXPerson元類進行偏移得到NSObject根元類指標,列印NSObject根元類取得isa

④由NSObject根元類進行偏移得到NSObject根元類本身指標

⑤列印NSObject根類取得isa

⑥由NSObject根類進行偏移得到NSObject根元類指標

結論:

例項物件-> 類物件 -> 元類 -> 根元類 -> 根元類(本身)

NSObject(根類) -> 根元類 -> 根元類(本身)

指向根元類的isa都是一樣的

2.2.通過NSObject檢視isa走向

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // NSObject例項物件
        NSObject *object1 = [NSObject alloc];
        // NSObject類
        Class class = object_getClass(object1);
        // NSObject元類
        Class metaClass = object_getClass(class);
        // NSObject根元類
        Class rootMetaClass = object_getClass(metaClass);
        // NSObject根根元類
        Class rootRootMetaClass = object_getClass(rootMetaClass);
        NSLog(@"\n%p 例項物件\n%p 類\n%p 元類\n%p 根元類\n%p 根根元類",object1,class,metaClass,rootMetaClass,rootRootMetaClass);
    }
    return 0;
}
複製程式碼
0x100660ba0 例項物件
0x7fffacd3d140 類
0x7fffacd3d0f0 元類
0x7fffacd3d0f0 根元類
0x7fffacd3d0f0 根根元類
複製程式碼

因為是NSObject(根類)它的元類就是根元類——輸出可得根元類指向自己

2.3 證明類、元類是系統建立的

①執行時偽證法

iOS探索 isa初始化&指向分析
main之前FXPerson類FXPerson元類已經存在在記憶體中,不過此時程式已經在執行了,並沒有什麼說服力

②檢視MachO檔案法

iOS探索 isa初始化&指向分析
FXPerson已經存在在MachO檔案中

結論:
物件是程式猿根據類例項化的
類是程式碼編寫的,記憶體中只有一份,是系統建立的
元類是系統編譯時,系統編譯器建立的,便於方法的編譯
複製程式碼

3.isa走點陣圖

iOS探索 isa初始化&指向分析
isa走位(虛線):例項物件-> 類物件 -> 元類 -> 根元類 -> 根元類(本身)

繼承關係(實線):NSObject父類為nil,根元類的父類為NSObject

小彩蛋——編譯器優化

iOS探索 isa初始化&指向分析

  • None[-O0]: 不優化。在這種設定下, 編譯器的目標是降低編譯消耗,保證除錯時輸出期望的結果。程式的語句之間是獨立的:如果在程式的停在某一行的斷點出,我們可以給任何變數賦新值抑或是將程式計數器指向方法中的任何一個語句,並且能得到一個和原始碼完全一致的執行結果。
  • Fast[-O1]: 大函式所需的編譯時間和記憶體消耗都會稍微增加。在這種設定下,編譯器會嘗試減小程式碼檔案的大小,減少執行時間,但並不執行需要大量編譯時間的優化。在蘋果的編譯器中,在優化過程中,嚴格別名,塊重排和塊間的排程都會被預設禁止掉。
  • Faster[-O2]: 編譯器執行所有不涉及時間空間交換的所有的支援的優化選項。在這種設定下,編譯器不會進行迴圈展開、函式內聯或暫存器重新命名。和'Fast[-O1]'項相比,此設定會增加編譯時間和生成程式碼的效能。
  • Fastest[-O3]: 在開啟'Fast[-O1]'項支援的所有優化項的同時,開啟函式內聯和暫存器重新命名選項。這個設定有可能會導致二進位制檔案變大。
  • Fastest, Smallest[-Os]: 優化大小。這個設定開啟了'Fast[-O1]'項中的所有不增加程式碼大小的優化選項,並會進一步的執行可以減小程式碼大小的優化。
  • Fastest, Aggressive Optimizations[-Ofast]: 這個設定開啟了'Fastest[-O3]'中的所有優化選項,同時也開啟了可能會打破嚴格編譯標準的積極優化,但並不會影響執行良好的程式碼。

Debug預設不優化,Relese預設Fastest, Smallest[-Os]

相關文章