美文网首页iOS 开发 Objective-C
iOS 底层 day12 runtime method详解

iOS 底层 day12 runtime method详解

作者: 望穿秋水小作坊 | 来源:发表于2020-09-04 11:55 被阅读0次

一、class_rw_t 数据结构

Method 相关结构体以及存贮结构

二、method_t 数据结构

method_t
  • 和其他编程语言一样,一个方法我们需要保存它的函数名返回值参数函数地址
1. SEL
  • SEL 代表方法名/函数名,一般叫做选择器,底层结构跟 char* 类似,我们可以把它看成方法名字符串

  • 可以通过 @selector()sel_registerName() 获得

  • 可以通过 sel_getName()NSStringFromSelector() 转成字符串

  • 不同类中相同名字的方法,所对应的方法选择器是相同的

SEL sel1 = @selector(setTest:);
SEL sel2 = sel_registerName("setTest:");
// 打印日志
(lldb) p/x sel1
(SEL) $0 = 0x00007fff3a201718 "setTest:"
(lldb) p/x sel2
(SEL) $1 = 0x00007fff3a201718 "setTest:"
  • 换句话说:名字相同的方法,内存中只会保存一份 SEL
2. IMP
注意图中红框位置
  • 由此我们可以得出 IMP 所存放的就是函数的地址
3. Types
  • types 采用了类型编码技术(TypeEncodings),存贮方法的返回值类型参数类型

  • 简单来说就是将各种类型映射成各种符号,这个对照表苹果官网有,类似于如下:

部分对照表
  • 接下来,我们拿一个具体方法解析一下
图解 types

三、cache_t 方法缓存

struct objc_class : objc_object {
    Class isa;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

我们之前学过,当我们调用一个实例方法时,会先从 isa 指针入手,查找类对象的方法列表,然后使用 superclass 逐级查询父类的类对象方法列表。而我们现实代码中,通常来说都有一些常用的方法,如果每次都要走上面的步骤去查找,效率就会显得比较低。所以苹果设计了方法缓存机制,让方法缓存在cache中。

图解整个过程

有如下代码结构:

  • Person 继承自 NSObject,有 - (void)personTest; 方法
  • Student 继承自 Person,有 - (void)studentTest; 方法
  • GoodStudent 继承自 Student,有 - (void)goodStudentTest; 方法
1. 当我们调用 GoodStudent- (void)personTest; 方法,该方法会被缓存到哪里?
int main(int argc, const char * argv[]) {
    GoodStudent *goodStudent = [[GoodStudent alloc] init];
    mj_objc_class *goodSudentClass = (__bridge mj_objc_class *)[GoodStudent class];
    mj_objc_class *personClass = (__bridge mj_objc_class *)[Person class];
    NSLog(@"断点1");
    [goodStudent personTest];
    NSLog(@"断点2");
    return 0;
}
  • 断点1,我们观察 goodSudentClasscache 属性 获得 _occupied = 1

  • 断点1,我们观察 personClasscache 属性 获得 _occupied = 0

  • 断点2,我们观察 goodSudentClasscache 属性 获得 _occupied = 2

  • 断点2,我们观察 personClasscache 属性 获得 _occupied = 0

  • 为什么- (void)personTest; 方法未调用时候, goodSudentClasscache 属性的 _occupied = 1呢?别忘记了 -(id)init; 方法哟

  • 由此,我们可以得出子类调用父类的方法时,父类方法会被缓存到子类类对象方法缓存列表

2. 上面提到方法缓存列表是一个散列表,如何证明,OC 内部使用的散列算法是 @selctor(funName) & mask 这种算法呢?方法一:从源码中寻找
源码中缓存列表查找过程
3. 上面提到方法缓存列表是一个散列表,如何证明,OC 内部使用的散列算法是 @selctor(funName) & mask 这种算法呢?方法二:从代码中证明
  • 我们编写如下代码
int main(int argc, const char * argv[]) {
    GoodStudent *goodStudent = [[GoodStudent alloc] init];
    mj_objc_class *goodSudentClass = (__bridge mj_objc_class *)[GoodStudent class];
    
    [goodStudent studentTest];
    [goodStudent personTest];
    
    
    int begin1 = (unsigned long)@selector(studentTest) & 3;
    int begin2 = (unsigned long)@selector(init) & 3;
    int begin3 = (unsigned long)@selector(personTest) & 3;
    
    NSLog(@"studentTest-index:%d",begin1);
    NSLog(@"init-index:%d",begin2);
    NSLog(@"personTest-index:%d",begin3);
    
    bucket_t *buckets = goodSudentClass->cache._buckets;
    
    for (int i = 0; i <= goodSudentClass->cache._mask ; i++) {
        bucket_t bucket = *(buckets+i);
        printf("索引值:%d , 方法名:%s , 方法地址:%p \n",i,bucket._key, bucket._imp);
    }
    
    NSLog(@"断点1");
    return 0;
}
  • 输出日志如下(真机运行 arm64):
 Demo[3887:171505] studentTest-index:3
 Demo[3887:171505] init-index:1
 Demo[3887:171505] personTest-index:3
索引值:0 , 方法名:(null) , 方法地址:0x0 
索引值:1 , 方法名:init , 方法地址:0x1055891ad 
索引值:2 , 方法名:personTest , 方法地址:0x104c575d0 
索引值:3 , 方法名:studentTest , 方法地址:0x104c575a0 
  • 我们发现 -(id)int-(void)studentTest 方法的索引值和我们计算出来的一致

  • 然而-(void)personTest 方法因为也是 3,这就发生了重复,这种重复我们叫做哈希碰撞

  • 所以 -(void)personTest 需要另外找个位置存贮,从上述源码分析中,我们发现 return i ? i-1 : mask; 充分说明会往上找位置,所以 -(void)personTest被存贮在了索引为 2的位置。这一点,我们从代码中也验证成功_

4. 如果方法缓存列表 满了会怎么做呢?
方法缓存散列表扩容示意图
  • 当缓存占用的长度是总长度的 3/4 时,会进行扩容
  • 新缓存列表长度是旧的两倍
  • 旧的列表会被释放,不会覆盖到新的列表

相关文章

网友评论

    本文标题:iOS 底层 day12 runtime method详解

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