Objective-C類和對象的內(nèi)存分布

之前在別人博客下面看到了一個問題辛块,覺得挺有意思的畔派。但是自己想回答的時候又發(fā)現(xiàn)好像有一些知識點還不是很熟悉,覺得有點迷糊润绵,所以準(zhǔn)備再研究一下底層再來回答問題∠咭現(xiàn)在把這個坑填上吧。


image

OC對象的指針類型

Objective-C是一門動態(tài)語言授药,而動態(tài)語言是在運行時確定數(shù)據(jù)類型士嚎,變量使用之前不需要類型聲明。但是我們在寫代碼的時候還是要給對象一個類型或者使用id的悔叽,我自己覺得這么做是為了通過編譯(例如聲明了類型為NSObject的實例sark莱衩,卻調(diào)用了方法foo,那么編譯就通不過了)娇澎。
實際上動態(tài)語言的一個特性多態(tài)就是這么實現(xiàn)的笨蚁,即用父類的指針指向子類的實例。

對象的內(nèi)存分布

還是舉個例子會明白一點趟庄。需要注意的是需要在模擬器上調(diào)試括细,在真機調(diào)試會有問題的。

@interface Father : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation ViewController
- (void)foo {
    Father *father1 = [Father new];
    father1.name = @"001";
    id father2 = [Father new];
}
@end

調(diào)試之前戚啥,我們要明白幾點常識奋单。在計算機中每個字節(jié)都是有一個地址的,每個字節(jié)有8個bit猫十,每個bit可以存儲1或者0览濒,這8個bit就是這個字節(jié)的值呆盖。在小端系統(tǒng)中,低位的值存儲在低地址上贷笛。
使用 x 命令調(diào)試应又。格式:x/<n/f/u> <addr>

  • x 顯示內(nèi)存
  • n 正整數(shù),表示需要顯示的內(nèi)存單元的個數(shù)
  • f 表示addr指向的內(nèi)存內(nèi)容的輸出格式
    • s: 對應(yīng)輸出字符串
    • x: 按十六進制格式顯示變量
    • d: 按十進制格式顯示變量
    • c: 按字符格式顯示變量
  • u 以多少個字節(jié)作為一個內(nèi)存單元
    • b: 1 byte
    • h: 2 bytes
    • w: 4 bytes
    • g: 8 bytes

打斷點乏苦,然后輸入命令: x/8xg father1, 即:以8個字節(jié)為一個單元株扛,從 father1 指針的地址開始起8個單元的值

(lldb) x/8xg father1
0x6000000128f0: 0x000000010be34050 0x000000010bdcc058
                Class              name
0x600000012900: 0x00006000000128a0 0x0000000100000002
0x600000012910: 0x000000010f8f8e58 0x0000000000000000
0x600000012920: 0x0000000000000000 0x0000000000000000

(lldb) x/8xg father2
0x600000012490: 0x000000010be34050 0x0000000000000000
                Class              name
0x6000000124a0: 0xbadd2dcdc19dbead 0x00006000000124f0
0x6000000124b0: 0x0000000000000000 0x0000000000000000
0x6000000124c0: 0x00007f8ae3c140c0 0x00006080000092b0

這里我提前將這些地址代表的意思標(biāo)注好了。
father2雖然是id類型的汇荐,但是它跟father1第一個8字節(jié)所存儲的地址是相同的洞就,都是0x000000010be34050。其實這個地址就是 Father類的地址拢驾。我們可以使用下面的方法驗證:

(lldb) po (Class)0x000000010be34050
Father

所以一個實例對象第一個8字節(jié)存儲的是這個類的指針奖磁,那么后面的字節(jié)存儲的是什么呢?答案是這個實例的成員變量繁疤,在上面的例子中我們給實例father1的成員變量name賦值了001, 現(xiàn)在讓我們驗證一下:

(lldb) po (id)0x000000010bdcc058
001

因為我們沒有對father2的成員變量 name 賦值咖为,所以這8個字節(jié)的值是空的。


打開 runtime 750版本源碼稠腊,查看 id 和 Class 的定義

typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
private:
    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
};

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

    class_rw_t *data() { 
        return bits.data();
    }
}
  1. id的定義很簡單躁染,是一個指向 objc_object 的指針,而 objc_object 只有一個私有成員變量 isa架忌。objc_class 繼承于 objc_object吞彤,所以你也可以用 id 來聲明 Class 的變量,例如id foo = [NSObject class];叹放。
  2. isa是一個聯(lián)合體饰恕,里面的 struct 在不同架構(gòu)的CPU中定義是不同的。在 64 位CPU中井仰,isa 可以用來存儲更多的信息埋嵌,例如引用計數(shù),是否有關(guān)聯(lián)對象等俱恶,可以看我的這篇博客Objective-C引用計數(shù)原理

使用clang rewrite-objc ViewController.m將代碼轉(zhuǎn)化成C++實現(xiàn)雹嗦,可以看到 Father 這個類變成了如下的結(jié)構(gòu)體

struct Father_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
};
struct NSObject_IMPL {
    Class isa;
};

看到這個結(jié)構(gòu)體你是不是就明白了為什么對象的內(nèi)存分布是下圖這個樣子的?
需要注意的是合是,NSObject 的實例雖然理論上只有8個字節(jié)了罪,但是它的實例實際上有 16 個字節(jié),后面8個字節(jié)是空的聪全。

實例內(nèi)存分布圖

研究到這里泊藕,我們就可以回答開頭的那個問題了。

  1. 指針的類型是id類型难礼,而指針指向的類型可以是別的類娃圆。因為 OC 是動態(tài)語言汽久,變量的類型需要在運行時才能夠確定。
  2. 指針保存的是對象內(nèi)存的首地址
  3. 64位平臺中踊餐,對象首地址開始的8個字節(jié)存儲的是類的指針。也就是通過這個才能確定該類的類型

是不是很簡單臀稚!下面繼續(xù)讓我們研究下 Class 的內(nèi)存分布問題

Class的內(nèi)存分布

讓我們繼續(xù)回到之前的代碼調(diào)試吝岭。上一節(jié)中我們已經(jīng)知道了Father類的地址了

(lldb) x/16xg 0x000000010be34050
0x10be34050: 0x000000010be34028 0x000000010f8f8e58
             meta-class         superClass
0x10be34060: 0x00006000000972f0 0x0000000200000003
             bucket_t *_buckets _mask    _occupied   
0x10be34070: 0x0000600000074302 0x000000010f8f8e08
0x10be34080: 0x000000010f8f8e08 0x000000010f548520
0x10be34090: 0x0000000000000000 0x000000010bdd7df0
0x10be340a0: 0x000000010be34078 0x000000010f8f8e58
0x10be340b0: 0x000000010f548520 0x0000000000000000
0x10be340c0: 0x000000010bdd7e38 0x000000010f8f8e08

PS: 注意不要使用真機來調(diào)試,因為我調(diào)試的時候發(fā)現(xiàn)跳不到那個內(nèi)存地址中吧寺,但在模擬器中沒這個問題...

配套的我們把 objc_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

    class_rw_t *data() { 
        return bits.data();
    }
}

因為 objc_class 繼承于 objc_object,所以 Class 的第一個8字節(jié)還是 isa 指針稚机,也就是一個指向元類(meta-Class)的指針幕帆。如果你不知道元類是什么意思的話就去百度,我也懶得講了赖条。第2個8字節(jié)儲存的是指向父類的指針失乾。先讓我們驗證一下

lldb) po (Class)0x000000010be34028
Father

(lldb) po (Class)0x000000010f8f8e58
NSObject

結(jié)論正確。讓我們接著看cache_t的定義:

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
}
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif
}

cache_t關(guān)系到方法查找的緩存纬乍。當(dāng)對實例發(fā)送消息后碱茁,會先到Class的緩存中查找有沒有該方法的緩存,如果有則直接調(diào)用方法的實現(xiàn)仿贬,提高效率纽竣。
大致可以看出,bucket_t是一個哈希表茧泪,根據(jù)_key找到其映射的方法實現(xiàn)_imp蜓氨,而_key就是 SEL(方法的名字 const char *)。cache_t是中的_mask_occupied是兩個4字節(jié)的變量队伟,應(yīng)該代表的是緩存的數(shù)量穴吹。所以,Class 第三個8字節(jié)存儲的是bucket_t *類型的指針缰泡,第4個8字節(jié)保存的是 _mask 和 _occupied刀荒。因為是小端,低位地址存儲低位的數(shù)據(jù)棘钞,所以 _mask 的值是0x00000003缠借,而 _occupied 的值是0x00000002

接下來看 Class 的第3個成員變量class_data_bits_t bits;

struct class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
    
    public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
}

在64位下,uintptr_t 為8個字節(jié)宜猜。class_data_bits_t 的公共方法有很多泼返,主要是配合掩碼進行一些讀寫操作。
繼續(xù)看class_rw_t的定義

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
};

在結(jié)構(gòu)體中姨拥,你可以看到有一個成員變量的類型是class_ro_t绅喉,是不是很像class_rw_t渠鸽。從字面意思上可以猜測,一個是readwriite柴罐,一個是readonly徽缚。因為 OC 是動態(tài)語言,可以在運行時添加方法和成員變量革屠,運行時添加的方法或者成員變量就是添加到class_rw_t上的凿试,而class_ro_t存儲的是一些編譯后Class的信息。
class_data_bits_t的定義中似芝,我們知道了需要掩碼FAST_DATA_MASK才能得到 class_rw_t 的地址那婉。下面是 class_rw_t的內(nèi)存分布

// 得到class_rw_t的內(nèi)存地址
0x0000600000074302 & 0x00007ffffffffff8 = 0x600000074300;

(lldb) x/16xg 0x600000074300
0x600000074300: 0x00000000800a0000 0x000000010bdd7da8
                flags      version ro
0x600000074310: 0x000000010bdd7d18 0x000000010bdd7d90
                methods            properties
0x600000074320: 0x0000000000000000 0x000000010be33f60
                protocols          firstSubclass
0x600000074330: 0x000000010ee88c68 0x0000000000000000
                nextSiblingClass   demangledName
0x600000074340: 0xbadd2dcdc19dbead 0x0000600000074240

因為在代碼中我還聲明了一個 Father 的子類 Son,沒想到在這里出現(xiàn)党瓮,沒錯详炬,就是這個 firstSubclass。至于如果有多個子類寞奸,確定哪個是 firstSubclass 我就不清楚了呛谜。。枪萄。

(lldb) po (Class)0x000000010be33f60
Son

再來看一下class_ro_t的定義:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}

然后是它的內(nèi)存分布:

(lldb) x/16xg 0x000000010bdd7da8
0x10bdd7da8: 0x0000000800000184 0x0000000000000010
            flags instanceStart instanceSize reserved
0x10bdd7db8: 0x000000010bd3ea79 0x000000010bd3eafc
             ivarLayout         name
0x10bdd7dc8: 0x000000010bdd7d18 0x0000000000000000
             baseMethodList     baseProtocols
0x10bdd7dd8: 0x000000010bdd7d68 0x0000000000000000
             ivars              weakIvarLayout
0x10bdd7de8: 0x000000010bdd7d90 0x0000002800000081
             baseProperties
  1. 可以看到 ro 的成員變量中有instanceStartinstanceSize呻率。這兩個值的作用是非脆弱成員變量。即如果基類如果增加了成員變量呻引,不需要重新編譯礼仗,只需要在初始化系統(tǒng)自動修改instanceStartinstanceSize的值,就能夠繼續(xù)使用子類逻悠。具體你可以看我的這篇博客 談Objective-C類成員變量
  2. ivarLayout 記錄了那些是 storng 的ivar
  3. name 存儲的是這個類的名字元践,你可以使用po (char *)0x000000010bd3eafc打印該名字
  4. ivars 存儲的是該類的成員變量(不包括關(guān)聯(lián)對象)
  5. weakIvarLayout 記錄了哪一些是 weak 的ivar

還可以看到 ro 的baseMethodList和rw的methods的地址都是0x000000010bdd7d18,ro 的baseProperties和rw的properties的地址都是0x000000010bdd7d90

實際上 rw 的三個成員變量童谒,methods, properties, protocols的類型都繼承于list_array_tt单旁,這個列表可能有以下3中值:1. 空值 2. 指向列表的指針 3. 指向列表的指針的數(shù)組。所以這就是為什么Class可以在類目中添加方法和協(xié)議饥伊,只需要在這個列表數(shù)組中再添加一個指向類目中方法和協(xié)議列表的指針就好了象浑。
因為在這個實例中沒有使用類目添加方法,所以rw中methods數(shù)組僅有一個值琅豆,這個值等于ro的baseMethodList愉豺。

先來研究methods

struct method_t {
   SEL name;
   const char *types;
   MethodListIMP imp;
}

struct method_list_t {
   uint32_t entsizeAndFlags;
  uint32_t count;
  method_t first;
}

(lldb) x/16xg 0x000000010bdd7d18
0x10bdd7d18: 0x000000030000001a 0x000000010f547965
         entsizeAndFlags count name
0x10bdd7d28: 0x000000010bd41271 0x000000010b7e01e0
            types              imp
0x10bdd7d38: 0x000000010fd3a28e 0x000000010bd41284
            name               types
0x10bdd7d48: 0x000000010b7e0180 0x0000000112f11912
            imp                name
0x10bdd7d58: 0x000000010bd4128c 0x000000010b7e01a0
            types              imp
0x10bdd7d68: 0x0000000100000020 0x000000010be30c50
0x10bdd7d78: 0x000000010bd19fc8 0x000000010bd4130b
0x10bdd7d88: 0x0000000800000003 0x0000000100000010

entsizeAndFlags 第一個4字節(jié)保存的是 entsize 和標(biāo)記, entsize 我的理解好像是method_t的長度茫因。第二個4字節(jié)保存的是方法的數(shù)量蚪拦,在上面的例子中我們可以知道一共保存了3個方法。后面保存了3個method_t的實例,每個實例占用了24個字節(jié)驰贷。每個 method_t 實例盛嘿,第一個8字節(jié)為 sel,即方法名字括袒;第二個8自己保存了方法的參數(shù)類型次兆;第3個8字節(jié)是方法的函數(shù)指針。我們把上面保存的3個方法的信息按順序打印出來

  • .cxx_destruct v16@0:8
  • name @16@0:8
  • setName: v24@0:8@16

第2和第3個方法比較好理解锹锰,系統(tǒng)為我們自動生成了屬性 name 的 getter 和 setter 方法类垦。
第1個方法cxx_destruct 的作用是在delloc時釋放該類的成員變量的,具體你可以看這篇博客 探究ARC下dealloc實現(xiàn)

properties 與 methods 類似城须,因為繼承與同一個結(jié)構(gòu)體。這里簡單分析一下米苹,內(nèi)存分布為 entsizeAndFlags(4字節(jié)), count(4字節(jié))糕伐,property_t數(shù)組。property_t里面有兩個成員變量蘸嘶,一個是屬性的名字良瞧,一個是屬性的屬性。训唱。褥蚯。


大致上這就是 Class 的內(nèi)存分布了,下面這張圖能夠簡要的概括了:

類的內(nèi)存分布

引用

ObjectC對象內(nèi)存布局分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末况增,一起剝皮案震驚了整個濱河市赞庶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌澳骤,老刑警劉巖歧强,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異为肮,居然都是意外死亡摊册,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進店門颊艳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茅特,“玉大人,你說我怎么就攤上這事棋枕“仔蓿” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵重斑,是天一觀的道長熬荆。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么卤恳? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任累盗,我火速辦了婚禮,結(jié)果婚禮上突琳,老公的妹妹穿的比我還像新娘若债。我一直安慰自己,他們只是感情好拆融,可當(dāng)我...
    茶點故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布蠢琳。 她就那樣靜靜地躺著,像睡著了一般镜豹。 火紅的嫁衣襯著肌膚如雪傲须。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天趟脂,我揣著相機與錄音泰讽,去河邊找鬼。 笑死昔期,一個胖子當(dāng)著我的面吹牛已卸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播硼一,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼累澡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了般贼?” 一聲冷哼從身側(cè)響起愧哟,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哼蛆,沒想到半個月后翅雏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡人芽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年望几,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萤厅。...
    茶點故事閱讀 40,865評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡橄抹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惕味,到底是詐尸還是另有隱情楼誓,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布名挥,位于F島的核電站疟羹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜榄融,卻給世界環(huán)境...
    茶點故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一参淫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧愧杯,春花似錦涎才、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至跌前,卻和暖如春棕兼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背抵乓。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工伴挚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人臂寝。 一個月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像摊灭,于是被迫代替她去往敵國和親咆贬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容