前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >runtime的那些事(四)—— selector、IMP、Method

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

作者头像
我只不过是出来写写代码
发布2019-07-19 10:53:30
2K0
发布2019-07-19 10:53:30
举报

之前对 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<const method_t&,
                                    const method_t&, bool>
    {
        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 源码上研究下消息传递的完整流程。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019.07.18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
    • 1. selector
      • 2. IMP
        • 3. Method
          • 1. selector
            • 关于 sel_registerName() 函数的底层实现
          • 2. IMP
            • 3. Method
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档