Objective-C类和对象的内存分布

之前在别人博客下面看到了一个问题,觉得挺有意思的。但是自己想回答的时候又发现好像有一些知识点还不是很熟悉,觉得有点迷糊,所以准备再研究一下底层再来回答问题。现在把这个坑填上吧。


image

OC对象的指针类型

Objective-C是一门动态语言,而动态语言是在运行时确定数据类型,变量使用之前不需要类型声明。但是我们在写代码的时候还是要给对象一个类型或者使用id的,我自己觉得这么做是为了通过编译(例如声明了类型为NSObject的实例sark,却调用了方法foo,那么编译就通不过了)。
实际上动态语言的一个特性多态就是这么实现的,即用父类的指针指向子类的实例。

对象的内存分布

还是举个例子会明白一点。需要注意的是需要在模拟器上调试,在真机调试会有问题的。

@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

调试之前,我们要明白几点常识。在计算机中每个字节都是有一个地址的,每个字节有8个bit,每个bit可以存储1或者0,这8个bit就是这个字节的值。在小端系统中,低位的值存储在低地址上。
使用 x 命令调试。格式:x/<n/f/u> <addr>

  • x 显示内存
  • n 正整数,表示需要显示的内存单元的个数
  • f 表示addr指向的内存内容的输出格式
    • s: 对应输出字符串
    • x: 按十六进制格式显示变量
    • d: 按十进制格式显示变量
    • c: 按字符格式显示变量
  • u 以多少个字节作为一个内存单元
    • b: 1 byte
    • h: 2 bytes
    • w: 4 bytes
    • g: 8 bytes

打断点,然后输入命令: x/8xg father1, 即:以8个字节为一个单元,从 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

这里我提前将这些地址代表的意思标注好了。
father2虽然是id类型的,但是它跟father1第一个8字节所存储的地址是相同的,都是0x000000010be34050。其实这个地址就是 Father类的地址。我们可以使用下面的方法验证:

(lldb) po (Class)0x000000010be34050
Father

所以一个实例对象第一个8字节存储的是这个类的指针,那么后面的字节存储的是什么呢?答案是这个实例的成员变量,在上面的例子中我们给实例father1的成员变量name赋值了001, 现在让我们验证一下:

(lldb) po (id)0x000000010bdcc058
001

因为我们没有对father2的成员变量 name 赋值,所以这8个字节的值是空的。


打开 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是一个联合体,里面的 struct 在不同架构的CPU中定义是不同的。在 64 位CPU中,isa 可以用来存储更多的信息,例如引用计数,是否有关联对象等,可以看我的这篇博客Objective-C引用计数原理

使用clang rewrite-objc ViewController.m将代码转化成C++实现,可以看到 Father 这个类变成了如下的结构体

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

看到这个结构体你是不是就明白了为什么对象的内存分布是下图这个样子的?
需要注意的是,NSObject 的实例虽然理论上只有8个字节,但是它的实例实际上有 16 个字节,后面8个字节是空的。

实例内存分布图

研究到这里,我们就可以回答开头的那个问题了。

  1. 指针的类型是id类型,而指针指向的类型可以是别的类。因为 OC 是动态语言,变量的类型需要在运行时才能够确定。
  2. 指针保存的是对象内存的首地址
  3. 64位平台中,对象首地址开始的8个字节存储的是类的指针。也就是通过这个才能确定该类的类型

是不是很简单!下面继续让我们研究下 Class 的内存分布问题

Class的内存分布

让我们继续回到之前的代码调试。上一节中我们已经知道了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: 注意不要使用真机来调试,因为我调试的时候发现跳不到那个内存地址中,但在模拟器中没这个问题...

配套的我们把 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字节还是 isa 指针,也就是一个指向元类(meta-Class)的指针。如果你不知道元类是什么意思的话就去百度,我也懒得讲了。第2个8字节储存的是指向父类的指针。先让我们验证一下

lldb) po (Class)0x000000010be34028
Father

(lldb) po (Class)0x000000010f8f8e58
NSObject

结论正确。让我们接着看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关系到方法查找的缓存。当对实例发送消息后,会先到Class的缓存中查找有没有该方法的缓存,如果有则直接调用方法的实现,提高效率。
大致可以看出,bucket_t是一个哈希表,根据_key找到其映射的方法实现_imp,而_key就是 SEL(方法的名字 const char *)。cache_t是中的_mask_occupied是两个4字节的变量,应该代表的是缓存的数量。所以,Class 第三个8字节存储的是bucket_t *类型的指针,第4个8字节保存的是 _mask 和 _occupied。因为是小端,低位地址存储低位的数据,所以 _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个字节。class_data_bits_t 的公共方法有很多,主要是配合掩码进行一些读写操作。
继续看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
};

在结构体中,你可以看到有一个成员变量的类型是class_ro_t,是不是很像class_rw_t。从字面意思上可以猜测,一个是readwriite,一个是readonly。因为 OC 是动态语言,可以在运行时添加方法和成员变量,运行时添加的方法或者成员变量就是添加到class_rw_t上的,而class_ro_t存储的是一些编译后Class的信息。
class_data_bits_t的定义中,我们知道了需要掩码FAST_DATA_MASK才能得到 class_rw_t 的地址。下面是 class_rw_t的内存分布

// 得到class_rw_t的内存地址
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,没想到在这里出现,没错,就是这个 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;
}

然后是它的内存分布:

(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。这两个值的作用是非脆弱成员变量。即如果基类如果增加了成员变量,不需要重新编译,只需要在初始化系统自动修改instanceStartinstanceSize的值,就能够继续使用子类。具体你可以看我的这篇博客 谈Objective-C类成员变量
  2. ivarLayout 记录了那些是 storng 的ivar
  3. name 存储的是这个类的名字,你可以使用po (char *)0x000000010bd3eafc打印该名字
  4. ivars 存储的是该类的成员变量(不包括关联对象)
  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. 指向列表的指针的数组。所以这就是为什么Class可以在类目中添加方法和协议,只需要在这个列表数组中再添加一个指向类目中方法和协议列表的指针就好了。
因为在这个实例中没有使用类目添加方法,所以rw中methods数组仅有一个值,这个值等于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字节保存的是 entsize 和标记, entsize 我的理解好像是method_t的长度。第二个4字节保存的是方法的数量,在上面的例子中我们可以知道一共保存了3个方法。后面保存了3个method_t的实例,每个实例占用了24个字节。每个 method_t 实例,第一个8字节为 sel,即方法名字;第二个8自己保存了方法的参数类型;第3个8字节是方法的函数指针。我们把上面保存的3个方法的信息按顺序打印出来

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

第2和第3个方法比较好理解,系统为我们自动生成了属性 name 的 getter 和 setter 方法。
第1个方法cxx_destruct 的作用是在delloc时释放该类的成员变量的,具体你可以看这篇博客 探究ARC下dealloc实现

properties 与 methods 类似,因为继承与同一个结构体。这里简单分析一下,内存分布为 entsizeAndFlags(4字节), count(4字节),property_t数组。property_t里面有两个成员变量,一个是属性的名字,一个是属性的属性。。。


大致上这就是 Class 的内存分布了,下面这张图能够简要的概括了:

类的内存分布

引用

ObjectC对象内存布局分析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容