iOS OC对象的本质研究

什么是对象,OC对象和其他的结构有什么不同,为什么要了解和研究对象的本质.

  • 了解了对象的本质,也可以写出更加优秀,内存占用更合理的代码,提高系统的性能。
  • 了解了本质,才可以写出更优秀的代码,更好的完成需求。

OC ———> C/C++ ———> 汇编语言———> 机器语言

开发过iOS的同学,都知道OC会编译成C++,那么OC在内存中是怎么存储的?是以什么方式存储的呢?

  • 下面分析一段代码
NSObject *obj = [[NSObject alloc] init];
  • 按住command点进去可以看到苹果官方对NSObject的声明
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
  • 将OC转化成C++,请移步 OC转化C++​​​​​​​
  • 拿到转化后的C++代码,我们发现NSObject在C++中成了这
//NSObject底层实现
struct NSObject_IMPL {
    Class isa; 
};

struct 结构体吗,OC对象转化成C++代码后就是通过结构体的形式存储在内存中的。一个对象可能多个不同类型的属性,所以以结构体的形式存储。

通过上面的探索我们得出OC对象在编译的过程中转化成了C++代码,并且以结构体的形式在内存中存储。并且由一个指针指向这块内存空间,那么问题来了一个NSObject对象在没有成员自定义成员变量的情况下在内存中占了多大的内存空间。

//NSObject底层实现
struct NSObject_IMPL {
    Class isa; 
};

这行代码中的isa是个指针,那么一个指针在64bit环境下占8个字节,在32bit环境下占4个字节。那个NSObject只拥有一个Class isa成员变量是不是占有8个字节呢?

验证

// 获得NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
输出 8

// 获得obj指针所指向内存的大小 >> 16
 NSLog(@"%zd", malloc_size((__bridge const void *)obj));
输出 16

一个 NSObject 对象在创建后系统分配了16个字节,只是自己的成员变量占用了8个字节
或者可以说成
系统分配了16个字节给NSObject对象(通过malloc_size函数获得) 但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

通过malloc_size方法的底层实现可以知道,当系统对一个对象分配的内存小于16字节时,系统会强制分配16字节,也就是说对于一个对象系统最少分配16字节。

进一步验证

  • 创建一个Person类,添加两个成员变量
@interface Person : NSObject
{
    @public
    int _no;
    int _age;
}
@end

@implementation Person

@end
  • 拿到C++代码,是这样的
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _no;
    int _age;
};

接下来看看NSObject_IMPL是怎么样的

struct NSObject_IMPL {
    Class isa;
};

Person对象转化成C++后最终结果

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

但我们在执行这段代码的时候

Student *stu = [[Student alloc] init];
stu->_no = 4;
stu->_age = 5;
        
NSLog(@"%zd", class_getInstanceSize([Student class]));
NSLog(@"%zd", malloc_size((__bridge const void *)stu));

实际是stu 指针指向了struct Student_IMPL这个结构体所对应的内存空间,这个结构体的内存空间地址,就是该结构体第一个元素的内存地址 Class isa;也就是isa指针指向的地址

  • 验证
struct Student_IMPL *stuImpl = (__bridge struct Student_IMPL *)stu;
NSLog(@"no is %d, age is %d", stuImpl->_no, stuImpl->_age);
输出 4 5
  • 那个这个Student对象占据内存空间大小?系统给该对象分配内存大小呢?

 一个指针占8个字节,一个int类型占4个字节,两个那就是8个字节,合计16个字节

NSLog(@"%zd", class_getInstanceSize([Student class]));
输出 16
NSLog(@"%zd", malloc_size((__bridge const void *)stu));
输出 16

NSObject对象和Student对象所对应的内存图

那么

  • 创建一个Person类,写一个成员变量
@interface Person : NSObject
{
    @public
    int _no;
}
@end

@implementation Person

@end
  • 再创建一个Student类继承与Person类,写一个成员变量
@interface Student : Person
{
    @public
    int _age;
}
@end

@implementation Student

@end

那么Person 和 Student 分别占用多大的内存呢?

答案:16,16
通过上面的验证可以知道Person 转化成C++

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; //8字节
    int _no; //四字节
};

那么这两个字节相加应该是12字节?为什么是16字节呢,

上面说到过,苹果为一个实例对象开辟的最小内存空间为16字节,还有就是内存对齐原则,分配的总内存数是最大成员变量的整数倍,其中最大的成员变量是Class isa指针,8字节,整数倍也就是16字节

分析:通过上面的验证也可以知道Student 转化成C++

struct Student_IMPL {
    struct Person_IMPL Person_IVARS; //8字节
    int _no; //四字节
    int _age//四字节
};

三个成员变量相加正好16字节,又是8的整数倍,所以分配16字节

写了这么多只写到了成员变量那么属性呢?

@interface Student : NSObject
{
    @public
    int _age;
}
@property (copy,assign) int weight;
@end

@implementation Student

@end

那么创建一个这样的Student对象占多大的内存呢?

答案: 16个 属性weight 会自动生成_weight 成员变量,当然也还有get和set方法,那么两个成员变量8字节,一个指针8字节,相加16字节。

总结:一个实例对象在创建时至少分配16字节,每个对象都会有一个isa指针,再加上成员变量所占空间就是分配的总空间。

补充:指针类型成员变量如NSString *str, double number, long height 占8字节在64bit机器下,int float 占4字节,OC对象分配内存遵循内存对齐法则。

苹果内存对齐法则:分配的总内存数是最大成员变量的整数倍
那么真的是这样吗?还有什么其他的因素吗?

下面一探究竟:

  • 创建一个Person对象
@interface Person : NSObject
{
    int _age;
    int _height;
    int _no;
}
@end

@implementation Person

@end

  • 执行代码
MJPerson *p = [[MJPerson alloc] init];
NSLog(@"%zd %zd",
              class_getInstanceSize([MJPerson class]), // 24
              malloc_size((__bridge const void *)(p))); // 32
输出 24 32
  • 分析:
    一个Person对象,有_age,_height,_no,isa,这四个成员变量三个int类型,12字节,一个isa 指针8字节,总共20字节。
  • 内存对齐法则:成员变量所占内存最大成员变量的整数倍,所以占用了24字节。malloc_size 这个方法返回32字节,说明操作系统实际为这个对象分配了32字节,这个为什么?

结论:操作系统为对象分配的内存空间都是16的整数倍,所以也就有了malloc_size 这个方法返回32字节了

上面一篇文章讲了OC对象的本质,编译成C++对象是以什么形式存储的,一个对象占多少内存空间等问题;

那么在OC语言里面,又分为几种对象呢?

类对象分为三种:

  1. 实例对象
  2. 类对象
  3. 元类对象

这三中类型的对象之间是什么关系?每种类型的对象又有什么特点呢?

  • 实例对象

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];        
NSLog(@"%p %p",
              object1,
              object2);

打印:0x100648340 0x100647440

总结:不多讲了,第一篇文章已经写的很详细,
object1、object2是NSObject的instance对象(实例对象)
它们是不同的两个对象,分别占据着两块不同的内存

存储的是各个成员变量的值,还有一个isa指针,一个类在内存中的实例对象可以有多个。

内存存储图示

实例对象内存存储图示.png

  • 类对象

NSObject *object1 = [[NSObject alloc] init];
Class objectClass1 = [object1 class];
 
// 还可以通过一个runtime函数     
Class objectClass3 = object_getClass(object1);

object_getClass 函数:传入实例对象返回类对象,传入类对象,返回元类对象
下面实现以下代码

NSObject *object1 = [[NSObject alloc] init];
NSObject *object2 = [[NSObject alloc] init];
        
Class objectClass1 = [object1 class];
Class objectClass2 = [object2 class];
Class objectClass3 = object_getClass(object1);
Class objectClass4 = object_getClass(object2);
Class objectClass5 = [NSObject class];
        
NSLog(@"%p %p",
              object1,
              object2);
        
NSLog(@"%p %p %p %p %p",
              objectClass1,
              objectClass2,
              objectClass3,
              objectClass4,
              objectClass5);

打印:0x100648340 0x100647440
     0x7fff9dab4118 0x7fff9dab4118 0x7fff9dab4118 0x7fff9dab4118 0x7fff9dab4118

会发现两个实例对象的内存地址是不一样的,而通过两个实例对象获得的5个类对象的指针都是一样的,说明,一个类在内存中只有一个类对象。

总结:
objectClass1 ~ objectClass5都是NSObject的class对象(类对象)
它们是同一个对象。每个类在内存中有且只有一个class对象

内存存储图示

  • 元类对象

// object_getClass  函数:传入实例对象返回类对象,传入类对象,返回元类对象
Class objectClass3 = object_getClass([NSObject class]);

总结:
objectMetaClass是NSObject的meta-class对象(元类对象)
每个类在内存中有且只有一个meta-class对象
meta-class对象和class对象的内存结构是一样的,但是用途不一样,在内存中存储的信息

关于如何证明三种类型的对象分别存储的什么信息,通过阅读苹果源码可知

图示


想要更深层次的了解类的本质还是需要自己去阅读源码

分析:实例对象,类对象和元类对象中都有isa指针,而类对象和元类对象中除了含有isa指针之外还有superclass指针,

问题:isa指针和superclass指针分别有什么作用呢?

  • 分析 创建一个Person类给person类分别声明一个对象方法和一个类方法
// Person
@interface Person : NSObject <NSCopying>
{
    @public
    int _age;
}
@property (nonatomic, assign) int no;
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end

@implementation Person
- (void)personInstanceMethod{
}
+ (void)personClassMethod{
}
@end

// 调用 
Person *person = [[Person alloc] init];
[person personInstanceMethod];
[Person personClassMethod];
  • isa指针作用
    OC语言是消息机制 当执行[person personInstanceMethod]; 这行代码时 实际会被编译成 objc_msgSend(person, @selector(personInstanceMethod))进行调用,那么刚刚说到实例对象中不存储方法,那么当调用时这个对象方法是怎么获得的呢?
    答案:当实例对象调用对象方法时是通过isa指针,指向自己的类对象,找到类对象中的方法信息,进行调用。同理,当执行 [Person personClassMethod]; 这行代码时,实例对象通过isa指针指向自己的类对象,又通过类对象中的isa指针指向元类对象,获取到类方法信息。

    图示isa指针作用:

  • 创建一个Person类给person类分别声明一个对象方法和一个类方法,再创建一个继承于Person类的Student类给Student类分别声明一个对象方法和一个类方法

// Person
@interface Person : NSObject <NSCopying>
{
    @public
    int _age;
}
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end

@implementation Person

- (void)personInstanceMethod
{
    
}
+ (void)personClassMethod
{
    
}
@end

// Student
@interface Student : Person <NSCoding>
{
@public
    int _weight;
}
- (void)studentInstanceMethod;
+ (void)studentClassMethod;
@end

@implementation MJStudent
- (void)studentInstanceMethod
{
    
}
+ (void)studentClassMethod
{
    
}
@end

// 调用
Student *student = [[Student alloc] init];
[student personInstanceMethod];
[Student personClassMethod];     
  • superclass指针作用
    总结:当执行[student personInstanceMethod]这行代码时 student对象通过isa指针找到自己类对象,结果发现类对象中没有personInstanceMethod这个方法,然后利用superclass指针找到父类也就是Person的类对象,查看是否有 personInstanceMethod 方法信息,如果有 就调用,没有的话继续向上查找。同理:当执行[Student personClassMethod]; 这行代码时,student对象通过isa指针找到自己类对象,又通过类对象的isa指针找到自己的元类对象结果发现元类对象中没有personClassMethod这个方法,元类对象利用superclass指针向上逐级寻找父类的元类对象是否有此方法。直到NSobject停止,如果找到就调用,找不到就会奔溃 unrecognized selector sent to class 0x1000011a0'

图示superclass指针作用:

最后总结:

instance -> 实例对象,class -> 类对象,meta-class -> 元类对象

1.instance的isa指向class
2.class的isa指向meta-class
3.meta-class的isa指向基类的meta-class
4.class的superclass指向父类的class
5.如果没有父类,superclass指针为nil
6.meta-class的superclass指向父类的meta-class
7.基类的meta-class的superclass指向基类的class
8.instance调用对象方法的轨迹
9.isa找到class,方法不存在,就通过superclass找父类
10.class调用类方法的轨迹
11.isa找meta-class,方法不存在,就通过superclass找父类

图示:

  • 画一下[student personInstanceMethod]这行代码的执行顺序

  • 画一下[Student load]这行代码的执行顺序

猜你喜欢

转载自blog.csdn.net/yezuiqingxin/article/details/119572540