Jerry's Blog

Less is more, Live and learning

Hello,I'm an iOS developer in China.


对象、类、元类、isa指针之间的爱恨情仇

前言

在iOS开发中,对象、类的使用可以说是无处不在,伴随着整个项目的开发周期,也是程序的重要组成部分。但是在日常开发中,很难直观的见到元类、isa指针,那么它们究竟是谁,在开发中起着什么作用呢?下面我们就分别介绍下这几位亲兄弟的结构体定义,以及之间的关联关系。

类(class)的结构体定义

类对象(Class)是由程序员定义并在运行时有编译器创建的,他没有自己的实例变量,这里需要注意的是类的成员变量实例方法列表是属于实例对象的,但其存储于类对象当中。下图指出类的结构体定义:

// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

// OC1.0 class 结构体定义
struct objc_class {
	Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
	Class _Nullable super_class                              OBJC2_UNAVAILABLE;
	const char * _Nonnull name                               OBJC2_UNAVAILABLE;
	long version                                             OBJC2_UNAVAILABLE;
	long info                                                OBJC2_UNAVAILABLE;
	long instance_size                                       OBJC2_UNAVAILABLE;
	struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
	struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
	struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
	struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */


// OC2.0 class 结构体定义,我们注意到上面的定义后面的标识符OBJC2_UNAVAILABLE,意思是已经不适用ObjC2.0了,现在ObjC2.0 对objc_class的定义如下:
struct objc_class : objc_object {
    isa_t isa;
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;
};

我们在objc/objc.h 源码文件中找到了Class 类结构体的定义,下面我们分析并说明一下几个关键的结构体成员:

  • isa: 是一个不能为空(null)的Class对象,它指向该类的元类(meta class)。
  • super_class:是一个可以为空的Class对象,它指向当前类的父类。会一直追溯到根类(NSObject),那么根类(NSObject)的父类是指向谁的呢?通过分析根类的父类是指向空(nil)的,这样也就可以解释为什么super_class 是可以为空的。
  • ivars:是实例变量列表,保存着该类的所有实例变量
  • methodLists:是方法列表,保存着实例方法和类方法
  • protocols:是协议列表,保存该类的协议方法
  • cache:是一个缓存,用于缓存最近使用过的方法。

上面我们根据Class 类的结构体,分析内部成员的作用及含义。想必大家已经对Class类的内部结构有了一个初步的认识。如果想再深入理解isasuper_class在不同情况下都是如何进行指向的,我们再看下面的一个图示来逐步分析:

img1

首先,注意图中的注释,实线的箭头(→ superclass)代表父类指针,虚线的箭头(→isa)代表isa指针,从上至下的深色方框依次代表:根类-> 父类-> 子类,下面咱们从上至下层层分析上图的含义:

  • 第一层:
    • isa指针: 根类的实例对象(instance Of Root class)isa指向根类(Root class),根类的isa指针指向根元类(Root meta class),根元类的isa指向它本身。
    • superclass指针:根类(Root class)的superclass指向nil,根元类的父类指向根类。
  • 第二层:
    • isa指针:父类的实例对象(instance of superclass)的isa指向父类(superclass),父类的isa指向父元类,父元类的isa指向根元类。
    • superclass指针:父类的superclass指向根类,父元类的superclass指向根元类。
  • 第三层:
    • isa指针:子类实例对象(instance of subclass)的isa指向子类,子类的isa指向子元类,子元类的isa指向根元类。
    • superclass指针:子类的superclass指向父类,子元类的superclass指向父元类。

动态创建一个类

// 创建一个新类和元类
Class objc_allocateClassPair ( Class superclass, const char *name, size_t extraBytes ); //如果创建的是root class,则superclass为Nil。extraBytes通常为0
// 销毁一个类及其相关联的类
void objc_disposeClassPair ( Class cls ); //在运行中还存在或存在子类实例,就不能够调用这个。
// 在应用中注册由objc_allocateClassPair创建的类
void objc_registerClassPair ( Class cls ); //创建了新类后,然后使用class_addMethod,class_addIvar函数为新类添加方法,实例变量和属性后再调用这个来注册类,再之后就能够用了。

对象(object)的结构体定义

// Represents an instance of a class.
struct objc_object {
	Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

我们可以看到,objc_object代表一个实例对象,它的结构体内部有一个不为空的isa指针,isa是指向对象所属类的指针。另外,我们常用的id类型是一个指向objc_object结构体的指针,它可以指向任意一个oc对象。

// A pointer to an instance of a class.
typedef struct objc_object {
    Class isa;
} *id;

元类(Meta class)的结构体定义

元类(Meta class) 就是类对象每个类都有自己的元类,也就是objc_class结构体里面的isa指针所指向的类。元类用来表述类对象本身所具备的元数据, 因为元类中存储着类对象的方法列表,即类方法

下面将介绍实例对象、类、元类之间的关系,如下图所示:

img2

当向对象发消息时,runtime会在这个对象所属类的实例方法列表中查找消息对应的方法实现(IMP)。但当向类发送消息时,runtime就会在这个类的元类(meta class)方法列表里查找。所有的元类(meta class),包括根类(Root class),父类(Superclass),子类(Subclass)的isa最终都指向根类(Root class)的元类(meta class),这样能够形成一个闭环。可以总结为:

  • 当发送消息给实例对象时,runtime函数会在此实例对象所属类的实例方法列表中查找方法实现(IMP)并调用,此为实例方法调用
  • 当发送消息给类对象时,runtime函数会在此实例对象所属类的元类的方法列表中查找方法实例(IMP)并调用,此为类方法调用

延伸扩展

方法的调用流程

OC的方法调用本质上是给对象发送消息,即调用runtime的消息发送函数,如下所示:

/*
	param: self(id) 接收消息的对象,
	param: op(SEL)  发送的消息,即方法签名(@selector(method_name))
*/
id _Nullable  objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

通过对objc_msgSend()函数的源码分析,总结以下调用流程:

  1. 检查 selector 是否需要忽略
  2. 检查 target 是否为 nil,如果是 nil 就直接 cleanup,然后 return
  3. 在 target 的 Class 中根据 selector 去找 IMP

寻找IMP的流程

  1. 在当前 class 的方法缓存里寻找(cache methodLists)
  2. 找到了跳到对应的方法实现,没找到继续往下执行
  3. 从当前 class 的 方法列表里查找(methodLists),找到了添加到缓存列表里,然后跳转到对应的方法实现;没找到继续往下执行
  4. 从 superClass 的缓存列表和方法列表里查找,直到找到基类为止
  5. 以上步骤还找不到 IMP,则进入消息动态处理消息转发(objc_msgForward)流程,详见这篇文章

我们从objc-class.mm 官方源码中找到了寻找IMP的实现过程,如下:

IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls  ||  !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// Translate forwarding function to C-callable external version
if (!imp) {
return _objc_msgForward;
}
return imp;
}

参考资料

http://limbank.com/2018/03/09/post18/

https://tingxins.com/2017/05/metaclass-class-relationship/

最近的文章

iOS中各种“锁”的理解及应用

前言通常在一般的iOS应用开发中会很少碰到使用“锁”的业务逻辑,但是在需要使用多线程技术,解决大多数场景写的业务逻辑时,会使用到线程锁来保证临界数据的读写安全性。当然,“锁”的概念在计算机科学及应用中也是举足轻重的,对于要写出高质量、高性能、安全可靠的代码来说,也是非常重要的。预备知识 线程调度 计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别...…

iOS继续阅读