[源码阅读] Objective-C SEL 选择子
源码阅读 - Objective-C 中的 SEL 1. SEL 是什么 2. SEL 如何注册与获取的 3. 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 中管理选择的存储结构有两个
static const objc_selopt_t *builtins = NULL;
共享缓存builtins
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]
);
- 运行时针对特定方法白名单处理, 进行打桩记录等功能