之前对 NSObject 类内部结构体做了一个基本的分析。原本是想从 runtime 层面上整理消息传递流程,但为了能够顺畅的整理知识点,决定这篇还是先整理几个非常重要的结构体概念。
目录
1. selector
2. IMP
3. Method
1. selector
selector 是指方法选择器,在面向对象里可以理解为函数的指针。@selector()
作用就是在指定类中寻找指定名称的方法。
&emsp关于 selector 的用法,其返回类型为 SEL。关于 SEL 的定义,最权威的还是在官方文档中的解释。SEL官方文档链接
关于官方文档对于 SEL 的声明,翻译过来大意如下:selector 方法选择器用于在运行时表示方法的名称,一个 selector 选择器其实就是已经向运行时注册或者映射过的C字符串,通过编译器生成的 selector 选择器在类加载时由运行时自动映射。允许在运行时添加新的 selector 选择器,并可以使用函数 sel_registerName
检索已有的 selector 选择器。但是在使用 selector 选择器时,必须使用函数 sel_registerName
或者 Objective-C 编译器的指令 @selector()
返回的值,而不能直接将 C字符串强制转换成 SEL。
关于 SEL 在 runtime 中的定义,在 runtime 源码中仅仅是找到了结构体的声明。
typedef struct objc_selector *SEL;
虽然看不到关于 struct objc_selector
的内部声明,但是可以去推测内部结构。在结构体中,一定会有一个 char
类型的变量用于存储该函数名的C字符串。
关于 selector 创建与获取,不管是创建 @selector()
、还是获取 NSSelectorFromString()
与 method_getName()
,其底层的实现都是通过 sel_registerName
函数来实现的。
关于 sel_registerName() 函数的底层实现
从 runtime 源码 objc-sel.mm 文件中找到了其定义。
SEL sel_registerName(const char *name) {
return __sel_registerName(name, 1, 1); // YES lock, YES copy
}
内部通过C函数 static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
来完成实现。
static SEL __sel_registerName(const char *name, bool shouldLock, bool copy)
{
SEL result = 0;
if (shouldLock) selLock.assertUnlocked();
else selLock.assertLocked();
if (!name) return (SEL)0;
result = search_builtins(name);
if (result) return result;
conditional_mutex_locker_t lock(selLock, shouldLock);
if (namedSelectors) {
result = (SEL)NXMapGet(namedSelectors, name);
}
if (result) return result;
// No match. Insert.
if (!namedSelectors) {
namedSelectors = NXCreateMapTable(NXStrValueMapPrototype,
(unsigned)SelrefCount);
}
if (!result) {
result = sel_alloc(name, copy);
// fixme choose a better container (hash not map for starters)
NXMapInsert(namedSelectors, sel_getName(result), result);
}
return result;
}
上述函数中,result
SEL 类型的变量就是最终返回的结果。从源码中初步看了下,会发生四种不同的 SEL 类型结果返回情况。从上往下的顺序依次是:
- 当传入方法名为 nil 时,则直接返回内容为0的值;
- 再传入的方法名与 builtins 中的进行比对,若存在相同方法名,则直接返回 builtins 中的方法名。
(PS:此处的 builtins 作用为生成一个共享缓存,用于保存预先优化过的选择器,以此可以实现更快速地查找方法,该函数的实现是由 C++ 定义的命名空间 objc_opt 来完成。关于 builtins 的实现原理就不展开了,以后有时间再细细研究 C++ 的命名空间以及 objc_opt 的内部细节。) - 若上述流程未找到,则将传入的方法名作为 key,去 NXMapTable 中去搜索 SEL 类型的结果。 NXMapTable 的作用就是将方法名与对应的 SEL 字符串进行绑定映射,并存入该哈希表中。
- 若上述哈希表依然没有找到,则会将当前的方法名创建新的 SEL,并将 SEL 插入至 NXMapTable 中保存与对应方法名的映射关系。同时将该方法名创建的 SEL 作为返回值返回。
创建 selector 途径有:
sel_registerName
@selector()
获取 selector 的途径有:
NSSelectorFromString()
method_getName()
通过官方文档对 NSSelectorFromString
的解释,将一个方法名的UTF-8编码字符串传给 sel_registerName
函数并返回 SEL;关于 method_getName()
函数的实现,通过 runtime 源码层面也可以发现也是通过 sel_registerName
来完成;而编译器指令 @selector()
因此,关于 selector 的简要总结:
- selector 返回的类型为 SEL;
- SEL 是指向 objc_selector 结构体的指针;
- objc_selector 虽然并没有公开结构体的实现,但其内部至少存在一个保存 selector 名字的字符串变量;
- 关于 selector 的创建,若与共享缓存、NXMapTable映射表中的都未注册,则创建一个新的 SEL 并插入至 NXMapTable 中,同时保存于方法名的映射关系。
2. IMP
IMP 表示指向方法实现地址的指针,当发起 Objective-C 消息后,最终要执行的代码就是由 IMP 指针来决定,SEL 的目的是为了查找方法最终实现的 IMP。若通过获取到实例对象指定方法的 IMP 并直接调用,则可以绕过消息传递流程,直接执行 IMP 对应的方法,这样可以提升访问效率。但也就意味着编译器并不会检查直接通过 IMP 去执行指定的方法,编译时期编译器并不能判断是否调用 IMP 错误,只有在运行时执行到 IMP 指向的方法实现时,才能判断是否正确。
关于 IMP 的定义
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
第一个参数传入一个指向 self 指针(指定类生成的实例对象的内存,或者类方法时指向元类的指针),第二个参数传入方法选择器,后续参数为可配置参数。
调用 IMP 的方式在默认生成的项目工程下,调用编译器获取 IMP 会直接报错,项目配置中默认为下图配置:
这样的话,IMP 被定义为无参数无返回类型的函数,关闭即可。还有更高效的方法,就是重新定义一个和有参数的 IMP 指针相同类型的指针,并把获取到 IMP 时将其强转为该类型。
3. Method
Method 结构体定义 typedef struct method_t *Method;
,顺藤摸瓜去查看 method_t
的结构体内容。
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
struct SortBySELAddress :
public std::binary_function
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
结构体中,有关键作用的成员变量包含 SEL name;
方法名、const char *types;
返回类型的 encode 码以及 MethodListIMP imp;
方法地址的指针。
关于 Method 的存储位置,在runtime的那些事(二)——NSObject数据结构文章中已经有过说明,在编译时存放于 objc_class
-> class_data_bits_t bits
-> class_ro_t
-> method_arrary_t *baseMethodList
中,而到了运行时 Method 会再存放于 objc_class
-> class_data_bits_t bits
-> class_rw_t
-> method_arrary_t methods
中。
关于 Method 的初始化,是在 static Class realizeClass(Class cls)
函数中完成的,runtime的那些事(二)——NSObject数据结构也针对该函数做了源码层面的分析,这里不再进行说明。
在 Objective-C 语言中,允许我们通过 BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
函数在运行时动态加载新的 Method 方法。
BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return NO;
mutex_locker_t lock(runtimeLock);
return ! addMethod(cls, name, imp, types ?: "", NO);
}
static IMP
addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
{
IMP result = nil;
runtimeLock.assertLocked();
checkIsKnownClass(cls);
assert(types);
assert(cls->isRealized());
method_t *m;
if ((m = getMethodNoSuper_nolock(cls, name))) {
// already exists
if (!replace) {
result = m->imp;
} else {
result = _method_setImplementation(cls, m, imp);
}
} else {
// fixme optimize
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 = strdupIfMutable(types);
newlist->first.imp = imp;
prepareMethodLists(cls, &newlist, 1, NO, NO);
cls->data()->methods.attachLists(&newlist, 1);
flushCaches(cls);
result = nil;
}
return result;
}
在向 Class 添加 Method 时,判断要添加的 Method 是否已存在。若存在相同的 SEL 方法名,根据 BOOL 类型变量 replace 判断,若为 NO,则从已有的 Method 中取出 IMP 并返回;若为 YES则会将新的 IMP 与 对应的 SEL 方法名进行映射绑定。当 Class 中不存在指定的 SEL 方法名,则会向 Class 结构体中 class_rw_t
下的 method_array_t *methods
列表中注册添加新的 Method ,添加完成后当前 Class 类的内存地址发生变化,必须清除 Class 类以及子类的 bucket 缓存。
此篇文章,先对 selector、IMP、Method 的概念做一次整理,下一篇文章会尝试从 runtime 源码上研究下消息传递的完整流程。