问题: 简述一下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 来进行相关实现.

方法缓存的过程,主要由以下几个步骤完成.

  1. 查找到方法之后, 调用 cache_t 对象中得 insert 方法进行方法的插入.
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    ...
}
  1. 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);
    }
  1. 通过 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);
}
  1. 通过 索引查找空位置,并插入方法缓存.
  • 如果当前索引内存中已经缓存其他方法,那么通过 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 整体分为三大步.

  1. 消息发送
  • 判断 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中.

  1. 动态方法解析
  • 如果在消息发送阶段未找到方法函数,那么就会调用 resolveClassMethodresolveInstancMethod 进入动态方法解析阶段.

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);
    }
  • 如果过动态方法解析过程没有任何操作,则会进 消息转发阶段.

  1. 消息转发
  • 底层的 __forwarding__ 函数中会调用 forwardingTargetForSelector:.
  • 可以在 forwardingTargetForSelector: 指定消息接受者.
  • 如果没有指定消息接受者,那么会调用 methodSignatureForSelector:,可在其中设置方法签名,也就是字符串编码形成的types.
  • 如果设置了方法签名,则会调用 forwardInvocation:,参数为 NSInvocation类型,包含方法调用者、签名、方法名.
  • NSInvocation 可以通过 invoke 调用函数.
  • 如果forwardingTargetForSelector:methodSignatureForSelector: 都 没有指定,导致最终消息发送失败,触发doesRegisterSelector 那么则会爆 unrecognized selector sent to instance .... 错误.

问题: super 关键字调用方法 和 self 关键字调用方法有什么不同?


super 关键字调用方法只是查找方法的位置从父类开始,但是消息的接受者仍然是当前类.


IT界无底坑洞栋主 欢迎加Q骚扰:676758285