0%

iOS底层探索 - 动态方法决议

前面两篇文章我们探索了消息查找的流程,包括快速查找和慢速查找,本文开始探索消息查找未找到时候的处理流程。

unrecognized selector

unrecognized selector是我们开发中很熟悉的一种错误,就是找不到方法的时候报错的提示,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  JSPerson.h
@interface JSPerson : NSObject

- (void)sayNB;

@end
// JSPerson.m
@implementation JSPerson

@end
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *person = [JSPerson alloc];
[person sayNB];
}
return 0;
}

我们定义一个JSPerson类,声明一个sayNB方法,但是并没有实现,因为oc是一种动态语言,可以在运行时添加方法实现,所以没有实现编译期并不会报错。在main函数里实例化一个JSPerson对象,调用sayNB方法,这个时候程序就会崩溃,报错:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[JSPerson sayNB]: unrecognized selector sent to instance 0x101844590'

这个错误是怎么来的呢,我们从上一节最后方法找不到的地方开始看,当没有找到方法实现时,lookUpImpOrForward方法返回值是imp = forward_imp;_objc_msgForward_impcacheobjc源码工程里全局搜索这个关键字,在objc-msg-arm64.s文件的745行找到实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward

END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward

adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
///返回x17
TailCallFunctionPointer x17

END_ENTRY __objc_msgForward

这段代码比较简单,调用了__objc_forward_handler方法,继续搜索__objc_forward_handler方法,并没有找到方法实现,根据前面的经验,去掉_继续搜索objc_forward_handler,在objc_runtime.mm文件中找到了实现:

1
2
3
4
5
6
7
8
9
10
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

这段代码就显而易见了,找不到方法的时候会执行unrecognized selector报错。

动态方法决议

了解了unrecognized selector后我们回到lookUpImpOrForward的流程,当递归完查找所有父类流程没有找到方法实现会继续执行方法后面的代码:

1
2
3
4
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}

behaviorlookUpImpOrForward方法的最后一个参数,通过找到方法的调用behavior=LOOKUP_INITIALIZE | LOOKUP_RESOLVER=3LOOKUP_RESOLVER=2

1
2
3
4
5
6
7
	// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};

这个if代码块其实是巧妙的使用了单例思想只会执行一次:

  • 初始behavior & LOOKUP_RESOLVER = 3 & 2 = 2,条件为true执行
  • behavior ^= LOOKUP_RESOLVER = 3 ^ 2 = 1
  • 如果后面再次进行if判断,behavior & LOOKUP_RESOLVER = 1 & 2 = 0,条件为false就不会执行代码块。

前面if代码块会执行方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//判断是否是元类
if (! cls->isMetaClass()) {
//实例方法 解析
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//类方法 解析
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
// 如果前面处理了 重新进行查找
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

实例方法流程

我们先看实例方法,实例方法会调用resolveInstanceMethod方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
//resolveInstanceMethod 类方法
SEL resolve_sel = @selector(resolveInstanceMethod:);

if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
// Resolver not implemented.
// 因为系统有默认实现这里不会执行到
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);//resolved 表示方法返回值
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
// 继续查找 一次
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

if (resolved && PrintResolving) {
if (imp) {
//找到了imp
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// resolved = true 但是不没有对应的处理方法
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

可以看到,其实是在类中添加一个resolveInstanceMethod:进行处理,我们在前面例子的基础上给JSPerson类添加一个resolveInstanceMethod:方法处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation JSPerson

- (void)saySomethingDefalut{
NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(sayNB)) {
IMP sayDefalutImp = class_getMethodImplementation(self, @selector(saySomethingDefalut));
Method method = class_getInstanceMethod(self, @selector(saySomethingDefalut));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayDefalutImp, type);
}
return [super resolveInstanceMethod:sel];
}
@end

重新运行main方法,发现程序可以正常运行了,控制台打印结果

1
2
3
resolveInstanceMethod :JSPerson-sayNB
resolveInstanceMethod :JSPerson-encodeWithOSLogCoder:options:maxLength:
<JSPerson: 0x10194fab0> - -[JSPerson saySomethingDefalut]

通过打印结果我们看到确实进入了resolveInstanceMethod,而且最终执行了saySomethingDefalut方法,实例方法的动态方法解析正常流程就是这样了。

还有一个异常情况就是,如果我们不添加方法处理,只是打印执行NSLog方法:

1
2
3
4
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}

这个时候程序崩溃是毋庸置疑的,因为这样写相当于没有处理,但是这时候控制台的打印结果是:

1
2
3
resolveInstanceMethod :JSPerson-sayNB
resolveInstanceMethod :JSPerson-sayNB
-[JSPerson sayNB]: unrecognized selector sent to instance 0x10193a810

resolveInstanceMethod执行了两次,这是为什么呢,我们在resolveInstanceMethod方法添加断点,使用bt命令查看调用栈:

  • 第一次调用,就是我们刚刚探索的来源是libobjc框架的_objc_msgSend_uncached方法。调用信息如下图

    1625382251804

  • 第二次调用,消息的来源是CoreFoundation___forwarding___方法,下一篇我们会探索,调用信息如下图

    1625382659258

类方法流程

类方法的流程就是在resolveClassMethod实现,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
//入参是元类 返回是当前类
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//向当前类发送消息
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}

这个流程和实例方法很相似,在类中添加一个resolveClassMethod:的类方法处理,同样前面的例子我们验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@interface JSPerson : NSObject

+ (void)sayHowAreYou;

@end

@implementation JSPerson

+ (void)sayAndYou{
NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"resolveClassMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(sayHowAreYou)) {
IMP sayAndYouImp = class_getMethodImplementation(objc_getMetaClass("JSPerson"), @selector(sayAndYou));
Method method = class_getInstanceMethod(objc_getMetaClass("JSPerson"), @selector(sayAndYou));
const char *type = method_getTypeEncoding(method);
return class_addMethod(objc_getMetaClass("JSPerson"), sel, sayAndYouImp, type);
}
return [super resolveClassMethod:sel];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
[JSPerson sayHowAreYou];
}
return 0;
}

编译运行发现程序正常运行了,打印结果为

1
2
resolveClassMethod :JSPerson-sayHowAreYou
JSPerson - +[JSPerson sayAndYou]
1
2
3
4
5
6
7
8
9
else{
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//类方法 解析
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}

细心地我们现在会发现在上面这段代码也就是类方法解析这个流程还有一个resolveInstanceMethod(inst, sel, cls);解析,这个原因是类方法在元类里是以实例方法的形式存在的,所以元类里resolveInstanceMethod对方法做了处理也会正常运行。

AOP思想处理动态方法决议

通过上面的动态方法解析流程,和前面我们探索的isa走位图和继承链,我们可以使用AOP的思想,全局处理类的动态方法解析。具体实现就是在NSObject的分类中添加resolveInstanceMethod:类方法,处理方法的动态解析,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@interface NSObject (JS)

@end
@implementation NSObject (LG)

- (void)saySomethingDefalut{
NSLog(@"%@ - %s",self , __func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod :%@-%@",self,NSStringFromSelector(sel));
if (sel == @selector(sayNB)) {
IMP sayDefalutImp = class_getMethodImplementation(self, @selector(saySomethingDefalut));
Method method = class_getInstanceMethod(self, @selector(saySomethingDefalut));
const char *type = method_getTypeEncoding(method);
return class_addMethod(self, sel, sayDefalutImp, type);
}
return NO;
}
@end

@interface JSPerson : NSObject

- (void)sayNB;

@end

@implementation JSPerson

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
JSPerson *person = [JSPerson alloc];
[person sayNB];
}
return 0;
}

运行项目发现正常运行,打印结果为:

1
2
resolveInstanceMethod :JSPerson-sayNB
<JSPerson: 0x100593ed0> - -[NSObject(LG) saySomethingDefalut]

总结

本节我们主要探讨了动态方法解析流程,分实例方法和类方法两种情况

  • 实例方法是在类中实现resolveInstanceMethod:的类方法,处理方法得动态绑定到异常处理的方法。
  • 类方法分两部分查找
    1. 搜索当前类的resolveClassMethod:方法。
    2. 搜索类的元类的resolveInstanceMethod:方法
  • 我们可以利用AOP思想在根类的分类中实现resolveInstanceMethod:从而完成全局方法的动态解析。

如果没有进行动态方法解析,接下来就会进入消息转发的流程,我们下一节探索。