第五部分,类和对象。

介绍 Class 和 Object 内部的构造和方法调用。

isa 和 IMP

isa

在 ObjC 中,任何对象都有 isa 指针。

Class 是一个 objc_class 结构类型的指针, id 是一个 objc_object 结构类型的指针。

isa,是一个 Class 类型的指针。每个实例对象都有一个 isa 指针,指向对象的类。而 Class 里面也有一个 isa 指针,指向 meta class(元类),即类对象是元类的实例。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。元类也有 isa 指针,它的 isa 指针最终指向的是一个根元类(root meta class)。根元类的 isa 指针指向本身,这样形成了一个封闭的内循环。

ObjC 对象的 isa 指针指向什么?有什么作用?

  1. 指向它的类对象
  2. 找到对象上的方法

IMP

IMP 就是 Implementation 的缩写,顾名思义,它是指向一个方法实现的指针,每一个方法都有一个对应的 IMP 指针。

1
typedef id (*IMP)(id, SEL, ...);

ObjC 对象如何进行内存布局?

所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中。

每个对象内部都有一个 isa 指针,指向它的类对象,类对象中存放着本对象的(对象方法列表、成员变量列表、协议列表…)。而且,类对象的内部也有一个 isa 指针指向 Meta Class,还有一个 Super Class 的指针指向它的父类对象。

ObjC 对象的结构图
isa 指针
根类的实例变量
倒数第二层父类的实例变量
父类的实例变量
类的实例变量

根对象就是 NSObject,它的 Super Class 指针指向 nil。

类对象中也有一个 isa 指针指向它的元类,即,类对象是元类的实例。元类内部存放的是类方法列表,根元类的 isa 指针指向自己,Super Class 指针指向 NSObject。

ObjC 中向一个 nil 对象发送消息将会怎么样?

结果

不会有任何作用。

  1. 如果方法的返回值是一个对象,那么发送给 nil 的消息将返回 nil。
  2. 如果方法的返回值为指针类型,其指针大小为小于或等于 sizeof(void*),float,double,long double,long long 的整型标量,将返回 0。
  3. 如果方法的返回值为结构体,返回值为 0,且结构体各个字段的值都将是 0。
  4. 如果方法的返回值非上述情况,那么发送给 nil 的消息的返回值将是未定义的。

原因

ObjC 是动态语言,每个方法在运行时会被动态转为消息发送,即 objc_msgSend(receiver, selector)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// runtime.h(类在runtime中的定义)

struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
  • isa:isa 指针指向 Meta Class,因为 ObjC 的类本身也是一个 Object,为了处理这个关系,runtime 就创造了 Meta Class,当给类发送消息时,实际上是把这个消息发送给了 Class Object。
  • super_class:父类
  • name:类名
  • version:类的版本信息,默认为 0
  • info:类信息,供运行时使用的一些位标示
  • instance_size:该类的实例变量的大小
  • objc_ivar_list:该类的成员变量列表
  • objc_method_list:类的方法定义列表
  • objc_cache:方法缓存,对象接到一个消息会根据 isa 指针查找消息对象,这是会在 method_list 中遍历,如果 cache 了,常用的方法调用时就能够提高调用的效率。
  • objc_protocol_list:协议列表

ObjC 在向一个对象发送消息时,runtime 会根据对象的 isa 指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_megSend 方法不会返回值,所谓的返回内容都是具体调用时执行的。

所以,向一个 nil 对象发送消息,首先在寻找对象的 isa 指针时就是 0 地址返回了,所以不会有任何作用。

ObjC 中向一个对象发送消息 [obj foo] 和 objc_msgSend() 函数之间有什么关系?

[obj foo] 编译之后就是 objc_msgSend(obj, @selector(foo))

1
((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo"));

什么时候会报 unrecognized selector 的异常?

  • 原因:当调用该对象上某个方法,而该对象上没有实现这个方法的时候。
  • 解决:消息转发机制。

每个方法运行时会被动态转为消息发送,objc_msgSend(receiver, selector);

ObjC 在向一个对象发送消息时,runtime 会根据对象的 isa 指针找到该对象实际所属的类,然后在改类的方法列表以及其父类方法列表中寻找方法运行。如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉,并且抛出异常,unrecognized selector sent to XXX

三次拯救崩溃的机会:

Method Resolution

ObjC 运行时会调用 +resolveInstancMethod:+resolveClassMethod:,让你有机会提供一个函数实现。

如果你添加了函数,那运行时系统就会重写启动一次消息发送的过程。

否则,运行时就会移到下一步,消息转发(Message Forwarding)。

Fast Forwarding

如果目标对象实现了 -forwardingTargetForSelector:,runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。

只要这个方法返回的不是 nil 和 self,整个消息发送的过程就会被重启,当然发送的对象就会变成你返回的那个对象。

否则,就会继续 Normal ForWarding。这里叫 Fast 只是为了区别下一步的转发机制,因为这一步不会创建任何新的对象,但是下一步转发会创建一个 NSInvocation 对象,所以相对更快一些。

Normal Forwarding

它会发送 -methodSignatureForSelector: 消息,获得函数的参数和返回值类型。

如果返回时 nil,则 runtime 会发出 -doseNotRecognizeSelector: 消息,程序这个时候也就挂掉了。

如果返回了一个函数签名,runtime 就会创建一个 NSInvocation 对象并发送 -forwardInvocation: 消息给目标对象。

objc_msgForward 函数是做什么的,直接调用它将会发生什么?

objc_msgForward 是 IMP 类型,用于消息转发:当一个对象发送一条消息,但它并没有实现的时候,objc_megForward 会尝试做消息转发。

直接调用时非常危险的事情,用不好会直接导致程序崩溃。用的好的案例:JSPatchRAC

类方法和实例方法有什么本质区别和联系?

类方法

  • self 是指类对象
  • 只能类对象调用
  • 能调用其他的类方法
  • 不能访问成员变量
  • 不能直接调用对象方法

实例方法

  • self 是实例对象
  • 只能实例对象调用
  • 能调用其他实例方法
  • 能访问成员变量
  • 能调用类方法(通过类名)