[iOS底层原理]从objc4源码看OC对象本质

245 阅读11分钟

写在前面

要了解OC对象的本质,则必须从objc4的源码来分析底层的实现,Object-C的底层都是通过C/C++来实现的,所以OC中的对象也会转化成C/C++中的某一个数据结构。

我们可以终端命令将Objective-C代码转换为C\C++代码,具体使用

  • xcrun表示利用xcode工具。
  • -sdk iphoneos用于指定SDK,表示说生成的c++代码是运行在iPhone上的。
  • clang是Mac上使用的编译器。
  • -arch arm64是指定架构,对于iOS平台,32位机器的架构是armv7,64位机器的架构是arm64,模拟器现在是x86_64架构。
  • -rewrite-objc mian.m 表示把oc代码转写成c/c++代码。
  • -o mian.cpp表示生成的c++代码并保存再mian.cpp文件中
# 没指定架构
clang -rewrite-objc main.m -o main.cpp
# 指定真机
xcrun -sdk iphoneos clang -rewrite-objc main.m  
# 指定模拟器
xcrun -sdk iphonesimulator clang -rewrite-objc main.m 
# 指定真机 64位机构
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

OC对象的内存布局

由于OC中大部分的对象都是以NSObject作为基类,所以可以从NSObject入手来分析对象的内存布局。

NSObject的底层实现

通过头文件NSObject.h文件可以看到NSObject类的定义如:

@interface NSObject <NSObject> {
    Class isa ;
}

以main文件举列,终端执行以下命令,将OC代码转成c/c++代码

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

最终在main.cpp中可以看到底层实现实质是一个结构体

struct NSObject_IMPL {
    Class isa;
};

而结构体中的成员isa是Class类型,通过objc4源码中的objc.h文件可以看到Class是一个指向object_class结构体的指针

typedef struct object_class *Class;

NSObject对象占用的内存大小

了解NSObject对象的底层结构后,可以思考下?:

问题:NSObject对象在64为环境下指针占8个字节,是不是意味着一个NSObject对象在内存中就占8个字节?

先上测试代码

NSObject *obj = [[NSObject alloc] init];
# 记得引入头文件#import <objc/runtime.h>   
NSLog(@"创建的对象至少需要的内存大小:%zd", class_getInstanceSize(obj.class));
# 记得引入头文件#import <malloc/malloc.h>
# __bridge const void * -> 桥接成C语言的指针
NSLog(@"系统实际分配的内存大小:%zd", malloc_size((__bridge const void *)(obj)));
}
#终端打印结果
2020-03-18 09:48:44.473843+0800 TEST[4823:66898] 创建的对象至少需要的内存大小:8
2020-03-18 09:48:44.474538+0800 TEST[4823:66898] 系统实际分配的内存大小:16

最终结论如下

创建一个NSObject对象,系统实际分配的内存大小是16个字节,真正使用的内存大小是8个字节

  • class_getInstanceSize:基于运行时runtime的一个函数,它返回的是类的一个实例的大小(结构体内存对齐的大小),对于NSObject对象而言是8个字节。
  • malloc_size:系统实际分配的内存大小。对NSObject而言分配的是16个字节
  • 结构体的内存对齐:
    结构体内存对齐的规则是结构体总大小必须是结构体中最大成员所占内存大小的倍数。

思考:为什么NSObject只需要占用8个字节就可以了,但系统却分配了16个字节?

  • 在MAC中以16为倍数是操作系统最优的内存分配方案,有利于内存管理
  • 底层源码要求操作系统分配的内存大小必须是16的倍数,如下:
   # malloc_size最终调用的是objc-runtime-new.h文件中的下面这个方法
   size_t instanceSize(size_t extraBytes) {
     size_t size = alignedInstanceSize() + extraBytes;
     CF requires all objects be at least 16 bytes.
     if (size < 16) size = 16;
     return size;
}

自定义对象的内存大小

先定义一个继承自NSObject的Student类,然后再来分析自定义对象的内存大小。

@interface Student : NSObject
@property (nonatomic , assign) int age;
@property (nonatomic , assign) int no;
@end

转换为C/C++后的底层结构如下:

struct NSObject_IMPL {
   Class isa;
};
struct Student_IMPL {
    //这个就是上面那个结构体里面的成员变量isa指针,子类包含了父类的结构体
   struct NSObject_IMPL NSObject_IVARS;//8个字节
   int age;//4个字节
   int height;//4个字节
};

结论猜想

  • 由于Student的构成=isa(8字节)+age(4字节)+no(4字节)=16个字节,那么Student的实例对象占用的内存和实际使用的内存都是是16个字

结论验证

 Student *stu = [[Student alloc] init];
 NSLog(@"%zd", class_getInstanceSize(stu.class));
 NSLog(@"%zd", malloc_size((__bridge const void *)stu));
 // 终端打印结果
 2020-03-18 10:39:04.355639+0800 TEST[5031:94902] 16
 2020-03-18 10:39:04.355700+0800 TEST[5031:94902] 16

所以自定义对象系统分配的内存和使用的内存都是16。

特殊情况下的内存大小

@interface Person : NSObject
@property (nonatomic , assign) int age;
@end
@interface Student : Person
@property (nonatomic , assign) int no;
@end

转换成C/C++后的底层数据结构

struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8
    int age; // 4
};
struct Student_IMPL {
    struct Person_IMPL Person_IVARS; // 16
    int no; // 4
};

思考:这里Student所占内存是16,20还是32呢

结论验证

 Student *stu = [[Student alloc] init];
 NSLog(@"%zd", class_getInstanceSize(stu.class));
 NSLog(@"%zd", malloc_size((__bridge const void *)stu));
 // 终端打印结果
 2020-03-18 10:39:04.355639+0800 TEST[5075:102665] 16
 2020-03-18 10:39:04.355700+0800 TEST[5075:102665] 16

很奇怪,如果按照之前所说的系统给一个对象分配的内存大小都应该是是16的倍数,那么这里应该是32为什么是16呢?

虽然Person系统分配的内存是16,但实际上只占用了12个字节,还有4个自己是空出来的,刚好可以分配给Student中的成员变量no使用,所以Student所占内存还是16。

OC底层中的3种对象类型

OC中的对象主要有3类,实列对象、类对象和元类对象。具体底层的数据结构就不详细描述,会在runtime相关的文章中进行详细阐述。

下面是类对象(元类对象)底层的结构图:

instance实列对象

实列对象是平时开发中最容易感知的,是通过alloc方法创建出来的对象,每次调用alloc方法都会生成新的instance对象。比如:

Student *student = [[Student alloc] init]
Person *person = [[Person alloc] init]

student 、person都是实例对象,因为他们都有自己的独有内存,所以是两个不同的对象,内存中存放的信息主要包括:

  • isa指针(因为基本上我们常用的类以及自定义类都继承自NSObject,所以我们这里讨论的instance里面都包含isa指针)
  • 其他成员变量

class类对象

OC中每个类都有一个与之对应的类对象,而且有且只有一个类对象,与实例对象相比,类对象的内存结构要复杂很多,内存中存储的信息主要包括:

  • isa指针
  • superclass指针
  • 类的属性信息(@property)
  • 类的对象方法信息(instance method,带减号那种)
  • 类的协议信息(protocol)
  • 类的成员变量信息(这里存储的是成员变量名字、类型等信息,这个和实例对象中存储的成员变量数据不是一个概念)

获取类对象的方法主要有以下几种

 Class cls1 = Student.class;
 Class cls2 = [Student new].class;
 Class cls3 = object_getClass([Student new]);
 NSLog(@"%p, %p, %p", cls1, cls2, cls3);
 // 打印结果
 2020-03-18 11:15:29.628881+0800 TEST[5147:115211] 0x7fff96d9a118, 0x7fff96d9a118, 0x7fff96d9a118

从打印结果可以看出,一个类的类对象是唯一的,在内存中只存一份。

meta-class元类对象

思考:类对象是用来存储实例对象的信息的,那类对象的信息(比如类的类方法信息)又是存在哪里呢?这就是将要要介绍的元类对象

元类对象和类对象的内存结构是一样的,只是具体存储的信息不同,用途也不同。每个类在内存中有且只有一个meta-class对象,元类对象存储的信息主要包括

  • isa 指针
  • superclass 指针
  • 类方法(+开头的方法)信息

获取元类对象的方法:

Student stuCls = [Student new];
 // 获取元类对象
Class metaCls = object_getClass(Student.Class);
 NSLog(@"Student的元类名字: %@", NSStringFromClass(cls));
NSLog(@"metaCls是否是元类:%d", class_isMetaClass(metaCls) 
// 打印结果
2020-03-18 11:31:09.494480+0800 TEST[5232:125347] Student的元类名字: NSObject
2020-03-18 11:31:09.494480+0800 TEST[5232:125347] metaCls是否是元类: 1

小结

获取类对象,元类对象常用方法的区别和注意点

Class objc_getClass(const char *aClassName)
  • 入参是一个字符串,也就是类名
  • 返回值是对应的class类对象
  • 因为我们通过字符串,只能定义类的名字,所以这个方法只能返回class对象
Class objc_getClass((id obj)
  • 入参obj可以是instance对象、class对象或者meta-class对象
  • 返回值
  1. 传入instance对象,返回对应的class对象
  2. 传入class对象,返回对应的meta-class对象
  3. 传入meta-class对象,返回NSObject(基类)的meta-class对象
// 底层源码实现,返回的是isa指针
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

Class方法

- (Class)class 
+ (Class)class
  • 不管(Class)class 还是 + (Class)class,都只能返回一个类的`class对象 ,不会是meta-class对象

OC之isa指针和superclass指针

所有继承自NSObject的对象都有isa指针,所有类对象和元类对象都有superclass指针,对于方法的调用,不管类方法还是实列方法,最终的本质都是消息调用,如下

//receiver: 是调用方法的对象
//op: 是要调用的方法名称字符串
id  objc_msgSend(id receiver, SEL op, ...)

那isa和superclass 指针在消息调用的作用是什么,下面通过Sutdent和Person来举列

@interface Person : NSObject
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end

@interface Student : Person

@end

为了完整描述了isa、superclass指针的作用,为了更加便于理解,我们在下面的图例中用Student代替subclass,Person代替superclass,NSObject代替rootclass来进行举列。

说明:

  1. instance的isa指向class(类对象)
  2. class的isa指向meta-class(元类对象)
  3. 所有元类的isa指针都是指向元类的根类(包括元类的根类的isa指针也是指向它自己)
  4. class的superclass指向父类的class,如果没有父类,superclass指针为nil
  5. meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class(NSObject)

实列方法的调用流程

如果调用的是实列对象的对象方法

  Student *stu = [[tudent alloc] init];
  [Student studentInstanceMethod];

但是实列对象的方法并不是存储在实例对象中,而是在类对象中,那实例对象如何查找到studentInstanceMethod这个方法呢?这里isa和superclass指针就起作用了。

  1. 先通过isa指针进入Student类的class对象,如果在其中找到了studentInstanceMethod方法就直接进行调用,调用过程结束,
  2. 没找到的话,就通过class对象的superclass指针进入Student类的父类,也就是Person类的class对象,看是否在Person类对象中有该方法
  3. 以此类推,一层一层往上寻找,如果最终到了基类,也就是NSObject类的class对象里面,还没找到的话,由于它的superclass为nil,最终就会碰到一个经典的报错[ERROR: unrecognized selector sent to instance],调用轨迹结束

类方法的调用流程

如果子类调用父类的类方法,调用轨迹如下图:

  [Student personClassMethod];

  1. 首先,通过Student的class对象的isa指针找到其meta-class对象,然后在方法列表里面寻找是否有personClassMethod方法,有的话就调用,调用逻辑结束。
  2. 没有的话,就通过meta-class对象的superclass指针找到Student的父类Person的meta-class对象,然后查找是否有该方法,找到就调用,调用逻辑结束。
  3. 没有的话,就通过Person的meta-class对象的superclass指针,重复上一步的流程
  4. 以此类推,通过meta-class对象的superclass指针,一层层往上查找
  5. 如果到了基类(NSObject)的meta-class还没能够找到,接下来的superclass指针会找到NSObject的class对象,你可能会奇怪,我们调用一个类方法,怎么跑到class对象里面来了,先保留你的疑问,只需记住,苹果确实是这么设计的,此时会继续在NSObject的class对象里面,找到就调用。
  6. 如果还没有找到,由于此时的superclass是nil,最终系统将给出报错