runtime的那些事(四)—— selector、IMP、Method

之前对 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 类型结果返回情况。从上往下的顺序依次是:

  1. 当传入方法名为 nil 时,则直接返回内容为0的值;
  2. 再传入的方法名与 builtins 中的进行比对,若存在相同方法名,则直接返回 builtins 中的方法名。
    (PS:此处的 builtins 作用为生成一个共享缓存,用于保存预先优化过的选择器,以此可以实现更快速地查找方法,该函数的实现是由 C++ 定义的命名空间 objc_opt 来完成。关于 builtins 的实现原理就不展开了,以后有时间再细细研究 C++ 的命名空间以及 objc_opt 的内部细节。)
  3. 若上述流程未找到,则将传入的方法名作为 key,去 NXMapTable 中去搜索 SEL 类型的结果。 NXMapTable 的作用就是将方法名与对应的 SEL 字符串进行绑定映射,并存入该哈希表中。
  4. 若上述哈希表依然没有找到,则会将当前的方法名创建新的 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 源码上研究下消息传递的完整流程。

你可能感兴趣的:(runtime的那些事(四)—— selector、IMP、Method)