SEL:从字符串到指针的intern之旅

0 阅读2分钟

把方法名字符串(如 "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(被拘禁/关在内部),指把字符串“关进”一个内部统一管理的池子里,不再让副本到处乱跑。