2019-10-15
类的方法包括实例方法(instance method)和类方法(class method),两者保存在完全不同的地方,实例方法保存在类的方法列表中,类方法保存在元类的方法列表中。在Runtime源代码解读(实现面向对象初探)中已介绍过方法的基本数据结构objc_method,以及方法的基本响应链,本文介绍方法列表的具体实现原理。
一、数据结构
在 Runtime源代码解读2(类和对象)中介绍过:类的class_ro_t包含method_list_t类型的baseMethodList成员;类的class_rw_t包含method_array_t类型的methods成员。两者同样保存方法列表,却使用不同的数据结构,那么它们之间有什么关系呢?由于class_ro_t是保存的基本上是编译时决议的数据,baseMethodList显然是保存类定义时所定义的方法,这些方法是编译时决议的。而class_rw_t的methods实际上才是类的完整方法列表,而且class_rw_t的methods包含了class_ro_t的baseMethodList。
类的class_rw_t保存方法列表保存形式并不是简单的一维数组结构,而是二维数组结构,这是为了区分类构建阶段定义的基本方法,以及不同分类之间的定义的方法。objc_class保存方法列表的数组结构是method_array_t类,method_array_t继承list_array_tt模板类。
1.1 list_array_tt 二维数组容器
Runtime 定义list_array_tt类模板表示二维数组容器。list_array_tt保存的数据主体是一个联合体,包含list和arrayAndFlag成员,表示:容器要么保存一维数组,此时联合体直接保存一维数组的地址,地址的最低位必为0;要么保存二维数组,此时联合体保存二维数组的首个列表元素的地址,且最低位置为1。调用hasArray()方法可以查询容器是否保存的是二维数组,返回arrayAndFlag & 1,即通过最低位是否为1进行判断;调用list_array_tt的array()方法可以获取二维数组容器的地址,返回arrayAndFlag & ~1,即忽略最低位。
当联合体保存二维数组时,联合体的arrayAndFlag指向list_array_tt内嵌定义的array_t结构体。该结构体是简单的一维数组容器,但其元素为指向列表容器的指针,因此array_t的本质是二维数组。调用array_t的byteSize()可以返回列表容器占用的总字节数,为array_t结构体本身的size与保存列表元素的连续内存区块的size之和。
// 二维数组容器模板
template <typename Element, typename List>
class list_array_tt {
// 定义二维数组的外层一维数组容器
struct array_t {
uint32_t count;
List* lists[0];
static size_t byteSize(uint32_t count) {
return sizeof(array_t) + count*sizeof(lists[0]);
}
// 计算容器占用字节数
size_t byteSize() {
return byteSize(count);
}
};
protected:
// 内嵌迭代器的定义(后文 1.1.1 节介绍)
...
private:
union {
List* list; // 要么指向一维数组容器
uintptr_t arrayAndFlag; // 要么指向二维数组容器
};
// 容器是否保存的是二维数组
bool hasArray() const {
return arrayAndFlag & 1;
}
// 获取容器保存的二维数组的地址
array_t *array() {
return (array_t *)(arrayAndFlag & ~1);
}
void setArray(array_t *array) {
arrayAndFlag = (uintptr_t)array | 1;
}
public:
// 计算方法列表二维数组容器中所有方法的数量
uint32_t count() {
uint32_t result = 0;
for (auto lists = beginLists(), end = endLists();
lists != end;
++lists)
{
result += (*lists)->count;
}
return result;
}
// 获取方法列表二维数组容器的起始迭代器
iterator begin() {
return iterator(beginLists(), endLists());
}
// 获取方法列表二维数组容器的结束迭代器
iterator end() {
List **e = endLists();
return iterator(e, e);
}
// 获取方法列表二维数组容器包含的方法列表数量
uint32_t countLists() {
if (hasArray()) {
return array()->count;
} else if (list) {
return 1;
} else {
return 0;
}
}
// 获取方法列表二维数组容器的起始地址
List** beginLists() {
if (hasArray()) {
return array()->lists;
} else {
return &list;
}
}
// 获取方法列表二维数组容器的结束地址
List** endLists() {
if (hasArray()) {
return array()->lists + array()->count;
} else if (list) {
return &list + 1;
} else {
return &list;
}
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
// 后文 1.1.2 节介绍
...
}
void tryFree() {
// 后文 1.1.2 节介绍
...
}
template<typename Result> Result duplicate() {
// 后文 1.1.2 节介绍
...
}
};
1.1.1 list_array_tt 的内嵌迭代器
list_array_tt容器模板的内嵌迭代器iterator类的定义如下,包含三个成员:
lists:当前迭代到的数组的位置;listsEnd:二维数组的外层容器的结尾;m:当前迭代到的数组 中的元素的位置;mEnd:当前迭代到的数组的结尾;
注意:构建
list_array_tt的迭代器时,只能从方法列表到方法列表,不能从容器中的方法列表的某个元素的迭代器开始迭代。
class iterator {
List **lists;
List **listsEnd;
typename List::iterator m, mEnd;
public:
// 构建从begin指向的列表,到end指向的列表的迭代器
iterator(List **begin, List **end)
: lists(begin), listsEnd(end)
{
if (begin != end) {
m = (*begin)->begin();
mEnd = (*begin)->end();
}
}
const Element& operator * () const {
return *m;
}
Element& operator * () {
return *m;
}
bool operator != (const iterator& rhs) const {
if (lists != rhs.lists) return true;
if (lists == listsEnd) return false; // m is undefined
if (m != rhs.m) return true;
return false;
}
//迭代时,若达到当前数组的结尾,则切换到下一个数组的开头
const iterator& operator ++ () {
assert(m != mEnd);
m++;
if (m == mEnd) {
assert(lists != listsEnd);
lists++;
if (lists != listsEnd) {
m = (*lists)->begin();
mEnd = (*lists)->end();
}
}
return *this;
}
};
1.1.2 list_array_tt 公开的操作方法
list_array_tt容器模板的三个操作类型的公开方法如下:
attachLists(...):将列表元素添加到二维数组容器的开头,注意到list_array_t没有定义构造函数,这是因为构造逻辑均在attachLists(...)中,包含容器从空转化为保存一位数组,再转化为保存二维数组的处理逻辑;tryFree():释放二维数组容器占用内存;duplicate(...):复制二维数组容器;
// 向二维数组容器中添加列表
void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;
if (hasArray()) {
// 当容器中保存的是二维数组
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount))); // 分配新内存空间重新构建容器
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0])); // 转移容器原内容到新内存空间
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0])); // 将需要新增的方法列表拷贝到新内存空间
}
else if (!list && addedCount == 1) {
// 当容器中无任何方法,直接将list成员指向addedLists
list = addedLists[0];
}
else {
// 当容器中保存的是一维数组
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
// 释放容器占用的内存空间
void tryFree() {
if (hasArray()) {
for (uint32_t i = 0; i < array()->count; i++) {
try_free(array()->lists[I]);
}
try_free(array());
}
else if (list) {
try_free(list);
}
}
// 拷贝容器
template<typename Result>
Result duplicate() {
Result result;
if (hasArray()) {
array_t *a = array();
result.setArray((array_t *)memdup(a, a->byteSize()));
for (uint32_t i = 0; i < a->count; i++) {
result.array()->lists[i] = a->lists[i]->duplicate();
}
} else if (list) {
result.list = list->duplicate();
} else {
result.list = nil;
}
return result;
}
1.2 method_array_t 方法列表二维数组容器
类的方法列表信息保存在class_rw_t结构体的methods成员中,类型为method_array_t。method_array_t是按list_array_tt模板构建,以method_t(方法)为元素,以method_list_t(方法列表)为列表容器的二维数组容器,用于存储类的方法列表。method_list_t继承自entsize_list_tt顺序表模板,entsize_list_tt在 Runtime源代码解读2(类和对象) 介绍过,是具有固定类型元素的顺序表容器,注意到FlagMask指定为0x03,因此method_list_t的entsizeAndFlags成员最低两位预留有特殊功能:若最低两位均为1,表示该方法列表已排序。
由method_array_t中扩展list_array_tt的方法的方法名可见,method_array_t是为 category 量身定做的,显然 category 中定义的所有方法都存储在该容器中,每个 category 定义的方法对应method_array_t二维数组容器中的一个元素,也就是一个方法列表method_list_t结构体的指针。扩展的方法如下:
beginCategoryMethodLists():指向容器的第一个数组;endCategoryMethodLists(Class cls):当类的class_rw_t中不存在baseMethodList时,直接返回容器最后一个数组,当存在baseMethodList时,返回容器的倒数第二个数组。
static uint32_t fixed_up_method_list = 3;
// 方法列表
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {
// 查询方法列表是否已排序
bool isFixedUp() const {
return flags() == fixed_up_method_list;
}
// 标记方法列表已排序
void setFixedUp() {
runtimeLock.assertLocked();
assert(!isFixedUp());
entsizeAndFlags = entsize() | fixed_up_method_list;
}
// 新增返回方法在顺序表中的索引值的方法
uint32_t indexOfMethod(const method_t *meth) const {
uint32_t I =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this) / entsize());
assert(i < count);
return I;
}
};
// 方法列表二维数组容器
class method_array_t :
public list_array_tt<method_t, method_list_t>
{
typedef list_array_tt<method_t, method_list_t> Super;
public:
method_list_t **beginCategoryMethodLists() {
return beginLists();
}
method_list_t **endCategoryMethodLists(Class cls) {
method_list_t **mlists = beginLists();
method_list_t **mlistsEnd = endLists();
if (mlists == mlistsEnd || !cls->data()->ro->baseMethods())
{
return mlistsEnd;
}
return mlistsEnd - 1;
};
method_array_t duplicate() {
return Super::duplicate<method_array_t>();
}
};
在 Runtime源代码解读2(类和对象) 中介绍过类的加载过程。从 class realizing 时调用的methodizeClass(...)函数的处理逻辑可以看出:class_rw_t中的method_array_t容器保存了类的完整方法列表,包括静态编译的类的基本方法、运行时决议的 category 中的方法以及运行时动态添加的方法。而且class_rw_t中method_array_t容器的最后一个数组实际上就是class_ro_t的baseMethodList。
再结合 1.1.2 介绍的list_array_tt的attachLists(...)方法逻辑,可以基本了解方法列表容器的工作机制。当使用class_addMethod(...)动态添加类,或者应用加载阶段加载 category 时,均调用了该方法。由于attachLists(...)添加方法时,将方法添加到容器的开头,将原有的method_list_t集体后移,因此类的同名方法的IMP的优先级从高到低排序如下:
- 通过
class_addMethod(...)动态添加的方法; - 后编译的类的 category 中的方法;
- 先编译的类的 category 中的方法;
- 类实现的方法;
- 类的父类实现的方法;
类的方法列表的结构可总结如下图所示。其中绿色表示method_list_t容器,蓝色表示保存method_t结构体,黄色表示指向method_list_t结构体的指针。上图为method_array_t保存一维数组容器(method_list_t)时的内存结构,此时method_array_t的联合体list成员有效;下图为method_array_t保存二维数组容器时的内存结构,此时method_array_t的联合体arrayAndFlag成员有效。
二、 动态添加方法的实现原理
运行时调用class_addMethod (...)函数可以给类动态添加方法,调用class_replaceMethod(...)可以替换方法的实现。实际上两者都调用了addMethod(...)函数,原理是先根据传入的方法名、方法IMP、类型编码构建method_t,然后新建method_list_t方法列表容器将method_t添加到其中,最后调用attachList(...)将方法列表添加到类的class_rw_t的methods方法列表二维数组容器中。
class_addMethod (...)的源代码如下:
// 添加方法
BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
rwlock_writer_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
// 替换方法实现
IMP
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
rwlock_writer_t lock(runtimeLock);
return addMethod(cls, name, imp, types ?: "", YES);
}
static IMP
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
runtimeLock.assertWriting();
assert(types);
assert(cls->isRealized());
method_t *m;
if ((m = getMethodNoSuper_nolock(cls, name))) {
// 若方法已经存在
if (!replace) {
result = m->imp;
} else {
result = _method_setImplementation(cls, m, imp); // 设置方法IMP
}
} else {
// 若方法不存在,则构建方法列表容器
method_list_t *newlist;
newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
newlist->entsizeAndFlags =
(uint32_t)sizeof(method_t) | fixed_up_method_list;
newlist->count = 1;
newlist->first.name = name;
newlist->first.types = strdup(types);
if (!ignoreSelector(name)) {
newlist->first.imp = imp;
} else {
newlist->first.imp = (IMP)&_objc_ignored_method;
}
prepareMethodLists(cls, &newlist, 1, NO, NO); // 后文 2.1 中详细介绍
// 添加方法到类的class_rw_t的methods中
cls->data()->methods.attachLists(&newlist, 1);
flushCaches(cls); // 后文 2.2中详细介绍
result = nil;
}
return result;
}
// 设置方法IMP
static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
runtimeLock.assertWriting();
if (!m) return nil;
if (!imp) return nil;
// 对于打开ARC的编译环境,忽略retain/release等MRC方法
if (ignoreSelector(m->name)) {
return m->imp;
}
IMP old = m->imp;
m->imp = imp;
flushCaches(cls); // 清空cls类的方法缓冲
updateCustomRR_AWZ(cls, m); // 对retain/release等MRC方法,以及allocWithZone方法的特殊处理
return old;
}
向类动态添加方法,类的方法列表结构前后对比图示如下。其中红色标记部分为新增结构。处理过程为:将添加方法封装到method_list_t容器,然后插入到method_array_t的外层一位数组开头,其他元素整体后移,外层一位数组长度增加1。
2.1 method_list_t 排序
addMethod(...)函数中还调用了prepareMethodLists(...)函数,该方法主要调用了fixupMethodList(...)函数。fixupMethodList(...)做了三件事情:
- 调用
sel_registerNameNoLock函数将方法列表中所有方法的SEL注册到内存,注册的内存实际是以 方法名字符串 为键值,将SEL插入到系统维护的namedSelectors哈希表中; - 对方法列表内的所有方法进行排序;
- 将方法列表状态标记为
isFixedUp,表示方法列表已排序;
之所以对方法列表排序,是为了使方法列表支持二分查找,从而降低通过SEL在方法列表中搜索IMP的时间复杂度。prepareMethodLists(...)函数相关代码如下:
static void
prepareMethodLists(Class cls, method_list_t **addedLists, int addedCount,
bool baseMethods, bool methodsFromBundle)
{
if (addedCount == 0) return;
for (int i = 0; i < addedCount; i++) {
method_list_t *mlist = addedLists[I];
assert(mlist);
// Fixup selectors if necessary
if (!mlist->isFixedUp()) {
fixupMethodList(mlist, methodsFromBundle, true/*sort*/);
}
}
}
static void
fixupMethodList(method_list_t *mlist, bool bundleCopy, bool sort)
{
for (auto& meth : *mlist) {
const char *name = sel_cname(meth.name);
// 注册方法列表中的方法名
SEL sel = sel_registerNameNoLock(name, bundleCopy);
meth.name = sel;
if (ignoreSelector(sel)) {
meth.imp = (IMP)&_objc_ignored_method;
}
}
// 按照SEL对方法列表排序
if (sort) {
method_t::SortBySELAddress sorter;
std::stable_sort(mlist->begin(), mlist->end(), sorter);
}
// 标记方法列表已经排好序并固定。
// method_list_t的entsizeAndFlags最低两位均为1表示方法列表已经排好序并固定
mlist->setFixedUp();
}
2.2 方法缓冲
所谓方法缓冲(cache),是使用哈希表保存类的最近被调用的方法,利用哈希表的 O(1) 的查询时间复杂度提高方法调用的效率。objc_class的cache_t cache成员就是类的方法缓冲。cache_t结构体代码如下。实际上是指向一个可以动态扩容的 Key-Value 形式的哈希表,以方法名SEL为Key,以方法IMP为Value,分别对应bucket_t结构体的_sel、_imp成员。cache_t结构体包含以下成员:
_buckets:保存哈希表所有元素的数组,元素类型为bucket_t结构体,不是指向bucket_t的指针;
_mask:间接表示哈希表的容量,为全部位为1的二进制数。哈希表容量最小为4,满足公式:哈希表容量 = _mask + 1。扩容时设置_mask =(_mask + 1) & _mask;
_occupied:哈希表实际缓存的方法个数;
前文addMethod(...)的代码中,调用flushCache(...)时用于清空指定类的方法缓冲。之所以清空是因为addMethod(...)可能会触发方法IMP变更,必须保证方法缓冲中保存的所有方法SEL与IMP映射的正确性。关于方法缓冲的具体细节见 Runtime 源代码objc-cache.mm不深入介绍。
// 方法缓冲哈希表的元素,_sel为key,_imp为value
struct bucket_t {
private:
#if __arm64__
uintptr_t _imp;
SEL _sel;
#else
SEL _sel;
uintptr_t _imp;
#endif
...
public:
...
};
// 方法缓冲的数据结构
struct cache_t {
struct bucket_t *_buckets; //保存哈希表所有元素的数组
mask_t _mask; // 间接表示哈希表的容量
mask_t _occupied; //
public:
struct bucket_t *buckets();
mask_t mask();
mask_t occupied();
void incrementOccupied();
void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
void initializeToEmpty();
mask_t capacity();
bool isConstantEmptyCache();
bool canBeFreed();
static size_t bytesForCapacity(uint32_t cap);
static struct bucket_t * endMarker(struct bucket_t *b, uint32_t cap);
void expand();
void reallocate(mask_t oldCapacity, mask_t newCapacity);
struct bucket_t * find(cache_key_t key, id receiver);
static void bad_cache(id receiver, SEL sel, Class isa) __attribute__((noreturn));
};
三、方法的完整响应流程
调用方法常用的方式包括:
@interface定义的公开接口;NSObject类performSelector系列方法;NSInvocation类触发;objc_msgSend(...)消息发送函数;
注意:
<objc/message.h>公开的接口形式是无参的,可以尝试用类似BOOL (*msg)(Class, SEL, id) = (typeof(msg))objc_msgSend处理,做强制转换后再调用msg函数指针以实现正常传参,第一个参数是接收消息的对象(实例方法)/类(类方法),第二个参数是SEL方法选择器,后续可变长参数列表为方法的参数列表
形式各有不同,但是本质上是做同样的事情:首先转化为objc_msgSend(...)消息发送的形式,然后通过 target、selector 定位到方法的IMP,最后调用IMP指向的函数指针。于是关键点来到了方法IMP定位上。从class_getMethodImplementation(Class cls, SEL sel)源代码可以知道 Runtime 如何通过方法名SEL定位 方法的实现IMP。代码如下:
IMP class_getMethodImplementation(Class cls, SEL sel)
{
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil,
YES/*initialize*/, YES/*cache*/, YES/*resolver*/);
// 继承链无该SEL对应的IMP,则从消息转发流程中搜索IMP
if (!imp) {
return _objc_msgForward;
}
return imp;
}
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
最后定位到关键代码lookUpImpOrForward(...),顾名思义是用于在类中查找IMP,或者进入转发。根据代码逻辑,总结方法根据方法名SEL定位方法的实现IMP的主要处理流程如下:
- 在类的方法缓冲中搜索方法,搜索到则立刻返回;
- 判断类是否已完成 class realizing,若未完成,则先进行 class realizing,因为在class realizing 完成之前,类的
class_rw_t中的方法列表都有可能是不完整的; - 若类未初始化,则先执行类的
initialize()方法,因为其中可能包含动态添加方法逻辑; - 再次在类的方法缓冲中搜索方法,搜索到则立刻返回,因为都可能触发方法缓冲更新;
- 遍历类的
method_array_t方法列表二维数组容器中的所有method_list_t,在method_list_t中搜索方法。若method_list_t已排序,则使用二分法搜索,若method_list_t未排序,则用简单线性搜索。搜索到则立刻返回; - 沿着继承链在父类中递归地搜索方法。搜索到则立刻返回;
- 尝试方法动态解析(在 3.1 中详细介绍),若返回
true,则回到第4步再次尝试搜索; - 若方法动态解析返回
false,则触发消息转发流程。返回_objc_msgForward_impcache,返回该IMP表示需要进入消息转发流程;
注意:
initialize()方法和load()方法的作用有点相似,都是用于类的初始化。但两者在行为上有很大区别:1、load()方法不具有继承特性,继承链上的所有类定义的所有load()方法均会被调用,load()方法中禁止使用super关键字;intialize()具有继承特性,可使用super关键字,其行为相当于普通的类方法。2、load()方法在类加载时触发,且 runtime 严格控制一个类的load()方法只会被执行一次;initialize()方法 在第一次调用类的方法时,在lookUpImpOrForward(...)中检查类是否初始化时触发,因此若父类实现了initialize()方法,但是其所有子类均未实现,则无论是第一次调用子类还是父类的方法,都会触发父类的initialize()方法。
lookUpImpOrForward(...)的源代码看起来很长,但是基本上每一步都包含很关键的信息,对理解消息的响应流程非常重要。因此只删除代码中读写锁、调试相关代码,在代码中做了详细的注释。
// Runtime预定义的统一的触发方法动态解析的SEL,注意实际值应该并不是NULL
SEL SEL_resolveInstanceMethod = NULL;
// Runtime预定义的标记进入消息转发流程的IMP
extern void _objc_msgForward_impcache(void);
// 从cls类的`cache_t`方法缓存哈希表中查询SEL对应的IMP
extern IMP cache_getImp(Class cls, SEL sel);
// 在cls类查询SEL对应的IMP
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
Class curClass;
IMP imp = nil;
Method meth;
bool triedResolver = NO;
// 在方法缓冲中搜索方法
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// 方法固定相关操作
if (!cls->isRealized()) {
realizeClass(cls);
}
// 若类的initialize()方法未调用,则先进行类的initialize初始化过程
if (initialize && !cls->isInitialized()) {
_class_initialize (_class_getNonMetaClass(cls, inst)); //调用类的initialize()方法
}
retry:
// 若ARC编译选项打开则需要忽略retain、release等方法,忽略
if (ignoreSelector(sel)) {
imp = _objc_ignored_method;
cache_fill(cls, sel, imp, inst);
goto done;
}
// 在类的cache_t方法缓存哈希表中查询SEL对应的IMP
imp = cache_getImp(cls, sel);
if (imp) goto done;
// 在类的method_array_t方法列表容器中查询SEL对应的IMP
meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
goto done;
}
// 沿着类的继承链循环。在各级类及父类的方法缓冲、方法列表容器中查询SEL对应的IMP
curClass = cls;
while ((curClass = curClass->superclass)) {
// 在类的方法缓存中查询
imp = cache_getImp(curClass, sel);
if (imp) {
if (imp != (IMP)_objc_msgForward_impcache) {
// 若搜索到IMP,则将IMP写入父类的方法缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
break;
}
}
// 在类的方法列表中查询
meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
// 尝试动态解析方法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
// 触发消息转发流程
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
done:
return imp;
}
// 当前类的方法列表中搜索方法
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
// 简单线性遍历method_list_t搜索方法名为sel的方法,查到立刻返回
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
//对于isFixedUp的方法列表用二分法搜索,这也是fixedUp时需要对方法列表排序的原因
return findMethodInSortedMethodList(sel, mlist);
} else {
// 线性搜索,简单遍历
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
return nil;
}
// 用二分查找从已排序的method_list_t中搜索方法名为key的方法
static method_t *findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
assert(list);
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
3.1 方法动态解析及消息转发
当类及其继承链的方法列表不存在SEL对应的方法时,会进入方法动态解析 以及 消息转发流程,Runtime 只公布了触发逻辑,实现细节则完全隐藏在objc_msgSend的实现中。触发方法动态解析通过objc_msgSend向类发送SEL_resolveInstanceMethod消息,推测objc_msgSend内部只是直接调用类的resolveInstanceMethod、resolveClassMethod方法查询SEL是否有动态解析,有则返回true反之返回false。
所谓动态方法解析实际上就是对不常用到的方法,在正式调用到该方法时才将方法添加到类的方法列表。因此,resolveInstanceMethod、resolveClassMethod中的代码一般是对需要动态解析的SEL,调用class_addMethod(...)动态添加该SEL对应的方法,并返回YES。如果只是返回YES的话,而不添加方法很可能造成的后果是responseToSelector虽然可以返回YES,但是performSelector时仍然抛出unrecognized selector异常。
方法动态解析相关代码如下。
// 动态解析方法
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// 动态解析实例方法
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// 动态解析类方法
_class_resolveClassMethod(cls, sel, inst);
// 这里为什么要给元类再动态解析实例方法
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
// 动态解析类方法
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
// 若类未实现resolveClassMethod方法,则不走方法动态解析流程
if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 触发方法动态解析流程
bool resolved = msg(_class_getNonMetaClass(cls, inst),
SEL_resolveClassMethod, sel);
// lookUpImpOrNil触发将动态解析的SEL与IMP的映射添加到类的方法缓存
// 这样下次调用SEL时可直接从cache_t中查询到IMP
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
}
}
// 动态解析实例方法
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
// 若类未实现resolveInstanceMethod方法,则不走方法动态解析流程
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
// 触发方法动态解析流程
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// lookUpImpOrNil触发将动态解析的SEL与IMP的映射添加到类的方法缓存
// 这样下次调用SEL时可直接从cache_t中查询到IMP
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
}
注意:完成动态解析后,又立即调用了
lookUpImpOrNil(...),其目的是将resolveInstanceMethod、resolveClassMethod方法实现代码中为响应SEL而动态添加的IMP加入到类的方法缓冲,则下一次调用可直接从方法缓冲中获取。
关于方法动态解析和消息转发流程的内部实现代码,Runtime 公开的源代码并不多。尤其是消息转发流程的实现,基本隐藏在objc_msgSend函数的内部实现中。但是网上有很多关于 Runtime 消息转发机制的博文,这里只贴出方法动态解析及消息转发流程的大致流程不再赘述。图中从左到右第一列是指,在类的继承链上存在响应方法;第二列是进入方法动态解析流程(method resolution);第三列是进入消息快速转发流程(fast forwarding);第四列是进入消息完整转发流程(normal forwarding)。
四、总结
-
class_ro_t的baseMethodList只保存了类的基本方法列表,只包含类定义时定义的方法,这些方法都是编译时决议的; 类的class_rw_t的methods保存了类的完整方法列表,除了包含基本方法列表外,还包含运行时决议的,类的分类中定义的方法以及动态添加的方法; -
class_rw_t的methods使用method_array_t二维数组容器保存,包含一个或多个method_list_t结构体,其中method_array_t的外层一位数组容器的最后一个元素为指向class_ro_t的baseMethodList的指针; -
类的 class realizing 阶段载入方法列表时,需要对所有方法列表根据
SEL进行排序,这是为了在搜索方法时能根据SEL进行二分法搜索提高方法搜索效率; -
调用
class_addMethod(...)添加方法时,将方法封装成method_list_t,并添加到class_rw_t的methods的开头,其他元素整体后移; -
类的方法缓冲缓存最近调用方法的
SEL和IMP的映射,是可动态扩容的 Key-Value 形式的哈希表,可通过方法SEL查找IMP,引入方法缓冲可提高方法搜索的效率; -
调用方法的本质是通过
objc_msgSend向 target 发送 selector 消息,target 响应消息时,先后在类的方法缓冲、类的方法列表、继承链上所有类的方法缓冲和方法列表中搜索方法IMP,若方法无响应,则触发方法动态解析过程,执行resolveInstanceMethod、resolveClassMethod中的方法动态解析逻辑,若方法动态解析过程无响应,则进入消息转发流程; -
下一篇介绍属性列表的实现。