把方法名字符串(如 "viewDidLoad")放进全局唯一表,得到一个唯一的 SEL。这常发生在Runtime 加载镜像时的类/元数据装载的阶段,也有可能是运行时,第一次用sel_registerName("foo:") / NSSelectorFromString 时,若尚未注册,会在此时插入唯一表并得到 SEL。这是一个用空间换时间的典型例子。
#import <objc/runtime.h>
#import <objc/message.h>
void testSelectorUniq(void) {
SEL s1 = sel_registerName("foo:");
SEL s2 = @selector(foo:);
SEL s3 = NSSelectorFromString(@"foo:");
NSLog(@"s1=%p s2=%p s3=%p equal12=%d equal23=%d",
s1, s2, s3, s1 == s2, s2 == s3);
}
//s1=0x106c9c040 s2=0x106c9c040 s3=0x106c9c040 equal12=1 equal23=1
你会看到同名 selector 通常是同一个 SEL(地址相同),这就是“注册/唯一化”,这一过程就是intern,把相同内容的字符串只保留一份,后面都复用这份对象,以及它的指针/地址。所以比较时可以比对指针,不必逐字符比对。
关于SEL的定义在 objc/runtime.h 里大致是:
typedef struct objc_selector *SEL;
SEL是指向objc_selector的指针,虽然objc_selector 的具体布局不对外公开(opaque)。但是仍然由此可看出,后期SEL之间的比对就不需要方法名称逐字符比较了,使用指针判等将大幅提高查找效率。
首次接触intern,你可能会问不同类的viewDidLoad的SEL是一样的吗?答案是是的,SEL 像方法名的“身份证号”,或者说intern操作是产生了一个存放(多个无重复字符串和指向这些字符串的指针)数据的表格,这个表格和方法的真正实现没有联系。举个真正使用SEL的例子:
objc_msgSend(receiver, sel, ...)
receiver 是对象实例,它的isa 指向它的类对象 Class
Class 里有 cache(快速路径):
Class (MyClass)
+------------------+
| cache_t | key: SEL -> value: IMP
| [SEL -> IMP] |
+------------------+
| method_list |
| superclass ----> |
+------------------+
如果cache里没有,去method list 里按 SEL 找
……
SEL的主要的功能就是从各种receiver中找到真正的方法实现。
这里的cache可以理解为hash表,method_list可以理解为数组,查找方法实现,前部分要么哈希要么遍历method_list,但最终都要进行判等,这就到了SEL的舒适区。除了intern,为了提高查找效率,运行时还会对很多类的方法表做指针排序,并打上 fixed-up 标记,这样的类就可以使用二分查找了,时间复杂度直接从O(n)减为O(log n)
为什么是 intern这个单词?
词源来自 internal(内部的)或 interned(被拘禁/关在内部),指把字符串“关进”一个内部统一管理的池子里,不再让副本到处乱跑。