阅读 135

iOS底层分析之objc_msgSend消息转发

和谐学习!不急不躁!!我是你们的老朋友小青龙~

前言

前面文章《iOS底层分析之objc_msgSend消息动态决议》讲述了,当对象方法类方法找不到的时候,系统会在报错之前给予一次补救机会:实现resolveInstanceMethod:resolveClassMethod:方法。 那么,如果没有实现又当如何呢?

思维引导

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    ...
    checkIsKnownClass(cls);
    ...
    for (unsigned attempts = unreasonableClassCount();;) {
        ///查找缓存,若当前类的方法列表里找不到,递归去逐个找父类、父类的父类,
        ///直到父类为nil也没找到就赋值一个默认_objc_msgForward_impcache
        ...
    }
    ...
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        ///消息动态决议,且只会执行一次,
        ///具体为什么看前面“消息动态决议”文章,搜索”单例解释“
        return resolveMethod_locked(inst, sel, cls, behavior);
    }
    ...
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    ...
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
复制代码

我们发现,这个函数已经走完了。。。如果那两个补救方法都不实现,就返回一个nil,下面也没有相关思路的代码供我们继续探索,这。。。怎么办呢?

日志查看

我们再一次的阅读lookUpImpOrForward函数,发现在返回imp之前做了一件事情: log_and_fill_cache(日志和填充缓存),点击进去:

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    //要触发logMessageSend函数,需要objcMsgLogEnabled为真
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cls->cache.insert(sel, imp, receiver);
}
复制代码

我们发现在写日志的时候调用了一个函数logMessageSend

static int objcMsgLogFD = -1;

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char	buf[ 1024 ];

    // Create/open the log file
    //根据英文注释,我们可以知道日志文件存在于/temp/msgSends-XX文件
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
复制代码

根据上面的logMessageSend代码,我们得到了日志文件的存储地址:/tmp/msgSends-XX;

但是触发logMessageSend,需要objcMsgLogEnabled不等于false,而我们点击objcMsgLogEnabled发现它默认就是false,所以肯定有什么函数可以设置它,那么全局搜一下“objcMsgLogEnabled”,发现只有这个地方会对objcMsgLogEnabled的值进行修改:

image.png


我们分析一下instrumentObjcMessageSends代码:

void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;
    ///objcMsgLogEnabled是false,这里意思是如果flag为false就return
    if (objcMsgLogEnabled == enable)
        return;

    //如果flag为真,就开启 - 刷新所有缓存
    if (enable)
        _objc_flush_caches(Nil);

    // 同步 - 我们的日志文件
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
复制代码

那么怎么样才能触发instrumentObjcMessageSends呢,全局搜索一下:

// env NSObjCMessageLoggingEnabled
OBJC_EXPORT void
instrumentObjcMessageSends(BOOL flag)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码

我们惊喜的发现,有个关键修饰词OBJC_EXPORT,意思是对外抛出,可以供我们开发者在oc层面调用。 说到这里,不知道大家有没有注意到,平时写分类的时候,会用到objc_setAssociatedObject函数,我们也全局搜一下:

OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
复制代码
故此,可以得出结论:
  • 调用instrumentObjcMessageSends(BOOL flag),才会使得objcMsgLogEnabled变为true;这样在查找imp最后一步才会触发logMessageSend函数,也就是打印日志文件。
回到我们的代码:
//声明instrumentObjcMessageSends
extern void instrumentObjcMessageSends(BOOL flag);

//定义SSJAnimal类
@interface SSJAnimal : NSObject
- (void)sleep;
+ (void)eat;
@end
@implementation SSJAnimal
@end

//main函数
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SSJAnimal *animal = [SSJAnimal alloc];
        instrumentObjcMessageSends(YES);
        [animal sleep];
        [SSJAnimal eat];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}
复制代码

运行之后,会报错“-[SSJAnimal sleep]: unrecognized selector sent to instance 0x100408540”,这个不用管,我们前往“/tmp/

image.png 可以看到这样的信息:

+ SSJAnimal NSObject resolveInstanceMethod:
+ SSJAnimal NSObject resolveInstanceMethod:
- SSJAnimal NSObject forwardingTargetForSelector:
- SSJAnimal NSObject forwardingTargetForSelector:
- SSJAnimal NSObject methodSignatureForSelector:
- SSJAnimal NSObject methodSignatureForSelector:
- SSJAnimal NSObject class
+ SSJAnimal NSObject resolveInstanceMethod:
+ SSJAnimal NSObject resolveInstanceMethod:
- SSJAnimal NSObject doesNotRecognizeSelector:
- SSJAnimal NSObject doesNotRecognizeSelector:
- SSJAnimal NSObject class
- OS_xpc_serializer OS_xpc_object dealloc
- OS_object NSObject dealloc
+ OS_xpc_payload NSObject class
- OS_xpc_payload OS_xpc_payload dealloc
- NSObject NSObject dealloc
...
...
复制代码

其中resolveInstanceMethod是我们所熟悉的,是消息动态决议那一步,系统给我们的一次补救机会。那么forwardingTargetForSelectormethodSignatureForSelectordoesNotRecognizeSelector又是什么?

消息快速转发

回到源码,全局搜索forwardingTargetForSelector发现找不到相关描述, 那么我们打开Xcode - Window - Developer Documentation,搜索一下:

image.png

  • 中式英语翻译:返回一个对象,成为未被识别的消息的第一指向。

意思就是消息的重定向。 我们继续看文档里对的描述:

Discussion
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result, that returned object is used as the new receiver object and the message dispatch resumes to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)
If you implement this method in a non-root class, if your class has nothing to return for the given selector then you should return the result of invoking supers implementation.
This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.
复制代码
翻译过来大概意思:
  • 如果A对象实现了forwardingTargetForSelector:方法,并且返回一个非nil非self的结果,那么这个返回结果将被作为新的消息接收者,加入返回的是self,就会进入无限循环
  • 还有一个更高级的 forwardInvocation:方法,不过在转发速度上它不如forwardingTargetForSelector:,但是却可以处理更多的信息。
    简单来说,如果只是确定新的接收者,那么forwardingTargetForSelector:将是更好的选择,如果需要对参数进行操作,选择forwardInvocation:更好。

我们来实操一下啊: 定义一个SSJMethodForwordClass类,用来作为新的消息接收者:

@interface SSJMethodForwordClass : NSObject
@end

@implementation SSJMethodForwordClass
///实现sleep方法
- (void)sleep{
    NSLog(@"SSJMethodForwordClass  sleep");
}
@end
复制代码

对SSJAnimal进行添加forwardingTargetForSelector:方法:

@implementation SSJAnimal
- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"-%s   sel-%@",__func__,NSStringFromSelector(aSelector));
    ///这里我们设定新的消息接收者是SSJMethodForwordClass对象
    return [SSJMethodForwordClass alloc];
}
@end
复制代码

运行看效果:

image.png

消息慢速转发

那如果SSJMethodForwordClass里并没有实现sleep方法呢,又当如何?

同样的方法,在文档里搜索methodSignatureForSelector

image.png

Discussion
This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.
//该方法用于协议的实现。此方法还用于必须创建NSInvocation对象的情况,
//例如在消息转发期间。如果您的对象维护委托或能够处理它不直接实现的消息,
//则应该重写此方法以返回适当的方法签名。

//简单例子就是methodSignatureForSelector需要返回方法签名

复制代码

methodSignatureForSelector文档底下有提到关联文件- forwardInvocation:,点进去看下:

Discussion
When an object is sent a message for which it has no corresponding method, the runtime system gives the receiver an opportunity to delegate the message to another receiver. It delegates the message by creating an NSInvocation object representing the message and sending the receiver a forwardInvocation: message containing this NSInvocation object as the argument. The receiver’s forwardInvocation: method can then choose to forward the message to another object. (If that object can’t respond to the message either, it too will be given a chance to forward it.)
The forwardInvocation: message thus allows an object to establish relationships with other objects that will, for certain messages, act on its behalf. The forwarding object is, in a sense, able to “inherit” some of the characteristics of the object it forwards the message to.

翻译:运行时系统通过给消息接收者发送`forwardInvocation:`,且参数是表示消息的
NSInvocation,接收方可以在`forwardInvocation:`将消息转发给另一个对象。
复制代码

然后我们还看到这样一段黄色背景标注的话:

image.png 翻译过来大概意思是:

  • 若要响应对象本身无法识别的方法,除了forwardInvocation:还必须重写methodSignatureForSelector:
  • 转发消息的机制使用从methodSignatureForSelector:获取的信息来创建要转发的NSInvocation对象。
  • 重写方法必须为给定的选择器提供适当的方法签名,方法可以是预先制定一个方法签名,也可以是向另一个对象请求一个方法签名。
所以我们要做的事情是两个:
  • 实现methodSignatureForSelector:
  • 实现forwardInvocation:
//消息慢速转发
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"-%s   sel-%@",__func__,NSStringFromSelector(aSelector));
    if(aSelector == @selector(sleep)){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
///anInvocation包含了方法签名的信息,
//在forwardInvocation方法里,我们可以对异常方法调用进行处理或不处理,都不会崩溃
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"-%s   numberOfArguments-%lu",__func__,(unsigned long)anInvocation.methodSignature.numberOfArguments);
}
复制代码

运行结果:

image.png

当然我们也可以在forwardInvocation:对未实现方法进行处理

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    ///定义一个新的消息接收者,把未实现的消息丢给它来实现
    SSJMethodForwordClass *methodForwordClass = [SSJMethodForwordClass alloc];
    ///如果methodForwordClass能够响应,就把消息转发给它
    if ([methodForwordClass respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:methodForwordClass];
        NSLog(@"%@找不到?问题不大,我已经处理了~",NSStringFromSelector(anInvocation.selector));
    }else{
        ///处理不了,那就不处理;并且做上传日志等操作
        NSLog(@"哎呀呀呀呀呀,出问题啦!!!!%@找不到啦~~",NSStringFromSelector(anInvocation.selector));
    }
}
复制代码

运行结果:

image.png

我们继续看文档:

image.png

其中有一段是这样描述的:
A forwardInvocation: method might also involve several other objects in the response to a given message, 
rather than forward it to just one.
大概意思是说,消息的新的接收者不唯一,未实现的SEL可以交给多个接收者去处理。
复制代码

那就按照官方提示,对方法进行改进

//消息慢速转发

//.h文件就不写了,直接展示.m
@implementation SSJMethodForwordClass
- (void)sleep{
    NSLog(@"SSJMethodForwordClass  sleep");
}

@implementation SSJMethodForwordOtherClass
- (void)sleep{
    NSLog(@"SSJMethodForwordOtherClass  sleep");
}

// methodSignatureForSelector
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSLog(@"-%s   sel-%@",__func__,NSStringFromSelector(aSelector));
    //既然已经打算把处理写在forwardInvocation:里,这里直接返回NSMethodSignature即可
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// forwardInvocation
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    ///定义第一个消息接收者: methodForwordOtherClass
    SSJMethodForwordOtherClass *methodForwordOtherClass = [SSJMethodForwordOtherClass alloc];
    if ([methodForwordOtherClass respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:methodForwordOtherClass];
        NSLog(@"%@找不到?问题不大,SSJMethodForwordOtherClass已经处理了~",NSStringFromSelector(anInvocation.selector));
        NSLog(@"\n");
    }
    ///定义第二个消息接收者: methodForwordClass
    SSJMethodForwordClass *methodForwordClass = [SSJMethodForwordClass alloc];
    ///如果methodForwordClass能够响应,就把消息转发给它
    if ([methodForwordClass respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:methodForwordClass];
        NSLog(@"%@找不到?问题不大,SSJMethodForwordClass已经处理了~",NSStringFromSelector(anInvocation.selector));
        NSLog(@"\n");
    }else{
        ///处理不了,那就不处理;并且做上传日志等操作
        NSLog(@"哎呀呀呀呀呀,出问题啦!!!!%@找不到啦~~",NSStringFromSelector(anInvocation.selector));
//        [super forwardInvocation:anInvocation];
    }
    
}
复制代码

运行结果: image.png

这里需要注意一点:

要测试methodSignatureForSelector:方法,需要先注释forwardingTargetForSelector:方法,因为 forwardingTargetForSelector:返回的是SSJMethodForwordClass对象,如果SSJMethodForwordClass对象没有实现sleep方法,那就会进入下一个循环,继续寻找SSJMethodForwordClass的forwardingTargetForSelector:,然后发现SSJMethodForwordClass并未实现forwardingTargetForSelector:,所以报错提示找不到方法。

doesNotRecognizeSelector:

我们再来看看,日志打印里最后一个doesNotRecognizeSelector:函数,文档里搜索doesNotRecognizeSelector:

Discussion
The runtime system invokes this method whenever an object receives an aSelector message it
can’t respond to or forward. This method, in turn, raises an NSInvalidArgumentException,
and generates an error message.

翻译:
每当对象接收到它无法响应或转发的aSelector消息时,运行时系统就会调用此方法。
此方法反过来引发NSInvalidArgumentException,并生成错误消息。
复制代码

由此可知,当消息转发都失败的时候,系统在运行时调用doesNotRecognizeSelector:来抛出异常。
objc源码里搜索一下doesNotRecognizeSelector:

// Replaced by CF (throws an NSException)
+ (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("+[%s %s]: unrecognized selector sent to instance %p", 
                class_getName(self), sel_getName(sel), self);
}

// Replaced by CF (throws an NSException)
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}
复制代码

发现的的确确就是报异常,跟我们控制台显示的格式一摸一样。

拓展

前面是通过查看日志打印及结合文档的方式,才知道消息快速转发消息慢速转发的存在,那么有没有其它办法让我们确确实实的看到它们在底层有被调用到呢?

正常情况下,没有实现消息转发,调用一个未实现的方法报错信息是这样的:

image.png 控制台输入bt打印堆栈信息:

image.png 我们发现,在回到main函数之前,系统执行了CoreFoundation里的两个函数:___forwarding____CF_forwarding_prep_0
在objc源码里搜不到这两个东西,于是我们就想到了从CoreFoundation源码入手,看看里面是否可以找到,结果还是没有。
于是我们想到了从编译好的CoreFoundation可执行文件(获取方式在下面)入手,对其进行反汇编,这里提供一个工具:

  • hopper
  • 网上找了张图,介绍了hooper的界面每个地方都是干什么的

1972799-871e5f22a2bfc5ae.png

CoreFoundation可执行文件拖到hopper工具里:

image.png

image.png

接下来,我们搜索___forwarding___,找到对应的代码: image.png

我们看到这样一段代码:

image.png 判断类是否能够响应forwardingTargetForSelector:,如果响应了就会进入下面的流程,后面将taggedpointer设置为新的消息接收者;如果不能响应,就进入loc_64a67,我们点进去看下:

image.png 不能响应_forwardStackInvocation:(forwardStackInvocation并没有对外抛出)就进入loc_64c19,我们进入loc_64c19

image.png 到底,我们就可以知道,在底层里面,方法动态协议未实现,报错之前确确实实按照先后顺序检测了几个方法是否有被实现:

  1. forwardingTargetForSelector:(重新设置消息接收者)
  2. methodSignatureForSelector:(返回方法签名)
  3. _forwardStackInvocation: (系统没有对外开放)
  4. forwardInvocation:

获取CoreFoundation可执行文件

Xcode控制台输入image list :

image.png 发现它存在于/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation

总结

慢速查找没有找到imp之后:

  1. 方法动态决议
    判断消息接收者
        实例对象  resolveInstanceMethod
        类       resolveClassMethod
复制代码
  1. forwardingTargetForSelector
  2. methodSignatureForSelector
  3. forwardInvocation
  4. 抛出异常

代码已上传

账号:pan.baidu.com/s/1M5-LHGIi…
密码:ptxj

文章分类
iOS
文章标签