正在加载今日诗词....
10 min read

[源码阅读] Objective-C SEL 选择子

源码阅读 - Objective-C 中的 SEL 1. SEL 是什么 2. SEL 如何注册与获取的 3. SEL 的系统优化
[源码阅读] Objective-C SEL 选择子

简单认为 SEL 是 c语言 字符串是不够的, iOS对其有自己的管理系统

SEL 认识

查看 文档 介绍 sel- document

SEL
Defines an opaque type that represents a method selector.

Declaration
typedef struct objc_selector *SEL;


Discussion
Method selectors are used to represent the name of a method at runtime. A method selector is a C string that has been registered (or “mapped“) with the Objective-C runtime. Selectors generated by the compiler are automatically mapped by the runtime when the class is loaded.

You can add new selectors at runtime and retrieve existing selectors using the function sel_registerName.

When using selectors, you must use the value returned from sel_registerName or the Objective-C compiler directive @selector(). You cannot simply cast a C string to SEL.

简单来说 定义的一个不透明指针, 指向 objc_selector 的方法选择子.
关于讨论部分: 方法选择子能够表述,运行时一个方法的名称. OC 在运行时会将 c 语言字符串注册或者映射为 一个选择子. 运行时加载每个类或者分类的时候,将会把字符串与选择子自动映射.

开发者可以使用 sel_registerName 去注册一个选择子或者获取已经注册的选择子. 开发时只能使用 sel_registerName, @selector()NSSelectorFromString 来获取选择子, 直接强转字符串为 SEL 是不被允许的.

比如:

void testSEL(void){
    // unsafe way crash
    const char *name = "new";
    SEL sel = (SEL)name;
    //  SEL sel = @selector(new); 正确的方式
    NSObject *obj = [NSObject performSelector:sel];
    NSLog(@"obj:%@",obj);
}

上面这段代码会直接 doesNotRecognizeSelector 崩溃掉 , 虽然可以转换成 SEL, 但是这个 SEL 并没有被注册到运行时系统的存储结构中, 比如 后面要讲到的 共享缓存 buildins 或者 MXMapTable . 而 使用 SEL sel = @selector(new); 则正常运行.

源码中使用 BOOL sel_isMapped(SEL sel) 可以来检查 SEL 是否有效.

查看底层代码的一种方式(仅供参考,并不是系统的真正实现方式, 系统实际调用会更复杂,且iOS编译会有优化), 将一段 OC 方法调用, 用 clang -rewrite 命令转换成 c++ 代码, 实际上调用方法, 会转换成

    MNJDog *dog = ((MNJDog *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MNJDog"), sel_registerName("new"));

sel_registerName("new")

查找相关源码 objc-sel.mm 可以找到

SEL sel_registerName(const char *name) {
    // 内部使用 mutex_t 互斥锁保证安全
    return __sel_registerName(name, 1, 1);     // YES lock, YES copy
}

sel_registerName 方法会返回一个 SEL, 也就上文档中定义的 objc_selector 指针.

同样的 API 也是会生成 SEL 指针

FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);

OC 的所有运行时方法都离不开 SEL 这个参数.

源码阅读

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;

    // (应用间)共享缓存中查找已经c注册的选择子, 已经存在,直接返回, 因为很多系统的调用都是重复使用的, 不需要每个应用创建一份映射表
    result = search_builtins(name);
    if (result) return result;
    
    // 使用条件锁, 分类加载时 注册不需要
    conditional_mutex_locker_t lock(selLock, shouldLock);
    // 本应用内部的全局映射表 NXMapTable
    if (namedSelectors) {
        result = (SEL)NXMapGet(namedSelectors, name);
    }
    if (result) return result;

    // No match. Insert. 查询不到, 则说明没有映射, 需要新增一条记录, 因为时本应用内的选择子, 所以使用 全局的 NXMapTable 映射处理
    if (!namedSelectors) {
        namedSelectors = NXCreateMapTable(NXStrValueMapPrototype, 
                                          (unsigned)SelrefCount);
    }
    if (!result) {
        // 这里给新的SEL分配内存, 注册的时候
        result = sel_alloc(name, copy);
        // fixme choose a better container (hash not map for starters)
        NXMapInsert(namedSelectors, sel_getName(result), result);
    }

    return result;
}

iOS 中管理选择的存储结构有两个

  1. static const objc_selopt_t *builtins = NULL; 共享缓存 builtins
  2. static NXMapTable *namedSelectors; 应用内全局的 Map

因为系统 sdk 的方法选择子都是共享库中的, 所以不需要重复注册, 使用 builtins 来进行应用间共享即可.

// builtins 作用为生成一个共享缓存,用于保存预先优化过的选择器,以此可以实现更快速地查找方法,该函数的实现是由 C++ 定义的命名空间 objc_opt 来完成
static SEL search_builtins(const char *name) 
{
#if SUPPORT_PREOPT
    if (builtins) return (SEL)builtins->get(name);
#endif
    return nil;
}

// Precomputed selector table.
// Edit objc-sel-table.s if you change this structure.
struct objc_selopt_t : objc_stringhash_t { 
    const char *get(const char *key) const 
    {
        uint32_t h = getIndex(key);
        if (h == INDEX_NOT_FOUND) return NULL;
        
        return (const char *)this + offsets()[h];
    }
};

底层代码在 objc-shared-cache.h文件中查找, 存储也是通过一个 hash 哈希表处理的.预处理的看来没有多线程问题.

而开发者自建应用内很多自己声明的 选择子,则单独处理, 存储在 NXMapTable 中.

NXMapTable *NXCreateMapTableFromZone(NXMapTablePrototype prototype, unsigned capacity, void *z) {
    // 分配 NXMapTable 结构体
    NXMapTable			*table = (NXMapTable *)malloc_zone_malloc((malloc_zone_t *)z, sizeof(NXMapTable));
    // 创建 proto 并赋值给 table结构体 成员变量
    NXMapTablePrototype		*proto;
    if (! prototypes) prototypes = NXCreateHashTable(protoPrototype, 0, NULL);
    if (! prototype.hash || ! prototype.isEqual || ! prototype.free || prototype.style) {
	_objc_inform("*** NXCreateMapTable: invalid creation parameters\n");
	return NULL;
    }
    proto = (NXMapTablePrototype *)NXHashGet(prototypes, &prototype); 
    if (! proto) {
	proto = (NXMapTablePrototype *)malloc(sizeof(NXMapTablePrototype));
	*proto = prototype;
    	(void)NXHashInsert(prototypes, proto);
    }
    // 赋值
    table->prototype = proto; table->count = 0;
    /// 科学记数都用上了
    table->nbBucketsMinusOne = exp2u(log2u(capacity)+1) - 1;
    // hash数组的内存分配, 并给 内部元素 MapPair 初始化赋值, 比如key 为NX_MAPNOTAKEY value 为 NULL
    table->buckets = allocBuckets(z, table->nbBucketsMinusOne + 1);
    return table;
}

对于 NXMapTable 比较有意思的, 一个是 proto 的作用, 一个是 hash 算法的实现

typedef struct _NXMapTable {
    /* private data structure; may change */
    const struct _NXMapTablePrototype	* _Nonnull prototype;
    unsigned	count;
    unsigned	nbBucketsMinusOne;
    void	* _Nullable buckets;
} NXMapTable OBJC_MAP_AVAILABILITY;

typedef struct _NXMapTablePrototype {
    unsigned	(* _Nonnull hash)(NXMapTable * _Nonnull,
                                  const void * _Nullable key);
    int		(* _Nonnull isEqual)(NXMapTable * _Nonnull,
                                     const void * _Nullable key1,
                                     const void * _Nullable key2);
    void	(* _Nonnull free)(NXMapTable * _Nonnull,
                                  void * _Nullable key,
                                  void * _Nullable value);
    int		style; /* reserved for future expansion; currently 0 */
} NXMapTablePrototype OBJC_MAP_AVAILABILITY;

对于 c 语言的结构体来说, 我们知道结构体不能存储 函数, 那么这里的解决方式是 使用 _NXMapTablePrototype 结构体的指针成员变量 来记录 这些函数指针.
这里有 hash - isEqual - free 三个函数指针成员. 对应下面的全局 NXMapTablePrototype 结构体有两种

const NXMapTablePrototype NXPtrValueMapPrototype = {
    _mapPtrHash, _mapPtrIsEqual, _mapNoFree, 0
};

const NXMapTablePrototype NXStrValueMapPrototype = {
    _mapStrHash, _mapStrIsEqual, _mapNoFree, 0
};

上面创建应用内 NXMapTable 使用的是 NXStrValueMapPrototype ,而使用 NXPtrValueMapPrototype 的 运行时类加载时会使用到, 比如 static NXMapTable *remappedClasses(bool create) , static NXMapTable *unattachedCategories(void) 这个暂时不在本文的讨论范围.

NXStrValueMapPrototype 中记录了三个函数的指针. 比较关心的一个是, 字符串的 hash 计算方式 _mapStrHash 对应源码在 maptable.mm

static unsigned _mapStrHash(NXMapTable *table, const void *key) {
    // 最终返回值 无符号 防止溢出问题 unsigned(-1) 0xFFFFFFFF  size: 4字节 : 4294967295
    unsigned		hash = 0;
    unsigned char	*s = (unsigned char *)key;
    /* unsigned to avoid a sign-extend */
    /* unroll the loop */
    
    // 分别按照unsigned 的4个字节为单位进行异或处理 <<0 第一个字节 <<8 第二个字节 << 16 第三个 <<24 第四个
    // 这里是可能是 将hash unsigned 的 4个字节 二进制位 都有同样个数的字符参与计算, 这样生成的hashcode 冲突会低一些
    // 比如 字符串 "abcd", 那么相当于 4个字节, 一个Char 占用一个字节 , 而 "abcdefgh" -> "a^e", "b^f" , "c^g", "d^h" 这些值分别占用一个字节
    if (s) for (; ; ) {
	if (*s == '\0') break;
	hash ^= *s++;
	if (*s == '\0') break;
	hash ^= *s++ << 8;
	if (*s == '\0') break;
	hash ^= *s++ << 16; // 举例子 : hash ^= *s++ << 16 -> value = (*s++) << 16 , 然后 hash ^= value;
	if (*s == '\0') break;
	hash ^= *s++ << 24;
    }
    
    // xorHash 是一个非常有效的 hash , 查看参考文档 http://www.iotshare.org/archives/137.html 对于字符串hash函数的介绍
    return xorHash(hash);
}

/**
 这个函数叫“扰动函数”。
 XOR 运算有一个很奇妙的特点:如果对一个值连续做两次 XOR,会返回这个值本身
 1111 1111 1111 0001 二进制 也就是2个字节
 65521 十进制
 fff1 十六禁止
 */
static INLINE unsigned xorHash(unsigned hash) { 
    unsigned xored = (hash & 0xffff) ^ (hash >> 16);// 右移两个字节 相等于只取 unsigned 的一半高位
    return ((xored * 65521) + hash);
}

// 占位的函数啊
static void _mapNoFree(NXMapTable *table, void *key, void *value) {}

通过上面的哈希计算就能产生一个固定字节大小的 无符号整数. 然后转换成索引, 存储和查询字符串就方便很多, 直接 O(1) 就可以. 具体的查找 SEL 的 函数

__sel_registerName -> NXMapGet ->  _NXMapMember
void *NXMapGet(NXMapTable *table, const void *key) {
    void	*value;
    // 初始化 pair的 节点key 是 NX_MAPNOTAKEY , 代表未被占用的 pair
    return (_NXMapMember(table, key, &value) != NX_MAPNOTAKEY) ? value : NULL;
}

static INLINE void *_NXMapMember(NXMapTable *table, const void *key, void **value) {
    // 获取键值对
    MapPair	*pairs = (MapPair *)table->buckets;
    // hash出索引
    unsigned	index = bucketOf(table, key);
    // 找到索引所在 pair 的位置
    MapPair	*pair = pairs + index;
    // 无效key
    if (pair->key == NX_MAPNOTAKEY) return NX_MAPNOTAKEY;
    validateKey(table, pair, index, index);
    // 已存在的是不是和当前的key 一致
    if (isEqual(table, pair->key, key)) {
    // 奇怪 value没用到啊
	*value = (void *)pair->value;
    // 存在即返回, 这里应该和buildins的区别是 一个系统共享的, 一个是应用内的
	return (void *)pair->key;
    } else {
	unsigned	index2 = index;
    // 解决哈希冲突的方式, 开放寻址法 
	while ((index2 = nextIndex(table, index2)) != index) {
	    pair = pairs + index2;
	    if (pair->key == NX_MAPNOTAKEY) return NX_MAPNOTAKEY;
	    validateKey(table, pair, index, index2);
	    if (isEqual(table, pair->key, key)) {
	    	*value = (void *)pair->value;
		return (void *)pair->key;
	    }
	}
	return NX_MAPNOTAKEY;
    }
}

其他两个函数实现

static int _mapStrIsEqual(NXMapTable *table, const void *key1, const void *key2) {
// 地址相同
    if (key1 == key2) return YES;
    // 当其中一个为空的时候, 返回另一个的字符数
    if (! key1) return ! strlen ((char *) key2);
    if (! key2) return ! strlen ((char *) key1);
    // 优化判断 大部分字符串的首字母都不一样, 这样提效很多
    if (((char *) key1)[0] != ((char *) key2)[0]) return NO;
    // 最后才去进行字符串的比较 O(n) 
    return (strcmp((char *) key1, (char *) key2)) ? NO : YES;
}
    
static void _mapNoFree(NXMapTable *table, void *key, void *value) {}
  • 查看是否 SEL 被 map 了 , 这里相当于检测有效的 SEL , 比如无效的包括 未注册的 SEL 和用字符串强转的 SEL
BOOL sel_isMapped(SEL sel) 
{
    // 指针判空
    if (!sel) return NO;
    // 转字符串
    const char *name = (const char *)(void *)sel;
    // 查询共享缓存
    if (sel == search_builtins(name)) return YES;
    // 加锁查看 应用内的 NXMapTable
    mutex_locker_t lock(selLock);
    if (namedSelectors) {
        return (sel == (SEL)NXMapGet(namedSelectors, name));
    }
    return false;
}

应用

开发中, 关于 SEL 用到的地方好像不是很多的样子哈哈

  • 字符串转 SEL

使用方式
@selector(new)
NSSelectorFromString
__sel_registerName (基本开发不用的一个)

  • KVO 中经常利用 KVC 语法来监听属性变化

建议使用NSStringFromSelector(@selector(isFinished)) 这种语法避免硬编码的问题. key-value-observing nshipster

  • SEL 中的参数类型编码等 type-encodings

  • performSelector: 的内存泄露问题

#define SuppressPerformSelectorLeakWarning(Stuff) \
    do { \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \
        Stuff; \
        _Pragma("clang diagnostic pop") \
    } while (0)

示例:

id result;
SuppressPerformSelectorLeakWarning(
    result = [_target performSelector:_action withObject:self]
);

摘自 performselector-may-cause-a-leak-because-its-selector-is-unknown

  • @selector 未定义问题
#define SUPPRESS_UNDECLARED_SELECTOR_LEAK_WARNING(Stuff) \
    do { \
        _Pragma("clang diagnostic push") \
        _Pragma("clang diagnostic ignored \"-Wundeclared-selector\"") \
        Stuff; \
        _Pragma("clang diagnostic pop") \
    } while (0)

示例

id result;
SUPPRESS_UNDECLARED_SELECTOR_LEAK_WARNING(
    result = [_target performSelector:_action withObject:self]
);
  • 运行时针对特定方法白名单处理, 进行打桩记录等功能