1. 消息查找
Objective-C 具有很强的动态性,它将静态语言在编译和链接时期做的工作,放置到运行时来处理. Runtime就是这门语言的基础.
在Objective-C中, 某个对象进行方法调用, 多被称作向对象发送消息。我们如果需要了解这个问题就需要向底层探究.我们平时编写的Objective-C代码,底层实现其实都是C\C++代码
如图:
OC-C++-.png
所以我们通过向下探究来研究一下,示例如下:
@interface Person : NSObject
- (void)run;
@end
@implementation Person
- (void)run {
NSLog(@"person is running");
}
@end
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
[p run];
}
那么如何将Objective-C代码转换为C\C++代码呢? 苹果为我们提供了工具:
规范:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
操作: xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc ViewController.m -o ViewController.mm
[person run] 经过编译后如下调用的是objc_msgSend方法
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));
}
通过转变为C++代码我们发现
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));
objc_msgSend()函数被调用, 通过查看Runtime 找到对应的函数定义. OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);
self:消息的接收者 , op: 消息的方法名,C 字符串 ... :参数列表
那么对象调用方法, 是怎么进行转发的呢, 我们特地准备了一张图来描述:
消息转发机制.png我们可以看到消息的处理分为两个阶段, "动态方法决议" 和 "消息转发", 只有在动态方法决议期间找不到方法时候才会进行下一步. 那我们先探究一下第一个阶段是怎么进行的. 在研究第一个阶段之前, 我们再来看一张经典图:
类继承.jpg oc类图.png oc继承图.png
在这两张图中,京D巴拉巴拉(京DBLABLA)就是实例, 小汽车类就是类对象, 小汽车类指向小汽车元类.小汽车元类指向 根元类, 根元类指向自己形成闭环, 通过图中的关系, 我们知道了实例对象通过isa指针,找到类对象, 类对象通过isa指针找到元类, 元类通过isa指针找到根元类对象. 根元类isa指向自己形成闭环, 但是superclass 指向NSObject, 所以可以说NSObject 就是消息机制的核心.
OK, 在我们了解了OC基本概念之后回归到函数本身
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("person_run"));
通过cmd+shift+o
快捷键输入函数名 找到objc/runtime.h objc_msgSend函数函数定义如下, 但是函数中参数的sel_registerName("person_run")是什么, 我们继续查找, 同样在objc/runtime.h 中找到定义. 该函数在Objective-C Runtime系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器.
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);
OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)
我们既然了解了函数, 我们从新回顾一下上边的"OC RUNTIME的基本概念"这张图. 回顾后,我们继续探究Class是什么? id 关键字是什么, 老方法,通过cmd+shift+o
快捷键输入函数名,我们发现如下
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
Class 和 id 都是 objc_object 结构体, 继续查看objc_object
是什么 我们看到
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;
// 最新版本objc4-208 显示如下
struct objc_class {
struct objc_class *isa;
struct objc_class *super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
#if defined(Release3CompatibilityBuild)
struct objc_method_list *methods;
#else
struct objc_method_list **methodLists;
#endif
struct objc_cache *cache;
struct objc_protocol_list *protocols;
};
在上边结构体中我们需要重点关注几个关键字
-
isa 指向父类的指针.
-
methodLists 一个类的方法分发表.
当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中isa指针也会被初始化,让对象可以访问类及类的继承体系。
iOS-类熟悉继承.png
如上图所示的继承关系, 就意味着我们寻找一个实例方法, 在student找不到, 通过isa指针指向的Person, 在Person中继续寻找,直到NSObject. 例如 alloc 函数, 会通过isa 一层一层直到NSObject为止.
objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果 没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依 此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。
通过阅读神经病院 Objective-C Runtime 住院第二天:消息发送与转发 文章找到一段
#include <objc/objc-runtime.h>
id c_objc_msgSend( struct objc_class /* ahem */ *self, SEL _cmd, ...)
{
struct objc_class *cls;
struct objc_cache *cache;
unsigned int hash;
struct objc_method *method;
unsigned int index;
if( self)
{
cls = self->isa;
cache = cls->cache;
hash = cache->mask;
index = (unsigned int) _cmd & hash;
do
{
method = cache->buckets[ index];
if( ! method)
goto recache;
index = (index + 1) & cache->mask;
}
while( method->method_name != _cmd);
return( (*method->method_imp)( (id) self, _cmd));
}
return( (id) self);
recache:
/* ... */
return( 0);
}
通过C版本的objc_msgSend的源码版本,画了一张大致示意图:
此图描述仅供参考, 实际查找方式没进行深入研究, 请勿严重拍砖
iOS-实现消息转发.png iOS-isa-方法类方法.png iOS-消息转发-superclass.png通过遍历类对象方法, meta-class, 找不到匹配的方法, 通过superclass 继续在父类中继续同样的操作,找到后加入实例类对象的cache, 如果遍历到NSObject还是找不到就开始了 消息转发.
2.消息转发
进入消息转发就不得再展示一次该图.
消息转发机制.png
对象在收到无法响应的消息后,会调用其所属类的下列方法
/**
* 如果尚未实现的方法是实例方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增实例方法用以处理selector
*/
+ (BOOL)resolveInstanceMethod:(SEL)selector;
/**
* 如果尚未实现的方法是类方法,则调用此函数
*
* @param selector 未处理的方法
*
* @return 返回布尔值,表示是否能新增类方法用以处理selector
*/
+ (BOOL)resolveClassMethod:(SEL)selector;
备援接收者 或者 快转发
如果无法动态解析方法,运行期系统就会询问是否能将消息转给其他接收者来处理,对应的方法为
/**
* 此方法询问是否能将消息转给其他接收者来处理
*
* @param aSelector 未处理的方法
*
* @return 如果当前接收者能找到备援对象,就将其返回;否则返回nil;
*/
- (id)forwardingTargetForSelector:(SEL)aSelector;
完整的消息转发机制
如果前面两步都无法处理消息,就会启动完整的消息转发机制。首先创建 NSInvocation 对象,把尚未处理的那条消息有关的全部细节装在里面,在触发 NSInvocation 对象时,消息派发系统(message-dispatch system)将会把消息指派给目标对象。对应的方法为
/**
* 获取指定selector的方法签名
*
* @param aSelector aSelector 未处理的方法
*
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
/**
* 消息派发系统通过此方法,将消息派发给目标对象
*
* @param anInvocation 之前创建的NSInvocation实例对象,用于装载有关消息的所有内容
*/
- (void)forwardInvocation:(NSInvocation *)anInvocation;
这个方法可以实现的很简单,通过改变调用的目标对象,使得消息在新目标对象上得以调用即可。然而这样实现的效果与 备援接收者 差不多,所以很少人会这么做。更加有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数、修改 selector 等等。
- Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
- (void)run;
+ (void)eat;
@end
- Person.m
#import "Person.h"
#import <objc/runtime.h>
#import "Teacher.h"
@implementation Person
// 并没有真正实现run 方法, 和 eat 方法
void eatClass(id self, SEL cmd) {
NSLog(@"类方法eat的实现");
}
void runClass(id self, SEL cmd) {
NSLog(@"方法run的实现");
}
// first
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel == @selector(eat))
{
NSLog(@"eat static method 调用resolveClassMethod");
BOOL success = class_addMethod(object_getClass([self class]), sel, (IMP)eatClass, "v@:");
return success;
}
return [super resolveClassMethod:sel];
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(run)) {
NSLog(@"run method , 调用resolveInstanceMethod");
BOOL success = class_addMethod([self class], sel, (IMP)runClass, "v@:");
return success;
// return NO; // 返回NO 会进行快转发.
}
return [super resolveInstanceMethod:sel];
}
// second
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
if (aSelector == @selector(run)) {
// return nil; // 返回nil 会进行完整转发.
return [[Teacher alloc] init];
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
// third
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if (sel_isEqual(aSelector, @selector(run))) {
signature = [[[Teacher alloc] init] methodSignatureForSelector:aSelector];
}
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSString *sel = NSStringFromSelector(anInvocation.selector);
if ([sel isEqualToString:@"run"]) {
[anInvocation invokeWithTarget:[[Teacher alloc] init]];
} else {
[super forwardInvocation:anInvocation];
}
}
demo 地址
实际应用
参考
Objective-C Runtime 运行时之三:方法与消息
神经病院 Objective-C Runtime 住院第二天:消息发送与转发
Effective Objective-C Notes:理解消息传递机制
美团-技术专家-臧成威
李明杰-讲师
网友评论