方便记忆:
- 实质作用:从OC到C语言的过渡
- 消息传递方法:objec_msgSend 方法
- objc_method 结构体内容:SEL method_name 方法名、char *method_types 方法类型、IMP method_imp 方法实现(方法指针)
- 消息转发:unrecognized selector 三次机会
- 动态方法解析:对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。
- 备用接收者:- (id)forwardingTargetForSelector:(SEL)aSelector;这是一个实例方法,因为是询问该实例对象是否有其他实例对象可以接收这个未知的selector,如果没有就返回nil
- 完整转发:- (void)forwardInvocation: (NSInvocation*)invocation;调用这个方法如果不能处理就会调用父类的相关方法,一直到NSObject的这个方法,如果NSObject都无法处理就会调用doesNotRecognizeSelector:方法抛出异常。
- 用途:做异常处理收集,避免crash,增强用户体验
- 常见的应用场景:
- 关联对象(Objective-C Associated Objects):给分类增加属性
- 方法魔法(Method Swizzling):方法添加和替换和KVO实现
- 消息转发(热更新):解决Bug(JSPatch)
- 实现NSCoding的自动归档和自动解档
- 实现字典和模型的自动转换(MJExtension)
Runtime 相关介绍
Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 编译语言 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。
Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。理解 Objective-C 的 Runtime 机制可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。
1.Runtime 版本演化
Runtime其实有两个版本: “modern” 和 “legacy”。
我们现在用的 Objective-C 2.0 采用的是现行 (Modern) 版的 Runtime 系统,只能运行在 iOS 和 macOS 10.5 之后的 64 位程序中。
而 macOS 较老的32位程序仍采用 Objective-C 1.0 中的(早期)Legacy 版本的 Runtime 系统。
这两个版本最大的区别在于当你更改一个类的实例变量的布局时,在早期版本中你需要重新编译它的子类,而现行版就不需要。
2.Runtime 实质作用
高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C语言的过渡就是由runtime来实现的。
然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。
OC 中一个对象方法
[obj foo];
编译器转成消息发送
objc_msgSend(obj, foo)
在上段代码中 Runtime 的执行流程:
- 首先,通过obj的isa指针找到它的 class ;
- 在 class 的 method list 找 foo ;
- 如果 class 中没到 foo,继续往它的 superclass 中找 ;
- 一旦找到 foo 这个函数,就去执行它的实现IMP 。
3.objc_cache 缓存机制
但是,上面这种实现有个问题,效率低。
一个class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。
这也就是 objc_class 中另一个重要成员 objc_cache 做的事情,再找到 foo 之后,把 foo 的 method_name 作为key,method_imp 作为 value 给存起来。当再次收到 foo 消息的时候,可以直接在 cache 里找到,避免去遍历 objc_method_list。
从前面的源代码可以看到objc_cache是存在objc_class 结构体中的。
4.objec_msgSend 方法
objec_msgSend的方法定义如下:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体:
//对象 struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; //类 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; //方法列表 struct objc_method_list { struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; } OBJC2_UNAVAILABLE; //方法 struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
- 1.系统首先找到消息的接收对象,然后通过对象的isa找到它的类。
- 2.在它的类中查找method_list,是否有selector方法。
- 3.没有则查找父类的method_list。
- 4.找到对应的method,执行它的IMP。
- 5.转发IMP的return值。
消息传递用到的概念:
1.类对象(objc_class)
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。
typedef struct objc_class *Class;
查看objc/runtime.h中objc_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;
struct objc_class结构体定义了很多变量,通过命名不难发现,结构体里保存了指向父类的指针、类的名字、版本、实例大小、实例变量列表、方法列表、缓存、遵守的协议列表等,一个类包含的信息也不就正是这些吗?
没错,类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),该结构体的第一个成员变量也是isa指针,这就说明了Class本身其实也是一个对象,因此我们称之为类对象,类对象在编译期产生用于创建实例对象,是单例。
2.实例(objc_object)
/// Represents an instance of a class. struct objc_object { Class isa OBJC_ISA_AVAILABILITY; }; /// A pointer to an instance of a class. typedef struct objc_object *id;
类对象中的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?
就是从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:
3.元类(Meta Class)
通过上图我们可以看出整个体系构成了一个自闭环,struct objc_object结构体实例它的isa指针指向类对象,类对象的isa指针指向了元类,super_class指针指向了父类的类对象,而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了自己。
元类(Meta Class)是一个类对象的类。在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
4.Method(objc_method)
先看下定义
runtime.h /// An opaque type that represents a method in a class definition.代表类定义中一个方法的不透明类型 typedef struct objc_method *Method; struct objc_method { SEL method_name OBJC2_UNAVAILABLE; // 方法名 char *method_types OBJC2_UNAVAILABLE; // 方法类型 IMP method_imp OBJC2_UNAVAILABLE; // 方法实现 }
Method和我们平时理解的函数是一致的,就是表示能够独立完成一个功能的一段代码,比如:
- (void)logName { NSLog(@"name"); }
这段代码,就是一个函数。 我们来看下objc_method这个结构体的内容:
- SEL method_name 方法名
- char *method_types 方法类型
- IMP method_imp 方法实现
在这个结构体重,我们已经看到了SEL和IMP,说明SEL和IMP其实都是Method的属性。 我们接着来看SEL。