Runtime源代码解读(实现面向对象初探)

2019-09-26html

The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.(Objective-C Runtimegit

文章的开头是Apple Documentation对runtime的定义,很官方也很抽象。我的对runtime的理解是:在狭义上,runtime用面向过程的C语言实现了面向对象特性,也就是实现了类和对象;在广义上,runtime实现了Objective-C语言的动态特性(深刻Objective-C的动态特性)。动态特性主要包括动态类型(Dynamic typing)、动态绑定(Dynamic binding)和动态加载(Dynamic loading)。与之相对应,runtime具体实现了ClassNSObject抽象类型、Class的继承链、方法的响应链、方法动态解析以及消息转发流程、运行时动态加载类型/方法/属性等动态元素。github

动态加载类库属于dyld范畴(源代码),固然runtime为了实现动态特性须要依赖dyld的API。objective-c

1、类和对象

若是要找出Objective-C中最动态的两个类型,那必定是Class(类)和id(对象的引用),二者也恰是runtime实现面向对象的基础。经过#import <objc/runtime.h>#import <objc/objc.h>语句跳转到runtime.h、objc.h头文件,能够从中找到类和对象定义的代码:算法

  • objc_class结构体表示类。其中super_class指针指向父类用于实现类的继承特性,instanceSize记录类的实例占用内存大小,ivars描述类的成员变量列表,methodLists保存类的方法列表,protocols保存类所遵循的协议列表,cache是方法缓存用于记录最近使用的方法,其余成员能够暂不关注;
  • Class是类的引用;
  • objc_object结构体表示对象,仅包含isa指针,指向对象的类(新版本runtime中isa指针不必定简单指向对象的类);
  • id表示指向objc_object结构体的指针,也就是对象引用,本质是对象的地址;

重要提醒:从#import <objc/runtime.h>#import <objc/objc.h>语句跳转到的runtime.hobjc/objc.h头文件中,都是旧版本runtime的代码。凡新旧代码处理逻辑存在不一样之处的,文中有特别声明。数组

/* 对象的引用的定义 */
typedef struct objc_object *id;

/** 对象的定义 */
struct objc_object {
    Class _Nonnull isa;  // 对象的类
};

/** 类的定义 */
typedef struct objc_class *Class;
struct objc_class {
    Class _Nonnull isa;  // 元类

    Class _Nullable super_class;  // 父类
    const char * _Nonnull name;   // 类名
    long version;
    long info;
    long instance_size;  // 实例的大小
    struct objc_ivar_list * _Nullable ivars;  // 成员列表
    struct objc_method_list * _Nullable * _Nullable methodLists;  // 方法列表
    struct objc_cache * _Nonnull cache;  // 方法缓存
    struct objc_protocol_list * _Nullable protocols;  // 所遵循的协议列表
};
复制代码

objc_object结构体只有一个指针类型的isa成员,也就是说一个objc_object仅占用了8个字节内存,但并不说明对象仅占8个字节内存空间。当调用类的allocallocWithZone方法构建对象时,runtime会分配类的instanceSize大小的连续内存空间用于保存对象。在该内存块的前8个字节写入类的地址,其他空间用于保存其余成员变量。最后构建方法返回的id其实是指向该内存块的首地址的指针。缓存

注意:以上是旧版本runtime构建对象的处理,在新版本runtime中略有不一样。缘由是新版本runtime从新定义了isa指针数据结构,并且在引入Non-fragile instance variable机制后instanceSize再也不是固定的值,这些会在后续的文章中介绍。bash

1.1 继承链

根据objc_classsuper_class成员能够创建起类的继承结构,因为类的super_class指向单一的类,这是Objective-C之因此是单继承语言的缘由。类的继承链的顶端是是根类(root class),一般是NSObject,根类的super_class指向NULL。数据结构

objc_objectobject_class结构体均包含isa指针,在runtime中类也是对象,是对象就会有类型,类的类型是元类(meta class)object_classisa指针指向元类。元类也是用objc_class结构体保存,也就是说元类也是类。元类也具备继承特性,继承链的顶端是根元类(root meta class)。根元类的是根类的元类,根元类的super_class指向根类。判断objc_class是否为元类的方式有两种:架构

  • 根据version的值,全部元类的version值为6(新版本runtime中是7),非元类为0
  • 元类的isa指针指向根元类,包括根元类本身。

至此,总结出runtime的继承结构以下图所示。

Runtime中类的继承结构.jpg

2、成员变量

objc_ivar结构体表示类的成员变量。

  • objc_ivarivar_name为成员变量名;
  • ivar_type为成员变量的类型编码(官方文档),用字符串表示成员变量的数据类型,例如:'@'表示成员变量保存对象的引用,可使用@encode()以类型为参数查询类型编码;
  • offset为成员变量的在实例内存块中的偏移。
struct objc_ivar {
    char * _Nullable ivar_name;
    char * _Nullable ivar_type;
    int ivar_offset;
#ifdef __LP64__
    int space;
#endif
} 
复制代码

objc_ivar_list结构体表示类的成员变量列表。objc_ivar_listobjc_ivar结构体的数组ivar_list用于保存类的全部成员变量,ivar_count为成员变量列表的长度。

struct objc_ivar_list {
    int ivar_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1];
} 
复制代码

2.1 成员变量布局

类构建成员变量列表的过程,包含肯定成员变量布局(ivar layout) 的过程。成员变量布局就是定义对象占用内存空间中哪块区域保存哪一个成员变量,具体为肯定类的instanceSize、内存对齐字节数、成员变量的offset。类的继承链上全部类的成员变量布局,共同构成了对象内存布局(object layout)。成员变量布局和对象内存布局的关系能够用一个公式表示:类的对象内存布局 = 父类的对象内存布局 + 类的成员变量布局。成员变量布局的计算法则以下:

  • 成员变量的偏移量offset必须大于等于父类的instanceSize
  • 成员变量的布局和结构体的对齐遵循一样的准则,类的对齐字节数必须大于等于父类的对齐字节数。例如,占用4字节的int类型成员变量的起始内存地址必须是4的倍数,占用8字节的id类型成员变量的起始内存地址必须是8的倍数;
  • instanceSize的计算公式是类的instanceSize = 父类的instanceSize + 类的成员变量在实例中占用的内存空间 + 对齐填补字节instanceSize必须是类的对齐字节数的整数倍;

NSObject类的定义中,包含一个Class类型的isa成员,所以实际上isa指针的8个字节内存空间也属于对象内存布局的范畴。

举个具体的例子:用如下代码定义一个继承NSObjectTestObjectLayout类:

@interface TestObjectLayout : NSObject{
    bool bo;
    int num;
    char ch;
    id obj;
}
@end

@implementation TestObjectLayout

@end
复制代码

其成员变量布局的计算过程以下:

  • instanceSize初始化为父类的instanceSize的值,并按父类的对齐字节数对齐。父类NSObject仅包含isa一个成员变量,isa占用8个字节offset为0,所以父类instanceSize为8,按8字节对齐;
  • instanceSize初始化,按照对齐法则依次添加成员变量,并更新instanceSizebo按字节对齐(注意bool类型占用1字节空间并非1位),偏移量为8,instanceSize更新为16;
  • num按4字节对齐,偏移量为12,instanceSize仍为16;
  • ch按字节对齐,偏移量为16,instanceSize更新为24;
  • obj按8字节对齐,偏移量为24,instanceSize更新为32。最终肯定instanceSize为32字节,按8字节对齐。

由上文对象内存布局计算公式能够看出,计算类的对象内存布局其实是从根类开始递推计算继承链上全部类成员变量布局的过程(也可视为从类递归到根类)。假设TestObjectLayout对象的起始内存地址为0x100BB134000,则按照上述步骤可计算该对象内存布局以下图所示:

实例内存图.jpg

类的构建过程之因此要计算类的成员变量布局,是由于构建一个对象时须要肯定须要为对象分配的内存空间大小,且构建对象仅返回对象的内存首地址,而经过成员变量的offset结合ivar_type,则能够轻而易取地经过对象地址定位到保存成员变量的内存空间。

注意:新版本runtime的成员变量列表保存位置有所变化。

3、方法

objc_method结构体表示方法,其中:

  • SEL类型的method_name表示方法名;
  • 字符串类型的method_types表示方法的类型编码,类型编码描述了方法的返回值类型以及参数类型;
  • IMP类型的method_imp表示方法的实现。
/* 对象的方法的定义 */
struct objc_method {
    SEL method_name;      // 方法选择器,即方法名
    char *method_types;   // 方法的类型编码
    IMP method_imp;       // 方法的实现,即方法的函数指针、方法的IMP
}  

/* 方法选择器定义 */
typedef struct objc_selector *SEL;

/* 方法实现定义 */
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 

复制代码

objc_method_list结构体表示方法列表。objc_method_list结构与objc_ivar_list相似

struct objc_method_list {
    struct objc_method_list *obsolete;

    int method_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_method method_list[1];
} 
复制代码

注意到类的定义中,方法列表methodLists的类型为struct objc_method_list * _Nullable * _Nullable,所以类的方法列表的保存形式是二维数组,也就是数组的数组。

注意:新版本runtime的方法列表数据结构有较大变化。

3.1 方法的响应链

类包含实例方法(instance method)和类方法(class method),两种方法是保存在彻底不一样的地方。实例方法保存在类的方法列表中,类方法保存在元类的方法列表中

调用实例方法和类方法,接收消息的对象是不同的,实例方法接收消息的对象是实例,类方法接收消息的对象是类,例如:[someObj init][NSObject alloc]。这是由于两种方法保存在彻底不一样的地方,实例方法保存在“类”的方法列表中,类方法保存在“元类”的方法列表中。那么 runtime 响应实例方法和类方法的流程有什么不同呢?用下图能够表示,其中:

  • 子类SubClass包含subClassInstanceMethod实例方法,图中橘色线表示该消息的响应过程;
  • 根类RootClass包含rootInstanceMethod实例方法,图中洋红色线表示该消息的响应过程;
  • 根类RootClass包含rootClassMethod类方法,图中蓝色线表示该消息的响应过程;

基本消息响应链.jpg

综上,runtime 基于继承结构的基本响应链都有相同的结构:一、根据接收消息的对象的isa指针找到对象的类;二、从对象的类开始沿着其superclass串联起来的继承链,搜索可响应该消息的类,直到根类为止;三、若继承链上没有可响应该消息的类,则开始消息转发流程。

4、属性、协议、分类

runtime.h头文件中公布的属性、协议、分类等元素的相关信息不多,但这些均可以肯定是类须要保存的元数据。这里仅简单收集其中公布的代码。

//属性相关数据结构
typedef struct objc_property *objc_property_t;

typedef struct {
    const char * _Nonnull name;  // 属性名
    const char * _Nonnull value;  // 特性的值
} objc_property_attribute_t;  // 属性的特性

//协议相关数据结构
#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};

//分类相关数据结构
typedef struct objc_category *Category;

struct objc_category {
    char * _Nonnull category_name;  //分类名
    char * _Nonnull class_name;  //分类所扩展的类名
    struct objc_method_list * _Nullable instance_methods;  //实例方法列表
    struct objc_method_list * _Nullable class_methods;  //类方法列表
    struct objc_protocol_list * _Nullable protocols; //协议列表
}
复制代码

5、总结

前文不止一次提到,本文引用的runtime代码是旧版本的代码,之因此要从分析旧版本代码入手,是由于首先新版本代码在主体架构上仍然沿用了旧版本,只是在部分数据结构和实现细节上作了优化,分析旧版本接口文件已经足以对runtime框架有一个整体的认知,这样有利于对庞大的runtime源代码工程,有选择性、有针对性的学习;另外,从存在缺陷的旧代码入手,有助于加深新版本之因此要作优化的缘由,并从中借鉴到代码优化的一些经验方法。

相关文章
相关标签/搜索