Objective-C Runtime 总结:消息机制 篇

摘录:
http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/
http://www.codeceo.com/article/objective-c-runtime-class.html

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。

与Runtime交互

Objc 从三种不同的层级上与 Runtime 系统进行交互,分别是通过 Objective-C 源代码,通过 Foundation 框架的NSObject类定义的方法,通过对 runtime 函数的直接调用。

  • Objective-C源代码

大部分情况下你就只管写你的Objc代码就行,runtime 系统自动在幕后辛勤劳作着。
还记得引言中举的例子吧,消息的执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数,Objc中的类、方法和协议等在 runtime 中都由一些数据结构来定义,这些内容在后面会讲到。(比如objc_msgSend函数及其参数列表中的id和SEL都是啥)

  • NSObject的方法

Cocoa 中大多数类都继承于NSObject类,也就自然继承了它的方法。最特殊的例外是NSProxy,它是个抽象超类,它实现了一些消息转发有关的方法,可以通过继承它来实现一个其他类的替身类或是虚拟出一个不存在的类,说白了就是领导把自己展现给大家风光无限,但是把活儿都交给幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用,比如description方法需要你重载它并为你定义的类提供描述内容。NSObject还有些方法能在运行时获得类的信息,并检查一些特性,比如class返回对象的类;isKindOfClass:和isMemberOfClass:则检查对象是否在指定的类继承体系中;respondsToSelector:检查对象能否响应指定的消息;conformsToProtocol:检查对象是否实现了指定协议类的方法;methodForSelector:则返回指定方法实现的地址。

  • Runtime的函数

Runtime 系统是一个由一系列函数和数据结构组成,具有公共接口的动态共享库。头文件存放于/usr/include/objc目录下。许多函数允许你用纯C代码来重复实现 Objc 中同样的功能。虽然有一些方法构成了NSObject类的基础,但是你在写 Objc 代码时一般不会直接用到这些函数的,除非是写一些 Objc 与其他语言的桥接或是底层的debug工作。在Objective-C Runtime Reference中有对 Runtime 函数的详细文档。

Runtime消息传递

Objc 中发送消息是用中括号([])把接收者和消息括起来,而直到运行时才会把消息与方法实现绑定。
[receiver message]会被编译器转化为:
id objc_msgSend ( id self, SEL op, ... )
如果消息含有参数,则为:
objc_msgSend(id, op, arg1, arg2, ...)

这里简单讲一下必要的参数:

  • id:指针,可指向任意对象,定义为typedef struct objc_object *id;
  • SEL:区分方法的 ID,是个映射到方法的C字符串,可以用 Objc 编译器命令@selector()来获取,定义为typedef struct objc_selector *SEL; ,不同类中相同名字的方法所对应的方法选择器是相同的。
  • IMP:它就是一个函数指针,指向了这个方法的实现,就是最终要执行的那段代码,这是由编译器生成的。定义为typedef id (*IMP)(id, SEL, ...); ,
    当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。IMP指向的方法与objc_msgSend函数类型相同,参数都包含id和SEL类型,前面说过,不同类中相同名字的方法所对应的方法选择器是相同的,而每个对象中的SEL对应的方法实现肯定是唯一的,通过一组id和SEL参数就能确定唯一的方法实现地址,即 知道id,SEL便可以确定IMP。

消息发送步骤:
1. 检测这个 selector 是不是要忽略的。
2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会Crash,因为会被忽略掉。
3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。
4. 如果 cache 找不到就找一下方法分发表。
5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。
6. 如果还找不到就要开始进入动态方法解析了,后面会提到

PS:这里说的分发表其实就是Class中的方法列表,它将方法选择器和方法实现地址联系起来

这里写图片描述

动态方法解析

当一个消息发送过程中,如果找不到对应方法的实现,便会进行动态方法解析,可让我们动态绑定方法实现
设有B类,声明了方法resolveThisMethodDynamically

@interface B : NSObject
- (void)resolveThisMethodDynamically;
@end

然后直接调用该方法

B *b=[B new];
[b resolveThisMethodDynamically];

正常情况下由于没有方法实现,程序崩溃。然而,我们可以在B类中通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现,
因为当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会

void dynamicMethodIMP(){
    // implementation ....
    NSLog(@"这是dynamicMethodIMP");
}

//动态绑定实例方法实现IMP
 + (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, "v");//其中 “v@:” 表示返回值和参数,这个符号涉及 Type Encoding
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

结果,调用[b resolveThisMethodDynamically]最终会执行void dynamicMethodIMP() 方法,达到动态绑定方法实现的效果。

PS:

  • 前提是没有找到对应方法的实现,runtime才会调用resolveInstanceMethod:或resolveClassMethod:
    说明:class_addMethod最后一个参数是Type
    Encoding

  • 如果 respondsToSelector: 或
    instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会
    所以执行

[b respondsToSelector:@selector(resolveThisMethodDynamically)]

是返回YES的。

  • 动态方法解析会在消息转发机制浸入前执行,如果你想让该方法选择器被传送到转发机制,那么就让resolveInstanceMethod:返回NO

    在讲消息转发前我们先看一下整个转发机制的流程
    这里写图片描述

消息转发

重定向

在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象。
前提是,先让resolveInstanceMethod:返回NO,才会调用forwardingTargetForSelector:
我们在B类中重载方法:

//先返回NO
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
        if (aSEL == @selector(resolveThisMethodDynamically))     {
        return NO;
    }
    return [super resolveInstanceMethod:aSEL];
}

//更改接受者
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(resolveThisMethodDynamically)){
        return [OtherObject new]; //返回另外一个对象,将该消息重定向给别人,变成[otherObject resolveThisMethodDynamically]
    }
    return [super forwardingTargetForSelector:aSelector];
}

返回另外一个对象,将该消息重定向给别人,变成[otherObject resolveThisMethodDynamically]。
毕竟消息转发要耗费更多时间,抓住这次机会将消息重定向给别人是个不错的选择。
PS:如果此方法返回nil或self,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。

转发

当动态方法解析不作处理返回NO时,则会调用forwardingTargetForSelector更改接受者,若返回nil或self,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:

//转发
- (void)forwardInvocation:(NSInvocation *)anInvocation //anInvocation封装了原始的消息和消息的参数
{
    id someOtherObject=[OtherObject new];

    if ([someOtherObject respondsToSelector:
         [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。
这里需要注意的是参数anInvocation是从哪的来的呢?其实在forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。
所以所以我们在重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,并且返回不为空的methodSignature,否则会crash崩溃

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

    if (aSelector==@selector(resolveThisMethodDynamically)) {
        // Type Encoding: v->void 、 @->id 、 :->SEL
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];//v@: 这里v代表函数返回类型void,后面三个字符参上
    }else{
        return [super methodSignatureForSelector:aSelector];
    }

}

说明:这里[NSMethodSignature signatureWithObjCTypes:"v@:"]中的v@: 是Type Encoding,表示了resolveThisMethodDynamically的返回类型和参数类型,但这里方法并没有带参数,为何会有@:呢,下面来解释一下:
Objective-C中的方法默认被隐藏了两个参数:self和_cmd。self指向对象本身,_cmd指向方法本身。
被指定为动态实现的方法的参数类型有如下的要求:
A.第一个参数类型必须是id(就是self的类型)
B.第二个参数类型必须是SEL(就是_cmd的类型)
C.从第三个参数起,可以按照原方法的参数类型定义,
如:-(void)setName:(NSString)*name 对应Type Encoding为v@:@
最后的@表示参数name的类型

PS:
1.转发和继承相似,可以用于为Objc编程添加一些多继承的效果,就好像继承了ViewController的方法一样
2.尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector: 和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链

Method Swizzling

之前所说的消息转发虽然功能强大,但需要我们了解并且能更改对应类的源代码,因为我们需要实现自己的转发逻辑。当我们无法触碰到某个类的源代码,却想更改这个类某个方法的实现时,该怎么办呢?可能继承类并重写方法是一种想法,但是有时无法达到目的。这里介绍的是 Method Swizzling ,它通过重新映射方法对应的实现来达到“偷天换日”的目的。跟消息转发相比,Method Swizzling 的做法更为隐蔽,甚至有些冒险,也增大了debug的难度。

这里摘抄一个例子:
将UIViewController类的viewWillAppear:方法和xxx_viewWillAppear:方法的实现相互调换
在ViewController类里重写load:

+ (void)load {
    Class aClass = [self class];

    SEL originalSelector = @selector(viewWillAppear:);
    SEL swizzledSelector = @selector(xxx_viewWillAppear:);

    Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

    // When swizzling a class method, use the following:
    // Class aClass = object_getClass((id)self);
    // ...
    // Method originalMethod = class_getClassMethod(aClass, originalSelector);
    // Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);
    //object_getClass((id)self) 与 [self class] 返回的结果类型都是 Class,但前者为元类,后者为其本身,因为此时 self 为 Class 而不是实

    BOOL didAddMethod =
    class_addMethod(aClass,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));

    //如果类中不存在要替换的方法,那就先用class_addMethod和class_replaceMethod函数添加和替换两个方法的实现
    if (didAddMethod) {
        class_replaceMethod(aClass,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        //如果类中已经有了想要替换的方法,那么就调用method_exchangeImplementations函数交换了两个方法的 IMP
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }

}

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

Swizzling 应该在+load方法中实现,因为+load是在一个类最开始加载时调用。
xxx_viewWillAppear:方法的定义看似是递归调用引发死循环,其实不会的。因为[self xxx_viewWillAppear:animated]消息会动态找到xxx_viewWillAppear:方法的实现,而它的实现已经被我们与viewWillAppear:方法实现进行了互换,所以这段代码不仅不会死循环,如果你把[self xxx_viewWillAppear:animated]换成[self viewWillAppear:animated]反而会引发死循环

PS:如果类中没有想被替换实现的原方法时,class_replaceMethod相当于直接调用class_addMethod向类中添加该方法的实现;否则调用method_setImplementation方法,types参数会被忽略。method_exchangeImplementations方法做的事情与如下的原子操作等价

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值