曾觉得 iOS 很好学,也想着学一段时间就可以精通这门语言,但是随着开发的越来越深入,才能意识到,iOS 绝不像外表这样简单,他的内涵真是太深了,感觉自己真是一个什么也不知道的 Objc 小白。
Runtime 和消息发送机制是理解 iOS 运行过程避不开的一道坎,虽然平时很少用,但是却是我们 Objc 程序员需要了解的。
Runtime
因为Objc是一门动态语言,所以它总会在运行时(而不是编译时)进行工作。所以光有一个编译器是不够的,还需要一个运行时系统(runtime system)执行编译后代码。这便是Runtime系统,它是整个Objc运行框架的基石。
Objc与Runtime的交互
objc从三种不同的层级上与Runtime系统交互,分别是:
Objective-C 源代码
部分情况下,runtime都是系统在幕后执行,我们只需要在前台好好写Objc代码就行。
消息执行会使用到一些编译器为实现动态语言特性而创建的数据结构和函数。
Objc中的类、方法和协议等在runtime中都由一些数据结构定义
NSObject的方法
Cocoa中大多数类都继承于NSObject
类,所以也就继承了它的方法(NSProxy除外)。
NSObject中有许多的方法,自然也有许多作用,比如
- 抽象接口作用,比如
description
方法需要重载它并为你定义的类提供描述内容。 - 在运行时获得类的信息并检查一些特性,比如
class
返回对象的类isKindOfClass:
和isMemberofClass:
则检查对象是否在指定的类继承体系中。respondsToSelector:
检查对象能否响应指定消息(是否有指定方法)。conformsToProtocol
检查对象是否实现了指定协议方法methodForSelector:
返回指定方法实现的地址
Runtime的函数
Runtime系统是一个有一系列函数和数据结构组成,具有公共接口的动态共享库。头文件在/user/include/objc
中。在Objective-C Runtime Reference中有对Runtime函数的详细文档。
Runtime基础数据结构
在一个类似[a someFuc]的方法调用中,编译阶段编译器并不知道someFuc要实现哪一段代码而只是确定了要向接受者发送someFuc消息,只有到运行的时候,才会发送消息进行方法的确定。这里我们可以看一下objc的底层实现。
1 | //main.m |
以上函数在底层其实是这样的
如上图所示,其实Objc所有方法在底层都会变成一个函数,那就是objc_msgSend()
。
1 | id objc_msgSend ( id self, SEL op, ...); |
这里面有两个参数值得注意,一个是id
,一个是SEL
,鉴于id比较复杂,我们先讲讲SEL
SEL
它是selector
在Objc中的表示类型。selector
是方法选择器,相当于区分各个方法的一个ID,这个ID的数据结构就是SEL
1 | typedef struct objc_selector *SEL; |
我们可以用Objc编译器命令@selector()
或Runtime的sel_registerName
获得一个SEL
类型的方法选择器。
id
作为开发者,大家应该对id都不会陌生,它是一个指向类实例的指针
1 | typedef struct objc_object *id; |
在这之中,objc_object
是这样的一个结构体
1 | //objc-private.h |
结构体重包含一个isa
指针,类型为isa_t
根据isa
就可以找到对象所属的类。
isa
指针又涉及到引用计数原理的知识了,这里就不做详尽描述了。
objc_object中又有属性值得我们注意
Class
Class
其实是一个指向objc_class
结构体的指针
1 | typedef struct objc_class *Class; |
这个objc_class
又包含很多方法了
1 | struct objc_class : objc_object { |
我们可以看到objc_class
继承于objc_object
,所以我们可以说一个Objc类本来就是一个对象。
为了处理类和对象的关系,runtime创建了一种叫元类(Meta Class)的东西,类对象所属类型就叫元类,它用来表述类对象本身所具备的元数据。这就是类方法的定义,每个类仅有一个类对象,每个类也只有一个与之相关的元类。
当我们使用类似[p alloc]
的类方法时,事实上是把这个消息发送给了一个类对象,这个类对象必须是一个元类的实例,而这个元类也是一个**根元类(root meta class)**的实例。所有元类都指向根元类为其超类。所有元类的方法列表都有能够响应消息的类方法。
所以当[p alloc]
这条消息发给类对象的时候,objc_msgSend()
会去它的元类里面去查找能够响应消息的方法,如果找到了,然后就对这个类对象执行方法调用。
根据上图,我们可以看到方法,类,元类的关系。有趣的是根元类的超类是根类(根类在实际运用中就是NSObject
),isa
指向了自己。
而NSObject
的超类为nil
,也就是说它没有超类。
可以看到运行时一个类还关联了它的超类指针(superclass),类名,成员变量,方法,缓存,还有附属协议。
cache_t
1 | struct cache_t { |
_buckets
存储IMP
。_mask
和_occupied
对应vtable
。
cache
是优化的一个机制,如果我们实例对象每收到一个消息都去isa
指向的类方法列表中遍历,那效率就太低了。
所以系统会把调用的方法存到cache
中,然后在收到消息后优先在cache
中查找(理论上讲 如果一个方法被调用一次,那它就很有可能在今后还会被调用)。
bucket_t
中存储了指针与IMP的键值对:
1 | struct bucket_t { |
详细的细节都在objc-cache.mm
文件中
class_data_bits_t
class_data_bits_t
包含的信息太多了,主要有class_rw_t
,retain/release/autorelease/retaincount
和alloc
等信息。
1 | //objc-runtime-new.h |
联系前面的Class
,我们可以注意到objc_class
的data
方法返回的是class_data_bits_t
的data
方法,最终返回的是class_rw_t
,有好几层。
在class_data_bits_t
里又包含了一个bits
,这个指针跟不同的FAST_
前缀的掩码做按位与操作,可获得不同的数据。bits
在内存中每个位的含义有三种排列顺序:
64位不兼容中每个宏对应含义如下:
1 | // class is a Swift class |
在这里除了FAST_DATA_MASK
是用一段空间储存数据外,其他宏都是用1bit存bool值。
class_data_bits_t
提供了三个方法用于位操作:getBit
,setBits
和clearBits
而FAST_DATA_MASK
的存储区域里面其实就是存储了指向class_rw_t
的指针
1 | class_rw_t* data() { |
Category
1 | typedef struct category_t *Category; |
Category
为现有的类提供了拓展,存储了类别中可以拓展的实例方法、实例属性和类方法、类属性(objc2016新增特性)。
1 | struct category_t { |
App 启动加载镜像文件的时候,会简介调用到attachCategories
函数,完成向类中添加Category
的工作。
Method
1 | typedef struct method_t *Method; |
它存储了方法名,方法类型和方法实现:
1 | struct method_t { |
方法名类型为SEL
,方法类型types
是个char
指针,存储着方法的参数类型和返回值类型。
imp
指向了方法实现,其实是一个函数指针。
Ivar
1 | typedef struct ivar_t *Ivar; |
IMP
IMP
在objc.h
中为:
1 | typedef void (*IMP)(void /* id, SEL, ... */); |
它就是一个函数指针,由编译器生成。当我们发起一个Objc消息后,最终会执行什么代码,就由这个指针指定。IMP
这个函数指针指向了方法的实现。
感悟
iOS Runtime真是博大精深,这还没走到最深层,就由一大堆底层概念,所以 学习之路漫漫啊。