阅读 300

iOS底层原理07: 消息发送和消息转发

一、消息发送

    1. objc_msgSend()分析

通过之前的源码分析, 我们知道IOS的消息发送是通过 objc_msgSend(id, sel) 来进行的. 今天来具体分析一下objc_msgSend() 函数.

在源码中, 我们可以看到objc_msgSend()是定义在汇编代码中, 由于实力不够, 还看不是很懂......通过大致跟踪, 可以发现最后是调用了lookUpImpOrForward()函数. 我们可以在源码中找到这个函数的核心代码如下:

  1. 进入for循环后, 先进行cache判断, 如果cache_getImp()可以找到缓存的方法, 那么就直接到done_unlock返回imp
  2. 如果找不到缓存, 就在当前class中找meth, 如果找到的话跳到done, 进行方法缓存, 然后返回imp
  3. 当前类找不到的话, 将curClass赋值为superClass
  4. 然后在superClass中继续查找cache_getImp(), 如果找到的话跳转到done. 找不到就进入下一次递归循环
  5. 在for循环递归完成后, 还没有找到imp的话就说明没有实现这个方法, 就会进入到方法动态解析
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    IMP imp = nil;
    Class curClass;
    curClass = cls;

    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            // 步骤1, cache 查找
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // 步骤2, 当前类 查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            //  步骤3, curClass赋值为superClass
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                imp = forward_imp;
                break;
            }
        }

        // 步骤4: 查找superClass的缓存
        imp = cache_getImp(curClass, sel);
        if (fastpath(imp)) {
            goto done;
        }
    }

    // 步骤5: 所有的superClass都递归以后, 如果还是没有找到, 就进入消息转发流程
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}
复制代码
    1. getMethodNoSuper_nolock()分析

继续看下查找本类方法时的源码, 可以看到, 是通过类的methods进行for循环遍历

static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) {
    
    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}
复制代码
    1. log_and_fill_cache()函数

当在类中找到对应的imp时, 会将找到的方法添加到缓存中, 代码如下: 直接将sel, imp插入到类的cache中

static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
    cls->cache.insert(sel, imp, receiver);
}
复制代码

总结:

  1. iOS消息发送时会先查找当前类的缓存, 如果命中缓存, 则直接返回;
  2. 如果没有命中缓存, 则在本来的方法列表中查找, 如果命中, 则将方法加入缓存并返回;
  3. 如果没找到, 则递归到自己的父类, 同样先查找父类的缓存, 缓存找不到查找方法列表......
  4. 递归结束都没有找到就会进入方法动态解析

二、类中cache的设计

在上一节我们看到, 在消息发送是, 会先查找缓存, 然后在查找本类方法列表, 找到后还会将imp存到缓存中, 我们来研究下类的cache是如何设计的

    1. 类中的cache: 通过源码我们可以看到, cache 在类中声明, 是一个cache_t类型
struct objc_class : objc_object {
    // ...
    cache_t cache; // formerly cache pointer and vtable
    // ...
}

复制代码
    1. cache_t主要源码如下
struct cache_t {

public:
    unsigned capacity() const;  /// 散列表的容量
    struct bucket_t *buckets() const; /// 真是记录缓存的散列表
    Class cls() const; /// 对应的类
 
    mask_t occupied() const;  /// 散列表已占用的数量

    void initializeToEmpty();   /// 初始化方法
    void insert(SEL sel, IMP imp, id receiver);  /// 插入方法
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len); /// 复制缓存
    void destroy();  /// 销毁
    void eraseNolock(const char *func); /// 全部擦除

};
复制代码
    1. insert() 函数处理添加缓存逻辑, 其中也包含散列表扩容等操作
void cache_t::insert(SEL sel, IMP imp, id receiver)
{

    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // 如果散列表是空的, 就进行空间申请, 默认大小为4
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
        // 如果占用空间小于 3/4 or 7/8 则不做处理 继续使用
    }
#if CACHE_ALLOW_FULL_UTILIZATION
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
    
    }
#endif
    else {
        // 如果超过的话, 会进行 *2 倍的大小扩容, 但是不能超过MAX_CACHE_SIZE(1<<16)
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();  // 拿到散列表
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m); // 根据sel 和 当前容量计算哈希
    mask_t i = begin;

    do {   // 根据哈希将sel放进散列表, 如果当前idx已存放了imp, 则进行cache_next()计算新的idx
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();  // 将当前占用空间+1 然后更新成员属性
            // 加入散列表
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    /**  cache_next其实就是将 上次计算的结果i - 1
    static inline mask_t cache_next(mask_t i, mask_t mask) {
        return i ? i-1 : mask;
    }
    */

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}
复制代码

三、找不到方法实现后的动态方法解析和消息转发

动态方法解析

    1. 开发者可以手动实现以下方法, 来通过runtime动态添加方法实现
// 类方法
+ (BOOL)resolveClassMethod:(SEL)sel {
    // 使用runtime动态添加方法
}

// 实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 使用runtime动态添加方法
}
复制代码
    1. 在消息发送的最后, 如果找不到方法实现, 会调用resolveMethod_locked()函数进入动态方法解析
static  IMP resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{

    if (! cls->isMetaClass()) {
        // 如果是对象, 则尝试调用类对象的方法 [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // 如果是类对象, 会先尝试调用元类对象的方法 [nonMetaClass resolveClassMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            // 然后在尝试调用 [cls resolveInstanceMethod:sel]
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

复制代码
    1. resolveInstanceMethod()会找到开发者实现的sel
static void resolveInstanceMethod(id inst, SEL sel, Class cls) {

    // 定义 resolveInstanceMethod: 方法
    SEL resolve_sel = @selector(resolveInstanceMethod:);
    // 然后去cache和类方法列表中查找, 看开发者是否实现, 如果没实现则直接返回
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);

    // 将查找结果缓存起来, 防止以后多次进入动态方法解析
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);
    // ...
}
复制代码

如果resolveInstanceMethod: 无法找到对应实现, 那么lookUpImpOrForward() 函数就会返回nil, 这是就会进入消息转发的流程

消息转发

    1. 首先会调用- (id)forwardingTargetForSelector:(SEL)aSelector{} 方法(以对象方法为例, 类方法相同)

在forwardingTargetForSelector: 中返回一个实现了对应方法的对象, 然后会调用 objc_msgSend(返回的对象, imp)进行消息发送

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
}
复制代码
    1. 如果在forwardingTargetForSelector返回nil, 则会调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{} 方法

在methodSignatureForSelector: 方法中返回一个OC方法定义(如 "v@0:8"), 然后就会执行 - (void)forwardInvocation:(NSInvocation *)anInvocation{} 方法.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
}
复制代码
    1. 如果所有methodSignatureForSelector:返回的是nil, 那么就会报错 doesNotRecognizeSelector
文章分类
iOS
文章标签