OC对象的本质及底层探究

Objective-C中的对象,简称OC对象,主要分为3种:

  • instance对象(实例对象)
  • class对象(类对象)
  • meta-class对象(元类对象)

instance对象

instance对象就是通过类对象alloc或者new操作所创建的,每次调用alloc都会产生新的instance对象。在这个过程中会拷贝实例所属类的成员变量,但不拷贝类对象中定义的方法。
instance对象在内存中存储的信息包括:


instance对象内存中存储信息

因为OC的对象结构都是通过基础C\C++的结构实现的,所以我们通过创建OC文件及对象,并将OC文件转化为C++文件来探究OC对象的本质。

创建一个命令行项目,OC代码如下:

@interface Person : NSObject
{
    @public
    int _age;
    int _no;
}
@end

@implementation Person


@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p1 = [[Person alloc] init];
        p1->_age = 3;
        
        Person *p2 = [[Person alloc] init];
        p2->_age = 4;
    }
    return 0;
}

我们通过命令行将OC的main.m文件转化为C++文件

clang -rewrite-objc main.m -o main.cpp // 这种方式没有指定架构例如arm64架构 其中cpp代表(c plus plus)
生成 main.cpp

我们可以指定架构模式的命令行,使用xcode工具 xcrun

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 
生成 main-arm64.cpp 

通过反编译oc代码为c++代码,可以发现instance对象实际结构为:

struct NSObject_IMPL {
    Class isa;
};
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _no;
};

相当于

struct Person_IMPL {
    Class isa;
    int _age;
    int _no;
};

class对象

class对象在内存中存储的信息主要包括
1.isa指针
2.superclass指针
3.成员变量信息(ivar)
4.属性信息(@property)
5.对象方法信息(instance method)
6.方法缓存(cache)
7.协议信息(protocol)
......


class对象内存中存储信息

成员变量(ivar)的值是储存在instance对象中的,因为只有当我们创建实例对象的时候才为成员变量赋值。但是成员变量叫什么名字,是什么类型,只需要一份就可以了。所以存储在class对象中。

属性(property)和成员变量是不同的,属性是添加了存取方法的成员变量。

class是一个objc_class结构体指针,通过查看runtime源码,搜索struct objc_class,可以得知其主要结构如下:

struct objc_class

元类对象(meta-class)

meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的主要信息包括
1.isa指针
2.superclass指针
3.类方法信息(class method)
......


meta-class内存中存储信息

meta-class对象和class对象的内存结构是一样的,所以meta-class中也有类的属性信息,类的对象方法信息等成员变量,但其中的值是空的。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //instance对象,实例对象
        NSObject *object1 = [[NSObject alloc] init];
        NSObject *object2 = [[NSObject alloc] init];
        
        //class对象,类对象
        //class方法返回的一直是class对象,类对象
        Class objectClass1 = [object1 class];
        Class objectClass2 = [object2 class];
        Class objectClass3 = object_getClass(object1);
        Class objectClass4 = object_getClass(object2);
        Class objectClass5 = [NSObject class];
        
        // meta-class对象,元类对象
        // 将类对象当做参数传入,获得元类对象
        Class objectMetaClass = object_getClass(objectClass5);
        
        NSLog(@"instance - %p %p",
              object1,
              object2);
        
        NSLog(@"class - %p %p %p %p %p %d",
              objectClass1,
              objectClass2,
              objectClass3,
              objectClass4,
              objectClass5,
              class_isMetaClass(objectClass3));
        
        NSLog(@"objectMetaClass - %p %d", objectMetaClass, class_isMetaClass(objectMetaClass));

    }
    return 0;
}
2019-06-11 20:43:35.089627+0800 OC对象本质[29156:6815927] instance - 0x100602640 0x100602270
2019-06-11 20:43:35.090136+0800 OC对象本质[29156:6815927] class - 0x7fff9bec1140 0x7fff9bec1140 0x7fff9bec1140 0x7fff9bec1140 0x7fff9bec1140 0
2019-06-11 20:43:35.090148+0800 OC对象本质[29156:6815927] objectMetaClass - 0x7fff9bec10f0 1

程序中只会存在一个class对象,也只有一个meta-class对象,但是class对象通过alloc或者new可以创建多个instance对象。

isa指针和superclass指针

结合文章后面的面试问题详细讲解(见文章下面面试题3)

以上为我的总结,现在我们用三个面试问题来探究底层的原理。

1.一个NSObject对象占用多少内存?

思考:一个OC对象在系统中是如何分配内存的。
NSObject对象的底层,通过将OC代码编译成C++代码(编译方法如文章最开始所说),其实就是一个结构体

struct NSObject_IMPL {
    Class isa;
};

我们发现这个结构体只有一个成员isa指针,而指针在64位架构中占8个字节。也就是说一个NSObject对象所需的内存是8个字节。但是实际情况系统具体在内存中分配了多少内存呢?
OC代码

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [[NSObject alloc] init];
        // 获得NSObject实例对象的成员变量所占用的大小 >> 8
        NSLog(@"%zd", class_getInstanceSize([NSObject class]));
        // 获得obj指针所指向内存的大小 >> 16
        NSLog(@"%zd", malloc_size((__bridge const void *)obj));
    }
    return 0;
}

运行结果

2019-06-12 14:44:51.903416+0800 OC对象内存分布[32699:8399663] 8
2019-06-12 14:44:51.906796+0800 OC对象内存分布[32699:8399663] 16

NSObject对象大小是8,但是系统分配的内存是16。这样这个面试题就有了答案。

但是为什么结果会是16而不是8呢?
继续举一个例子,Person类继承NSObject,看其内存分配情况

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Person : NSObject
{
    int _age;
    int _no;
    NSString *_name;
}

@end

@implementation Person

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        NSLog(@"%zd", class_getInstanceSize([Person class]));
        NSLog(@"%zd", malloc_size((__bridge const void *)p));
    }
    return 0;
}

运行结果

2019-06-12 14:46:32.696414+0800 OC对象内存分布[32713:8405320] 24
2019-06-12 14:46:32.696602+0800 OC对象内存分布[32713:8405320] 32

编译代码成c++,发现Person_IMPL结构体为

struct Person_IMPL {
    Class isa;          // 8个字节
    int _age;           //4个字节
    int _no;            //4个字节
    NSString *_name;    //8字节
};

同样Person对象大小是24,但是系统分配内存为32。

窥探内存结构

实时查看内存数据

方式一:通过打断点
Debug Workflow -> viewMemory address中输入对象p的地址

p内存地址

从上图我们可以发现,截图中分别标出了每个成员变量对应的值,读取数据从高位开始读,isa值为0x001d800100001215(8字节),_age值为0x00000005(10进制=5)(4字节),_no值为0x00000006(10进制=6)(4字节),_name值为0x0000000100001048(8字节)

方式二:通过lldb指令,xcode自带的调试器

memory read 0x100507e40

// 增加读取条件
// memory read/数量格式字节数  内存地址
// 简写 x/数量格式字节数  内存地址
// 格式 x是16进制,f是浮点,d是10进制
// 字节大小   b:byte 1字节,h:half word 2字节,w:word 4字节,g:giant word 8字节

示例:x/4xw    //   /后面表示如何读取数据 w表示4个字节4个字节读取,x表示以16进制的方式读取数据,4则表示读取4次

同时也可以通过lldb修改内存中的值,在不清楚实际情况时慎用

memory write 0x100507e48 6
将_no的值改为了6,因为第一个成员变量isa的地址为0x100507e40,并且isa占用内存为8字节,所以第二个成员变量的地址为0x100507e48
(lldb) memory read 0x100507e40
0x100507e40: 15 12 00 00 01 80 1d 00 05 00 00 00 06 00 00 00  ................
0x100507e50: 48 10 00 00 01 00 00 00 00 00 00 00 00 00 00 00  H...............
(lldb) x/4xw 0x100507e40
0x100507e40: 0x00001215 0x001d8001 0x00000005 0x00000006
(lldb) x/2xg 0x100507e40
0x100507e40: 0x001d800100001215 0x0000000600000005
(lldb) x/4dw 0x100507e40
0x100507e40: 4629
0x100507e44: 1933313
0x100507e48: 5
0x100507e4c: 6
(lldb) memory write 0x100507e48 6
(lldb) po p->_age
6

(lldb) po p->_no
6

(lldb) 

上面提出的问题,为什么对象本身大小和系统分配的大小不是完全相等,其实是因为系统在分配内存之前会先进行计算,这个计算要符合内存对齐原则。内存未对齐的情况下会大大降低CPU的读取性能。
以下通过查看runtime源码,来验证这个说法。

当类对象调用alloc之后,系统底层会调用class_createInstances这个方法,顺着这个方法往下走,会进入instanceSize方法计算大小,而计算方法就是word_align,内存对齐。当计算值小于16时,强制等于16。

class_createInstances(Class cls, size_t extraBytes, 
                      id *results, unsigned num_requested)
{
    return _class_createInstancesFromZone(cls, extraBytes, nil, 
                                          results, num_requested);
}

_class_createInstancesFromZone(Class cls, size_t extraBytes, void *zone, 
                               id *results, unsigned num_requested)
{
    unsigned num_allocated;
    if (!cls) return 0;

    size_t size = cls->instanceSize(extraBytes);
}

size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    } 

#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。
为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
总结内存对齐两大原则
原则1:前面的地址必须是后面的地址正数倍,不是就补齐。
原则2:整个Struct的地址必须是最大字节的整数倍。

ios系统在64bit环境下的内存分配规则
1.分配最小内存为16字节
2.分配字节总大小是16的正整数倍

通过以上原则我们可以知道为什么NSObject对象大小为8实际分配大小为16,也知道Person对象大小为24实际分配大小为32了。

2.OC的类信息存放在哪里?

查看runtime源码,Class类型其实是一个objc_class指针

/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

网上很多人的文章是用这个代码来讲解的,目前其实已经过时OBJC2_UNAVAILABLE,但是类主要的信息还是类似的,具有一定的参考意义

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

最新的底层代码结构是这样的,以下代码有删减,只展示主要信息

struct objc_class{
    Class isa;
    Class superclass;
    cache_t cache;             // 方法缓存
    class_data_bits_t bits;    // 用于获取具体类信息

    class_rw_t *data() {       //可读可写信息
        return bits.data();
    }
}
bits数据进行&FAST_DATA_MASK运算得到下面数据
struct class_rw_t {
    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;
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;          //instance对象占用的内存空间
#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;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

通过源码可以得出,Class类型包含isa指针、superclass指针、成员变量列表、属性列表、方法列表、协议列表、已调用方法缓存、类名、实例变量大小等信息

class对象和meta-class对象同为Class类型,但是两者也有区别。对象方法保存在class中,类方法保存在meta-class中;isa指针和superclass指针的指向也分别不同。

如何证明对象方法是保存在class中?如何证明类方法时保存在meta-class中?以及方法的调用本质及流程是如何?这些问题我将在下一篇文章中讲解

3.isa指针和superclass指针到底指向哪里?

1.当instance调用对象方法的时候,从上面讲到,对象方法是存储在class对象中的,那么要找到实例方法,就必须找到class对象,那么此时isa的作用就来了。

  • instance的isa指向class,当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用。

2.当class对象调用类方法的时候,同上,类方法是存储在meta-class对象中的。那么要找到类方法,就需要找到meta-class对象。

  • class的isa指向meta-class,当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用。
    isa指针指向图

3.当instance调用其父类对象方法的时候,又是怎么找到父类对象方法的呢?此时就需要使用到class类对象superclass指针。

  • 当Student的instance对象要调用父类的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用。
  • 如果Person发现自己没有响应的对象方法,就会通过superclass随着继承链一直向上到父类中寻找,直到找到基类的class,一直找到基类的class都没有找到就会报错。
    class对象的superclass指针指向图

4.当class对象调用父类的类方法时,此时既需要使用isa指针又需要使用superclass指针。

  • 当Student的class要调用父类的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用。
  • 如果Person的meta-class没有响应的类方法时,就会通过superclass随着继承链一直向上到父类中寻找,直到找到基类的meta-class,此时没有找到响应方法,不会报错。
  • 接下来会来到基类的class中去找,只要基类的class中有相同方法名的方法就会调用(无论对象方法或者类方法),此时还没找到,才会报错。
    meta-class对象的superclass指针指向图

最后请看这张经典的isa、superclass指向图,一切就更加明了


官方isa、superclass指向图

isa,superclass总结
isa指针

  • instance的isa指向所属class
  • class的isa指向所属meta-class
  • meta-class的isa指向所属基类的meta-class

superclass

  • class的superclass指向父类的class,如果该类已经是基类,则superclass为nil
  • meta-class的superclass指向父类的meta-class,如果该类是基类的meta-class,则superclass指向基类的class

instance对象调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类
class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类

如何证明isa指针和superclass的指针真如上所说呢?

64bit环境,instance的isa指向class,以及class的isa指向meta-class都是经过一次位运算的(isa & ISA_MASK)。通过runtime源码,找到以下内容

# if __arm64__  //iOS程序
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__  //MAC程序
#   define ISA_MASK        0x00007ffffffffff8ULL
#endif

首先证明instance的isa是指向class的

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface Person : NSObject

@end

@implementation Person

@end

@interface Student : Person

@end

@implementation Student

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];

        Class personClass = [Person class];

        Class personMetaClass = object_getClass(personClass);

        NSLog(@"%p %p %p", person, personClass, personMetaClass);
    }
    return 0;
}

程序运行时打断点,在控制台进行lldb操作,由下图结果得证。


isa指向验证1.png

再证明class的isa指针是指向meta-class的

因为在外部不能直接打印出class的isa地址,所以我仿照系统自定义一个class结构体,暴露出isa指针,然后将personClass强转为我定义的类型,从而拿到class的isa地址。
同样,程序运行时打断点,在控制台进行lldb操作,由下图结果得证。


isa指向验证2

接下来证明superclass是指向其父类的
因为在外部也不能直接打印出class的superclass地址,用以上同样方法自定义class结构体。

superclass指向验证

面试题总结

  1. 一个NSObject对象占用多少内存?
    答:一个指针变量所占用的大小(64bit占8个字节,32bit占4个字节),iOS系统中占用16个字节。
  2. OC的类信息存放在哪里?
    答:成员变量的具体值存放在instance对象。对象方法,协议,属性,成员变量信息存放在class对象。类方法信息存放在meta-class对象。
  3. isa指针和superclass指针到底指向哪里?
    答:1.instance的isa指向所属class,class的isa指向所属meta-class,meta-class的isa指向所属基类的meta-class。2.class的superclass指向父类的class,如果该类已经是基类,则superclass为nil;meta-class的superclass指向父类的meta-class,如果该类是基类的meta-class,则superclass指向基类的class。

文中如果有不对的地方欢迎指出
本文有参考https://www.jianshu.com/p/aa7ccadeca88

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

推荐阅读更多精彩内容