OC底层原理:消息流程探索

文章目录

cache

基本结构

OC的方法缓存本质就是:一个 哈希表(Hash Table) ,底层数组叫 buckets,数组里每个元素存:

  • SEL
  • IMP

SEL -> IMP

方法名 -> 方法实现地址

其目的只有一个:加速 objc_msgSend 方法查找。

我们首先看一下cache_t这种类型的结构是咋样的

c++ 复制代码
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };

我们看cache_t这里的源码,发现了三个结构,而真机架构里mask和bucket在一起是为了优化,可以用各自的掩码来获取相应数据

  1. CACHE_MASK_STORAGE_OUTLINED表示运行的环境模拟器或者macOS
  2. CACHE_MASK_STORAGE_HIGH_16表示运行环境是64位的真机
  3. CACHE_MASK_STORAGE_LOW_4表示运行环境是非64位的真机

这里是cache_t的结构

我们在看一下bucket结构体,主要区别是sel和imp顺序的不同

c++ 复制代码
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__//不同结构有不同结果的arm64真机
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else//非真机
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

从上面结构体知,cache中缓存的是sel-imp,整体结构图如下

cache里插入一个新缓存的方法

这里是我的这个版本里的方法:

c++ 复制代码
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    lockdebug::assert_locked(&runtimeLock);//运行时锁检查,确保当前线程已经持有runtimeLock
//因为cache 插入是线程敏感操作,不能多个线程同时改 cache导致bucket数据错乱
    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {//类没 initialize 完之前不允许缓存
        return;
    }

    if (isConstantOptimizedCache()) {//检查是否预缓存优化(通常位于系统库里,存储在只读的内存段,尝试写入的时候会发生错误)
        _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                    cls()->nameForLogging());
    }

#if DEBUG_TASK_THREADS
    return _collecting_in_critical();//调试用,可以检测线程竞争
#else
#if CONFIG_USE_CACHE_LOCK
    mutex_locker_t lock(cacheUpdateLock);//资源获取即初始化风格的锁守卫对象,在构造函数中自动获取互斥锁,析构函数中自动释放锁。专门保护缓存更新的锁可以减少全局锁的持有时间,提高并发性能
#endif//防止多个线程同时向一个类的缓存里插入数据导致数据竞争

    ASSERT(sel != 0 && cls()->isInitialized());//sel 不能为空类必须初始化完成,否则runtime状态异常

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;//表示插入后 cache 已使用数量
    unsigned oldCapacity = capacity(), capacity = oldCapacity;//capacity表示bucket数组长度
    if (slowpath(isConstantEmptyCache())) {
      //说明当前 cache 还没真正分配 bucket
        // Cache is read-only. Replace it.
      // 给容量附上初始值,x86_64为4,arm64为2
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
      //这里本质上是申请新bucket数组->重新hash->迁移旧bucket->释放旧bucket
    }
   // 在arm64下,缓存的大小 <= buckets长度的7/8  不扩容
    // 在x86_64下,缓存的大小 <= buckets长度的3/4  不扩容
    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.
    }
#if CACHE_ALLOW_FULL_UTILIZATION
 
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) {
      //capacity <= FULL_UTILIZATION_CACHE_SIZE小 cache 特殊优化
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;//扩容的核心就是新建更大的buckets,一般是容量*=2
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
      // 创建新的扩容后的buckets,释放旧的buckets
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
  //获取bucket数组指针
    mask_t m = capacity - 1;
  //m是buckets的长度-1
    mask_t begin = cache_hash(sel, m);
  // 通过hash计算出要插入的方法在buckets上的起始位置
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
  //下面是插入核心逻辑
    do {
        if (fastpath(b[i].sel() == 0)) {//当前hash计算出来的buckets在i的位置为空,说明没缓存
          //更新使用数量
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
      //若已经有相同的se l,说明其他线程已经插入了,直接返回即可
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));//本质是i+1,若计算出来的起始位置i存在hash冲突,就改变i的值,即增大i

    bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

SEL -> IMP 通过哈希算法插入到 cache 的 bucket 数组中,并在必要时进行扩容。

这里其实复杂的主要原因也是因为性能而使用了用了:哈希表、线性探测、负载因子、动态扩容、位运算 hash、小 cache 优化、线程锁等

核心流程:

  1. 检查运行时状态

  2. 判断 cache 是否可用

  3. 判断是否需要扩容

  4. 计算 hash 下标

  5. 处理 hash 冲突

  6. 插入 SEL 和 IMP

处理扩容

缓存不是无限大的,如果块满了,查找冲突会越来越严重,这个时候就需要扩容(扩容的条件上面源码里给出了)扩容的核心就是新建立更大的buckets,但是不会超过MAX_CACHE_SIZE

我们首先要获取到buckets指针,得到buckets的最大的下标m,且通过cache_hash函数计算要缓存的方法在buckets里的起始位置,最后通过一个hash函数来处理下标值

可以把buckets看作是hash表

c++ 复制代码
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;//把SEL转化成unsigned long类型,一个很大的数字,然后通过哈希计算压缩成数组大小范围内
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

cache_hash保证起始位置一定在buckets最大下标里。

这里可能会出现一个哈希冲突,这里就需要前面说的cache_next这个处理哈希冲突的函数了:

c++ 复制代码
#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif

Runtime

runtime我们看名字就能知道,他的意思是运行时,和编译时是有区别的

OC的编译器只生成发送消息的指令,具体的接受处理全是在运行时进行决定的,运行时的一切操作都基于三个核心的结构体:objc_objectobjc_classMethod

  • 运行时是代码跑起来,被装到内存中的一个过程,如果这个过程出错,程序就会崩溃,是一个动态的阶段
  • 编译时是源代码翻译成机器能识别的代码的过程,主要是对语言进一个最基本的检查报错,即词法分析,语法分析等,是一个静态状态

三个核心结构体的具体内容:

c++ 复制代码
struct objc_object {
	Class isa;
};

struct objc_class {
  Class isa;
  Class super_class;
  Cache_t *cache;
  Class_data_bits_t bits;
};

struct method_t {
  SEL name;
  const char* types;
  IMP imp;
};

runtime的使用有以下三种方式,其三种实现方法与编译层和底层的关系如图所示

  • 通过OC代码,例如 [person sayNB]
  • 通过NSObject方法,例如isKindOfClass
  • 通过Runtime API,例如class_getInstanceSize

其中的compiler就是我们了解的编译器,即LLVM,例如OC的alloc 对应底层的objc_allocruntime system libarary 就是底层库

三个动态特点

动态类型

OC里的方法是动态类型的,所以我们可以在运行时发送信息给对象,对象可以依据收到的类型执行方法,和静态语言类型不同,静态类型在编译时就必须确定引用哪种对象,动态类型就更加泛化

objective-c 复制代码
id someObject = [[NSString alloc] initWithString:@"Hello world"];
someObject = [[NSDate alloc] init];

动态绑定

动态绑定是指方法调用可以在运行时解析,而不是在编译的时候,这一位之OC对象在运行时决定要执行对象的哪一个方法,而不是在编译的时候.这种机制是通过消息传递实现的,使得你可以在程序运行期间改变对象的调用方法,即可以在运行的时候再寻找方法

动态语言

OC能被称为动态语言的一个核心点就是消息转发机制 ,消息转发机制允许我们截取并处理未被对象识别的消息

探索方法的本质

c++ 复制代码
CJLPerson *person = [CJLPerson alloc];
//编译前
[person sayHello];
//编译后
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

通过上面示例我们看出,方法的本质其实就是objc_msgSend消息发送,objc_msgSend()里面有两个参数:结构体、sel()

SEL

SEL是一个方法选择器,用来告诉编译器我们当前想要调用哪一个方法

apple官方公示的SEL:

c++ 复制代码
typedef struct objc_selector *SEL;

SEL是一个不透明的类型,代表方法的选择子,定义:

c++ 复制代码
struct objc_selector {  
  void *sel_id;  
  const char *sel_types;  
};  

运行的时候,方法选择器就是用来代表方法的名字,一个方法选择器就是一个c的字符串,在oc运行的时候被注册,编译器生成的选择器在类加载的时候由运行的时候自动映射,并且可以在运行时添加新的选择器,并且使用sel_registerName函数检索现有的选择器。

OC会在编译的时候根据方法名生成一个唯一的区分的id,这个id是SEL类型的,只要方法签名相同,SEL返回就相同,在Runtime中维护了一个SEL的表,这个表按照NSSet来存储,只要SEL相同就会被看作同一个方法并加载到表中,所以在oc开发里要尽量避免方法重载

runtime内部维护了一个全局的唯一字符串池或者哈希表

IMP

指向方法实现的首地址,定义:

c++ 复制代码
#if !OBJC_OLD_DISPATCH_PROTOTYPES//现代定义
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else//旧版定义
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP的数据类型是指针,指向方法实现开始的位置

Method

不透明的类型,表示类中方法定义的方法

这里看出Method是一个结构体类型的指针

c++ 复制代码
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name   OBJC2_UNAVAILABLE; //表示方法名的字符串
    char * _Nullable method_types   OBJC2_UNAVAILABLE; //char* 类型的,表示方法的类型,包含返回值和参数的类型
    IMP _Nonnull method_imp    OBJC2_UNAVAILABLE; //IMP类型,指向方法实现地址的指针
}                                                            OBJC2_UNAVAILABLE;

Method操作函数如下

c++ 复制代码
//方法操作的主要函数:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types); // 添加方法
Method class_getInstanceMethod(Class cls, SEL name); // 获取实例方法
Method class_getClassMethod(Class cls, SEL name); // 获取类方法
Method *class_copyMethodList(Class cls, unsigned int *outCount); // 获取所有方法的数组
// 替代方法的实现
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types); 
// 交换两个方法的实现
method_exchangeImplementations(Method m1, Method m2)

小结

  • SEL(方法选择器)本质上是方法名的唯一标识,可以理解为一个字符串名称。

    编译时会生成对应的选择器,程序运行时由 Objective-C Runtime 统一注册和映射。

    可以把它理解为"书的标题"。

  • IMP 是一个函数指针,指向方法实现代码的内存地址。

    当找到对应方法后,最终调用的就是这个函数。

    可以理解为"书翻到的具体页码"。

  • Method 是 Runtime 中描述方法的结构体,内部主要包含:

    • SEL:方法名
    • IMP:方法实现
    • types:函数类型编码(返回值、参数类型等)

    它相当于把一个方法的完整信息封装起来。

    可以理解为"书中某一章节的完整内容"。

子类调用父类方法

定义子类和父类

首先我们先定义两个类,一个类继承另一个类

c++ 复制代码
- (void)sayBye {
    NSLog(@"%@ say: %s", [self class], __func__);
    NSLog(@"%@ say: %s", [super class], __func__);
}
//clang编译后
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rb_0ts7dvhs3zg9fc6kk2nvtlr00000gn_T_CJLPerson_d760c5_mi_2, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("CJLPerson"))}, sel_registerName("class")), __func__);

我们这里打印super class打印出来的结果还是我们的CJLTeacher,我们编译后super变成了objc_msgSendSuper

苹果官方文档对其方法解释为:

当遇到方法调用时,编译器会生成对以下函数之一的调用:objc_msgSend、objc_msgSend_stret、objc_msgSendSuper或objc_msgSendSuper_stret。

发送到对象超类的消息(使用super关键字)使用objc_msgSendSuper发送;其他消息使用objc_msgSend发送。使用objc_msgSendSuper_stret和objc_msgSend_stret发送以数据结构作为返回值的方法。

我们查看objc_msgSendSuper的源码,发现这个方法要两个参数:objc_super结构体和SEL

c++ 复制代码
((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("CJLPerson"))}

再看objc_super的源码,也需要两个参数:receiver和super_class

c++ 复制代码
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
    __unsafe_unretained _Nonnull Class super_class;

    /* super_class is the first class to search */
};
#endif

方法的接收和查找不一定是同一个人,方法的接收者是self,方法的查找是从super里进行查找的,从这里我们知道了方法的调用,首先是在类中查找,如果里没有找到,就会到类的父类里进行查找

super只是一个关键字,结构体里的super_class等于父类,代表是从父类对象里开始进行查找,不代表接受者receiver是父类对象。objc_msgSendSuper的区别在于找方法的初始位置不一样。

消息发送的流程

方法快速查找的流程图

我们查找这里有关objc_msgSend的源码实现,发现他是通过汇编实现的

汇编的优点:

  • 速度更快,直接使用参数,免去类大量参数拷贝的开销
  • 在函数和全局变量前面都会加下划线防止符号冲突
objc 复制代码
// 消息发送的汇编入口
// objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend 
    UNWIND _objc_msgSend, NoFrame 
  
// 判断接收者是否存在,其中p0是objc_msgSend的第一个参数,即消息接收者receiver
    cmp p0, #0          // nil check and tagged pointer check 
#if SUPPORT_TAGGED_POINTERS
//---- le小于 --支持taggedpointer(小对象类型)的流程
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
// p0为空的时候直接返回空,也就是接受者为nil的时候消息不会被做一个处理
	b.eq	LReturnZero
#endif
//取出isa指针
	ldr	p13, [x0]		// p13 = isa
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息,获得isa中的一个类信息
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
//如果有isa.走到cacheLookup方法面也就我们这里的核心部分快速查找流程
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//等于空返回空
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend

这里的步骤:

判断objc_msgSend方法的第一个参数receiver是否为空

  • 如果支持tagged pointer就跳转到LNilOrTaggedLNilOrTagged

    如果小对象为空,就直接返回空;不为空,就处理小对象的一个isa

  • 如果不是小对象,且receiver也不为空

    从receiver中取出isa存入p13寄存器,通过GetClassFromIsa_p16中,arm64架构下通过isa&ISA_MASK获取shiftcls位域的类信息,即class

寄存器:CPU内部高速临时存储单位,速度远快于内存,用来存放函数参数、内存地址、运算中间值,汇编底层依赖寄存器保证极致执行效率

消息发送会先通过缓存进行查找方法实现,如果在缓存中没有找到方法实现,就会进入慢速查找过程,去类的方法列表以及父类链中进行循环查找

获取isa完毕,进入慢速查找流程CacheLookup NORMAL

慢速查找lookUpImpOrForward

如果这个方法没在缓存里保存过,就会进入__objc_msgSend_uncached函数,在快速查找流程中,不管是checkmiss还是jumpmiss,最终都走的是__objc_msgSend_uncached

c++ 复制代码
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    
    //定义的消息转发,imp初始化为空
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;
    //curClass保存当前正在查找的类

    runtimeLock.assertUnlocked();
    
    if (slowpath(!cls->isInitialized())) {
        // The first message sent to a class is often +new or +alloc, or +self
        // which goes through objc_opt_* or various optimized entry points.
        //
        // However, the class isn't realized/initialized yet at this point,
        // and the optimized entry points fall down through objc_msgSend,
        // which ends up here.
        //
        // We really want to avoid caching these, as it can cause IMP caches
        // to be made with a single entry forever.
        //
        // Note that this check is racy as several threads might try to
        // message a given class for the first time at the same time,
        // in which case we might cache anyway.
        behavior |= LOOKUP_NOCACHE;
    }//判断类有没有初始化过,如果cls还没有初始化完成,那么给behavior加一个禁止缓存

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();//加锁,保证线程安全

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    checkIsKnownClass(cls);
    //确定当前类的一个继承链关系
    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
    //确保这个类是realized + initialized状态
    // runtimeLock may have been dropped but is now locked again
    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookup the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    for (unsigned attempts = unreasonableClassCount();;) { // 进入查找循环
        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) { //尝试查找方法
#if CONFIG_USE_PREOPT_CACHES
          //如果是常量优化缓存
          //再一次尝试从cache中查找imp
          //目的防止多线程操作的时候,刚好调用函数缓存进来了
            imp = cache_getImp(curClass, sel);
            if (imp) goto done_unlock;
            curClass = curClass->cache.preoptFallbackClass();
#endif
        } else {
            // curClass method list.
            method_t *meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                imp = meth->imp(false);
                goto done;
            }

            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); // 从父类的cache中去找,看能不能捡到imp
        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就被取反了,所以第二次进入的时候就不会进入了,因此动态解析的过程其实是一个执行一次的单例操作.
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior); //没找到,走methodresolver
    }

 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); //找到imp去缓存它
    }
 done_unlock:
    runtimeLock.unlock(); // 解锁并且返回
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

上面的主要步骤如下:

  1. 检查类是否被初始化,是否是一个已知的关系,确定继承关系,如果没初始化behavior |= LOOKUP_NOCACHE;这次查找结果先不缓存

    • 加锁保证线程安全runtimeLock.lock();保证 "方法查找 + cache 填充" 是原子的。
    • 检查是否是一个合法类checkIsKnownClass(cls);,防止伪造 Class 对象。
    • 确保类已 realized + initialized来确定继承关系保证当前 cls 已经可以安全查找方法
  2. 进入了循环查找方法的核心逻辑,从本类的method查找imp(查找的方式是getMethodNoSuper_nolock)

    整体逻辑:本类 method list->父类 cache->父类 method list->继续向上

    • 先查当前类的方法列表内部通常是:二分查找,method list 查找 SEL(只查当前类,不查父类)

    • 当前类没找到,去找父类curClass = curClass->getSuperclass()沿继承连向上

    • 先查父类cacheimp = cache_getImp(curClass, sel);

      • 因为:cache 查找速度远高于 method list所以runtime的策略是:

        先 method list

        后 superclass cache

        不是每次都查 method list。

      • 如果父类cache找到goto done;则说明父类之前已经缓存过该方法。

        这个时候会直接直接复用 IMP,后面还会把 IMP 再缓存到当前类 cache

    • 若父类cache没找到,就继续循环,下一轮当前 curClass 已经变成父类,就会又会查父类自己的 method list,所以流程大概是:

      当前类 method list->当前类 cache(objc_msgSend 已查过)->父类 cache->父类 method list->爷爷类 cache->爷爷类 method list...

    • 直到查找到根类curClass == nil,整个继承链都没找到方法此时imp = forward_imp;设置为消息转发 IMP,即_objc_msgForward_impcache后续进入消息转发流程。

    • 防止继承连死循环:if (--attempts == 0)用于判断父类链是否成环,若发生说明内存损坏,直接crash

  3. 跳出循环后的逻辑,如果找到了imp,就会把imp缓存到本类cache里log_and_fill_cache(cls, imp, sel, inst, curClass);

    这里不管是本类还是本类的父类找到了imp,都会缓存到最初的cls本类中去,下次使用的时候,缓存直接命中,这也就是Objective-C 消息机制越调越快的核心原因。

  4. 动态方法解析:若是整个继承链都没有找到,会进入if (behavior & LOOKUP_RESOLVER)进行动态方法解析,动态解析只会尝试一次(behavior ^= LOOKUP_RESOLVER;会把标记位清掉)

  5. 动态解析失败,就走消息转发

在类和父类继承链中查找imp的过程:

c++ 复制代码
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel); // 查找方法的函数
        if (m) return m;
    }

    return nil;
}

search_method_list_inline(const method_list_t *mlist, SEL sel) 
  // 这里面是查找函数的一个具体实现
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->isExpectedSize();
    
    if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
        return findMethodInSortedMethodList(sel, mlist); // 有序查找,采用二分查找的方式
    } else {
        // Linear search of unsorted method list
        if (auto *m = findMethodInUnsortedMethodList(sel, mlist)) //无序查找,采用遍历的方式
            return m;
    }

#if DEBUG
    // sanity-check negative results
    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name() == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }
#endif

    return nil;
}

这里主要的二分查找函数:

c++ 复制代码
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
    if (list->isSmallList()) {
        if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });
        } else {
            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });
        }
    } else {
        return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });
    }
}

ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
    ASSERT(list);

    auto first = list->begin();
    auto base = first;
    decltype(first) probe;

    uintptr_t keyValue = (uintptr_t)key;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
      //从首地址+下标 --> 移动到中间位置(count >> 1 右移1位即 count/2 = 4)
        
        uintptr_t probeValue = (uintptr_t)getName(probe);
        //如果查找的key的keyValue等于中间位置,就直接返回
        if (keyValue == probeValue) {
            // `probe` is a match.
            // Rewind looking for the *first* occurrence of this value.
            // This is required for correct category overrides.
            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
              //排除分类重名方法(方法的存储是先存储类方法,在存储分类---按照先进后出的原则,分类方法最先出,而我们要取的类方法,所以需要先排除分类方法)
                probe--;
            }
            return &*probe;
        }
        //如果keyValue大于probeValue 就往中间位置的右边进行一个查找
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

这里我们主要看下图来理解一下:

慢查找总结

  1. objc_msgSend 先进行当前类 cache 快速查找,若 cache miss,则进入 lookUpImpOrForward 慢速查找流程;进入后会先检查类是否已初始化、是否为合法类,并确保类已经完成 realize + initialize,同时加锁保证方法查找与 cache 写入的线程安全。
  2. 慢速查找会沿着类继承链循环查找 imp:
    • 先查当前类的 method list(二分查找 / 遍历查找)
    • 当前类未找到,则向父类查找
    • 先查父类 cache
    • 父类 cache 未命中,再查父类 method list
    • 然后继续沿继承链向上(父类 → 根类)不断重复"查 cache + 查 method list"的过程
  3. 查找过程中,只要任意环节找到 imp,就会跳出循环,并通过 log_and_fill_cacheimp 缓存到"最初发送消息的类"的 cache 中(无论 imp 是在本类还是父类找到的);若整个继承链都未找到方法,则将 imp 指向消息转发 _objc_msgForward_impcache,随后进入动态方法解析 resolveMethod_locked,解析失败后再进入完整的消息转发流程。

动态方法决议&消息转发

如果在快速查找以及慢速查找都没有找到方法实现的情况下,苹果给出两个方法:

  • 动态方法决议:慢速查找流程未找到之后,会执行一次动态方法决议
  • 消息转发:如果动态方法决议仍然没有找到实现,则进行消息转发

如果这两个建议都没有做任何操作,就会报我们日常开发中常见的方法未实现的崩溃报错

在慢速查找流程没有找到方法实现的时候,会有以下流程:

c++ 复制代码
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);
  //最后返回的都是在缓存中查找是否有对应的一个方法实现
}


IMP lookUpImpOrForwardTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior);
}

static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertUnlocked();

    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

    IMP imp = cache_getImp(cls, sel);
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
        return lookUpImpOrForward(inst, sel, cls, behavior);
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) { // 这里进入消息转发流程类
        return nil;
    }
    return imp;
}
//上面这个方法是_lookUpImpTryCache方法:主要作用是在方法缓存中查找给定的类和选择器(sel)对应的方法实现(IMP)。如果找到了,就直接返回这个方法实现。如果没有找到,就会调用 lookUpImpOrForward 函数,进一步查找方法实现或者进入消息转发(forwarding)流程。
  • 如果是类,执行实例方法的动态决议方法resolveInstanceMethod

  • 如果是元类,执行类方法的动态决议方法resolveClassMethod,如果在元类中没有找到或者未空,则在元类的实例方法的动态决议方法resolveInstanceMethod(因为类反复该在元类中是实例方法),所以还需要查找元类中实例方法的动态决议.

  • 如果动态方法决议中,将其指向了其他方法,则要继续查找对应的一个imp,就是慢速查找流程

实例方法

在快速-慢速查找均没有找到实例方法的实现,我们有一次挽救的机会,也就是动态方法决议

c++ 复制代码
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    lockdebug::assert_unlocked(&runtimeLock);
  //检查运行时锁状态
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);
  //定义动态方法解析方法的选择器,会去元类继承链里找是否有resolveInstacenMethod的imp,如果本类中没有实现它,最终找到NSObject的根元类的系统默认实现

  //检查类是否实现解析方法
    if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);//强制调用objc_msgSend,调用resolveInstanceMethod方法,去添加方法(调用Class_addMethod方法)

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);//再次查找实现

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p",
                         cls->isMetaClass() ? '+' : '-',
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel),
                         cls->isMetaClass() ? '+' : '-',
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

这里多次调用了lookUpImpOrNilTryCache函数,这个函数的逻辑:

c++ 复制代码
//运行时方法查询的快速通道
IMP lookUpImpOrNilTryCache(id inst, SEL sel, Class cls, int behavior)
{
    return _lookUpImpTryCache(inst, sel, cls, behavior | LOOKUP_NIL);
}

static IMP _lookUpImpTryCache(id inst, SEL sel, Class cls, int behavior)
{
    lockdebug::assert_unlocked(&runtimeLock);

    if (slowpath(!cls->isInitialized())) {
        // see comment in lookUpImpOrForward
        return lookUpImpOrForward(inst, sel, cls, behavior);
      //如果类没有初始化,那缓存就是乱序的,直接进入慢查找
    }

    IMP imp = cache_getImp(cls, sel);
  //快速查找
    if (imp != NULL) goto done;
#if CONFIG_USE_PREOPT_CACHES
  、//去一个全局共享缓存中查找
    if (fastpath(cls->cache.isConstantOptimizedCache(/* strict */true))) {
        imp = cache_getImp(cls->cache.preoptFallbackClass(), sel);
    }
#endif
    if (slowpath(imp == NULL)) {
        // dtrace probe
        OBJC_RUNTIME_CACHE_MISS(inst, sel, cls);

        return lookUpImpOrForward(inst, sel, cls, behavior);//慢速查找
    }

done:
    if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) {
        return nil;
    }
    return imp;
}

小结

  1. 当类方法查找失败时,runtime 会进入类方法动态解析流程。由于类方法本质上存储在"元类"的方法列表中,因此会先沿着元类继承链查找 resolveClassMethodimp;如果当前类元类实现了该方法,则直接调用;如果没有实现,则最终会在根元类 NSObject 中找到默认实现。
  2. runtime 会主动向"类对象"发送 resolveClassMethod:消息,尝试动态添加缺失的类方法。发送消息的过程本质仍然是一次标准的消息发送流程:
    • 先查类对象的 cache
    • 再查元类及元类继承链中的 method list
    • 找到 resolveClassMethodimp 后执行
    • 在方法内部通过 class_addMethod 绑定 sel + imp
  3. resolveClassMethod: 执行完成后,runtime 会重新查找原本缺失的类方法,并将查找到的 imp缓存到对应元类的 cache 中;由于类方法在底层是以"元类实例方法"的形式存在,因此类方法查找本质上就是:
    • 在元类中查找实例方法
    • 在元类 cache 中缓存实例方法 imp
    • 最终完成类方法调用。

类方法

类方法的源码流程:

c++ 复制代码
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
    lockdebug::assert_unlocked(&runtimeLock);
    ASSERT(cls->isRealized());
    ASSERT(cls->isMetaClass());

    if (!lookUpImpOrNilTryCache(inst, @selector(resolveClassMethod:), cls)) {
        // Resolver not implemented.
        return;
    }
		// 处理元类,nonmeta赋值成类对象
    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
  //给类对象发送resloveClassMethod消息,绑定sel和imp指针
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNilTryCache(inst, sel, cls);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p",
                         cls->isMetaClass() ? '+' : '-',
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel),
                         cls->isMetaClass() ? '+' : '-',
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
  1. 当类方法查找失败时,runtime 会进入类方法动态方法决议流程。由于类方法本质存储在"元类"中,因此会先沿着元类继承链查找 resolveClassMethodimp;如果当前类实现了 +resolveClassMethod:,则能在当前类的元类中找到对应 imp,否则最终会在根元类 NSObject 中找到默认实现。
  2. runtime 会通过 getMaybeUnrealizedNonMetaClass 获取当前元类对应的"类对象(nonmeta)",随后主动向类对象发送 resolveClassMethod: 消息;这个发送过程本质仍然是一次正常的消息发送流程,因此找到的 imp 最终也会被缓存到当前类(或元类)的 cache 中。
  3. resolveClassMethod: 执行完成后,runtime 会重新查找之前缺失的类方法;由于类方法在底层是以"元类实例方法"的形式存储,因此本质上是在元类中查找实例方法 imp。如果动态添加成功,则重新查找到对应 imp 并缓存;如果仍未找到,则继续进入后续的消息转发流程。

崩溃修改

由上我们可以实现一个简单的保底机制:

objc 复制代码
@interface GGObject : NSObject
- (void)sayHello;
- (void)say666;
- (void)sayYES;
@end

@implementation GGObject

- (void)say666 {
    NSLog(@"666");
}
- (void)sayYES {
    NSLog(@"YES");
}
//- (void)sayHello {
//    NSLog(@"Hello");
//}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(sayHello)) {
        NSLog(@"%@", NSStringFromSelector(sel));
        //获取已经实现的方法的imp
        IMP imp = class_getMethodImplementation(self, @selector(say666));
        //把 say666 的 IMP 拿过来,挂到了 sayHello 上。
        Method method = class_getInstanceMethod(self, @selector(say666));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self, sel, imp, type);
        //动态交给sayHello添加方法实现
    }
    return [super resolveInstanceMethod:sel];
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        GGObject *obj = [[GGObject alloc] init];
        [obj say666];
        [obj sayYES];
        [obj sayHello];
    }
    return 0;
}

输出结果

从这里我们看出Runtime 根本不关心:方法名是不是一致或者语义是不是对应,他只认

SEL -> IMP,方法本质上就是一个映射关系

我们当然也可以通过白名单\前缀匹配然后分流来优化我们的处理:

我们这里需要使用一个扩展

c++ 复制代码
#import "NSObject+SafeGuard.h"
#import <objc/runtime.h>

@implementation NSObject (SafeGuard)


// 动态方法真正执行的函数Objective-C 方法底层本质就是 C 函数
// 参数:self -> 当前对象
// _cmd -> 当前方法selector
void GGDynamicMethod(id self, SEL _cmd) {
    NSLog(@"%@ 方法未实现,已安全拦截",
    NSStringFromSelector(_cmd));
}
// Runtime动态方法解析
// 当对象调用了未实现的方法时,Runtime会进入此方法询问: "是否要动态添加方法实现"返回YES: 表示已经动态添加成功 Runtime会重新发送一次消息
// 返回NO:表示不处理,Runtime继续进入消息转发流程
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // SEL转NSString,方便做字符串判断和日志输出
    NSString *selName = NSStringFromSelector(sel);
    // 白名单过滤:只处理GG_前缀的方法,防止误拦截系统方法
    if ([selName hasPrefix:@"GG_"]) {

        NSLog(@"动态拦截未实现方法: %@", selName);
        // IMP本质就是函数指针,这里把C函数作为方法实现
        IMP imp = (IMP)GGDynamicMethod;
        // 方法类型编码:v -> void返回值,@ -> id self,: -> SEL _cmd
        const char *type = "v@:";
        // 动态给当前类添加方法实现,SEL->IMP
        return class_addMethod(self, sel, imp, type);
    }
    //对其他方法不处理,这里也主要是因为NSObject是根类所以不能用return [super resolveInstanceMethod:sel];
    return NO;
}

@end

打印结果:

Runtime概念 代码体现
SEL sel
IMP GGDynamicMethod
方法签名 "v@:"
动态解析 resolveInstanceMethod
动态添加方法 class_addMethod
消息补救 返回YES后重新发消息
防崩思想 未实现方法动态兜底

消息转发

如果前面一直都没找到方法的实现,这个时候就要用到消息转发流程,分为快速转发和慢速转发,在方法未实现崩溃报错的时候会经历两遍动态方法决议和两边快速转发,两边慢速转发

快速转发forwardingTargetForSelector

c++ 复制代码
if ((behavior & LOOKUP_NIL) && imp == (IMP)_objc_msgForward_impcache) { 
  // 这里进入消息转发流程类
        return nil;
    }

消息转发是从imp == (IMP)_objc_msgForward_impcache这个函数进入的,对应的是下面这个方法

c 复制代码
//快速转发,找不到方法,直接转发给别人处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(sayHello)) {
      NSLog(@"快速转发:sayHello转给CJLTeacher");
        return [CJLTeacher new]; // 返回其他类中实现过这个方法的方法
    }
    return [super forwardingTargetForSelector:aSelector];
}

这样就可以直接让class1的方法转发到class2里

慢速转发(methodSignatureForSelectorforwardInvocation)

也叫完整消息转发

如果第二次机会,即快速转发里还是没有找到,就进入最后一次挽救机会,即在LGPerson里重写methodSignatureForSelector

objc 复制代码
// 当Runtime进入完整消息转发阶段时
// 会先调用 methodSignatureForSelector:
// 要求开发者提供方法签名
//
// 如果返回nil:,Runtime认为无法处理该消息 程序直接崩溃
// 如果返回方法签名: Runtime会创建NSInvocation对象 并进入 forwardInvocation:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{

    NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));

    // v@: 表示:
    // void返回值
    // id self
    // SEL _cmd
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 完整消息转发最终阶段
// Runtime会把消息封装成NSInvocation对象,我们可以:
// 1. 转发给其他对象
// 2. 修改参数
// 3. 修改返回值
// 4. 安全拦截防止崩溃
- (void)forwardInvocation:(NSInvocation *)anInvocation{

    NSLog(@"%s - %@",__func__,anInvocation);
}

小结

OC方法调用本质是 objc_msgSend 消息发送。

消息发送过程本质是 Runtime 对 SEL 对应 IMP 的查找过程。

查找顺序:

cache -> 当前类 -> 父类链。

对象方法沿类继承链查找,

类方法沿元类继承链查找。

当方法不存在时,Runtime不会立即崩溃,

而是依次进入:动态方法解析->快速消息转发->完整消息转发

这是OC Runtime防止程序崩溃的"三次补救机制"。最终若仍无法处理消息,则触发 unrecognized selector 崩溃。

相关推荐
敲代码的鱼哇5 小时前
NFC读卡能力 支持安卓/iOS/鸿蒙 UTS插件
android·ios·harmonyos
浩宇软件开发15 小时前
SwiftUI入门 10 分钟学会做一个 App 引导页
ios·swiftui·swift
90后的晨仔16 小时前
SwiftUI 完全指南:从声明式 UI 到响应式架构的终点回顾
ios
90后的晨仔16 小时前
SwiftUI 多线程与并发编程深度总结
ios
90后的晨仔16 小时前
Combine 与系统框架集成:将响应式编程融入 Apple 生态
ios
90后的晨仔16 小时前
Combine 与 Swift Concurrency:响应式与并发的完美协奏
ios
90后的晨仔16 小时前
Combine 自定义 Subject:构建专属的响应式事件源
ios
90后的晨仔16 小时前
Combine 架构模式:构建响应式应用的蓝图
ios
90后的晨仔16 小时前
Combine 高级实践:多线程调度、调试与测试
ios