OC对象的本质

1,054 阅读6分钟

OC转换成C/C++

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

Objective-C被编译过程是:Objective-C--> C/C++ --> 汇编 --> 机器语言

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

如果要想看C++文件长什么样子?我们可以使用命令将OC代码转换成C/C++代码。 命令行如下:

xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  输出的CPP文件

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

NSObject来说,创建一个对象NSObject *obj = [[NSObject alloc]init]; 按住cmd直接查看

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

仅仅看到一个Class类型的isa,转换成C++文件再查看一下:

struct NSObject_IMPL {
    Class isa;
};

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

对于结构体来说,和数组一样。其第一个成员的地址,即为结构体对象的地址。 所以一个OC对象的地址,实际上就是其isa指针的地址。

如果这个类中有多个成员,会是什么样子呢?

@interface  Cat : NSObject
{
    NSString *_name;
    int _age;
}
@end
@implementation Cat
@end

转换后:

struct Cat_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *_name;
    int _age;
};

其中NSObject_IMPL是:
struct NSObject_IMPL {
   Class isa;
};

汇总后:
struct Cat_IMPL {
    Class isa;
    NSString *_name;
    int _age;
};

所以执行下面代码的时候:

Cat *cat = [[Cat alloc]init];
cat->_age = 10;
cat->_name = @"Tom";

可以看出cat这个实例对象是个指针,指向了一个struct Cat_IMPL结构所对应的内存空间;这个内存空间存储着实例对象各个成员变量的值;

这个内存空间的地址,就是该内存空间第一个元素的内存地址Class isa;而isa指针指向Cat这个类的类对象。

我们验证一下,将cat指针通过桥接方式(C语法)指向struct Cat_IMPL:

 Cat *cat = [[Cat alloc]init];
 cat->_age = 10;
 cat->_name = @"Tom";
 struct Cat_IMPL *catImpl = (__bridge struct Cat_IMPL *)cat;
 NSLog(@"name is %@, age is %d", catImpl->_name, catImpl->_age);
 
 打印结果:name is Tom, age is 10

所以说:Cat这个类,转换成C++就是struct Cat_IMPL这个结构体,而cat指针就指向一个这样结构的结构体。

实例对象占用多少空间?

cat这个对象所占用的内存空间也显而易见了:一个isa指针8个字节,一个name指针8个字节,一个int型4个字节,所以这个对象只需要20个字节来存储;真是这样吗?

结构体也有自己的对齐方式,结构体大小都是最大成员大小的的整数倍 但是考虑到结构体对齐,此处会有 8*3 = 24个字节;

但但是,OC中有自己的对齐方式,iOS系统分配内存时,都是16 * N,便于CPU快速访问,所以cat对象实际分配了32个字节;

查看占用空间的方式:

创建一个实例对象,至少需要多少内存
class_getInstanceSize([NSObject class])

创建一个实例对象,实际分配多少内存
malloc_size((__bridge const void *)obj)

OC中的对象

主要可以分为3种

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

实例对象

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

创建两个实例对象

NSObject *obj1 = [[NSObject alloc]init];
NSObject *obj2 = [[NSObject alloc]init];

obj1 obj2是不同的两个对象,分别占据着两块不同的内存,一个类在内存中的实例对象可以有多个。

实例对象在内存存储的信息包括:isa指针 、 其他成员变量的值

类对象

每个类只有一个类对象,也只有一个元类对象。

类对象,内存中只要一份
Class objectClass1 = [obj1 class];
Class objectClass2 = object_getClass(obj1);
Class objectClass3 = [NSObject class];

NSLog(@"objectClass1 = %p",objectClass1);
NSLog(@"objectClass2 = %p",objectClass2);
NSLog(@"objectClass3 = %p",objectClass3);

打印结果
objectClass1 = 0x7fff88a67e08
objectClass2 = 0x7fff88a67e08
objectClass3 = 0x7fff88a67e08

class方法返回的一直是类对象

class方法源码:

-(Class){
    return self->isa;
 }

+ (class){
    return self;
}

验证一下

Class objectClass4 = [[NSObject class]class];
Class objectClass5 = [[[NSObject class]class]class];

NSLog(@"objectClass4 = %p",objectClass4);
NSLog(@"objectClass5 = %p",objectClass5);

打印结果
objectClass4 = 0x7fff88a67e08
objectClass5 = 0x7fff88a67e08

类对象主要包括: isa指针superclass指针属性信息对象方法信息协议信息成员变量信息

元类对象

元类对象和类对象内存结构是一样的,但用途不一样。同样包含:

isa指针superclass指针属性信息对象方法信息协议信息成员变量信息

Class objectMetaClass = object_getClass([NSObject class]);

objectMetaClass是NSObject的meta-class对象(元类对象)

每个类在内存中有且只有一个meta-class对象

补充一下:

object_getClass(id obj)

传入的obj可能是instance对象class对象meta-class对象

返回值:

  • 如果是instance对象,返回class对象

  • 如果是class对象,返回meta-class对象

  • 如果是meta-class对象,返回NSObject(基类)的meta-class对象

实例对象、类对象、元类对象三者的关系

说道三者的关系就不得不提网上流传着那张图

总体来说:

  • 实例对象(instance)的isa指向类对象(class)
  • 类对象(class)的isa指向元类对象(meta-class
  • 元类对象(meta-clsaa)的isa指向基类的元类(meta-class
  • 类对象的superclass指向父类的类对象(class),如果没有父类,superclass指针为nil
  • 元类(meta-class)的superclass指向父类的元类(meta-class),基类元类(meta-class)的superclass指向基类的class
  • instance调用方法轨迹:isa找类对象,方法不存在,就通过superclasss找父类,调用父类的实例方法,依次找下去

补充:

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

关于基类元类的superclass指针为什么在找不到方法的时候会指向基类的类对象?

这是因为OC在调用方法的时候实际上是转换为C/C++去底层实现的; 但是C/C++的底层实现并没有区分类方法还是对象方法 也就是没有区分+ -

比如[NSObject test] 实际上是转换为了

objc_msgSend([NSObject class], @selector(test))

没有区分+ -号 所以在基类元类对象没有找到对应的类方法后,会去基类的类对象中查看是否有同名的对象方法,有的话就调用,再没有的话就进入动态方法解析、消息转发或者直接报错。

以上若有错误,欢迎指正。转载请注明出处。