iOS - 消息发送的完整流程

写在前面

在OC里面,调用对象的某个方法其实就是给这个对象发送一个消息,这个过程我们把它分为三大阶段,分别为:消息发送阶段、动态解析阶段、消息转发阶段,本文将细细剖析这三个阶段,但是在剖析这三大阶段之前我们需要先回顾一下Class的结构。

Class结构

苹果源码最新下载地址请点击:苹果源码
objc-runtime-new.h中可以看到objc_class结构如下:

struct objc_object {
    Class isa;
};

struct objc_class : objc_object {
      Class superclass; 
      cache_t cache;  // 方法缓存
      class_data_bits_t bits; // 获取具体类信息
      class_rw_t *data() const {
         return bits.data();
     }
    ...... 
};

从上面的结构我们可以看到有一个类cache_t,这个类就是专门拿来做方法缓存相关的类,结构如下:

struct cache_t {
    struct bucket_t *buckets();
    mask_t occupied();
    mask_t mask();
};

struct bucket_t {
    cache_key_t _key;
    IMP _imp;
};

class_data_bits_t用于获取具体的类信息,结构如下:

#define FAST_DATA_MASK          0x00007ffffffffff8UL
struct class_data_bits_t {
    uintptr_t bits;
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

// readWrite:可读可写
struct class_rw_t {
    uint32_t flags;
    uint32_t witness;
    Class firstSubclass;
    Class nextSiblingClass;
    class_rw_ext_t *ext;
    const class_ro_t *ro;
};

struct class_rw_ext_t {
  class_ro_t *ro;
  method_array_t methods;// 方法列表
  property_array_t properties; // 属性列表
  protocol_array_t protocols; // 协议列表
  char *demangledName;
   uint32_t version;
}

// readOnly:只读
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; 
#ifdef __LP64__
    uint32_t reserved;
#endif
    const uint8_t * ivarLayout;
    const char * name;  // 类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;  // 成员变量列表
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

分析到这里,Class结构我们已了解清楚,接下来就是调用对象的方法来研究一下消息发送的完整流程。

消息发送阶段

在OC里面,调用对象的某个方法就是给这个对象发送一条消息,这里我们新建一个Person类,以[person personRun]为例来看看消息发送阶段的流程。
【iOS重学】方法缓存cache_t的分析这篇文章中我们主要分析了方法缓存,建议大家先看一下缓存可以帮助我们理解接下来的流程。
我们知道OC中的方法调用其实就是转成objc_msgSend()函数的调用(load方法除外),如下:

1.png

// 消息发送阶段源码跟读顺序
1. objc-msg-arm64 汇编文件
    ENTRY _objc_msgSend
    b.le LNilOrTagged
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    .macro CacheLookup
    CacheHit // 命中缓存
    MissLabelDynamic // 其实就是__objc_msgSend_uncached
    STATIC_ENTRY __objc_msgSend_uncached
    MethodTableLookup
    .macro MethodTableLookup
    bl _lookupImpOrForward

2. objc-runtime-new.mm 文件
    lookupImpOrForward
    getMethodNoSuper_nolock(curClass, sel)
    curClass = curClass->getSuperclass()
    cache_getImp(curClass, sel) // 从父类缓存里面查找
    log_and_fill_cache // 缓存方法到消息接收者这个类

消息发送的流程图如下:


2.png

我们来验证一下是否真的缓存了调用的方法:

未调用personRun时,我们查一下在Person类的cache里面是否能找到personRun方法缓存:

Person *person = [[Person alloc] init];
mj_objc_class *personClass = (__bridge  mj_objc_class *)[Person class];
NSLog(@"%@ %p",NSStringFromSelector(@selector(personRun)), personClass->cache.imp(@selector(personRun)));

打印结果如下:

2022-04-10 13:11:30.367394+0800 RuntimeDemo[88049:12459843] personRun 0x0

结果分析:在cache并没有找到personRun的IMP。
调用personRun之后,我们查一下Person类的cache里面是否能找到personRun方法缓存:

Person *person = [[Person alloc] init];
[person personRun];
mj_objc_class *personClass = (__bridge  mj_objc_class *)[Person class];
NSLog(@"%@ %p",NSStringFromSelector(@selector(personRun)), personClass->cache.imp(@selector(personRun)));

打印结果如下:

2022-04-10 13:13:30.294687+0800 RuntimeDemo[88074:12461806] personRun 0x78cc0

结果分析:调用personRun之后,会把personRun缓存到方法缓存里面

动态方法解析阶段

当第一阶段【消息发送阶段】没有找到方法实现就会进入第二阶段【动态方法解析阶段】。

// 动态方法解析阶段源码跟读顺序
1. objc-runtime-new.mm 文件
  resolveMethod_locked
  resolveInstanceMethod 或 resolveClassMethod
  lookupImpOrNilTryCache
  _lookupImpTryCache
  lookupImpOrForward

动态方法解析的流程图如下:


3.png

动态方法解析流程

根据+ (BOOL)resolveInstanceMethod:(SEL)sel (实例方法调用这个)+ (BOOL)resolveClassMethod:(SEL)sel(类方法调用这个)来做动态方法解析,然后重新走一遍消息发送的流程(从消息接受者的方法缓存里面开始继续往下执行)

动态方法解析代码

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

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(personRun)) {
        Method otherMethod = class_getInstanceMethod(self, @selector(otherRun));
        IMP imp = class_getMethodImplementation(self, @selector(otherRun));
        class_addMethod(self, sel, method_getImplementation(otherMethod), method_getTypeEncoding(otherMethod));
        return  YES;
    }
    return [super resolveInstanceMethod:sel];
}

消息转发阶段

如果前面的两个阶段都没有实现,就会继续进入【消息转发】的流程。

// 消息转发阶段的源码跟读顺序
1. objc-msg-arm64 汇编文件
  forward_imp = _objc_msgForward_impcache
  STATIC_ENTRY __objc_msgForward_impcache
  b __objc_msgForward
  ENTRY __objc_msgForward
  ENTRY __objc_msgForward_stret
  __objc_forward_stret_handler
2. objc-runtime-new.mm 文件
  void *_objc_forward_stret_handler = (void *)objc_defaultForwardStretHandler;
3. CoreFoundation 框架
  __forwarding__ // 不开源

消息转发的流程图如下:


4.png

消息转发流程

消息转发流程也分为了两步:
第一步:forwardingTargetForSelector:方法是指把响应这个方法的对象转发给其他的对象,那么消息接受者就发生了变化,会重新调用一遍objc_MsgSend(消息接受者,SEL)流程
第二步:forwardingTargetForSelector: 方法返回为nil,继续检查methodSignatureForSelector:是否返回了一个方法签名,然后去执行forwardInvocation:方法

消息转发流程相关代码实现

  1. 实例方法流程
// 第一步
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(personRun)) {
        return [[Student alloc] init]; // 这里返回的是你想把这个消息转发给哪个对象
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 第二步
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(personRun)) {
        // ⚠️:这里的方法签名的types不能随便写 因为这里的方法签名决定了下一步的NSInvocation的返回值、参数类型等
        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
     [anInvocation invokeWithTarget:[Student new]];
  // 在这个方法里可以做任何我们想做的事情
}

类方法流程

// 第一步
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(personTest)) {
        return [Student class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 第二步
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(personTest)) {
        return [Student methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[Student class]];
}

+ (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"%s",__func__);
}

关于NSInvocation

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
// 方法签名
@property (readonly, retain) NSMethodSignature *methodSignature;

// retain所有参数 防止参数被dealloc
- (void)retainArguments;
// 参数是否都被retained
@property (readonly) BOOL argumentsRetained;
// 消息接收者
@property (nullable, assign) id target;
// 方法名
@property SEL selector;

// 获取返回值
- (void)getReturnValue:(void *)retLoc;
// 设置返回值
- (void)setReturnValue:(void *)retLoc;
// 获取idx的参数
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
// 设置idx的参数
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
// 调用
- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end

大家有兴趣的话可以去试试NSInvacation的使用。

最后

如果按照上面的三大流程都走完之后依然没有找到相应的方法实现,那这个调用最后就会调用doesNotRecognizeSelecto:抛出异常,如果错误请多多指教,最后欢迎去我的个人技术博客逛逛。

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

推荐阅读更多精彩内容