美文网首页程序员iOS面试iOS
Runtime - 方法发送机制土味讲解

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

作者: 小蠢驴打代码 | 来源:发表于2019-03-17 23:38 被阅读6次

    面试驱动技术合集(初中级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

    相关文章

      网友评论

        本文标题:Runtime - 方法发送机制土味讲解

        本文链接:https://www.haomeiwen.com/subject/ysqmmqtx.html