MST

星途 面试题库

面试题:Objective-C中SEL类型与@selector的底层原理及优化策略

深入探讨Objective-C中SEL类型和@selector的底层实现原理。例如,SEL对象在运行时是如何存储和查找的,@selector宏在编译期和运行期分别做了哪些操作。基于这些底层原理,提出一些优化SEL和@selector使用的策略,尤其是在性能敏感的应用场景中,如何减少查找SEL对象的开销以及避免不必要的动态方法解析。并结合LLVM编译器和Objective-C运行时库的源码分析,阐述你的优化策略的可行性。
36.7万 热度难度
编程语言Objective-C

知识考点

AI 面试

面试题答案

一键面试

SEL类型底层实现原理

  1. 存储
    • 在Objective - C运行时,SEL本质是一个指向const char *类型的指针。每个方法在编译时,编译器会为其生成一个唯一的SEL。这些SEL被存储在一个哈希表中,这个哈希表在运行时被加载到内存。例如,在objc - runtime - new.h中,objc_class结构体中有一个method_list_t类型的成员用于存储方法列表,而每个方法都关联一个SEL。
    • 这种哈希表的存储方式使得在运行时通过SEL查找方法相对高效,因为哈希表的查找时间复杂度接近O(1)。
  2. 查找
    • 当消息发送机制在运行时查找方法实现时,首先根据对象的类找到对应的objc_class结构体。
    • 然后在objc_class的方法列表中通过SEL查找对应的method_t结构体,method_t结构体中包含了方法的具体实现地址。如果在当前类中未找到,会沿着继承体系向父类查找,直到找到方法或者到达根类(NSObject)。例如,在objc - runtime - new.mm中的lookUpImpOrForward函数实现了这个查找过程。

@selector宏底层实现原理

  1. 编译期
    • @selector宏在编译期会将方法名转换为一个唯一的SEL。编译器会检查方法名的拼写是否正确以及该方法是否在类的接口中声明。如果方法名拼写错误或者未声明,编译器会报错。例如,假设有一个类MyClass@selector(methodInMyClass)在编译期会确保methodInMyClass方法在MyClass类的接口或者协议中有声明。
  2. 运行期
    • 在运行期,@selector生成的SEL被用于消息发送机制。运行时系统通过SEL在类的方法列表中查找对应的方法实现。由于SEL在编译期就已经确定,在运行时不需要再进行方法名到SEL的转换,提高了消息发送的效率。

优化SEL和@selector使用的策略

  1. 减少查找SEL对象的开销
    • 缓存SEL:在性能敏感的应用场景中,可以在类加载时缓存常用的SEL。例如,在一个频繁调用特定方法的类中,可以在+load方法中缓存@selector,这样在后续调用时就不需要每次都生成SEL。
    • 使用类簇:对于具有相似功能的一组类,可以使用类簇模式。通过在基类中缓存SEL,子类复用这些SEL,减少重复查找。例如,NSArray类簇中,不同的具体子类可以复用基类中与常见数组操作相关的SEL。
  2. 避免不必要的动态方法解析
    • 确保方法实现存在:在编译期确保所有可能被调用的方法都有实现,避免在运行时触发动态方法解析。可以通过静态分析工具检查未实现的方法。
    • 提前绑定方法:使用NSObjectperformSelector:withObject:afterDelay:方法时,如果知道会频繁调用某个方法,可以提前使用performSelector:withObject:方法绑定一次,避免后续每次都触发动态解析。

基于LLVM编译器和Objective - C运行时库源码分析优化策略的可行性

  1. 缓存SEL可行性
    • 从LLVM编译器角度,@selector在编译期生成唯一SEL,这为缓存SEL提供了基础。从运行时库源码看,objc_class的方法列表结构使得缓存SEL后可以直接通过SEL快速查找方法实现。例如,在objc - runtime - new.mm中,lookUpImpOrForward函数的实现依赖于SEL查找,缓存SEL可以减少该函数的查找次数,从而提高性能。
  2. 使用类簇可行性
    • LLVM编译器在处理类簇相关代码时,对基类和子类的处理机制是统一的,允许在基类缓存SEL供子类使用。在运行时库中,类的继承体系和方法查找机制支持类簇模式下SEL的复用,因为查找方法时会沿着继承体系进行,子类可以找到基类缓存的SEL对应的方法实现。
  3. 确保方法实现存在可行性
    • LLVM编译器会在编译期检查方法声明,如果确保所有调用的方法都有声明且实现,就可以避免运行时的动态方法解析。从运行时库源码看,动态方法解析相关函数(如resolveInstanceMethod:等)只有在方法未找到时才会被调用,所以确保方法实现存在可以绕过这些性能开销较大的动态解析流程。
  4. 提前绑定方法可行性
    • LLVM编译器对performSelector系列方法的处理机制允许提前绑定。从运行时库源码角度,performSelector:withObject:方法在调用时会查找方法实现并缓存,后续再调用performSelector:withObject:afterDelay:时,如果方法已缓存,就不会触发动态解析,从而提高性能。