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详解

相关推荐
若水无华2 天前
fiddler 配置ios手机代理调试
ios·智能手机·fiddler
Aress"2 天前
【ios越狱包安装失败?uniapp导出ipa文件如何安装到苹果手机】苹果IOS直接安装IPA文件
ios·uni-app·ipa安装
Jouzzy2 天前
【iOS安全】Dopamine越狱 iPhone X iOS 16.6 (20G75) | 解决Jailbreak failed with error
安全·ios·iphone
瓜子三百克2 天前
采用sherpa-onnx 实现 ios语音唤起的调研
macos·ios·cocoa
左钦杨2 天前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
努力成为包租婆2 天前
SDK does not contain ‘libarclite‘ at the path
ios
安和昂3 天前
【iOS】Tagged Pointer
macos·ios·cocoa
I烟雨云渊T3 天前
iOS 阅后即焚功能的实现
macos·ios·cocoa
struggle20253 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
Unlimitedz3 天前
iOS视频编码详细步骤(视频编码器,基于 VideoToolbox,支持硬件编码 H264/H265)
ios·音视频