iOS RunTime Advance 01

RunTime一直是iOS开发中非常重要的而且必须要理解的东西,最近在学习RunTime,有自己的一些心得,现在记录下来,便于以后查阅
理解 Objective-C 的 Runtime 机制可以帮我们更好的了解OC语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。

什么是RunTime

Objective-C 扩展了 C 语言,并加入了面向对象特性和 Smalltalk 式的消息传递机制。而这个扩展的核心是一个用 C 和 汇编 写的 Runtime 库。它是 Objective-C 面向对象和动态机制的基石。苹果和GNU各自维护一个开源的 runtime 版本,这两个版本之间都在努力的保持一致。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统(runtime)来动态的 创建类和对象、进行消息传递和转发

高级语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言。

OC –> runtime –> C –> 编译汇编 –> 机器语言

从OC到C语言的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类转变为面向过程的结构体。

消息传递——RunTime的核心

在Objective-C中,使用[receiver message]语法并不会马上执行receiver对象的message方法的代码,而是向receiver发送一条message消息,这条消息可能由receiver来处理,也可能由转发给其他对象来处理,也有可能假装没有接收到这条消息而没有处理。

如下:
一个对象的方法像这样[obj foo],编译器转成消息发送objc_msgSend(obj, foo)

由此,其实[receiver message]被编译器转化为:

1
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

这就是一个标准C语言函数的形式,其中两个数据类型:id 和 SEL 是实现的关键。

id
接下来看objc_msgSend第一个参数的数据类型id,id是通用类型指针,能够表示任何对象.

查看到id数据结构如下

1
2
3
4
5
6
7
8
9
10
11
12
#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.--> 类
typedef struct objc_class *Class;

/// Represents an instance of a class. --> 类的实例(对象)
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class. --> id
typedef struct objc_object *id;
#endif

id其实就是一个指向objc_object结构体指针,它包含一个Class isa成员,根据isa指针就可以顺藤摸瓜找到对象所属的类。

接下来就可以详细分析类和对象的结构了

类和对象的结构

我们看看对象(object),类(class)这两个结构体:

对象 objc_object

对象的构成比较简单:

1
2
3
4
/// Represents an instance of a class. --> 类的实例(对象)
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};

其中的Class,即objc_class结构体,可以在runtime.h文件中找到类结构体的定义。

类 objc_class

查看到objc_class结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

分析一些重要的成员变量表示什么意思和对应使用哪些数据结构。

isa

isa表示一个Class对象的Class,也就是Meta Class。在面向对象设计中,一切都是对象,Class在设计中本身也是一个对象。我们会在objc-runtime-new.h文件找到证据,发现objc_class有以下定义:

1
2
3
4
5
6
7
8
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

......
}

由此可见,结构体objc_class也是继承objc_object,说明Class在设计中本身也是一个对象

其实Meta Class也是一个Class,那么它也跟其他Class一样有自己的isasuper_class指针,使用它们可以确定一个类或者对象所属的类型:

上图实线是super_class指针,虚线是isa指针。有几个关键点需要解释以下:

  • Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
  • 每个Class都有一个isa指针指向唯一的Meta class
  • Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
  • 每个Meta class的isa指针都指向Root class (meta)。

super_class

super_class 表示实例对象对应的父类,与前面讨论的meta class中相同

name

name 字符串表示类名

ivars

ivars表示多个成员变量(属性),它指向objc_ivar_list结构体。在runtime.h可以看到它的定义:

1
2
3
4
5
6
7
8
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}

objc_ivar_list其实就是一个链表,存储多个objc_ivar,而objc_ivar结构体存储 类的单个成员变量 信息。

methodLists

methodLists表示方法列表,它指向objc_method_list结构体的二级指针.
可以动态修改 *methodLists 的值来添加成员方法,也是Category实现原理,同样也解释Category不能直接添加实例变量的原因。
在runtime.h可以看到它的定义:

1
2
3
4
5
6
7
8
9
10
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;

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

同理,objc_method_list也是一个链表,存储多个objc_method,而objc_method结构体存储类的某个方法的信息:

1
2
3
4
5
6
7
8
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

其实Method就是一个指向objc_method结构体指针,它存储了

  • 方法名(method_name) SEL类型
  • 方法类型(method_types) 字符串
  • 方法实现(method_imp) IMP类型
    而method_imp的数据类型是IMP,它是一个函数指针,后面会重点提及。

cache

cache用来缓存经常访问的方法,它指向objc_cache结构体

protocols

protocols表示类遵循哪些协议

以上通过类的结构分析,我们搞明白了对象和类的构成,以及id、类、对象之间的关系。

回到消息传递那里,另一个参数是 SEL,表示方法。在Method中,也看到了这个关键字。
SELIMP其实都是Method的属性。

方法

方法 Methodobjc_method

SEL(objc_selector)

objc_msgSend函数第二个参数类型为SEL,它是selector在OC中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的 ID,而这个 ID 的数据结构是SEL
先看下定义

1
2
3
4
5
6
Objc.h
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

@property SEL selector;
//A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

其实selector就是个映射到方法的C字符串,你可以用 Objective-C 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个 SEL 类型的方法选择器。

IMP

在上面讲Method时就说过,IMP本质上就是一个函数指针,指向方法的实现,在objc.h找到它的定义:

1
2
3
4
5
6
/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif

当你向某个对象发送一条信息,可以由IMP这个函数指针来指定方法的实现,它最终就会执行那段代码,这样可以绕开消息传递阶段而去执行另一个方法实现。

在iOS的runtime中,Method通过selector和IMP两个属性,实现了快速查询方法及实现,相对提高了性能,又保持了灵活性。

Cache(objc_cache) 缓存

顾名思义,Cache主要用来缓存,那它缓存什么呢?我们先在runtime.h文件看看它的定义:

1
2
3
4
5
6
7
typedef struct objc_cache *Cache                             OBJC2_UNAVAILABLE;

struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

Cache其实就是一个存储Method的链表,主要是为了优化方法调用的性能。当对象receiver调用方法message时,首先根据对象receiver的isa指针查找到它对应的类,然后在类的methodLists中搜索方法,如果没有找到,就使用super_class指针到父类中的methodLists查找,一旦找到就调用方法。如果没有找到,有可能消息转发,也可能忽略它。但这样查找方式效率太低,因为往往一个类大概只有20%的方法经常被调用,占总调用次数的80%。所以使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

坚持原创技术分享,您的支持将鼓励我继续创作!