【iOS】消息传递

Objective-C是一种动态语言,因此其很多行为是在运行时决定的。对于静态语言来说,函数的调用在编译时就已经确定。动态语言则不然,动态语言通过一些巧妙的机制使得函数的真实调用是在运行时决定的,即动态语言的特点是将一些决定性的工作从软件的编译时延迟到了软件的运行时。Objective-C主要是运用消息机制实现运行时特性。

使用消息发送代替函数调用

先回忆一个Objective-C语言的细节。当Objective-C中的某个对象调用了一个不存在的方法时,程序通常会产生Crash,并且控制台会输出信息,告诉开发者是因为调用了哪个方法造成的本次Crash,大致信息如下:

    unrecognized selector sent to instance xxx

从字面理解上面的信息,其意思为“向实例发送了无法识别的选择器”,这里的选择器可以理解为函数方法、实例即是对象,因此在Objective-C中,对象调用方法,实际上是向对象发送了方法选择器消息。

Objective-C这门语言的语法与大多数主流语言有很大差异,尤其是方法的调用,Objective-C采用中括号的方式调用方法,从行为上看,Objective-C语言的方法调用与其他静态语言类似,但是其有本质上的不同。Objective-C语言在编译时会将方法调用转换为消息发送,处理消息的对象和消息的处理方式可以在运行时灵活确定,因此消息传递是Objective-C语言运行时动态性的基础。

我们创建一个测试类,为其定义一个测试方法,示例代码如下:

    #import <Foundation/Foundation.h>
    @interface MyObject : NSObject
    - (void)hello;
    @end
    @implementation MyObject
    - (void)hello {
        NSLog(@"HelloWorld");
    }
    @end
    int main(int argc, const char * argv[]) {
        MyObject *obj = [[MyObject alloc] init];
        [obj hello];
        return 0;
    }

上面代码中创建了一个名为MyObject的类,在其中定义了一个名为hello的方法,在main函数中,对MyObject类进行了实例化,并且调用了对象的hello方法。运行代码,控制台将输出hello方法中打印的信息。对于上面代码中函数的调用,在编译时会被转换成C语言风格的消息发送函数。也可以手动调用C语言风格的消息发送函数来实现同样的效果,修改代码如下:

    #import <Foundation/Foundation.h>
    #import <objc/message.h>
    @interface MyObject : NSObject
    - (void)hello;
    @end
    @implementation MyObject
    - (void)hello {
        NSLog(@"HelloWorld");
    }
    @end
    int main(int argc, const char * argv[]) {
        MyObject *obj = [[MyObject alloc] init];
        ((void(*)(id, SEL))objc_msgSend)(obj, @selector(hello));
        return 0;
    }

运行代码,可以看到控制台中打印出了字符串“HelloWorld”,说明成功执行了MyObject实例对象的hello方法。上面调用的objc_msgSend函数用来发送消息,这个函数的第1个参数为消息要发送给的对象,第2个参数为要执行的方法选择器,后面还可以继续添加任意个数的参数,后面添加的参数都会作为方法选择器对应方法中的参数。注意,为了使编译能够顺利通过,需要根据传参的个数对objc_msgSend函数的类型进行强转。例如:

    #import <Foundation/Foundation.h>
    #import <objc/message.h>
    @interface MyObject : NSObject
    - (void)hello:(NSString *)name ;
    @end
    @implementation MyObject
    - (void)hello:(NSString *)name {
        NSLog(@"HelloWorld:%@", name);
    }
    @end
    int main(int argc, const char * argv[]) {
        MyObject *obj = [[MyObject alloc] init];
        ((void(*)(id, SEL, NSString *))objc_msgSend)(obj, @selector(hello:),
@"Lili");
        return 0;
    }

再次运行代码,通过控制台的打印可以看出参数已经被正确地传递进指定的方法。

消息传递的过程

在上一小节中,我们采用了直接发送消息的方式来调用对象的方法。@selector的本质是获取方法的签名,在Objective-C中,所有Objective-C类最终都将继承自NSObject类。在NSObject类中定义了一个名为isa的成员变量:

    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }

isaClass类型,表示当前对象所属于的类。Class实际上是Objective-C中定义的一个结构体,其中封装了类的基本信息,具体如下:

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指针找到对应的类,在类的方法列表中搜索对应签名的函数进行调用,并且消息的处理是基于继承链的向对象发送消息后,首先会从对象所属类的方法列表中寻找对应方法,如果当前类中没有找到,就向其父类中继续寻找,如果父类中依然没有找到对应方法,则会继续向上寻找,直到在继承链中找到要执行的方法,或者直到寻找到基类都没有找到再结束。如果最终没有找到对象要执行的方法,则Objective-C的默认处理会使程序抛出异常

在消息传递的过程中,还会发生一些有趣的事情。首先,如果对象对某个消息在整个继承链中都没有找到对应的方法,则之后会调用类的resolveInstanceMethod方法,这个方法的作用是动态处理不能识别的实例方法,与之对应的还有一个resolveClassMethod方法,这个方法用来动态处理不能识别的类方法。例如:

    #import <Foundation/Foundation.h>
    #import <objc/message.h>
    @interface MyObject : NSObject
    - (void)hello:(NSString *)name;
    @end
    @implementation MyObject
    - (void)hello:(NSString *)name {
        NSLog(@"HelloWorld:%@", name);
    }
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod");
        return [super resolveInstanceMethod:sel];
    }
    @end
    int main(int argc, const char * argv[]) {
        MyObject *obj = [[MyObject alloc] init];
        ((void(*)(id, SEL, NSString *))objc_msgSend)(obj, @selector(hello2:),
@"Lili");
        return 0;
    }

上面的代码通过发消息的方式调用了一个不存在的实例方法,运行代码后虽然程序依然会Crash,但是从控制台的打印信息可以看出,在程序崩溃前执行了resolveInstanceMethod方法,在这个方法中可以通过Objective-C提供的运行时函数来动态地处理不能识别的方法选择器,示例如下:

    void dynamicMethodIMP(id obj, SEL method, NSString *name) {
        NSLog(@"实例:%@, 方法名:%@, 参数:%@", obj, NSStringFromSelector(method),
name);
    }
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        NSLog(@"resolveInstanceMethod");
        if ([NSStringFromSelector(sel) isEqualToString:@"hello2:"]) {
            class_addMethod(self, sel, (void (*)(void))dynamicMethodIMP, "v@:@");
        }
        return [super resolveInstanceMethod:sel];
    }

再次运行代码,可以看到程序执行了我们动态添加的dynamicMethodIMP函数。上面代码的逻辑是当检查到不能识别的选择器后,调用class_addMethod函数在运行时动态地为类添加一个新的方法。class_addMethod中的几个参数都有特殊的意义:第1个参数为要添加方法的类;第2个参数为对应的方法签名;第3个参数为实现此方法的函数指针;第4个参数为一个字符串,用来表示所添加方法的返回值与参数类型,这个字符串中的首字母表示方法的返回值类型,后面的字母都表示参数类型。例如,在上面的示例代码中,“v@:@”表示返回值为void第1个参数为对象类型,第2个参数为SEL选择器类型,第3个参数为对象类型。在添加方法时,前两个参数是固定的,由系统调用时自动传入,字符与其表示的类型对应表如图所示。

【iOS】消息传递_第1张图片

resolveInstanceMethod给开发者提供了一种动态处理未识别选择器的方式。如果我们不对这个方法做任何额外的处理,则之后会进行消息转发流程,会调用类的实例方法forwardingTargetForSelector。我们可以通过这个方法返回一个实例对象,当前对象无法处理的消息会被转发给被返回的实例对象,示例代码如下:

【iOS】消息传递_第2张图片

上面的代码创建了一个名为MyObject2的类,其中对hello2方法进行了实现,在MyObject类实例对象的forwardingTargetForSelector方法中,返回了MyObject2类的实例对象,之后此消息会被转发给MyObject2实例,运行代码通过打印信息可以看到hello2方法被成功执行了。由于Objective-C提供了这样的消息转发机制,因此对象方法的执行就变得格外灵活,Objective-C本身是一种单继承的语言,即子类只能有一个父类,但是通过消息转发机制,我们可以实现类似多继承的功能。

通过forwardingTargetForSelector方法进行的消息转发也被称为直接转发,其直接将消息转发给指定的对象,如果不在此方法中处理,还有两个方法可以用来进行间接的消息转发。methodSignatureForSelector方法会被调用询问某个选择器的有效性,如果开发者认为有效,就需要将其包装为函数签名对象NSMethodSignature的实例进行返回,之后系统会调用forwardInvocation方法进行选择器方法的调用,示例如下:

【iOS】消息传递_第3张图片

运行代码,可以看到上面代码的作用实际上也是进行了消息的转发。

调用一个无法识别的方法时,在程序抛出异常之前会经历3个阶段:第1个阶段为动态处理阶段;第2个阶段为直接转发阶段;第3个阶段为间接转发阶段。任何一个阶段都可以在运行时动态改变程序的执行逻辑。这个完整的消息机制构建了Objectiive-C运行时的基础。如果某个消息最终没有被处理而产生了程序的Crash,那么其原因是最终执行到了NSObject类中定义的doesNotRecognizeSelector方法。我们可以通过重写这个方法来自定义控制台的信息输出,例如:

【iOS】消息传递_第4张图片

Objective-C语言的消息机制非常强大却并不复杂,其核心过程可用下图总结:

【iOS】消息传递_第5张图片

你可能感兴趣的:(ios,cocoa,xcode)