Runtime总结(一)

目录

1.Runtime简介

2.isa的结构及详解

3.Class的结构

4.方法缓存实现

1.Runtime简介

  • Objective-C 语言是一门动态语言。它把一些决策从编译阶段、链接阶段推迟到运行时阶段,实现该机制的基础就是 runtime(又叫作运行时)。

静态语言:在编译阶段就已确定所有变量的数据类型,同时也确定要调用的函数,以及函数的实现。常见的静态语言,如:swift , C/C++、Java 等。
动态语言:程序在运行时可以改变其结构。也就是说在运行时检查变量数据类型,同时在运行时才会根据函数名查找要调用的具体函数。如 Objective-C。

  • Runtime是什么?

Runtime 提供的接口基本都是 C 语言,源码由 C\C++\汇编语言编写。Runtime API 为 Objective-C 语言的动态属性提供支持,充当一种用于 Objective-C 语言的操作系统,使得该语言正常运转工作。

2.isa详解

要想彻底熟悉 Runtime,那么必须要Runtime底层常用的数据结构,比如 isa 指针,Class 的结构等,下面我们先来看看isa;
之前写的一篇文章也有提到isa指针的概念和作用 iOS 探究 OC对象、isa指针及KVO实现原理

现在我们深入探讨一下isa内部的结构:

  • 在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址 (作为了解)
  • 从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。

Runtime源码下载地址下载最新的Runtime源码,
objc_private.h文件中,我们可以找到OC对象的结构体定义和isa的定义:
精简主要代码如下

//对象结构体
struct objc_object {
private:
    isa_t isa; //isa指针,大小为8个字节
    
public:
    对象的相关方法
    ....
}

union isa_t {
    uintptr_t bits;
private:
    Class cls;
public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
复制代码

由上面在isa.h中找到ISA_BITFIELD在arm64中的定义

截屏2021-05-17 下午4.40.15.png

通过上面代码块的分析我们取出ISA_BITFIELD真机部分,将union isa_t的结构整理如下:

union isa_t
{
    Class cls;
    uintptr_t bits;
    struct{
        //1,代表优化过,使用位域存储更多的信息;0,代表普通的指针,存储着类/元类对象的内存地址
        uintptr_t nonpointer        : 1;
        
        //是否有设置过关联对象,如果没有,释放时会更快,
        //如果有的话销毁前会调用 _object_remove_assocations 函数根据关联策略循环释放每个关联对象
        uintptr_t has_assoc         : 1;
        
        //是否有C++的析构函数(.cxx_destruct),如果没有,释放时会更快
        //如果有的话对象销毁前会调用 object_cxxDestruct 函数去执行该类的析构函数
        uintptr_t has_cxx_dtor      : 1;
        
        //存储着Class、Meta-Class对象的内存地址信息
        //isa & ISA_MASK 得出该实例对象所属的的类的地址
        uintptr_t shiftcls          : 33;
        
        //用于在调试时分辨对象是否未完成初始化
        uintptr_t magic             : 6;
        
        //是否有被弱引用指向过,如果没有,释放时会更快
        // 如果有的话对象销毁前会调用 weak_clear_no_lock 函数把该对象的弱引用置为 nil,
        // 并调用 weak_entry_remove 把对象的 entry 从 weak_table 中移除
        uintptr_t weakly_referenced : 1;
        
        //占位符 无用
        uintptr_t unused            : 1;      
        
        //引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中
        uintptr_t has_sidetable_rc  : 1;
        
        //里面存储的值是引用计数器减1
        uintptr_t extra_rc          : 19;
    }
}

复制代码

上面union isa_t表示 isa_t为一个共用体,表示里面的所有变量共用一块内存,同时使用了位域来存储更多的信息(即struct中的参数,长度总和刚好是64位,即8个字节),通过位运算存取相关参数值。

3.Class的结构及方法缓存实现

在runtime源码中的objc-runtime-new.h文件中,找到objc_class结构体,即为class的实现,实现方法太长,下面为部分代码截图:

截屏2021-05-17 下午6.27.53.png

精简如下:

struct objc_class{
    Class isa; //
    Class superclass;
    cache_t chahe; //方法缓存
    class_data_bits_t bits;//用于获取具体类的信息(&FAST_DATA_MASK获取)
}
复制代码

通过class_data_bits_t bits可以获取到类的相关信息,在class_data_bits_t结构体中,我们可以看到如下方法:

截屏2021-05-17 下午7.26.45.png
可以看到通过bits & FAST_DATA_MASK返回了一个calss_rw_t的结构体,即类的信息。

  • 那么calss_rw_t里面包含什么呢?

这里点进去,把源码精简完后主要内容如下:

struct class_rw_t{
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro; //ro即read only 里面包含的信息都是只读的
    method_array_t methods //方法列表 二维数组里面是method_list_t
    property_array_t properties //属性列表 二维数组里面是property_list_t
    protocol_array_t protocols //协议列表 二维数组里面是protocol_list_t
    Class firstSubclass;
    Class nextSiblingClass;
}

复制代码

从class_rw_t里我们主要分析两个主要部分class_ro_t和methods、properties、protocols:

  • class_ro_t 里面包含类的初始化的一些参数和方法,都是只读不可修改的,把class_ro_t源码精简如下:
struct class_ro_t{
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif
    const char * name;  //类名
    method_list_t *baseMethodList    //方法列表 (一维数组)
    const ivar_list_t * ivars;       //成员变量列表 (一维数组)
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties; // 属性列表 (一维数组)
    protocol_list_t * baseProtocols; // 协议列表 (一维数组) 
}
复制代码

可以看出来都是类的基本信息和初始化的方法列表、属性列表、成员变量列表等。

  • methods、properties、protocols是二维数组,是可读可写的,包含了类的初始内容、分类的内容,结构如下,每个methods数组包含若干个method_list_t,每个method_list_t数组里面又有若干个method_t结构体,可参考下图

截屏2021-05-17 下午7.44.49.png

接下来再看看method_t是什么,在objc-runtime-new.h文件中找到struct method_t,主要参数如下:

struct method_t{
    SEL name; //函数名
    const char *types; //编码(返回值类型、参数类型)
    IMP imp; // 指向函数的指针(函数地址)
}
复制代码

我们可以把method_t理解为是对方法\函数的封装。

  • IMP代表函数的具体实现,每一个方法都默认带有两个隐式参数 self : 方法调用者 _cmd : 调用方法的标号 ,可以写也可以不写。
  • SEL代表方法\函数名,一般叫做选择器,底层结构跟char *类似

1.可以通过@selector()和sel_registerName()获得
2.可以通过sel_getName()和NSStringFromSelector()转成字符串
3.不同类中相同名字的方法,所对应的方法选择器是相同的

  • types包含了函数返回值、参数编码的字符串

image.png

iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码

截屏2021-05-18 上午11.25.59.png

下面写两个例子总结一下:

-(void)testMethods; 

//每个方法其实都是有两个隐式参数self和_cmd,所以上面的方法等价于下面的方法
-(void)testMethods:(id)self _cmd:(SEL)_cmd;

复制代码

通过断点分析可以看到testMethods的types是:v16@0:8
截屏2021-05-18 上午11.42.25.png
参考上面encode指令表格,v16@0:8每个字符含义如下:

截屏2021-05-18 上午11.57.06.png

我们再写一个复杂一些的函数:

- (int)test:(int)age height:(float)height;
转换如下:
- (int)test:(id)self _cmd:(SEL)_cmd (int)age height:(float)height;
复制代码

上面函数的types是i24@0:8i16f20:

i 表示返回值是int类型
24 表示所有参数占字节总和 id(8字节)+ SEL(8字节)+ int(4字节) + float(4字节) = 24
@ 表示对象类型
0 表示参数self是从0字节开始
: 表示SEL类型
8 表示参数_cmd是在参数self后面开始,即8字节后
i 表示int类型
16 表示age是在self和_cmd后面,即8+8 = 16
f 表示float类型
20 表示height是在self、_cmd、age后面,即8+8+4 = 20

总结
通过上面对class结构的内容分析,我们可以用下面一张图来表示各结构体之间的关系:

截屏2021-05-18 上午10.42.20.png

4.方法缓存实现

上面我们主要探究了class的结构以及他的详细信息,现在我们继续看看class中的方法缓存cache_t chahe

struct objc_class{
    Class isa; //
    Class superclass;
    cache_t chahe; //方法缓存
    class_data_bits_t bits;//用于获取具体类的信息(&FAST_DATA_MASK获取)
}
复制代码
  • 当我们调用方法时,大概的轨迹是在当前class找,没有找到就去,父类里面找,如果找到就返回,没找到会一层一层去父类找,一直到基类。最终找到的方法就会被Class内部结构中的cache_t,用散列表(哈希表)将方法缓存起来,下次调用直接去cache_t里面拿,从而提高方法查找速度。

详细的对象/类方法调用轨迹可参考之前的文章 iOS 探究 OC对象、isa指针及KVO实现原理

  • cache_t是如何对方法进行缓存的呢?下面先看下他的内部结构
struct cache_t {
    struct bucket_t *_buckets; // 散列表 数组
    mask_t _mask; // 散列表的长度 -1
    mask_t _occupied; // 已经缓存的方法数量
};
复制代码

bucket_t是以数组的方式存储方法列表的,看一下bucket_t内部结构

struct bucket_t {
private:
    cache_key_t _key; // SEL作为Key
    IMP _imp; // 函数的内存地址
};
复制代码

从源码中可以看出bucket_t中存储着SEL和_imp,通过key->value的形式,以SEL为key,函数实现的内存地址 _imp为value来存储方法。
为了方便理解,我们还是通过下面图示来表达他们的关系:

截屏2021-05-18 下午3.17.24.png

总结一下方法被缓存的流程:
1.第一次调用某方法,会根据当前类->父类->基类的轨迹找方法,找到了则把这个方法的信息(SEL,imp)保存进当前类的缓存列表中,并使用方法的SEL(方法名)作为key,函数实现的内存地址 imp为value来存储方法。
2.后面调用某方法,会先在类的缓存列表中查找,通过传进来的方法key,查找到这个方法的信息(类似字典),直接返回。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享