Runtime - 方法发送机制土味讲解

面试驱动技术合集(初中级iOS开发),关注仓库,及时获取更新 Interview-series

image

Class 结构详解

struct objc_class : objc_object {
    Class isa;
    Class superclass;
    cache_t cache;--> 方法缓存      
    class_data_bits_t bits;  
}
struct cache_t {
    struct bucket_t *_buckets;//散列表
    mask_t _mask;//散列表长度-1
    mask_t _occupied;//已经缓存的方法数量
    }
struct bucket_t {
    cache_key_t _key;//@selecter(xxx) 作为key
    MethodCacheIMP _imp;//函数的执行地址
    }
  • buckets 散列表,是一个数组,数组里面的每一个元素就是一个bucket_t,bucket_t里面存放两个
    • _key SEL作为key
    • _imp 函数的内存地址
  • _mask 散列表的长度
  • _occupied已经缓存的方法数量
image
  • 函数调用底层走的是objc_msgSend
image-20190313222359416

正常的流程:

  1. 对象通过isa,找到函数所在的类对象
  2. 这时候先做缓存查找,如果缓存的函数列表中没找到该方法
  3. 就去类的class_rw中的methods中找,如果找到了,调用并缓存该方法
  4. 如果类的class_rw中没找到该方法,通过superclass到父类中,走的逻辑还是先查缓存,缓存没有查类里面的方法。
  5. 最终如果在父类中调用到了,会将方法缓存到当前类的方法缓存列表中

方法缓存

如何进行缓存查找->使用散列表(散列表 - 空间换时间)

image-20190317205913318
image-20190313220800705
MNGirl *girl = [[MNGirl alloc]init];
mj_objc_class *girlClass = (__bridge mj_objc_class *)[MNGirl class];

[girl beauty];
[girl rich];

//遍历缓存(散列表长度 = mask + 1)
cache_t cache = girlClass->cache;
bucket_t *buckets = cache._buckets;

for (int i = 0; i < cache._mask + 1; i++) {
    
    bucket_t bucket = buckets[i];
    
    NSLog(@"%s %p", bucket,bucket._imp);
}

----------------------------------------
2019-03-13 22:11:42.911494+0800 rich 0x100000be0
2019-03-13 22:11:42.912946+0800 beauty 0x100000c10
2019-03-13 22:11:42.912970+0800 (null) 0x0
2019-03-13 22:11:42.913002+0800 init 0x7fff4f98ff4d

发现缓存中已经有三个方法了,分别是初始化调用的init,第一次调用的beauty和第二次调用的rich

散列表取方法

[girl beauty];
[girl rich];

//遍历缓存(散列表长度 = mask + 1)
cache_t cache = girlClass->cache;
bucket_t *buckets = cache._buckets;

bucket_t bucket = buckets[(long long)@selector(beauty) & cache._mask];

NSLog(@"%s %p", bucket,bucket._imp);

-----------------------------------------
2019-03-13 22:15:00 beauty 0x100000c60

确实是取方法的时候,不用遍历,通过@selector( ) & mask = index索引,数组同index就

注意,不一定每次都能准确的index索引,算出来的index取出来的内容不一定是想要的,但是经常是比较接近,最差的情况下,也只是一边的循环遍历

索引散列表效率远高于数组!

image-20190313223112407

方法查找的源码: bucket_t * cache_t::find(cache_key_t k, id receiver)

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);

bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0  ||  b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);

// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}

索引值 Index 的计算

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}

mask_t begin = cache_hash(k, m);

走的是 key & mask的方法, A & B 一定是小于 A的

 1111 0010
&0011 1111
----------
 0011 0010 <= 原来的值

哈希表的算法也有用求余的,和&类似

实现如下:

image-20190313223858753
(i = cache_next(i, m)) != begin

查找流程梳理: 比如起始下标是4, 总长度是6,目标不在列表中

  1. 取出index = 4的值,发现不是想要的,i - - 变成3
  2. 3 依次 - - 到0,然后mask长度开始 = 6继续
  3. 当6 又 - - 到起始index = 4的时候,说明已经遍历一圈了,还是没找到,方法缓存查找结束

OC的消息机制

三个阶段

  • 消息发送

  • 动态方法解析

  • 消息转发

消息发送

当前类查找顺序

  • 排序好的列表,采用二分查找算法查找对应的执行函数
  • 未排序的列表,采用一般遍历的方法查找对象执行函数

父类逐级查找

image
image

动态方法解析

@interface IOSer : NSObject

- (void)interview;

@end

@implementation IOSer

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

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(interview)) {
        
        Method method = class_getInstanceMethod(self, @selector(test));
        
        //动态添加interview方法
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
        
        return YES;
        
    }
    return [super resolveInstanceMethod:sel];
}

@end

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

//调用
IOSer *ios = [[IOSer alloc]init];
[ios interview];


---------------------------------------------
结果,不会crash,进入了动态添加的方法了
2019-03-17 21:33:51.475717+0800 Runtime-TriedResolverDemo[11419:9277997] -[IOSer test]
image-20190317214712857

消息转发流程

  • 消息转发流程1:forwardingTargetForSelector
@implementation IOSer

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

@interface Forwarding : NSObject

- (void)interview;

@end

@implementation Forwarding

- (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(interview)) {
    
        //objc_msgSend([[IOSer alloc]init],aSelector)
        //由IOSer作为消息转发的接收者
        return [[IOSer alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

---------------------------------------------------------------
调用
Forwarding *obj = [[Forwarding alloc]init];
[obj interview];


---------------------------------------------
结果,不会crash,进入了动态添加的方法了
2019-03-17 22:57:45.130805+0800 Runtime-TriedResolverDemo[13776:9355195] -[IOSer interview]
  • 消息转发流程2:forwardingTargetForSelector
@implementation Forwarding

//返回方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(interview)) {

        //v16@0:8 = void xxx (self,_cmd)
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

//NSInvocation - 方法调用
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //设置方法调用者
    [anInvocation invokeWithTarget:[[IOSer alloc]init]];
}

@end
  • NSInvocation 其实封装了一个方法调用,包括:
    • 方法名 - anInvocation.selector
    • 方法调用 - anInvocation.target
    • 方法参数 - anInvocation getArgument: atIndex:
image


冷门知识补充

//类方法的消息转发
[Forwarding test];

类方法也可以实现消息转发,但是用的是+ (id)forwardingTargetForSelector:(SEL)aSelector函数

因为__forwarding底层,是用receiver去发送 forwardingTargetForSelector消息,如果是类方法,receiver是类对象,所以要调用的是 “+” 方法

小tips:默认是没有+ (id)forwardingTargetForSelector:(SEL)aSelector方法,可以先打- (id)forwardingTargetForSelector:(SEL)aSelector,“-” 替换成“+”,完成~



友情演出:小马哥MJ

参考资料:

objc-msgsend

gun

libmalloc

objc4

Objective-C-Message-Sending-and-Forwarding

Type Encodings

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