2. OC对象底层探究 - 476139183/Learning-iOS GitHub Wiki

前要: OC 的代码转换

我们可以通过下面的命令 对指定文件进行代码转换。这样方便我们查看底层实现

//! 可以指定 arm64 架构, 如果需要链接其他框架,使用-framework参数。比如-framework UIKit
//! -o 输出的 cpp 文件
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc mian.m -o main-arm64.cpp

一. OC 对象的种类

万物皆对象,在iOS里面,对象又分3大种类:

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

1.1 instance 对象

instance 对象就是通过 alloc 出来的对象,每次调用 alloc 都会产生新的 instance 对象,分别占据着不同的内存。

而 instance 对象在内存存储的信息包括

  • isa 指针
  • 其他成员变量

这个我们在分析 OC 对象 的内存分布规则 中 已经可以得知, 比如下面这个例子:

Person *p1 = [[Person alloc] init];
Person *p2 = [[Person alloc] init];

那么他们的内存分配如下:

例1

这两个实例对象分别有自己的内存存储,存储着其他成员变量的值

1.2 Class 对象

我们通 class 方法或 runtime 的 object_getClass() 方法得到一个class对象。class对象也就是类对象。比如:

Class objectClass1 = [p1 class];
Class objectClass2 = [NSObject class];

// runtime
Class objectClass3 = object_getClass(p1);
Class objectClass4 = object_getClass(p2);

每一个类在内存中有且只有一个class对象。属于全局的

class对象在内存中存储的信息主要包括:
  • isa指针
  • superclass指针
  • 类的属性信息(@property),类的成员变量信息(ivar)
  • 类的对象方法信息(instance method),类的协议信息(protocol)

成员变量的具体值是存储在实例对象中的,因为不同实例对象是不共享成员变量的值的。但是成员变量叫什么名字,是什么类型,只需要有一份就可以了。所以存储在class对象中。方法,协议等同理

1.3 meta-class 对象

同样,我们也可以通过 runtime 的 object_getClassobjc_getMetaClass 获取元类对象代码:

//! runtime 中传入类对象 此时得到的就是元类对象
Class objectMetaClass = object_getClass([NSObject class]);
//! 或者调用更底层的方法获取元类对象
Class objectMetaClass1 = objc_getMetaClass("NSObject"); 
//! 判断该对象是否为元类对象
class_isMetaClass(objectMetaClass) 

每个类在内存中有且只有一个meta-class对象。 meta-class对象和class对象的内存结构是一样的,但是用途不一样。

元类对象在内存中存储的信息主要包括

  • isa指针
  • superclass指针
  • 类的类方法的信息(class method)

meta-class 对象和 class 对象的内存结构是一样的,所以 meta-class 中也有类的属性信息,类的对象方法信息等成员变量,但是其中的值可能是空的。

二. isa 指针

OC对象的基础类 就是 NSObject,而 我们发现 NSObject 的 OC 定义如下:

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

实际上,通过继承的关系,我们可以得出结论,所有的对象,都有一个 isa 指针,而这个isa指针指向 objc_class, 那么 **objc_class**又是什么呢?

isa 其实就是 it is a object,所以我们可以认为,只要有 isa 指针,那么它就是一个OC对象

我们 通过 objc4源码 可以得知其结构:

objc_class 继承自 objc_object。 这个 objc_object 也是一个 isa 指针。如果说,一个实例对象的 isa 指向的是它的 类对象(class),那么这个 类对象的isa指针就指向它的元类(metaclass),也即类的类,就是元类。 他们的关系如下:

这样,我们在方法的寻址时,逻辑步骤可以保持一致:

  • 当调用 实例方法的时候,我们可以通过 instanceisa 找到 class,然后在里面寻找 实例方法 进行调用。
  • 当调用 类方法的时候,我们通过 classisa 找到 meta-class,然后在里面寻找 类方法 进行调用。

三. superclass 指针

我们发现 类对象 和 元类对象 都有一个 superclass的指针,指向的也是一个 Class 类型。假定我们在创建一个 Student 类 继承自 Person 类,那么不难得出。他们的 superclass指针的指向:

  • 当Student的 instance对象要调用 Person 的对象方法时,会先通过 isa找到 Studentclass,然后通过 superclass找到 Person 的class,最后找到对象方法的实现进行调用

与之对应的,两个元类关系如下:

  • 当Student的 class 要调用 Person 的类方法时,会先通过 isa找到 Student 的 meta-class,然后通过 superclass 找到 Person 的 meta-class,最后找到类方法的实现进行调用
那么将整个关系串连起来的话,得到如下关系图:

很多关系看图应该很清晰了,那么有两点要总结一下:

  1. 元类的isa指向的是,根类的元类

  2. 根类的元类的isa指向自己,而它的super_class指向的是根类。

补充

我们在调用 [p class] 的时候,可以看到如下函数调用:

获取class的对象的时候,并不是直接取 isa 指针,而是对其进行了一次与运算。

也可以直接去查看 源码 struct objc_object {中的 ISA()函数

事实上从 64bit 开始,iOS 对 isa 做了掩码,需要进行一次位运算,才能计算出真实地址

MASK掩码:

# if __arm64__ 
#   define ISA_MASK        0x0000000ffffffff8ULL
# elif __x86_64__ 
#   define ISA_MASK        0x00007ffffffffff8ULL
# endif

 Person *person = [[Person alloc] init];
 NSLog(@"personClass=%p",[Person class]);
 NSLog(@"person=%p",person);

我们分别得出 实例对象和类对象的地址。

personClass=0x600000000350
person=0x100002bd8

我们先获取 person的isa指针的内存

(lldb) p/x person->isa
(Class) $0 = 0x001d800100002bd9 Person

因为我是在macOS环境编译代码的,所以用的是 x86_64 架构。我们对其进行一次位运算

(lldb) p/x 0x001d800100002bd9 & 0x00007ffffffffff8ULL
(unsigned long long) $1 = 0x0000000100002bd8

可以看到 进行了位运算之后,两次地址是一样的。同理可以推断 类对象 和 元类对象 也存在一样的位运算关系。

至此,我们算是完成了对 OC 对象的部分探究了

⚠️ **GitHub.com Fallback** ⚠️