Cx330

「重学iOS」Objective-C对象的本质及分类

2020-12-17 · 8 min read
Objective-C iOS

Objective-C的底层都是通过C/C++来实现的,所以Objective-C中的对象也会转化成C/C++中的某一个数据结构。

01

我们在终端里通过指令

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

将oc代码转化为c++代码,我们可以看到NSObject的底层结构是:

struct NSObject_IMPL {
    Class isa;
};

Class是一个指向对象的结构体指针

typedef struct objc_class *Class;

所以NSObject最终会转化成一个结构体,内部只有一个指向对象的结构体指针。

所以NSObject对象只会使用8个字节的内存空间来存储指针(当然实际上给它分配了16个内存空间)。

NSLog(@"%zd",class_getInstanceSize([NSObject class])); //实例对象的成员所占用的大小8 (实际使用的) 
NSLog(@"%zd",malloc_size((__bridge const void *)(obj))); //整个结构体占用的是16(实际分配的)

同时,通过阅读源码我们得知,当创建的对象分配的内存空间小于16个字节的时候,系统都会分配16个字节的空间,这属于是苹果规定。

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;
}

如果有一个student类继承了object并且有俩个int属性,那么student所占用的内存是多少呢?

03

student实际占用内存为16字节,系统分配的内存也是16字节。

假设有个person继承NSObject,student继承person,那么person和student各占用多少内存呢?

02

最终通过打印我们发现,person,实际占用16,系统分配16,student实际占用16,系统分配16.

为什么?person实际占用16??int 4个字节 isa 8个字节 应该是12个字节啊?这就涉及到了前面写到的结构体内存对齐了。


对象的分类

oc中的对象主要可以分为三类:

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

实例对象

就是通过类alloc出来的对象,每次调用alloc都会生成一个新的实例对象。

04

object1 和 object2 就是两个实例对象。

  • 实例对象在内存中存储的信息包括:

    • isa指针 (其实isa也算是对象的成员变量 也就是说实例对象内部只包含自己的成员变量)
    • 其他成员变量(这里是存储成员变量的具体值)
    05

类对象(class)

06

objectClass1-5 都是NSObject的类对象 ,因为每个类在内存中有且只有一个类对象 所以上面五个类对象其实是同一个对象。

  • 类对象在内存中存储的信息主要有:

    • isa指针
    • superclass指针
    • 成员变量(这里的成员变量只是描述性的,比如有哪些变量,是什么类型的,并不是实例对象的具体变量值)
    • 类的对象方法(-号开头的方法)
    • 类的协议信息和属性信息
    07

    类对象的本质结构

    08

    元类对象

    09

    objectMetaClass就是NSObject的元类对象,元类对象也是每个类在内存中有且只有一个,元类对象和类对象在结构上非常相似。

    • 元类对象在内存中春初的主要信息有:

      • isa指针
      • superclass指针
      • 类方法(+号开头的方法)
      10.jpg

我们看到通过object_getclass方法即能获得元类对象 也能获得类对象  通过查看源码我们可以得知object_getclass会判断传进来的参数是类对象还是实例对象  如果是实例对象则返回类对象  如果传进来的是类对象则返回元类对象

我们也可以通过下面的函数来判断对象是不是元类对象

11.png

也就是说通过alloc创建的是实例对象,通过object_getclass(类对象),创建的是元类对象,其他对象则是类对象,但是类对象和元类对象有且只有一个。

三类对象中 都含有isa指针,那么这个isa指针指向什么?

实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向基类的元类对象。

正是通过isa指针,才让三种对象产生关联。

比如说,一个实例对象想调用对象方法,但是对象方法存放在类对象中,那么就是通过isa找到对象方法再进行调用。

同理,当调用类方法的时候,类方法是存放在元类对象中的,类对象通过isa指针找到元类对象,读取类方法列表中的类方法进行调用。

12.jpg

superclass指针

在类对象和元类对象中都有一个superclass指针,其实这两种对象中的superclass指针作用类似,都是指向父类对象。

  • 类对象中的superclass指针

    比如现在有一个Person对象继承自NSObject,有一个Student继承自Person,当studen的实例对象调用对象方法的时候,首先实例对象会根据自己的isa指针去类对象中找有没有对应的方法 没有的话类对象会根据自己的superclass指针去父类的类对象中去查找(也就是student的类对象根据superclass指针去Person的类对象中去查找有没有对应的对象方法,再没有的话Person的类对象会根据自己的superclass指针去NSObject的类对象中去寻找,寻找到基类在没有对应方法的话就会报方法找不到的错误)

    13.jpg
  • 而元类对象中的superclass指针也是指引类对象去父类对象中寻找对应的类方法:

    按照上面的例子,Student这个类想调用一个类方法,首先是Student的类对象根据isa指针去Student的元类对象中查找有没有对应的类方法,没有的话Student的元类对象会根据自己的superclass指针去父类的元类对象(也就是Person的元类对象)中查找有没有对应的类方法,在没有的话Person的元类对象再根据自己的superclass指针去NSObject的元类对象中寻找,有的话进行调用,没有的话NSObject的元类对象会根据superclass指针去NSObject的类对象中去寻找是否有相同名称的对象方法(这个地方下面会具体讲到为什么基类的superclass指针会指向对应的类对象)。

    14.jpg
15.jpg

关于上面提到的为什么基类的superclass指针为什么在找不到方法的时候会指向基类的类对象,也就是为什么没有找到对应的类方法的情况下却可以调用同名对象方法?

  • 关于这一点我们通过代码来验证:

    首先我们新建一个NSObject的分类,在.h文件中声明一个test的类方法,但在.m文件中并未实现这个类方法  而是实现了同名的对象方法

    #import "NSObject+Test.h"
    
    @implementation NSObject (Test)
    
    //+ (void)test
    //{
    //    NSLog(@"+[NSObject test] - %p", self);
    //}
    
    - (void)test
    {
        NSLog(@"-[NSObject test] - %p", self);
    }
    
    @end
    

    我们调用类方法发现,及时没有对应的类方法,程序也可以正常运行,并且成功调用了同名的对象方法:

    16.jpg

    假如我们在m文件没有实现同名test的对象方法,那么程序会报错的:

    +[NSObject test]: unrecognized selector sent to class 0x7fffaddd7140

    关于在.h文件中有类方法的声明,这个是没有影响的,因为没有这个声明的话程序根本跑不起来 。我们关注的点是基类的superclass指针为什么在找不到方法的时候会指向基类的类对象寻找同名的对象方法。

    比如我们在.h文件中声明了test的对象方法,.m文件没有实现test方法,同样会报unrecognized错,这就是因为基类的对象方法中找不到方法后直接返回空值,而不是像类方法一样从元类对象找不到再去到类对象找同名对象方法。

    关于基类的superclass指针为什么在找不到方法的时候会指向基类的类对象,这是因为oc在调用方法的时候实际上是转换为c/c++去底层实现的,但是c/c++的底层实现并没有区分类方法还是对象方法,也就是没有区分+-号。

    比如

    [NSObject test];
    

    实际上是转换为了

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

    没有区分+-号,所以在基类元类对象没有找到对应的类方法后回去基类的类对象中查看是否有同名的对象方法 ,有的话就调用 ,再没有的话就报错了。

参考原文链接