Runtime(二)

(三)、objc_msgSend执行流程

OC中的方法调用,其实都是转换为objc_msgSend函数的调用

objc_msgSend的执行流程可以分为3大阶段:

  • 消息发送
  • 动态方法解析
  • 消息转发

阅读源码:objc_msgSend的源码是用汇编实现的:

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    /*x0寄存器:消息接收者,receiver*/
    cmp    p0, #0            // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    ldr    p13, [x0]        // p13 = isa
    GetClassFromIsa_p16 p13        // p16 = class
LGetIsaDone:
    CacheLookup NORMAL        // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
   b.eq    LReturnZero        // nil check

   // tagged
   adrp    x10, _objc_debug_taggedpointer_classes@PAGE
   add    x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
   ubfx    x11, x0, #60, #4
   ldr    x16, [x10, x11, LSL #3]
   adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
   add    x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
   cmp    x10, x16
   b.ne    LGetIsaDone

   // ext tagged
   adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
   add    x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
   ubfx    x11, x0, #52, #8
   ldr    x16, [x10, x11, LSL #3]
   b    LGetIsaDone
   // SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
   // x0 is already zero
   mov    x1, #0
   movi    d0, #0
   movi    d1, #0
   movi    d2, #0
   movi    d3, #0
   ret

  END_ENTRY _objc_msgSend
  • 源码执行流程
  • bjc-msg-arm64.s 文件 (汇编)
(1)ENTRY _objc_msgSend
(2)b.le   LNilOrTagged
(3)CacheLookup NORMAL
(4).macro CacheLookup
(5).macro CheckMiss
(6)STATIC_ENTRY __objc_msgSend_uncached
(7).macro MethodTableLookup
(8) __class_lookupMethodAndLoadCache3
  • objc-runtime-new.mm
(1)_class_lookupMethodAndLoadCache3
(2)lookUpImpOrForward
(3)getMethodNoSuper_nolock、search_method_list、log_and_fill_cache
(4)cache_getImp、log_and_fill_cache、getMethodNoSuper_nolock、log_and_fill_cache
(5)_class_resolveInstanceMethod
(6)_objc_msgForward_impcache

-objc-msg-arm64.s

(1) STATIC_ENTRY __objc_msgForward_impcache
(2) ENTRY __objc_msgForward
  • Core Foundation
__forwarding__(不开源)
(1)、objc_msgSend执行流程01-消息发送
消息发送流程
(2)objc_msgSend执行流程02-动态方法解析
动态方法解析
(3)objc_msgSend的执行流程03-消息转发(自己没有能力处理的时候,将消息转发给别人)
消息转发

(四)、面试题

(1)、讲一下OC的消息机制
  • OC中的方法调用其实都是转成了objc_msgSend函数的调用,给
    receiver(方法调用者)发送了一条消息(selector方法名)
  • objc_msgSend底层有3大阶段
    消息发送(当前类、父类中查找)、动态方法解析、消息转发
(2)、消息转发机制流程
(3)、什么是Runtime?平时项目中有用过么?
  • OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行。
  • OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数
  • 平时编写的OC代码,底层都是转换成了Runtime API进行调用
  • 具体应用
  • 利用关联对象(AssociatedObject)给分类添加属性
  • 遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
  • 交换方法实现(交换系统的方法)
  • 利用消息转发机制解决方法找不到的异常问题

(四)、具体应用

具体应用
(1)、动态方法交换:Method Swizzling

实现动态方法交换(Method Swizzling )是Runtime中最具盛名的应用场景,其原理是:通过Runtime获取到方法实现的地址(IMP),并且会清空方法缓存,进而动态交换两个方法的功能。使用到关键方法如下:

//获取类方法的Method
(1)Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
//获取实例对象方法的Method
(2)Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
//交换两个方法的实现
(3)void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
  • 交换两个方法的实现
    创建person类:HYPerson
HYPerson.h
#import <Foundation/Foundation.h>
@interface HYPerson : NSObject

- (void)run;
- (void)test;
@end


HYPerson.m
#import "HYPerson.h"
@implementation HYPerson

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

- (void)test
{
    NSLog(@"%s", __func__);
}
@end

main.m

#import <Foundation/Foundation.h>
#import "HYPerson.h"
#import <objc/runtime.h>

void exchangeMethod(){
    //交换方法实现
    HYPerson *person = [[HYPerson alloc] init];
    Method runMethod = class_getInstanceMethod([HYPerson class], @selector(run));
    Method testMethod = class_getInstanceMethod([HYPerson class], @selector(test));
    method_exchangeImplementations(runMethod, testMethod);

    [person run];
    [person test];
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        exchangeMethod();
    }
    return 0;
}

打印结果:

2020-01-19 10:57:11.221840+0800 runtime-API-方法[21956:117139] -[HYPerson test]
2020-01-19 10:57:11.222216+0800 runtime-API-方法[21956:117139] -[HYPerson run]
Program ended with exit code: 0
  • 拦截并替换系统方法:拦截button的点击事件,并统一处理
    创建UIControl的分类:UIControl+Extension
-------UIControl+Extension.h-------
#import <UIKit/UIKit.h>

@interface UIControl (Extension)

@end


-------UIControl+Extension.m--------
#import "UIControl+Extension.h"
#import <objc/runtime.h>

@implementation UIControl (Extension)

+(void)load{
    Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method method2 = class_getInstanceMethod(self, @selector(hy_sendAction:to:forEvent:));
    
    
    method_exchangeImplementations(method1, method2); //交换的是IMP(方法的内存地址),并且会清空方法缓存(cache)
    
}
- (void)hy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
//    NSLog(@"%@--%@--%@",self,target,event);
    if ([self isKindOfClass:[UIButton class]]) {
        //拦截了所有按钮的点击事件
        [self hy_sendAction:action to:target forEvent:event];//不覆盖系统的方法,依然实现系统的处理逻辑。
        //(看着像是死循环,其实不然,因为已经交换了方法,[self hy_sendAction:action to:target forEvent:event]内部实现是- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event )
    }else{
        
    }
}
@end
(2)、实现分类添加属性:

我们开发中常常使用分类Category为一些已有的类扩展功能。虽然继承也能够为已有类增加新的方法,而且相比类目更是具有增加属性的优势,但是继承毕竟是一个重量级的操作,添加不必要的继承关系无疑增加了代码的复杂度。

遗憾的是,OC的类目并不支持直接添加属性,如果我们直接在分类的声明中写入Property属性,那么只能为其生成set与get方法声明,却不能生成成员变量,直接调用这些属性还会造成崩溃。

所以为了实现给分类添加属性,我们还需借助Runtime的关联对象(Associated Objects)特性,它能够帮助我们在运行阶段将任意的属性关联到一个对象上;下面是相关的三个方法:

/**
 1.给对象设置关联属性
 @param object 需要设置关联属性的对象,即给哪个对象关联属性
 @param key 关联属性对应的key,可通过key获取这个属性,
 @param value 给关联属性设置的值
 @param policy 关联属性的存储策略(对应Property属性中的assign,copy,retain等)
 OBJC_ASSOCIATION_ASSIGN             @property(assign)。
 OBJC_ASSOCIATION_RETAIN_NONATOMIC   @property(strong, nonatomic)。
 OBJC_ASSOCIATION_COPY_NONATOMIC     @property(copy, nonatomic)。
 OBJC_ASSOCIATION_RETAIN             @property(strong,atomic)。
 OBJC_ASSOCIATION_COPY               @property(copy, atomic)。
 */
void objc_setAssociatedObject(id _Nonnull object,
                              const void * _Nonnull key,
                              id _Nullable value,
                              objc_AssociationPolicy policy)
/**
 2.通过key获取关联的属性
 @param object 从哪个对象中获取关联属性
 @param key 关联属性对应的key
 @return 返回关联属性的值
 */
id _Nullable objc_getAssociatedObject(id _Nonnull object,
                                      const void * _Nonnull key)
/**
 3.移除对象所关联的属性
 @param object 移除某个对象的所有关联属性
 */
void objc_removeAssociatedObjects(id _Nonnull object)

注意:key与关联属性一一对应,我们必须确保其全局唯一性,常用我们使用@selector(methodName)作为key。
创建一个UIImage的分类:UIImage+Tools

UIImage+Tools.h

#import <UIKit/UIKit.h>

@interface UIImage (Tools)

//添加一个新属性:图片网络链接
@property(nonatomic,copy)NSString *urlString;
- (void)clearAssociatedObjcet;
@end

UIImage+Tools.m

#import "UIImage+Tools.h"
#import <objc/runtime.h>
/*
 typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
     OBJC_ASSOCIATION_ASSIGN = 0,             //关联对象的属性是弱引用
     OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   //关联对象的属性是强引用并且关联对象不使用原子性
     OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     //关联对象的属性是copy并且关联对象不使用原子性
     OBJC_ASSOCIATION_RETAIN = 01401,         //关联对象的属性是copy并且关联对象使用原子性
     OBJC_ASSOCIATION_COPY = 01403            //关联对象的属性是copy并且关联对象使用原子性
 };
 */
@implementation UIImage (Tools)

- (void)setUrlString:(NSString *)urlString {
    objc_setAssociatedObject(self, @selector(urlString), urlString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)urlString {
    return objc_getAssociatedObject(self, @selector(urlString));
}
//添加一个自定义方法,用于清除所有关联属性
- (void)clearAssociatedObjcet{
    objc_removeAssociatedObjects(self);
}
@end

ViewController.m

#import "ViewController.h"
#include "UIImage+Tools.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    UIImage *image = [[UIImage alloc]init];
    image.urlString = @"图片";
    NSLog(@"%@",image.urlString);
    
    //清除关联的属性
    [image  clearAssociatedObjcet];
    
    NSLog(@"%@",image.urlString);
    
}

打印结果:

2020-01-19 15:08:05.847526+0800 给分类添加属性[37968:191750] https://www.1244124-7d013e9e252dba2a.png
2020-01-19 15:08:05.847687+0800 给分类添加属性[37968:191750] 清除关联后:(null)
(3)、获取类的详细信息:
  • 获取属性列表

代码实现

------HYPerson.h------
#import <Foundation/Foundation.h>

@interface HYPerson : NSObject
@property (assign, nonatomic) int ID;
@property (assign, nonatomic) int weight;
@property (nonatomic, assign) int age;
@property (copy, nonatomic) NSString *name;
@end

------main.m------
void propertyTest(){
    unsigned int count;
    objc_property_t *propertyList = class_copyPropertyList([HYPerson class], &count);
    for (unsigned int i = 0; i<count; i++) {
        const char *propertyName = property_getName(propertyList[I]);
        NSLog(@"PropertyName(%d): %@",i,[NSString stringWithUTF8String:propertyName]);
    }
    free(propertyList);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        propertyTest();
    }
    return 0;
}

打印结果:

2020-01-19 11:19:57.530384+0800 runtime-API-类[26215:142464] PropertyName(0): ID
2020-01-19 11:19:57.530727+0800 runtime-API-类[26215:142464] PropertyName(1): weight
2020-01-19 11:19:57.530773+0800 runtime-API-类[26215:142464] PropertyName(2): age
2020-01-19 11:19:57.530810+0800 runtime-API-类[26215:142464] PropertyName(3): name
Program ended with exit code: 0
  • 获取所有成员变量
------HYPerson.h------
#import <Foundation/Foundation.h>

@interface HYPerson : NSObject{
    int height;
    NSString *sex;
}
@property (nonatomic, assign) int age;
@property (copy, nonatomic) NSString *name;
@end
------main.m------
void ivarTest(){
    //成员变量
    unsigned int count;
    Ivar *ivars = class_copyIvarList([HYPerson class], &count);
    for (int i = 0; i < count; i++) {
        // 取出i位置的成员变量
        Ivar ivar = ivars[I];
        NSLog(@"Ivar(%d): %s      type:%s", i,ivar_getName(ivar), ivar_getTypeEncoding(ivar));
    }
    free(ivars);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ivarTest();
    }
    return 0;
}

打印结果:

2020-01-19 11:27:24.061335+0800 runtime-API-类[27683:154887] Ivar(0): height      type:i
2020-01-19 11:27:24.061911+0800 runtime-API-类[27683:154887] Ivar(1): name        type:@"NSString"
2020-01-19 11:27:24.061978+0800 runtime-API-类[27683:154887] Ivar(2): _age        type:i
2020-01-19 11:27:24.062018+0800 runtime-API-类[27683:154887] Ivar(3): _name       type:@"NSString"
Program ended with exit code: 0
  • 获取所有方法
------HYPerson.h------
#import <Foundation/Foundation.h>

@interface HYPerson : NSObject

@property (copy, nonatomic) NSString *name;
- (void)run;
@end
------main.m------
void methodTest(){
    unsigned int count;
    Method *methodList = class_copyMethodList([HYPerson class], &count);
    for (unsigned int i = 0; i<count; i++) {
        Method method = methodList[I];
        SEL mthodName = method_getName(method);
        NSLog(@"MethodName(%d): %@",i,NSStringFromSelector(mthodName));
    }
    free(methodList);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        methodTest();
    }
    return 0;
}

打印结果: .cxx_destruct

  • 当注释掉所有的成员变量的时候就不在调用了(其中也包括属性)就不会再调用这个方法了。
  • 后来了解到这个方法就是在ARC模式下,将所有的成员变量变成nil相当于MRC模式下的dealloc。
2020-01-19 11:32:57.717172+0800 runtime-API-类[28744:162966] MethodName(0): .cxx_destruct
2020-01-19 11:32:57.717580+0800 runtime-API-类[28744:162966] MethodName(1): name
2020-01-19 11:32:57.717635+0800 runtime-API-类[28744:162966] MethodName(2): setName:
2020-01-19 11:32:57.717681+0800 runtime-API-类[28744:162966] MethodName(3): run
Program ended with exit code: 0
  • 获取当前遵循的所有协议
void protocolTest(){
    unsigned int count;
    __unsafe_unretained Protocol **protocolList = class_copyProtocolList([HYPerson class], &count);
    for (int i=0; i<count; i++) {
        Protocol *protocal = protocolList[I];
        const char *protocolName = protocol_getName(protocal);
        NSLog(@"protocol(%d): %@",i, [NSString stringWithUTF8String:protocolName]);
    }
    free(protocolList);
}

注意:C语言中使用Copy操作的方法,要注意释放指针,防止内存泄漏。

(4)、解决同一方法高频率调用的效率问题:

Runtime源码中的IMP作为函数指针,指向方法的实现。通过它,我们可以绕开发送消息的过程来提高函数调用的效率。当我们需要持续大量重复调用某个方法的时候,会十分有用,具体代码示例如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    IMP imp = [self methodForSelector:@selector(sayHi)];
    imp();
}
- (void)sayHi {
    NSLog(@"hello world");
}

OC调用方法, 编译后就走objc_msgSend这套逻辑去了, 最终目的就是找到方法对应的函数指针(IMP), 再调用函数指针. 所以你找到IMP之后保存起来, 下次连续执行N次都可以直接调用IMP, 不需要走消息查找逻辑了, 效率自然就提高了。

(5)、方法动态解析与消息转发:

我们在分析了OC语言对应的底层C结构之后,现在可以进一步理解运行时的消息发送机制。先前讲到,OC调用方法被编译转化为如下的形式:

id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)

其实,除了常见的objc_msgSend,消息发送的方法还有objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper_stret等,如果消息传递给父类就使用带有super的方法,如果返回值是结构体而不是简单值就使用带有stret的值。

运行时阶段的消息发送的详细步骤如下:

  • 检测selector 是不是需要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会retain,release 这些函数了。
  • 检测target 是不是nil 对象。ObjC 的特性是允许对一个 nil对象执行任何一个方法不会 Crash,因为会被忽略掉。
  • 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,若可以找得到就跳到对应的函数去执行。
  • 如果在cache里找不到就找一下方法列表methodLists。
  • 如果methodLists找不到,就到父类的方法列表里寻找,一直找,直到找到NSObject类为止。
  • 如果还找不到,Runtime就提供了如下三种方法来处理:动态方法解析、消息接受者重定向、消息重定向,这三种方法的调用关系如下图:
    消息发送流程图
  • 动态方法解析(Dynamic Method Resolution)
    所谓动态解析,我们可以理解为通过cache和方法列表没有找到方法时,Runtime为我们提供一次动态添加方法实现的机会,主要用到的方法如下:
//OC方法:
//类方法未找到时调起,可于此添加类方法实现
+ (BOOL)resolveClassMethod:(SEL)sel
//实例方法未找到时调起,可于此添加实例方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel

//Runtime方法:
/**
 运行时方法:向指定类中添加特定方法实现的操作
 @param cls 被添加方法的类
 @param name selector方法名
 @param imp 指向实现方法的函数指针
 @param types imp函数实现的返回值与参数类型
 @return 添加方法是否成功
 */
BOOL class_addMethod(Class _Nullable cls,
                     SEL _Nonnull name,
                     IMP _Nonnull imp,
                     const char * _Nullable types)

下面使用一个示例来说明动态解析:Perosn类中声明方法却未添加实现,我们通过Runtime动态方法解析的操作为其他添加方法实现,具体代码如下:

HYPerson.h

#import <Foundation/Foundation.h>

@interface HYPerson : NSObject
//声明实例方法,但未实现
- (void)test;
//声明类方法,但未实现
+ (void)testTwo;
@end
HYPerson.m

#import "HYPerson.h"
#import <objc/runtime.h>
@implementation HYPerson

//重写父类方法:处理实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test)) {
        //获取其他方法
        Method  method = class_getInstanceMethod(self, @selector(other));

//        NSLog(@"%s,%s,%p",sel,method_getTypeEncoding(method),method_getImplementation(method));
        //动态添加test方法的实现
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

//重写父类方法:处理类方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(testTwo)) {
        Method  method = class_getClassMethod(object_getClass(self), @selector(otherClass));
        class_addMethod(object_getClass(self), sel, method_getImplementation(method), method_getTypeEncoding(method));
        return YES;
    }
    return [super resolveClassMethod:sel];
}

- (void)other {
    NSLog(@"%s",__func__);
}
+ (void)otherClass {
    NSLog(@"%s",__func__);
}
@end

调用:

#import <Foundation/Foundation.h>
#import "HYPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HYPerson *person = [[HYPerson alloc]init];
        [person test];

        [HYPerson testTwo];
    }
    return 0;
}

打印结果:

2020-01-19 16:36:10.115959+0800 objc_msgSend-动态解析[53652:288019] -[HYPerson other]
2020-01-19 16:36:10.116560+0800 objc_msgSend-动态解析[53652:288019] +[HYPerson otherClass]
Program ended with exit code: 0

注意:成功使用动态方法解析还有个前提,那就是我们必须存在可以处理消息的方法,比如上述代码中的other:与otherClass:

  • 消息接收者(receiver)重定向:

我们注意到动态方法解析过程中的两个resolve方法都返回了布尔值,当它们返回YES时方法即可正常执行,但是若它们返回NO,消息发送机制就进入了消息转发(Forwarding)的阶段了,我们可以使用Runtime通过下面的方法替换消息接收者的为其他对象,从而保证程序的继续执行。

//重定向类方法的消息接收者,返回一个类
+ (id)forwardingTargetForSelector:(SEL)aSelector

//重定向实例方法的消息接受者,返回一个实例对象
- (id)forwardingTargetForSelector:(SEL)aSelector

1)实例方法
创建HYPerson、HYCat两个类,无继承关系。
HYPerson:

HYPerson.h

#import <Foundation/Foundation.h>

@interface HYPerson : NSObject

- (void)test;
@end

-----------------------------------------------
HYPerson.m

#import "HYPerson.h"
#import "HYCat.h"
@implementation HYPerson

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
        return [[HYCat alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

HYCat:

HYCat.h
#import <Foundation/Foundation.h>

@interface HYCat : NSObject
- (void)test;
@end

-----------------------------------------------
HYCat.m
#import "HYCat.h"

@implementation HYCat
- (void)test{
    NSLog(@"%s",__func__);
}
@end

调用:

#import <Foundation/Foundation.h>
#import "HYPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HYPerson *person = [[HYPerson alloc]init];
        [person test];
    }
    return 0;
}

打印结果:

2020-01-19 17:00:45.258329+0800 objc_msgSend-消息转发[58056:316915] -[HYCat test]
Program ended with exit code: 0

2)类方法
创建HYPerson、HYCat两个类,无继承关系。
HYPerson:

HYPerson.h
#import <Foundation/Foundation.h>

@interface HYPerson : NSObject

+ (void)test;
@end

-----------------------------------------------
HYPerson.m
#import "HYPerson.h"
#import "HYCat.h"
@implementation HYPerson

+ (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
          return [HYCat class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

HYCat:

#import <Foundation/Foundation.h>

@interface HYCat : NSObject
+ (void)test;
@end

-----------------------------------------------

#import "HYCat.h"

@implementation HYCat
+ (void)test{
    NSLog(@"%s",__func__);
}
@end

调用:

#import <Foundation/Foundation.h>
#import "HYPerson.h"

//元类对象是一种特殊的类对象。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [HYPerson test];
    }
    return 0;
}

打印结果:

2020-01-19 17:04:12.960968+0800 objc_msgSend-消息转发[58716:322267] +[HYCat test]
Program ended with exit code: 0

注意:动态方法解析阶段返回NO时,我们可以通过forwardingTargetForSelector可以修改消息的接收者,该方法返回参数是一个对象,如果这个对象是非nil,非self,系统会将运行的消息转发给这个对象执行。否则,继续查找其他流程。

  • 消息重定向:
    当以上两种方法无法生效,那么这个对象会因为找不到相应的方法实现而无法响应消息,此时Runtime系统会通过forwardInvocation:消息通知该对象,给予此次消息发送最后一次寻找IMP的机会:
- (void)forwardInvocation:(NSInvocation *)anInvocation;

其实每个对象都从NSObject类中继承了forwardInvocation:方法,但是NSObject中的这个方法只是简单的调用了doesNotRecongnizeSelector:方法,提示我们错误。所以我们可以重写这个方法:对不能处理的消息做一些默认处理,也可以将消息转发给其他对象来处理,而不抛出错误。

我们注意到anInvocation是forwardInvocation唯一参数,它封装了原始的消息和消息参数。正是因为它,我们还不得不重写另一个函数:methodSignatureForSelector。这是因为在forwardInvocation: 消息发送前,Runtime系统会向对象发送methodSignatureForSelector消息,并取到返回的方法签名用于生成NSInvocation对象。

1)实例方法:
HYPerson类:

#import <Foundation/Foundation.h>

@interface HYPerson : NSObject

- (int)test:(int)age;
@end

---------------------------------------------------------
#import "HYPerson.h"
#import "HYCat.h"
@implementation HYPerson


//方法签名:返回值类型,参数类型
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(test:)) {
//        return [NSMethodSignature signatureWithObjCTypes:"v@:i"];
        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 参数顺序:receiver、selector、other arguments
//    int age;
//    [anInvocation getArgument:&age atIndex:2];
//    NSLog(@"%d", age + 10);
    
    
    // anInvocation.target == [[MJCat alloc] init]
    // anInvocation.selector == test:
    // anInvocation的参数:15
    // [[[MJCat alloc] init] test:15]
    [anInvocation invokeWithTarget:[[HYCat alloc] init]];
    
    int ret;
    [anInvocation getReturnValue:&ret];
    
    NSLog(@"%d", ret);
}
@end

注意:我们注意到[NSMethodSignature signatureWithObjCTypes:]方法中的特殊参数“v@”,具体可参考这里

HYCat类:

#import <Foundation/Foundation.h>

@interface HYCat : NSObject
- (int)test:(int)age;
@end

---------------------------------------------------------
#import "HYCat.h"

@implementation HYCat
- (int)test:(int)age
{
    return age * 2;
}
@end

调用:

#import <Foundation/Foundation.h>
#import "HYPerson.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        HYPerson *person = [[HYPerson alloc]init];
        [person test:15];
    }
    return 0;
}

打印结果:

2020-01-19 17:18:33.841385+0800 objc_msgSend-消息转发[61249:336859] 30
Program ended with exit code: 0

2)实例方法:

HYPerson类:

#import <Foundation/Foundation.h>

@interface HYPerson : NSObject

+ (int)test:(int)age;
@end

---------------------------------------------------------
#import "HYPerson.h"
#import "HYCat.h"
@implementation HYPerson

//方法签名:返回值类型,参数类型
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(test:)) {
        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[HYCat class]];
    int ret;
    [anInvocation getReturnValue:&ret];
    
    NSLog(@"%d", ret);
}
@end

注意:我们注意到[NSMethodSignature signatureWithObjCTypes:]方法中的特殊参数“v@”,具体可参考这里

HYCat类:

#import <Foundation/Foundation.h>

@interface HYCat : NSObject
+ (int)test:(int)age;
@end

---------------------------------------------------------
#import "HYCat.h"

@implementation HYCat
+ (int)test:(int)age
{
    return age * 2;
}
@end

调用:

#import <Foundation/Foundation.h>
#import "HYPerson.h"

//元类对象是一种特殊的类对象。
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [HYPerson test:15];
    }
    return 0;
}

打印结果:

2020-01-19 17:18:33.841385+0800 objc_msgSend-消息转发[61249:336859] 30
Program ended with exit code: 0
(6)、多继承的实现思路:
(7)、动态操作属性:
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容