iOS进阶之路iOS

OC 对象的本质

2018-03-13  本文已影响671人  一位不愿透露姓名的王先生_

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

一个指针变量所占用的大小(64bit->8个字节,32bit->4个字节)

我们平时编写的Objective-C代码,底层实现都是C/C++代码,Objective-C的面向对象都是基于C/C++的数据结构实现的。

Objective-C -> C/C++ -> 汇编语言 -> 机器语言

如果想研究一些本质问题,最好将Objective-C代码转化成C/C++代码,才比较容易分析出来原理。

Objective-C的对象、类主要是基于C/C++的什么数据结构实现的?

假设一个Person类,有下面属性

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

对应C/C++是以结构体的形式存在的

struct Person {
    int _age;
    int _no;
    double _height;
    char *_name;
}

main.m中下面代码转成C++代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSObject *obj = [[NSObject alloc] init];
        
        NSLog(@"%p", obj);
    }
    return 0;
}

终端输入如下命令

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

只生成在真机上arm64架构下的文件,如果不指定架构,则生成文件包含其它架构,并且文件要比这个大一点。

main.cpp中的cpp就是c plus plus的意思

并且项目中多了一个main-arm64.cpp文件

main-arm64.cpp文件用Xcode打开后,你会发现,虽然在main.m中只写了一行代码,转换成C++的代码就有3w多行。

main-arm64.cpp文件中可以发现在NSObjectIMPL(Imeplemetation)方法中只有一个Class isa

struct NSObject_IMPL {
    Class isa;
};

我们可以猜想在NSObject的底层实现,就是一个NSObject_IMPL

一个 OC 对象在内存中是如何布局的?

Foundation框架中我们可以看到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

可以省略成如下格式

@interface NSObject {
    Class isa;
}
@end

对比main-arm64.cppNSObject类的实现

struct NSObject_IMPL {
    Class isa;
};

相当于NSObject对象创建后在内存中就是以结构体的形式存在的。

该结构体中只有一个成员isa,为指针类型,在64位结构下占用8个字节。

NSObject对象的声明中除了Class isa;这个成员变量外,也还有其它的一些方法,只不过不存在于NSObject对象的存储空间。存处于其它对应的位置。

NSObject *obj = [[NSObject alloc] init];这句代码做了什么事情?

  1. alloc分配存储空间给NSObject对象
  2. 将存储空间的地址值(isa的地址值)赋值给obj指针进行存储。obj就可以指向NSObject对象。(obj指针存储的值就是NSObject对象中isa的地址值)

自定义类探究

上面研究了NSObject对象的本质,那么我们平时工作中创建的对象的本质是什么样的呢?

自定义一个Student类,直接写在main.m中。

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

@implementation Student

@end

将其转换成C++代码

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

我们可以发现在生成的C++文件内,Student类的实现是如下格式

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

这其中包含的struct NSObject_IMPL NSObject_IVARS;是指NSObject的实现,由于NSObject的实现里面只有一个Class isa;,所以NSObject_IMPL这个结构体占用的内存空间和单独一个isa占用的内存空间都是8个字节。

因此,上面的Student_IMPL结构体等价如下

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

所以,我们创建一个Student对象需要Class isa(8字节)+int _no(4字节)+int _age(4字节)=16字节

通过结构体访问属性

main.m中增加如下代码

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

用结构体去访问属性

Student *student = [[Student alloc] init];
student->_no = 4;
student->_age = 5;

struct Student_IMPL *studentImpl = (__bridge struct Student_IMPL *)student;
        
NSLog(@"_no = %d, _age = %d", studentImpl->_no, studentImpl->_age);

输出

_no = 4, _age = 5

由此说明,stundet指针指向的内存确实是Student_IMPL结构体。

通过运行时获取实例对象占用内存的大小

class_getInstanceSize()

我们同样可以通过上面的运行时方法获取到实例对象占用的内存大小

NSLog(@"%zd", class_getInstanceSize([NSObject class]));
NSLog(@"%zd", class_getInstanceSize([Student class]));

输出结果仍为

NSobject = 8
Student = 16

窥探内存结构

通过DebugView Memory查看

调用Debug->Debug Workflow->View Memory

在下图Address位置输入stundent对象地址后,我们就可以看到其内存结构

由于现在计算机普遍都是小端模式,计算机读取地址的时候也是从高地址开始读取。因此,成员内存地址分别为

0x001D808001000011E9
0x00000004
0x00000005

通过lldb查看

po(print object)打印出student的地址

(lldb) po student
<Student: 0x10040fb20>

再进行内存读取memory read (address)

(lldb) memory read 0x10040fb20
0x10040fb20: e9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00  ................
0x10040fb30: d0 fa 40 00 01 00 00 00 10 ee 40 00 01 00 00 00  ..@.......@.....

由此可以看到,和我们用DebugView Memory的内存结构是一样的。

拓展

x等价于memory read

(lldb) x 0x10040fb20
0x10040fb20: e9 11 00 00 01 80 1d 00 04 00 00 00 05 00 00 00  ................
0x10040fb30: d0 fa 40 00 01 00 00 00 10 ee 40 00 01 00 00 00  ..@.......@.....

以固定格式读取内存

4次、4个字节、16进制格式读取

(lldb) x/4xw 0x10040fb20
0x10040fb20: 0x000011e9 0x001d8001 0x00000004 0x00000005
读取内存
格式
字节大小
修改内存中的值

比如我们想要修改Student类中_no = 4;的值

先查看内存地址

(lldb) x/16xb 0x10040fb20
0x10040fb20: 0xe9 0x11 0x00 0x00 0x01 0x80 0x1d 0x00
0x10040fb28: 0x04 0x00 0x00 0x00 0x05 0x00 0x00 0x00

注意此时0x10040fb28地址中第一个字节的值为4,下面进行修改

memory write 0x10040fb28 6

可以看到,_no的值被修改为6了。

更复杂继承关系的探究

Student->Person->NSObject这种继承关系的本质是怎样的呢?

代码如下

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

@implementation Person

@end

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

@implementation Student

@end

如果再求PersonStudent类分别占用多少内存空间呢?

NSLog(@"%zd", class_getInstanceSize([Person class]));
NSLog(@"%zd", class_getInstanceSize([Student class]));

结果会输出

16
16

下面我们来探究一下,将代码转成C++代码,我们可以看到Student对象的实现,和Person对象的实现。

Student对象的实现中包含了Person对象的实现,Person对象的实现包含了NSObject对象的实现。

因此,Person对象的实现等同于如下

struct Person_IMPL {
    Class isa;
    int _age;
};

isa占用8字节,_age占用4字节,根据结构体所占用内存空间大小并根据内存对其原则可知,Person对象占用16个字节。注意 : 不是12个字节

Student对象的实现

struct Student_IMPL {
    struct Person_IMPL Person_IVARS;
    int _no;
};

里面包含Person对象的实现(占16字节)和int _no(4字节),计算Student对象所占的字节数,同样还是16字节,因为Person里虽然占了16个字节,但是实际上有4个字节是空闲的,直接给Student对象的int _no用就可以了,不用再开辟新的存储空间了。注意 : 不能算成是20个字节

假如Student对象中还有一个int _height对象呢,Student占用多少内存空间呢?

答案是24个,还是根据内存对齐原则算出的。

上一篇下一篇

猜你喜欢

热点阅读