iOS底层原理

小码哥底层原理笔记:OC对象的本质

2020-04-28  本文已影响0人  chilim

(默认64位系统下)
OC的对象结构都是通过基础C\C++的结构体实现的。

NSObject的本质

我们先看一个简单对象NSObject:

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

我们通过命令行将OC的mian.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 

在main-arm64.cpp 文件中搜索NSObjcet,可以找到NSObjcet_IMPL(IMPL代表 implementation 实现)代码如下:

struct NSObject_IMPL {
    Class isa; //8个字节
};
//指针
typedef struct objc_class *Class;

可见一个NSObject对象其实就是一个NSObject_IMPL结构体。
通过以下代码打印得知一个NSobject类实例对象的大小是8字节,也就说NSObject_IMPL结构体大小是占8字节,由于NSObject_IMPL结构体中只有一个isa指针成员变量,所以一个isa指针占8个字节。

//获得NSObject类实例对象的大小:8个字节
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

我们再通过以下代码看到iOS系统实际分配了16个字节给这个NSobject对象,但是NSobject对象实际只用了8个字节

//获得obj指针所指向的内存大小:16个字节,虽然分配了16但是实际只是用了8个字节
NSLog(@"%zd", malloc_size((__bridge const void *)obj));

当然方法也占用内存空间,但是实例对象并不保存方法,方法存在其他地方。

自定义类Person的本质

Person对象继承自NSObject

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

同理,我们转化成C++代码如下:

struct NSObject_IMPL {
    Class isa;
};

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;//占8字节,即:Class isa;
    int _age;//占4字节
    int _height;//占4字节
    int _no;//占4字节
};//20字节,内存占用大小应该是8的倍数,所以最终是占24个字节

因为OC对象的本质就是一个结构体,所以结构体占用多少自己,对象就占用多少字节。
我们分析,一个Person对象总共有4个成员变量,父类NSObject的NSObject_IMPL占8个字节,三个int成员变量分别占4个字节,所以加一起Person对象一共占20字节,但是内存占用大小应该是8的倍数,所以最终打印出来是占用24个字节。

NSLog(@"%zd", sizeof(struct Person_IMPL));//24结构体实际所占大小
Person *person = [[Person alloc] init];//实例对象的方法不存放方法
NSLog(@"person - %zd", class_getInstanceSize([Person class]));//24,对象实际所占大小

但正常的OC对象的属性是@property,如下代码:

@interface Person : NSObject
{
    int _age;
}
@property (nonatomic, assign) int height;//也是转化为成员变量保存
@end

实际上上述代码相当于:

@interface Person : NSObject
{
    int _age;
    int _height;
}
///拆分成成员变量和get,set方法,这里的方法不保存在实例对象中,保存在对象的分类中。因为方法只需要保存一份就行,如果保存在实例对象中,那么每次alloc初始化一个实例对象就要copy一份方法。这样就浪费内存了。
- (void)setHeight{
    _height = 0;
}
- (int)getHeight{
   retutrn _height;
}
@end

我们再来看一个更复杂一点的

/* Person */
@interface Person : NSObject
{
    int _age;
}
@end
@implementation Person
@end
/* Student */
@interface Student : Person
{
    int _no;
}
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSLog(@"%zd  %zd",
              class_getInstanceSize([Person class]),
              class_getInstanceSize([Student class])
              );
//实际分配了多少字节
NSLog(@"person - %zd  student - %zd", malloc_size((__bridge const void *)person),malloc_size((__bridge const void *)stu));
    }
    return 0;
}

打印结果:

16  16
person - 16  student - 16

我们依据上面的分析与发现,类对象实质上是以结构体的形式存储在内存中,画出真正的内存图例

Student类实例对象的存储结构
实际Person和Student对象在内存中都是以结构体的形式存储,看右半部分图可知。
我们发现只要是继承自NSObject的对象,那么底层结构体内一定有一个isa指针。因为NSObject实际上就是一个只包含isa指针的结构体
接下来我们分析Person实例对象中,NSObject_IMPL占用8个字节,_age占4个字节,由于一个OC对象至少占16字节,所以Person实例对象占16字节。在Student实例对象中Person_IMPL占12个字节,_no占4个字节,所以Student实例对象也是占16字节。
再来一个例子:
struct NSObject_IMPL {
    Class isa;
};

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;//占8字节
    int _age;//占4字节
    int _height;//占4字节
    int _no;//占4字节
};//20字节,内存占用大小应该是8的倍数,所以最终是占24个字节

@interface Person : NSObject
{
    int _age;
    int _height;
    int _no;
}

@end

@implementation Person

@end

@interface Student : Person
{
    int _weight;
}

@end

@implementation Student


@end

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


int main(int argc, char * argv[]) {
    @autoreleasepool {
        NSLog(@"%zd", sizeof(struct Person_IMPL));//24结构体实际所占大小
        Person *person = [[Person alloc] init];//实例对象的方法不存放方法
        NSLog(@"person - %zd", class_getInstanceSize([Person class]));//24,对象实际所占大小
        NSLog(@"person - %zd", malloc_size((__bridge const void *)person));//32, OC分配的大小,OC分配给对象的内存是16的倍数
    }
    return 0;
}

注意:class_getInstanceSize方法是获取实例对象实际占用的内存大小,malloc_size方法是获取系统分配给对象的内存大小
内存对齐原则
1、实际所占内存大小应该是8的倍数,例如算出来的实际所占大小是24,补齐之后应该是24。
2、iOS系统分配给对象的内存大小最少是16字节,并且是16的倍数,例如实际对象占用内存24,但是系统会分配32。

注:Double类型占8字节,其他如NSString,NSNumber等对象占16字节

面试题

一个NSObject对象占用多少内存?
答:系统分配了16个字节给NSObject(通过malloc_size函数获得),但是NSObject对象内部只使用了8个字节的空间(通过class_getInstanceSize函数获得)(64位系统下)

上一篇下一篇

猜你喜欢

热点阅读