iOS Runtime(二):消息机制

本篇解析版本为objc4-723

方法调用本质

在 OC 调用方法时,底层其实是一个消息发送的过程,有如下 OC 代码

objc 复制代码
Person *p = [Person new];
[p sayHello];

clang 一下可以看到 C++ 实现clang -rewrite-objc main.m -o main.cpp,得到结果如下

scss 复制代码
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));

可以看出,在调用方法时,编译器将它转成了objc_msgSend 消息发送了, 先说结论,消息发送的过程如下

  1. Runtime先通过对象p找到isa指针,判断isa指针是否为nil,为nil直接return。
  2. 若不为空则通过isa指针找到当前实例的类对象,在类对象下查找缓存是否有sayHello方法。
  3. 若在类对象缓存中找到sayHello方法,则直接调用IMP方法。
  4. 若在类对象缓存中没找到sayHello方法,则查找当前类对象的方法列表methodlist,若找到方法则将其添加到类对象的缓存中。
  5. 若在类对象方法列表中没找到sayHello方法,则继续到当前类的父类中以相同的方式查找(即类的缓存->类的方法列表)。
  6. 若在父类中找到sayHello方法,则将IMP添加到类对象缓存中。
  7. 若在父类中没找到sayHello方法,则继续查询父类的父类,直到追溯到最上层NSObject
  8. 若还是没有找到,则启用动态方法解析、备用接收者、消息转发三部曲,给程序最后一个机会
  9. 若还是没找到, Runtime 会抛出异常doesNotRecognizeSelector

综上,方法的查询流程基本就是查询
类对象中的缓存和方法列表->
父类中的缓存和方法列表->
父类的父类中的缓存和方法列表->

...->
NSObject 中的缓存和方法列表->
动态方法解析->
备用接收者->
消息转发\

下面我们结合代码,对上述流程的每一步进行解析。

isa指针

首先是 isa 指针,什么是 isa 指针?

打开 Runtime 源码,全局搜索isa_t,锁定objc-private.h类,里面有这么一段代码,isa指针是isa_t的实例。

c 复制代码
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
#endif
};

OC对象的本质,每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个联合体union结构,同时使用位域来存储更多的信息。 它是通过isabits进行位运算,取出响应位置的值,runtime中的isa是被联合体位域优化过的,它不单单是指向类对象了,而是把64位中的每一位都运用了起来,其中的shiftcls为33位,代表了类对象的地址,其他的位都有各自的用处。

  • nonpointer:表示是否对isa指针开启指针优化 0:不开启,表示纯isa指针。 1:开启,不单单是类对象的地址,isa中包含了类信息和对象的引用计数等。
  • has_assoc:关联对象标识位,0没有,1有,没有关联对象会释放的更快。
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象。
  • shiftcls:存储类指针class的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤用来存储类指针。
  • magic:固定值为0xd2,用于在调试时分辨对象是否完成初始化。
  • weakly_referenced:表示对象是否被指向或者曾经指向一个 ARC 的弱引用变量, 没有弱引⽤的对象可以更更快释放。
  • deallocating:标志对象是否正在释放内存。
  • has_sidetable_rc:当对象的引用计数大于10,以至于无法存储在isa指针中时,用散列表sidetable去计数。
  • extra_rc:表示该对象的引用计数,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数⼤于 10, 则需要使⽤到has_sidetable_rc。

下面看一下实例对象、类对象、元类之间关系的经典图示:

objc_msgSend流程解析

好了现在清楚了isa指针是怎么一回事儿了,接下来探索objc_msgSend底层实现。 打开 Runtime 源码,全局搜索objc_msgSend(,找到objc-msg-arm64.s这个类

objc_msgSend的方法调用分为两部分,快查询部分(汇编)和慢查询部分(C)。

快查询(汇编)

1.ENTRY _objc_msgSend

js 复制代码
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START

cmp x0, #0          // nil check and tagged pointer check
b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
ldr x13, [x0]       // x13 = isa
and x16, x13, #ISA_MASK // x16 = class  

上面代码的流程如下:

  • 首先ENTRY _objc_msgSend进入消息发送流程
  • 进入LNilOrTagged,当数据类型是NSTaggedPointer或消息名为空时,直接END_ENTRY _objc_msgSend,不为空则继续执行下一步

NSTaggedPointer类型的对象采用和isa一样的联合体位域的方式,可直接从地址中读取出想要的值,一般当数据类型的"value"足够小时,系统会自动转换为NSTaggedPointer类型,比如NSString转换为NSTaggedPointerString

2.CacheLookup NORMAL

接着进入缓存查询阶段

js 复制代码
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached
  • LGetIsaDone,这一步表示isa指针已经处理完,要开始缓存查询CacheLookup NORMAL(从注释可知要么从缓存中找到IMP,要么执行objc_msgSend_uncached 从方法列表中获取IMP)

进入CacheLookup部分,

js 复制代码
//代码剔除了混淆理解的部分
.macro CacheLookup
	// p1 = SEL, p16 = isa
	ldp	p10, p11, [x16, #CACHE]	// p10 = buckets, p11 = occupied|mask
	and	w12, w1, w11		// x12 = _cmd & mask
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
		             // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	ldp	p17, p9, [x12]		// {imp, sel} = *bucket
1:	cmp	p9, p1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: p12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	p12, p10		// wrap if bucket == buckets
	b.eq	3f
	ldp	p17, p9, [x12, #-BUCKET_SIZE]!	// {imp, sel} = *--bucket
	b	1b			// loop
.endmacro
  • CacheLookup NORMAL 去缓存中找IMP,会有以下2种情况
    • CacheHit在缓存中找到了则直接calls imp
    • CheckMiss没找到则执行objc_msgSend_uncached

CacheHit

CacheHit部分代码如下

js 复制代码
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x12	// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	AuthAndResignAsIMP x0, x12	// authenticate imp and re-sign as IMP
	ret				// return IMP
.elseif $0 == LOOKUP
	AuthAndResignAsIMP x17, x12	// authenticate imp and re-sign as IMP
	ret				// return imp via x17
.endmacro

这里如果缓存命中,则call imp直接调用方法实现了。

CheckMiss

若没命中接着看CheckMiss部分

js 复制代码
.macro CheckMiss
	// miss if bucket->sel == 0
.if $0 == GETIMP
	cbz	p9, LGetImpMiss
.elseif $0 == NORMAL
	cbz	p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
	cbz	p9, __objc_msgLookup_uncached
.endmacro

这里执行了objc_msgSend_uncached,进入__objc_msgSend_uncached部分

js 复制代码
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search

MethodTableLookup
TailCallFunctionPointer x17

END_ENTRY __objc_msgSend_uncached

这里告诉我们要执行MethodTableLookup

js 复制代码
.macro MethodTableLookup
    bl  __class_lookupMethodAndLoadCache3
.endmacro

这里可以得知MethodTableLookup实际调用的是objc-runtime-new.mm_class_lookupMethodAndLoadCache3方法,从这里开始便从汇编到C语言执行了。

慢查询部分(C)

1._class_lookupMethodAndLoadCache3

c 复制代码
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

_class_lookupMethodAndLoadCache3方法调用了lookUpImpOrForward函数

2.lookUpImpOrForward

完整函数为IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver),代码冗长,精简为伪代码如下:

objc 复制代码
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    ···
    ···
    // 首次从 _class_lookupMethodAndLoadCache3 进来,cache 为 NO
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    
 retry: 
    // 1. 首先查询类对象的缓存,存在则结束流程
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 2. 尝试从类对象中获取 method
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        // 2.1 若成功获得 method,则存入缓存并结束流程
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 3. 若类对象处没有获得 method,则循环遍历父类,尝试从父类获取
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            ···
            // 3.1 父类也是先查询缓存
            imp = cache_getImp(curClass, sel);
            if (imp) {
                log_and_fill_cache(cls, imp, sel, inst, curClass);
                goto done;
            }
            
            // 3.2 尝试从父类对象中获取 method
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    
    // 4. 如果以上都没获取到 IMP,则查看该类是否实现了动态方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();

        triedResolver = YES;
        goto retry;
    }

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    ···

    return imp;
}

getMethodNoSuper_nolock

c 复制代码
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) {
    //循环遍历method列表,查询方法
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        //mlists是method_list_t类型的方法列表,根据方法排序过或未排序采用不同的方式查询
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}

//具体的查询方法
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    ···
    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        //对于排序过的方法列表,采用二分查找的方式查找
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        // Linear search of unsorted method list
        //对于未排序的方法列表采用遍历的方式查找
        for (auto& meth : *mlist) {
            if (meth.name == sel) return &meth;
        }
    }
    ···
    return nil;
}

log_and_fill_cache

当从classmethod_list中获取到method,要执行log_and_fill_cache存储到缓存

c 复制代码
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
    cache_fill (cls, sel, imp, receiver);
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    //获取类的缓存
    cache_t *cache = getCache(cls);
    //这里可以知道cache是以方法名作为key
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    //在当前缓存大小大于缓存池容量的 3/4 时,需要扩大缓存,在低于 3/4 时可以正常存储
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        //满了就扩大缓存容量,扩大为2倍
        cache->expand();
    }

    // 把方法的IMP存储到cache中
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

存储缓存的方法,系统设计了一个动态数组,在元素个数高于 3/4 时需要扩容为 2 倍。

小结

lookUpImpOrForward查找imp可以分以下几点:

  1. 尝试获取类对象缓存imp = cache_getImp(cls, sel);
  2. 执行getMethodNoSuper_nolock(cls, sel),尝试从类对象的method_list中查询Method
    2.1. 若存在Method,则执行log_and_fill_cache存入缓存
    2.2. 不存在,则执行第 3 步
  3. 循环遍历父类,尝试从父类获取IMP
    3.1. 父类也是先查询缓存
    3.2. 查询父类对象的method_listgetMethodNoSuper_nolock(curClass, sel);,若查询到method则同样存入缓存,log_and_fill_cache
  4. 如果以上都没获取到 IMP,则触发_class_resolveMethod,查看该类是否实现了动态方法解析

3._class_resolveMethod

lookUpImpOrForward方法中,若查询类对象和其父类对象都没有找到IMP,则执行_class_resolveMethod方法,直接看代码

c 复制代码
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
   // 判断当前是否是元类
   if (! cls->isMetaClass()) {
       // 类,尝试找实例方法
       _class_resolveInstanceMethod(cls, sel, inst);
   }
   else {
       // 是元类,先找类方法
       _class_resolveClassMethod(cls, sel, inst);
       if (!lookUpImpOrNil(cls, sel, inst,
                           NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
       {
           // 为什么这里还要查找一次呢?下面会分析
           _class_resolveInstanceMethod(cls, sel, inst);
       }
   }
}

_class_resolveMethod的目的是调用执行类cls里实现的+ (BOOL)resolveInstanceMethod:方法或+ (BOOL)resolveClassMethod:,根据返回值判断是否有动态实现 (返回YES表示有动态实现,比如class_addMethod(self, sel, readImp, type))。

_class_resolveInstanceMethod

c 复制代码
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // 若类对象没有实现 + (BOOL)resolveInstanceMethod:(SEL)sel 方法
    // 则直接 return,lookUpImpOrForward 那层会执行 _cache_addForwardEntry(cls, sel) 寻找备用接受者
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    // 这里已经知道类对象实现了resolveInstanceMethod方法
    // objc_msgSend发送消息调用该方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // 既然 resolveInstanceMethod 已经处理了这个 sel,就再执行一次
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    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));
        }
    }
}

_class_resolveInstanceMethod里面调用了lookUpImpOrNil,进而会调用到lookUpImpOrForward函数,如果查不到会再执行_class_resolveMethod函数,形成了递归。

若实现了动态方法解析 ,并对类增加了IMP,则之后会再执行一次lookUpImpOrNil,这次会找到IMP并执行。

_class_resolveClassMethod

_class_resolveClassMethod_class_resolveInstanceMethod 逻辑差不多,只不过类方法是去元类里处理。

c 复制代码
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
     assert(cls->isMetaClass());
     // 去元类里面找 resolveClassMethod,没有找到直接返回空
     if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                          NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
     {
         // Resolver not implemented.
         return;
     }
     // 给类发送 resolveClassMethod 消息
     BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
     // _class_getNonMetaClass 对元类进行初始化准备,以及判断是否是根元类的一些判断,有兴趣的可以自己去看看
     bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                         SEL_resolveClassMethod, sel);
     // 再次去查找方法
     IMP imp = lookUpImpOrNil(cls, sel, inst, 
                              NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

     // 省略了一些不重要的报错信息代码
     ... 
}

小结

  • _class_resolveInstanceMethod
    • lookUpImpOrNil开始循环查找类、父类等是否实现了resolveInstanceMethod,为防止死循环,NSObject里实现了该方法,并返回NO,表示没有进行处理
    • objc_msgSend发送消息给类对象调用手动实现的resolveInstanceMethod
  • _class_resolveClassMethod
    • lookUpImpOrNil 检查是否实现了 resolve..方法
    • objc_msgSend 发送消息给类对象调用手动实现的resolveClassMethod,注意这里还是类对象,因为没办法发送给元类,元类是苹果虚拟出来的类

Q:如果是元类,为什么调用完_class_resolveClassMethod后要再调用一次_class_resolveInstanceMethod

因为类方法在元类中是以实例的形式存在的,

正常的查找步骤如下:
Person(类方法)->
元类(实例方法)->
根元类(实例方法)->
NSObject(实例方法)

如果我们有个NSObject 分类实现了+ (BOOL)resolveInstanceMethod:(SEL)sel处理了没有imp的情况,就解决了没调用到目标方法的问题,所以再次调用_class_resolveInstanceMethod其实是苹果爸爸给机会。

Q:为什么_class_resolveMethod函数里要判断非元类执行_class_resolveInstanceMethod,元类执行_class_resolveClassMethod

不要想复杂了,这里就是规定,

  • 动态决议类调用实例方法,就实现+ (BOOL)resolveInstanceMethod:(SEL)sel
  • 动态决议类调用类方法,就实现+ (BOOL)resolveClassMethod:(SEL)sel

系统内部会按需调用的。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver),当调用类方法时,cls指向元类,inst指向的类对象。

消息转发

如果上面的流程走完没有找到IMP,那么还有接下来三种补救方法,即动态方法解析、备用接收者、消息转发

1.动态方法解析

resolveInstanceMethodresolveClassMethod是系统给我们的一次机会,让我们可以针对没有实现的 sel 进行自定义操作 示例如下:

objc 复制代码
//调用对象的实例方法时,先执行resolveInstanceMethod方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        SEL readBookSel = @selector(readBook);
        Method readM = class_getInstanceMethod(self, readBookSel);
        IMP readImp = class_getMethodImplementation(self, readBookSel);
        const char *type = method_getTypeEncoding(readM);
        return class_addMethod(self, sel, readImp, type);
    }
    return [super resolveInstanceMethod:sel];
}

//调用类方法时,先执行resolveClassMethod方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(run)) {
        SEL readBookSel = @selector(readBook);
        Method readM = class_getInstanceMethod(self, readBookSel);
        IMP readImp = class_getMethodImplementation(self, readBookSel);
        const char *type = method_getTypeEncoding(readM);
        return class_addMethod(self, sel, readImp, type);
    }
    
    return [super resolveClassMethod:sel];
}

2.备用接收者

如果动态方法解析 依然没有结果,就会继续寻找备用接收者(id)forwardingTargetForSelector:(SEL)aSelector; 这个forwarding方法在runtime底层是通过汇编调用的,苹果并没有给出源码实现,但是我们可以通过打印runtime的方法调用信息(log),来查看runtime接下来调用了哪些方法.方式如下:

objectivec 复制代码
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        instrumentObjcMessageSends(YES);
        [LGPerson  walk];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

这样在finder中找到/private/tmp/文件夹,找到最新的msgSends-xxxx文件,这个文件就是LGPerson调用walk方法的时候所执行的方法信息,如图所示

显而易见的,LGPerson在动态方法解析后,继续调用了NSObject的动态方法解析,在都没有imp的情况下调用了forwardingTargetForSeletor(备用接收者)方法,当备用接受者也没有实现时,会调用最后一步methodSignatureForSelector(消息转发)

举例如下:

objc 复制代码
//寻找备用接收者,让别的类来提供同名的方法
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(walk)) {
        return [LGStudent new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

在forwardingTargetForSelector方法中我们可以坐哪些操作?

  1. 找备用接收者处理当前类没有实现的方法(注意:备用接收者的方法只能是实例方法)
  2. crash收集
  3. 防止崩溃

3.消息转发

如果备用接收者没有找到IMP方法,则执行消息转发,主要涉及以下两个方法

objc 复制代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

在动态方法解析和备用接收者都无法找到IMP时,系统会调用methodSignatureForSelector方法,如果方法返回nil,则系统直接调用doesNotRecognizeSelector方法使程序崩溃并打印崩溃信息;如果返回一个NSMethodSignature类型的函数签名,则系统会创建一个NSInvocation对象并调用forwardInvocation方法,我们可以在方法中指定别的方法或其他对象去执行这个的方法。 例子如下

objc 复制代码
//若返回函数签名,则继续调用forwardInvocation方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(run)) {
        //指定当前类的一个方法作为IMP
//        anInvocation.selector = @selector(readBook);
//        [anInvocation invoke];
        
        //指定其他类来执行这个IMP
        LGStudent *student = [LGStudent new];
        [anInvocation invokeWithTarget:student];
    }
}

参考链接

Runtime 源码
Runtime 可编译源码
苹果官方文档 Runtime
iOS Runtime详解

相关推荐
恋猫de小郭5 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
网安墨雨9 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
福大大架构师每日一题10 小时前
37.1 prometheus管理接口源码讲解
ios·iphone·prometheus
BangRaJun1 天前
LNCollectionView-替换幂率流体
算法·ios·设计
刘小哈哈哈1 天前
iOS 多个输入框弹出键盘处理
macos·ios·cocoa
靴子学长1 天前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
一如初夏丿2 天前
xcode15 报错 does not contain ‘libarclite‘
ios·xcode
杨武博2 天前
ios 混合开发应用白屏问题
ios
BangRaJun2 天前
LNCollectionView
android·ios·objective-c
二流小码农3 天前
鸿蒙元服务项目实战:终结篇之备忘录搜索功能实现
android·ios·harmonyos