Objective-C对象的本质

面试题

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

  • 系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
  • 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

对象的 isa 指针指向哪里?

  • instance对象的isa指向class对象
  • class对象的isa指向meta-class对象
  • meta-class对象的isa指向基类的meta-class对象

OC 的类信息存放在哪里?

  • 对象方法、属性、成员变量、协议信息,存放在class对象中
  • 类方法,存放在meta-class对象中
  • 成员变量的具体值,存放在instance对象

探寻OC对象的本质

我们平时编写的Objective-C代码,底层实现其实都是C\C++代码


OC代码转化过程

所以Objective-C的面向对象都是基于C\C++的数据结构实现的

思考:Objective-C的对象、类主要是基于C\C++的什么数据结构实现的?
答:结构体

将Objective-C代码转换为C\C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
如果需要链接其他框架,使用-framework参数。比如-framework UIKit

NSObject对象的内部实现

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
@end

转化为 C 语言其实就是一个结构体

struct NSObject_IMPL {
    Class isa;
};
// 查看Class本质
typedef struct objc_class *Class;

这个结构体占多大的内存空间呢,我们发现这个结构体只有一个成员变量,isa 指针,而指针在 64 位架构中占 8 个字节。但由于系统内存对齐是按16个字节对齐,对于小于16个字节的会补齐到16个字节,也就是说一个 NSObject 对象所占用的内存是 16 个字节,前面8个字节存储 isa 指针,后面没用到的内存填充 0。

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

为了探寻 OC 对象的内存中如何体现,我们来看下面一段代码

NSObject *objc = [[NSObject alloc] init];

上述一段代码中系统为 NSObject 对象分配 16 个字节的内存空间,其中8个字节用来存放一个成员 isa 指针。那么 isa 指针这个变量的地址就是结构体的地址,也就是 NSObject 对象的地址。
假设 isa 的地址为 0x100400110,那么上述代码分配存储空间给 NSObject 对象,然后将存储空间的地址赋值给 objc 指针。objc 指向内存中 NSObject 对象地址,即指向内存中的结构体,也就是 isa 的位置。


NSObject对象的内部实现

自定义类的内部实现

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Student *stu = [[Student alloc] init];
        stu -> _no = 4;
        stu -> _age = 5;
        
        NSLog(@"%@",stu);
    }
    return 0;
}
@end

将上述代码转化生成 C++ 文件。并查找 Student,发现 Student_IMPL

struct Student_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};

发现第一个是 NSObject_IMPL 的实现。而通过上面的探索,我们知道 NSObject_IMPL 内部其实就是 Class isa
那么我们假设 struct NSObject_IMPL NSObject_IVARS; 等价于 Class isa;
可以将上述代码转化为

struct Student_IMPL {
    Class *isa;
    int _no;
    int _age;
};

因此此结构占用多少存储空间,对象就占用多少存储空间。因此结构体占用的存储空间为,isa 指针 8 个字节 + int 类型 _no 4个字节 + int 类型 _age 4个字节空间共 16 个字节空间
那么上述代码实际上在内存中体现为,创建 Student 对象首先会分配 16 个字节,存储 3 个东西:isa 指针 8个字节,4个字节的_no,4个字节的 _age


自定义对象的内部实现

student 对象的3个变量分别有自己的地址。而 stu 指向 isa 指针的地址。因此 stu 的地址为 0x100400110,stu 对象的内存中占用 16 个字节的空间。并且经过赋值,_no 里面存储 4, _age 里面存储5
验证 Student 的内存中模样

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

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

@implementation Student

int main(int argc, const char * argv[]) {
    @autoreleasepool {
            // 强制转化
            struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
            NSLog(@"_no = %d, _age = %d", stuImpl->_no, stuImpl->_age); // 打印出 _no = 4, _age = 5
    }
    return 0;
}

上述代码将 oc 对象强转成 Student_IMPL 的结构体。也就是说把一个指向 oc 对象的指针,指向这种结构体。由于我们之前猜想,对象在内存中的布局与结构体在内存中的布局相同,那么如果转化成功,说明我们猜想正确。由此说明 stu 这个对象指向的内存确实是一个结构体。

实际上想要获取对象占用内存的大小,可以通过更便捷的运行时方法来获取

class_getInstanceSize([Student class])
NSLog(@"%zd,%zd", class_getInstanceSize([NSObject class]) ,class_getInstanceSize([Student class]));
// 打印信息 8和16

实时查看内存数据

方式一:Debug Workflow -> viewMemory address (Shift + Command + M) 中输入stu的地址
viewMemory address
方式二:也可以使用LLDB指令

常用LLDB指令

print、p:打印
po:打印对象

读取内存
memory read 内存地址
简写:x 内存地址

memory read 0x10074c450
x 0x10074c450

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

x/3xw 0x10010

修改内存中的值
memory write 内存地址 数值

memory write 0x0000010 10

LLDB 查看内存

复杂继承关系的类的内部实现

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

@implementation Person

@end

 //Student
@interface Student : Person
{
    int _no;
}
@end

@implementation Student

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Student *stu = [[Student alloc] init];
        NSLog(@"stu - %zd", class_getInstanceSize([Student class]));  //打印 stu - 16
        NSLog(@"stu - %zd", malloc_size((__bridge const void *)stu)); //打印 stu - 16
    }
    return 0;
}

【注意】2个容易混淆的函数

//创建一个实例对象,至少需要多少内存?
#import <objc/runtime.h>
class_getInstanceSize([NSObject class]);
//创建一个实例对象,实际上分配了多少内存?
#import <malloc/malloc.h>
malloc_size((__bridge const void *)obj);

思考:一个Person对象、一个Student对象占用多少内存空间?

继承关系的对象

由上图的 Person 对象和 Student 对象转化为 C++ 结构体可看出

  • Person_IMPL 结构体中可将 struct NSObject_IMPL NSObject_IVARS; 替换成 Class isa;,Person_IMPL 结构体中 isa 指针 8 个字节 + int 成员 _no 4个字节,但由于结构体内存对齐的规则,结构体的大小必须是最大成员大小的倍数,故Person_IMPL 结构体的大小为 16 个字节,由于系统为对象实际为对象分配的内存大小是16的倍数,所以一个Person对象实际占用了16个字节的内存空间;
  • Student_IMPL 结构体中可将 struct Person_IMPL Person_IVARS 替换为 Class isa; int _age;,Student_IMPL 结构体中 isa 指针 8 个字节 + int 成员 _no 4个字节 + int 成员 _age 4个字节 ,结构体内存对齐的规则可知Student_IMPL 结构体的大小为 16 个字节,由于系统为对象实际为对象分配的内存大小是16的倍数,所以一个Student对象实际占用了16个字节的内存空间;

OC对象的分类

Objective-C中的对象,简称OC对象,主要可以分为3种
  • instance对象(实例对象)
  • class对象(类对象)
  • meta-class对象(元类对象)
instance 对象

instance对象就是通过类alloc出来的对象,每次调用alloc都会产生新的instance对象

NSObjcet *object1 = [[NSObjcet alloc] init];
NSObjcet *object2 = [[NSObjcet alloc] init];

object1、object2是NSObject的instance对象(实例对象),它们是不同的两个对象,分别占据着两块不同的内存

instance对象在内存中存储的信息包括

  • isa指针
  • 其他成员变量
@interface Person : NSObject <NSCopying>
{
    @public
    int _age;
}
@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;
}
instance对象
class
 NSObject *object1 = [[NSObject alloc] init];
 NSObject *object2 = [[NSObject alloc] init];
 Class objectClass1 = [object1 class];
 Class objectClass2 = [object2 class];
 Class objectClass3 = [NSObject class];
 Class objectClass4 = object_getClass(object1); //Runtime API
 Class objectClass5 = object_getClass(object2); //Runtime API
  • objectClass1 ~ objectClass5都是NSObject的class对象(类对象)
  • 它们是同一个对象。每个类在内存中有且只有一个class对象

class对象在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的属性信息(@property)、类的对象方法信息(instance method)
  • 类的协议信息(protocol)、类的成员变量信息(ivar)
  • ......
class 对象
meta-class
// meta-class对象,元类对象
// 将类对象当做参数传入,获得元类对象
Class objectMetaClass = object_getClass([NSObject class]);
  • objectMetaClass是NSObject的meta-class对象(元类对象)
  • 每个类在内存中有且只有一个meta-class对象

meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的类方法信息(class method)
  • ......
meta-class

以下代码获取的objectClass是class对象,并不是meta-class对象

Class objectClass = [[NSObject class] class];

查看Class是否为meta-class

#import <objc/runtime.h>
Bool result = class_isMetaClass(objectMetaClass);

isa 指针

isa 指针-1
  • instance 的 isa 指向 class
    当调用对象方法时,通过instance的isa找到class,最后找到对象方法的实现进行调用
  • class的isa指向meta-class
    当调用类方法时,通过class的isa找到meta-class,最后找到类方法的实现进行调用
isa 指针 -2

ISA_MASK

从64bit开始,isa需要进行一次位运算,才能计算出真实地址

class对象的superclass指针

class对象的superclass指针

当Student的instance对象要调用Person的对象方法时,会先通过isa找到Student的class,然后通过superclass找到Person的class,最后找到对象方法的实现进行调用

meta-class对象的superclass指针

meta-class对象的superclass指针

当Student的class要调用Person的类方法时,会先通过isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法的实现进行调用

isa、superclass总结

isa、superclass总结
  • instance的isa指向class
  • class的isa指向meta-class
  • meta-class的isa指向基类的meta-class
  • class的superclass指向父类的class,如果没有父类,superclass指针为nil
  • meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  • instance调用对象方法的轨迹:isa找到class,方法不存在,就通过superclass找父类
  • class调用类方法的轨迹:isa找meta-class,方法不存在,就通过superclass找父类

窥探struct objc_class的结构

objc4源码下载

class、meta-class对象的本质结构

class、meta-class对象的本质结构都是struct objc_class
struct objc_class的结构

验证探索结论

自定义一个结构体,其结构和objc_class真实结构一样,当我们强制转化的时候,就会一一对应的赋值。就可以拿到结构体内部的信息。
仿照代码如下,提取其中需要使用到的信息,自定义的一个结构体。

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

#if __LP64__
typedef uint32_t mask_t;
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t cache_key_t;

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

struct cache_t {
    bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
};

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

struct method_list_t : entsize_list_tt {
    method_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

struct ivar_list_t : entsize_list_tt {
    ivar_t first;
};

struct property_t {
    const char *name;
    const char *attributes;
};

struct property_list_t : entsize_list_tt {
    property_t first;
};

struct chained_property_list {
    chained_property_list *next;
    uint32_t count;
    property_t list[0];
};

typedef uintptr_t protocol_ref_t;
struct protocol_list_t {
    uintptr_t count;
    protocol_ref_t list[0];
};

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;
};

struct class_rw_t {
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t * methods;    // 方法列表
    property_list_t *properties;    // 属性列表
    const protocol_list_t * protocols;  // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
};

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

/* OC对象 */
struct wkb_objc_object {
    void *isa;
};

/* 类对象 */
struct wkb_objc_class : wkb_objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
public:
    class_rw_t* data() {
        return bits.data();
    }
    
    wkb_objc_class* metaClass() {
        return (wkb_objc_class *)((long long)isa & ISA_MASK);
    }
};


#endif /* WKBClassInfo_h */

将自定义的类强制转化为自定义的精简的class结构体类型。

// objective-c++
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import "WKBClassInfo.h"

// WKBPerson
@interface WKBPerson : NSObject <NSCopying>
{
@public
    int _age;
}
@property (nonatomic, assign) int no;
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end

@implementation WKBPerson

- (void)test
{
    
}

- (void)personInstanceMethod
{
    
}
+ (void)personClassMethod
{
    
}
- (id)copyWithZone:(NSZone *)zone
{
    return nil;
}
@end

// WKBStudent
@interface WKBStudent : WKBPerson <NSCoding>
{
@public
    int _weight;
}
@property (nonatomic, assign) int height;
- (void)studentInstanceMethod;
+ (void)studentClassMethod;
@end

@implementation WKBStudent
- (void)test
{
    
}
- (void)studentInstanceMethod
{
    
}
+ (void)studentClassMethod
{
    
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
    return nil;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    
}
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *object = [[NSObject alloc] init];
        WKBPerson *person = [[WKBPerson alloc] init];
        WKBStudent *student = [[WKBStudent alloc] init];
        
        wkb_objc_class *objectClass = (__bridge wkb_objc_class *)([NSObject class]);
        wkb_objc_class *studentClass = (__bridge wkb_objc_class *)([WKBStudent class]);
        wkb_objc_class *personClass = (__bridge wkb_objc_class *)([WKBPerson class]);
        
        wkb_objc_class *objectMetaClass = objectClass->metaClass();
        wkb_objc_class *personMetaClass = personClass->metaClass();
        wkb_objc_class *studentMetaClass = studentClass->metaClass();
        
        class_rw_t *objectClassData = objectClass->data();
        class_rw_t *personClassData = personClass->data();
        class_rw_t *studentClassData = studentClass->data();
        
        class_rw_t *objectMetaClassData = objectMetaClass->data();
        class_rw_t *personMetaClassData = personMetaClass->data();
        class_rw_t *studentMetaClassData = studentMetaClass->data();
        
        // 0x00007ffffffffff8
        NSLog(@"%p %p %p %p %p %p",  objectClassData, personClassData, studentClassData,
              objectMetaClassData, personMetaClassData, studentMetaClassData);

        NSLog(@"1111");
    }
    return 0;
}
instance对象

首先看instance对象,我们知道,instance对象中存储着isa指针和其他成员变量,并且instance对象的isa指针是指向其类对象地址的。


instance对象

由图可知instance对象中确实存储了isa指针和其成员变量,同时将instance对象的isa指针经过&运算之后计算出的地址确实是其相应类对象的内存地址。

class对象

接着看class对象,我们知道class对象中存储着isa指针,superclass指针,以及类的属性信息,类的成员变量信息,类的对象方法,和类的协议信息,而这些信息存储在class对象的class_rw_t中,我们通过强制转化来窥探其中的内容

class - class_rw_t

通过模拟对person类对象调用.data函数,即对bits进行&FAST_DATA_MASK(0x00007ffffffffff8UL)运算,并转化为class_rw_t,即上图中的personClassData。发现成员变量信息,对象方法,属性等信息只显示first第一个,如果想要拿到更多的需要通过代码将指针后移获取。而上图中的instaceSize = 16也同person对象中isa指针8个字节+_age4个字节+_no4个字节相对应起来。
那么类对象中的isa指针和superclass指针的指向如何,我们来验证一下
类对象中的isa指针和superclass指针

meta-class对象

最后看meta-class元类对象,meta-class中存储着isa指针,superclass指针,以及类的类方法信息。同时我们知道meta-class元类对象与class类对象,具有相同的结构,只不过存储的信息不同,并且元类对象的isa指针指向基类的元类对象,基类的元类对象的isa指针指向自己。元类对象的superclass指针指向其父类的元类对象,基类的元类对象的superclass指针指向其类对象。


meta-class - class_rw_t

可以看到结构同personClassData相同,并且成员变量及属性列表等信息为空,而methods中存储着类方法personClassMethod。
接着来验证isa及superclass指针的指向


meta-class - isa 指针指向

上图中通过地址证明meta-class的isa指向基类的meta-class,基类的isa指针也指向自己。
meta-class-superclass指针

上图中通过地址证明meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class类。

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