问题: 简述一下method的内部结构.
注: 本问题回答基于
objc4-818.2版本
类/对象方法底层存储的结构体是 method_t
, method_t主要含有三个成员变量.
- SEL 类型的 name;
- char * 类型的 type;
- IMP 类型的 imp;
在 818.2版本中有 big small 之分,big就是先前传统意义上的三个成员变量.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
small 则是主要包含了三个偏移指针.个人猜想是用来压缩内存空间的.
struct small {
RelativePointer<const void *> name;
RelativePointer<const char *> types;
RelativePointer<IMP> imp;
...
};
但是对外的表现是一样的,不管是 name, type 还是 imp 现在的实现是通过 isSmall()
判断是 big存储结构 还是 small存储结构来进行不同的取值.
SEL name() const {
if (isSmall()) {
return (small().inSharedCache()
? (SEL)small().name.get()
: *(SEL *)small().name.get());
} else {
return big().name;
}
}
const char *types() const {
return isSmall() ? small().types.get() : big().types;
}
IMP imp(bool needsLock) const {
if (isSmall()) {
IMP imp = remappedImp(needsLock);
if (!imp)
imp = ptrauth_sign_unauthenticated(small().imp.get(),
ptrauth_key_function_pointer, 0);
return imp;
}
return big().imp;
}
不同类中相同方法名称所对应的 @selector(xxx)
相同且唯一.
tpye类型是通过 @encode
进行字符串编码生成的, 包含返回值类型与参数类型.
问题: 简述一下方法缓存的内部实现原理.
method的缓存是通过 散列表
来进行实现的. 其有C++对象 cache_t
来进行相关实现.
方法缓存的过程,主要由以下几个步骤完成.
- 查找到方法之后, 调用
cache_t
对象中得insert
方法进行方法的插入.
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
...
}
insert
第一步就是缓存容量的处理.
- 如果没有创建过缓存容器,则创建一个长度为2的容器,并且清除掉原来缓存的缓存方法.
- 如果容量超过 3/4, 那么进行 2 倍的扩容处理,并且清除掉原来缓存的缓存方法.
- 如果上面两者都不满足,则不进行扩容处理.
mask_t newOccupied = occupied() + 1;
unsigned oldCapacity = capacity(), capacity = oldCapacity;
if (slowpath(isConstantEmptyCache())) {
// Cache is read-only. Replace it.
if (!capacity) capacity = INIT_CACHE_SIZE;
reallocate(oldCapacity, capacity, /* freeOld */false);
}
else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
// Cache is less than 3/4 or 7/8 full. Use it as-is.
}
else {
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
if (capacity > MAX_CACHE_SIZE) {
capacity = MAX_CACHE_SIZE;
}
reallocate(oldCapacity, capacity, true);
}
- 通过 SEL 与 掩码mask 的按位与运算生成一个起始索引值.
mask_t m = capacity - 1; // 掩码mask实际 为 (缓存容量长度 -1)
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
value ^= value >> 7;
#endif
return (mask_t)(value & mask);
}
- 通过 索引查找空位置,并插入方法缓存.
- 如果当前索引内存中已经缓存其他方法,那么通过 cache_next 方法进行
索引-1
,并且继续查找. - 如果索引为0,那么则置为
mask掩码
,并且继续查找空位置准备插入方法缓存. - 如果先前已经插入过该方法缓存,则直接返回停止.
do {
if (fastpath(b[i].sel() == 0)) {
incrementOccupied();
b[i].set<Atomic, Encoded>(b, sel, imp, cls());
return;
}
if (b[i].sel() == sel) {
return;
}
} while (fastpath((i = cache_next(i, m)) != begin));
索引变更方法如下所示.
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;
}
#else
问题: 简述一下 objc_msgSend 的底层调用过程.
问题: 简述一下 iOS消息机制 的底层调用过程.
objc_msgSend 整体分为三大步.
- 消息发送
- 判断
receiver
是否为nil,如果为空,直接return. - 通过
ISA指针
找到类对象或者元类对象. - 查找当前 类对象或者元类对象 的cache成员变量,内部是通过
@seletor(xxx)
与mask掩码进行按位与
操作查找. - 如果整个散列表cache查找不到,那么就会去 class_rw_t 中查找 class_rw_ext_t成员变量中得
method_array_t
的 methodList,如果methodList已经进行了排序,那么就进行二分查找,如果没有进行排序,那么就普通遍历查找. - 如果当前类查找不到,那么就通过
superclass
指针往上查找父类,依次往复. - 如果查找完成,那么就调用该method_t中得 IMP 指针指向的函数体.并且将该方法函数通过与cache中mask掩码按位与(&)操作缓存到 cache中.
- 动态方法解析
- 如果在消息发送阶段未找到方法函数,那么就会调用
resolveClassMethod
和resolveInstancMethod
进入动态方法解析阶段.
PS: 类方法的动态方法解析会先调用
resolveClassMethod
,如果发现没找到方法,还会调用一次resolveInstancMethod
.
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
...
// 根据是类对象还是元类对象来执行不同的流程.
if (! cls->isMetaClass()) {
resolveInstanceMethod(inst, sel, cls);
}
else {
// 尝试调用元类对象动态方法解析
resolveClassMethod(inst, sel, cls);
// 判断是否已经存在,没有则会调用实例方法动态解析.
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
...
}
- 在动态方法解析阶段,我们可以
class_addMethod
函数动态添加其IMP函数实现与Type字符串编码. - 不管是否 动态添加完IMP函数实现与Type字符串编码,都会再次进入
消息发送阶段
查找方法.
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
...
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
- 当再次进入消息发送阶段,会根据
behavior
标志位阻止再次进入动态方法解析阶段.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
- 如果过动态方法解析过程没有任何操作,则会进
消息转发阶段
.
- 消息转发
- 底层的
__forwarding__
函数中会调用forwardingTargetForSelector:
. - 可以在
forwardingTargetForSelector:
指定消息接受者. - 如果没有指定消息接受者,那么会调用
methodSignatureForSelector:
,可在其中设置方法签名
,也就是字符串编码形成的types. - 如果设置了方法签名,则会调用
forwardInvocation:
,参数为NSInvocation
类型,包含方法调用者、签名、方法名. - NSInvocation 可以通过
invoke
调用函数. - 如果
forwardingTargetForSelector:
和methodSignatureForSelector:
都 没有指定,导致最终消息发送失败,触发doesRegisterSelector
那么则会爆unrecognized selector sent to instance ....
错误.
问题: super 关键字调用方法 和 self 关键字调用方法有什么不同?
super 关键字调用方法只是查找方法的位置从父类开始,但是消息的接受者仍然是当前类.
Comments | 0 条评论