runtime09 - 消息发送与转发机制

646 阅读4分钟

流程图:

yulingtianxia.com/blog/2016/0…

image.png

method_t 内存结构

iOS14以前是big

iOS14以后编译期间就确定下来的,是small,在运行期添加的方法,是big

small的核心原理是: 利用方法实现的地址在程序中的偏移量是固定的,而且不会脱离当前库的地址范围,所以用32位记录偏移量即可,利用程序加载的地址 + offset来代替真实的地址, 可以减少method占用的内存, 还可以优化以前rebase的时候, 需要修复method的真实地址所占用的时间

iOS14以后, 如果是对某方法执行了setImp,不能直接用small来存(一方面运行期添加的方法, 是big类型的, 另一方面, 可能交换系统方法, 所以靠32位的offset是无法做到的),那么会有一个全局的map来管理, key是method_t的地址, value是imp的值

struct method_t {
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
    struct small {
        RelativePointer<const void *> name;
        RelativePointer<const char *> types;
        RelativePointer<IMP> imp;
    }
    small &small() const {
        return *(struct small *)((uintptr_t)this & ~(uintptr_t)1);
    }
    big &big() const {
        ASSERT(!isSmall());
        return *(struct big *)this;
    }
}
template <typename T>
struct RelativePointer: nocopy_t {
    int32_t offset; // 这是一个 32 位有符号偏移量,提供 ±2GB 的范围。

    T get() const {
        if (offset == 0)
            return nullptr;
        uintptr_t base = (uintptr_t)&offset;
        uintptr_t signExtendedOffset = (uintptr_t)(intptr_t)offset;
        uintptr_t pointer = base + signExtendedOffset;
        return (T)pointer;
    }
};

iOS14以后,对方法有个优化 详细看这里

方法缓存

即使是在父类方法列表中找的方法,最终也会缓存在自己的缓存中

+ (BOOL)resolveInstanceMethod:(SEL)sel

  1. 可以在这个方法中, 利用runtime进行方法添加
  2. 在方法查找,发现在方法列表查找不到时, 如果发现实现了这个方法, 就会执行这个方法并标记动态解析过了, 重新进入lookUpImpOrForward的流程, 并本次流程中不会再次动态解析
  3. 返回值只会影响系统的信息打印, 不影响转发流程 (建议还是跟随系统建议, 进行了方法添加返回YES)

- (id)forwardingTargetForSelector:(SEL)selector

消息转发流程中, 可以利用该方法, 将自己未实现的方法转发给其他对象 可以利用该方法配合protocol 来变相实现多继承

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

为了配合runtime,编译器将方法的返回类型和参数类型都编码成一个字符串,也就是method_ttypes.

在方法调用时,会转换成objc_msgsend函数,并在函数中传递消息接收者,消息对应的方法名字,以及真实的参数列表

那么就可以利用types来用合适的类型来接收参数,返回返回值

NSMethodSignature,就是对types的一种OC封装,可以方便的获取方法的参数个数,类型以及返回值类型, 跟method_tSELIMP无关, 只跟types有关

@interface NSMethodSignature : NSObject

+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

@property (readonly) NSUInteger numberOfArguments;
- (const char *)getArgumentTypeAtIndex:(NSUInteger)idx NS_RETURNS_INNER_POINTER;

@property (readonly) NSUInteger frameLength;

- (BOOL)isOneway;

@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
@property (readonly) NSUInteger methodReturnLength;

@end

- (void)forwardInvocation:(NSInvocation *)anInvocation

NSInvocation 像是对消息发送的一个oc封装, 非常灵活

可以利用NSInvocation, 来获取传入的真实参数值, 改变某些传入的参数, 改变消息接收者, 设置改变要调用的SEL, 设置改变返回值

可以利用NSInvocation来做跨组件通信或者调用三方库的私有函数, 与-performSelector:类似,但是能传递无限制的参数 (缺点: 要利用反射, 硬编码会比较多, 有额外风险)

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

- (void)invoke;
- (void)invokeWithTarget:(id)target;

@end

源码

查找方法地址

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        behavior |= LOOKUP_NOCACHE;
    }

    runtimeLock.lock();
    checkIsKnownClass(cls);
   
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;
    
    // 在继承链, 从低到高的查找方法缓存和方法列表
    for (unsigned attempts = unreasonableClassCount();;) {
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }
            // 如果直到根类都找不到方法, 那么标志imp为`forward_imp`
            if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
                // No implementation found, and method resolver didn't help.
                // Use forwarding.
                imp = forward_imp;
                break;
            }
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        // 查找父类方法缓存
        imp = cache_getImp(curClass, sel);
        // 如果发现`imp`是`foward_imp`, 代表要进行消息转发, 直接break
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    // 方法动态解析
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
    // 标志已经进行过方法解析
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
        while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
            cls = cls->cache.preoptFallbackClass();
        }
#endif
// 缓存查找到的方法
        log_and_fill_cache(cls, imp, sel, inst, curClass);
    }
 done_unlock:
    runtimeLock.unlock();
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

方法动态解析

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());

    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}