探秘OC消息发送机制

发表于 2年以前  | 总阅读数:351 次

探秘OC消息发送机制

整体分析

runtime的消息机制总体可以被分为三部分,也会被拆分为四步或者其他拆分法,总之流程对就行。

  1. 消息发送:刚调用objc_msgSend函数后,内部的一些处理逻辑。会涉及到cache listmethod list等。
  2. 动态方法解析:允许开发者动态创建方法。
  3. 消息转发:进入消息转发阶段。

方法调用

流程

当一个对象被创建时,系统会为其分配内存,并完成默认的初始化工作,例如对实例变量进行初始化。对象第一个变量是指向其类对象的isa指针,isa指针可以访问其类对象,并且通过其类对象拥有访问其所有继承者链中的类。

当对象接收到一条消息时,消息函数随着对象isa指针到类的结构体中,在method list中查找方法selector。如果在本类中找不到对应的selector,则objc_msgSend会向其父类的method list中查找selector,如果还不能找到则沿着继承关系一直向上查找,直到找到NSObject类。

runtimeselector查找的过程做了优化,为类的结构体中增加了cache字段,每个类都有独立的cache,在一个selector被调用后就会加入到cache中。在每次搜索方法列表之前,都会先检查cache中有没有,如果没有才调用方法列表,这样会提高方法的查找效率。

如果通过OC代码的调用都会走消息发送的阶段,如果不想要消息发送的过程,可以获取到方法的函数指针直接调用。通过NSObjectmethodForSelector:方法可以获取到函数指针,获取到指针后需要对指针进行类型转换,转换为和调用函数相符的函数指针,然后发起调用即可。

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

objc_msgSend

OC中方法调用是通过runtime实现的,runtime进行方法调用本质上是发送消息,通过objc_msgSend()函数进行消息发送。

例如下面的OC代码会被转换为runtime代码。

原方法:[object testMethod]
转换后的调用:objc_msgSend(object, @selector(testMethod));

发送消息的第二个参数是一个SEL类型的参数,在项目里经常会出现,不同的类定义了相同的方法,这样就会有相同的SEL。那么问题就来了,也是很多人博客里都问过的一个问题,不同类的SEL是同一个吗?

然而,事实是通过我们的验证,创建两个不同的类,并定义两个相同的方法,通过@selector()获取SEL并打印。我们发现SEL都是同一个对象,地址都是相同的。由此证明,不同类的相同SEL是同一个对象。

@interface TestObject : NSObject
- (void)testMethod;
@end

@interface TestObject2 : NSObject
- (void)testMethod;
@end

// TestObject2实现文件也一样
@implementation TestObject
- (void)testMethod {
    NSLog(@"TestObject testMethod %p", @selector(testMethod));
}
@end

// 结果:
TestObject testMethod 0x100000f81
TestObject2 testMethod 0x100000f81

runtime中维护了一个SEL的表,这个表存储SEL不按照类来存储,只要相同的SEL就会被看做一个,并存储到表中。在项目加载时,会将所有方法都加载到这个表中,而动态生成的方法也会被加载到表中。

objc_msgSendSuper

一个对象被创建后,自身的类及其父类一直到NSObject类的部分,都会包含在对象的内存中,例如其父类的实例变量。当通过[super class]的方式调用其父类的方法时,会创建一个结构体。

struct objc_super { id receiver; Class class; };

super的调用会被转化为objc_msgSendSuper()的调用,并在其内部调用objc_msgSend()函数。有一点需要注意,尽管是通过[super class]的方式调用的,但传入的receiver对象仍然是self,返回结果也是selfclass

由此可知,当前对象无论调用任何方法,receiver都是当前对象。但是,objc_msgSendSuper方法的意义在于,查找方法实现时,从父类的类对象中去搜索,而不是从当前类对象中去搜索。

objc_msgSend(objc_super->receiver, @selector(class))

objc_msg.s中,存在多个版本的objc_msgSend函数。内部实现逻辑大体一致,都是通过汇编实现的,只是根据不同的情况有不同的调用。

objc_msgSend
objc_msgSend_fpret
objc_msgSend_fp2ret
objc_msgSend_stret
objc_msgSendSuper
objc_msgSendSuper_stret
objc_msgSendSuper2
objc_msgSendSuper2_stret

在上面源码中,带有super的会在外界传入一个objc_super的结构体对象。stret表示返回的是struct类型,super2objc_msgSendSuper()的一种实现方式,不对外暴露。

struct objc_super {
 id receiver;
 Class class;
};

fp则表示返回一个long double的浮点型,而fp2则返回一个complex long double的复杂浮点型,其他floatdouble的普通浮点型都用objc_msgSend。除了上面这些情况外,其他都通过objc_msgSend()调用。

隐藏参数

❝我们在方法内部可以通过self获取到当前对象,但是self又是从哪来的呢?

方法实现的本质也是C函数,C函数除了方法传入的参数外,还会有两个默认参数,这两个参数在通过objc_msgSend()调用时也会传入。这两个参数在runtime中并没有声明,而是在编译时自动生成的。

objc_msgSend的声明中可以看出这两个隐藏参数的存在。

objc_msgSend(void /* id self, SEL op, ... */ )
  • self,调用当前方法的对象。
  • _cmd,当前被调用方法的SEL

虽然这两个参数在调用和实现方法中都没有明确声明,但是我们仍然可以使用它。响应对象就是self,被调用方法的selector_cmd

- (void)method {
    id  target = getTheReceiver();
    SEL method = getTheMethod();

    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

源码分析

_objc_msgSend汇编

runtime中,objc_msgSend函数也是开源的,但其是通过汇编代码实现的,arm64架构代码可以在objc-msg-arm64.s中找到。在runtime中,很多执行频率比较高的函数,都是用汇编写的。

objc_msgSend并不是完全开源的,在_class_lookupMethodAndLoadCache3函数中已经获取到Class参数了。所以在下面中有一个肯定是对象中获取isa_t的过程,从方法命名和注释来看,应该是GetIsaFast汇编命令。如果这样的话,就可以从消息发送到调用流程衔接起来了。

ENTRY _objc_msgSend
 MESSENGER_START

 NilTest NORMAL

 GetIsaFast NORMAL  // r11 = self->isa
 CacheLookup NORMAL  // calls IMP on success

 NilTestSupport NORMAL

 GetIsaSupport    NORMAL

// cache miss: go search the method lists
LCacheMiss:
 // isa still in r11
 MethodTableLookup %a1, %a2 // r11 = IMP
 cmp %r11, %r11  // set eq (nonstret) for forwarding
 jmp *%r11   // goto *imp

 END_ENTRY _objc_msgSend
  • MESSENGER_START:消息开始执行。
  • NilTest:判断接收消息的对象是否为nil,如果为nil则直接返回,这就是对nil发送消息无效的原因。
  • GetIsaFast:快速获取到isa指向的对象,是一个类对象或元类对象。
  • CacheLookup:从cache list中获取缓存selector,如果查到则调用其对应的IMP
  • LCacheMiss:缓存没有命中,则执行此条汇编下面的方法。
  • MethodTableLookup:如果缓存中没有找到,则从method list中查找。

方法缓存

如果每次进行方法调用时,都按照对象模型来进行方法列表的查找,这样是很消耗时间的。runtime为了优化调用时间,在objc_class中添加了一个cache_t类型的cache字段,通过缓存来优化调用时间。

在执行objc_msgSend函数的消息发送过程中,同一个方法第一次调用是没有缓存的,但调用之后就会存在缓存,之后的调用就直接调用缓存。所以方法的调用,可以分为有缓存和无缓存两种,这两种情况下的调用堆栈是不同的。

首先是从缓存中查找IMP,但是由于cache3调用lookUpImpOrForward函数时,已经查找过cache了,所以传入的是NO,不进入查找cahce的代码块中。

struct cache_t {
    // 存储被缓存方法的哈希表
    struct bucket_t *_buckets;
    // 占用的总大小,哈希表的长度减一
    mask_t _mask;
    // 已使用大小
    mask_t _occupied;
}

在哈希表bucket_t的定义中,通过cache_key_t作为存储的key,在调用方法时会先查找缓存,如果key能匹配上,则调用对应的函数地址。

struct bucket_t {
    // SEL作为key
    cache_key_t _key;
    // 函数地址
    IMP _imp;
};

当给一个对象发送消息时,runtime会沿着isa找到对应的类对象,但并不会立刻查找method_list,而是先查找cache_list,如果有缓存的话优先查找缓存,没有再查找方法列表。

这是runtime对查找method的优化,理论上来说在cache中的method被访问的频率会更高。cache_listcache_t定义,内部有一个bucket_t的数组,数组中保存IMPkey,通过key找到对应的IMP并调用。具体源码可以查看objc-cache.mm

如果类对象没有被初始化,并且lookUpImpOrForward函数的initialize参数为YES,则表示需要对该类进行创建。函数内部主要是一些基础的初始化操作,而且会递归检查父类,如果父类未初始化,则先初始化其父类对象。随后会查找父类的cache,如果cache里没有就会查找父类的方法列表,以此类推。一直找到NSObject,如果依然没有则方法未找到并出现异常。

STATIC_ENTRY _cache_getImp

mov r9, r0
CacheLookup NORMAL
// cache hit, IMP in r12
mov r0, r12
bx lr   // return imp

CacheLookup2 GETIMP
// cache miss, return nil
mov r0, #0
bx lr

END_ENTRY _cache_getImp

下面会进入cache_getImp的代码中,然而这个函数不是开源的,但是有一部分源码可以看到,是通过汇编写的。其内部调用了CacheLookupCacheLookup2两个函数,这两个函数也都是汇编写的。

经过第一次调用后,就会存在缓存。进入objc_msgSend后会调用CacheLookup命令,如果找到缓存则直接调用。但是runtime并不是完全开源的,内部很多实现我们依然看不到,CacheLookup命令内部也一样,只能看到调用完命令后就开始执行我们的方法了。

CacheLookup NORMAL, CALL

cache list中找不到方法的情况下,会通过MethodTableLookup宏定义从类的方法列表中,查找对应的方法。在MethodTableLookup中本质上也是调用_class_lookupMethodAndLoadCache3函数,只是在传参时cache字段传NO,表示不从cache list中查找。

cache3函数中,是直接调用的lookUpImpOrForward函数,这个函数内部实现很复杂,可以看一下runtime analyze。在这个里面直接搜lookUpImpOrForward函数名即可,可以详细看一下内部实现逻辑。

runtime analyze链接:https://github.com/DeveloperErenLiu/RuntimeAnalyze

转发核心源码

在上面objc_msgSend汇编实现中,存在一个MethodTableLookup的汇编调用。在这条汇编调用中,调用了查找方法列表的C函数。下面是精简版代码。

需要注意的是,汇编中的代码会比C语言多一个下划线,以__class_lookupMethodAndLoadCache3函数的调用为例,如果是C语言应该去掉一个下划线,改成_class_lookupMethodAndLoadCache3来搜索。

.macro MethodTableLookup

   // 调用MethodTableLookup并在内部执行cache3函数(C函数)
 bl __class_lookupMethodAndLoadCache3
 mov r12, r0   // r12 = IMP

.endmacro

MethodTableLookup中通过调用_class_lookupMethodAndLoadCache3函数,来查找方法列表。函数内部是通过lookUpImpOrForward函数实现的,在调用时cache字段传入NO,表示不需要查找缓存了,因为在cache3函数上面已经通过汇编查找过了。

由于_class_lookupMethodAndLoadCache3函数只做了一个中转,所以在最新的objc4-779.1版本中已经没有这个函数,而是直接由汇编调用lookUpImpOrForward函数。

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    // 通过cache3内部调用lookUpImpOrForward函数
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward函数是支持多线程的,所以内部会有很多锁操作。其内部有一个rwlock_t类型的runtimeLock变量,有runtimeLock控制读写锁。其内部有很多逻辑代码,这里把函数内部实现做了精简,把核心代码贴到下面。

通过类对象的isRealized函数,判断当前类是否被实现,如果没有被实现,则通过realizeClass函数实现该类。在realizeClass函数中,会设置versionrwsuperClass等一些信息。

// 执行查找imp和转发的代码
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 如果cache是YES,则从缓存中查找IMP。如果是从cache3函数进来,则不会执行cache_getImp()函数
    if (cache) {
        // 通过cache_getImp函数查找IMP,查找到则返回IMP并结束调用
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.read();

    // 判断类是否已经被创建,如果没有被创建,则将类实例化
    if (!cls->isRealized()) {
        // 对类进行实例化操作
        realizeClass(cls);
    }

    // 第一次调用当前类的话,执行initialize的代码
    if (initialize  &&  !cls->isInitialized()) {
        // 对类进行初始化,并开辟内存空间
        _class_initialize (_class_getNonMetaClass(cls, inst));
    }

 retry:    
    runtimeLock.assertReading();

    // 从子类的缓存列表中查找,如果从子类的缓存中找到则直接结束
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    {
        // 如果没有从子类的缓存列表中找到,则从子类的方法列表中查找
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果从子类的方法列表中查找到实现,则加入缓存并从Method获取IMP
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 如果在当前类中没有找到,则向父类查找方法,会逐级向上查找,直到NSObject根类
    {
        unsigned attempts = unreasonableClassCount();
        // 循环获取这个类的缓存IMP或方法列表的IMP
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }

            // 获取父类缓存的IMP
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 如果发现父类的缓存方法,会将父类方法缓存到子类的缓存列表中
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }

            // 如果父类缓存中没有,则查找父类的方法列表
            // 方法内部会判断方法数组是否排序,如果排序则使用二分查找,未排序则使用遍历
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 在父类的方法列表中查找到,将父类的方法缓存到子类的缓存列表中
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // 如果没有找到方法实现,则尝试动态方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }

    // 如果没有IMP被发现,并且动态方法解析也没有处理,则进入消息转发阶段
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

在方法第一次调用时,可以通过cache_getImp函数查找到缓存的IMP。但如果是第一次调用,就查不到缓存的IMP,就会进入到getMethodNoSuper_nolock函数中执行。下面是getMethod函数的关键代码。

getMethodNoSuper_nolock(Class cls, SEL sel) {
    // 根据for循环,从methodList列表中,从头开始遍历,每次遍历后向后移动一位地址。
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        // 对sel参数和method_t做匹配,如果匹配上则返回。
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

当调用一个对象的方法时,查找对象的方法,本质上就是遍历对象isa所指向类的方法列表,并用调用方法的SEL和遍历的method_t结构体的name字段做对比,如果相等则将IMP函数指针返回。

// 根据传入的SEL,查找对应的method_t结构体
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
    int methodListIsFixedUp = mlist->isFixedUp();
    int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);

    if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
        return findMethodInSortedMethodList(sel, mlist);
    } else {
        for (auto& meth : *mlist) {
            // SEL本质上就是字符串,查找的过程就是进行字符串对比
            if (meth.name == sel) return &meth;
        }
    }

    if (mlist->isFixedUp()) {
        for (auto& meth : *mlist) {
            if (meth.name == sel) {
                _objc_fatal("linear search worked when binary search did not");
            }
        }
    }

    return nil;
}

getMethod函数中,主要是对Classmethods方法列表进行查找和匹配。类的方法列表都在Classclass_data_bits_t中,通过data()函数从bits中获取到class_rw_t的结构体,然后获取到方法列表methods,并遍历方法列表。

如果从当前类中获取不到对应的IMP,则进入循环中。循环是从当前类出发,沿着继承者链的关系,一直向根类查找,直到找到对应的IMP实现。

查找步骤和上面也一样,先通过cache_getImp函数查找父类的缓存,如果找到则调用对应的实现。如果没找到缓存,表示第一次调用父类的方法,则调用getMethodNoSuper_nolock函数从方法列表中获取实现。

for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
{
    imp = cache_getImp(curClass, sel);
    if (imp) {
        if (imp != (IMP)_objc_msgForward_impcache) {
            log_and_fill_cache(cls, imp, sel, inst, curClass);
            goto done;
        }
    }

    Method meth = getMethodNoSuper_nolock(curClass, sel);
    if (meth) {
        log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
        imp = meth->imp;
        goto done;
    }
}

如果满足条件并且是第一次进行动态方法决议,则进入if语句中调用_class_resolveMethod函数。动态方法决议有两种,_class_resolveClassMethod类方法决议和_class_resolveInstanceMethod实例方法决议。

BOOL (*msg)(Class, SEL, SEL) = (__typeof__(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

在这两个动态方法决议的函数实现中,本质上都是通过objc_msgSend函数,调用NSObject中定义的resolveInstanceMethod:resolveClassMethod:两个方法。

可以在这两个方法中动态添加方法,添加方法实现后,会在下面执行goto retry,然后再次进入方法查找的过程中。如果对未实现的SEL添加了实现代码,则这次执行会找到对应的实现并调用,否则就会再次进入triedResolver的判断,由于已经执行过一次,triedResolverYES,则会进入消息转发流程。

triedResolver参数可以看出,动态方法决议的机会只有一次,如果这次再没有找到,则进入消息转发流程。

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

如果经过上面这些步骤,还是没有找到方法实现的话,则进入动态消息转发中。在动态消息转发中,还可以对没有实现的方法做一些弥补措施。

下面是通过objc_msgSend函数发送一条消息后,所经过的调用堆栈,调用顺序是从上到下的。

CacheLookup NORMAL, CALL
__objc_msgSend_uncached
MethodTableLookup NORMAL
_class_lookupMethodAndLoadCache3
lookUpImpOrForward

调用总结

在调用objc_msgSend函数后,会有一系列复杂的判断逻辑,总结如下。

  1. 判断当前调用的SEL是否需要忽略,例如Mac OS中的垃圾处理机制启动的话,则忽略retainrelease等方法,并返回一个_objc_ignored_methodIMP,用来标记忽略。
  2. 判断接收消息的对象是否为nil,因为在OC中对nil发消息是无效的,这是因为在调用时就通过判断条件过滤掉了。
  3. 从方法的缓存列表中查找,通过cache_getImp函数进行查找,如果找到缓存则直接返回IMP
  4. 查找当前类的method list,查找是否有对应的SEL,如果有则获取到Method对象,并从Method对象中获取IMP,并返回IMP(这步查找结果是Method对象)。
  5. 如果在当前类中没有找到SEL,则去父类中查找。首先查找cache list,如果缓存中没有则查找method list,并以此类推直到查找到NSObject为止。
  6. 如果在类的继承体系中,始终没有查找到对应的SEL,则进入动态方法解析中。可以在resolveInstanceMethodresolveClassMethod两个方法中动态添加实现。
  7. 动态消息解析如果没有做出响应,则进入动态消息转发阶段。此时可以在动态消息转发阶段做一些处理,否则就会Crash

动态消息解析

resolveInstanceMethod

当一个对象的方法被调用时,首先在对象所属的类中查找方法列表,如果当前类中没有则向父类查找,一种找到根类NSObject。如果始终没有找到方法实现,则进入动态消息解析。

当一个方法没有实现时,也就是在cache lsit和其继承关系的method list中,没有找到对应的方法。这时会进入消息转发阶段,但是在进入消息转发阶段前,runtime会给一次机会动态添加方法实现。

如果想实现动态方法解析,需要实现resolveInstanceMethod:resolveClassMethod:方法,在这两个方法中动态的添加方法实现。这两个方法都有一个BOOL返回值,返回NO则进入消息转发机制,返回YES则表示要进行动态消息转发。通过class_addMethod方法可以动态添加方法,添加方法时需要关联对应的函数指针,函数指针需要声明两个隐藏参数self_cmd

在通过class_addMethod函数动态添加实现时,后面有一个"v@:"来描述SEL对应的函数实现。

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v16@0:8");
          return YES;
    }
    return [super resolveInstanceMethod:sel];
}

消息转发和动态方法解析大部分是相同的,在消息转发之前一个类有机会动态解析这个方法。如果已经调用respondsToSelector:instancesRespondToSelector:方法,动态方法解析有机会优先为Selector添加IMP。如果你实现了resolveInstanceMethod:方法,但想要特定的Selector走消息转发流程,则将此方法返回NO即可。

也可以自定义一个方法,转发到某个固定的方法中。

动态消息解析的本质,就是有一次对未实现方法添加实现的机会,就是下图中的位置,随后会再走一遍消息发送流程。如果这次并未处理,则下次再执行消息发送流程,就会进入消息转发流程,因为triedResolver的判断,动态方法解析只会进入一次。

源码分析

lookUpImpOrForward函数中执行消息转发代码,会进入这里的实现。在下面会判断是否元类,如果非元类则执行resolveInstanceMethod函数,元类则执行resolveClassMethod函数。执行完消息转发后,会继续执行lookUpImpOrForward函数。

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

    if (! cls->isMetaClass()) {
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

resolveInstanceMethod为例,消息转发的实现很简单,就是调用resolveInstanceMethod方法,开发者可以在方法中进行动态添加方法实现,并没有其他操作。resolveInstanceMethod方法的返回值其实并不重要,这里只对返回值进行了打印,并没有其他影响结果的代码。

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, resolve_sel, sel);
    IMP imp = lookUpImpOrNil(inst, sel, cls);
}

消息转发

如果动态方法解析过程中依然没有处理未实现的方法调用,则进入消息转发阶段。

forwardingTargetForSelector

消息转发的最开始,可以在forwardingTargetForSelector:方法中将未实现的消息,转发给其他对象。可以在下面方法中,返回响应未实现方法的其他对象,系统将会调用这个对象的同名方法。

forwardingTargetForSelector方法并未开源,但通过汇编代码可以看到。方法的实现也比较简单,进行一些简单的判断后会通过objc_msgSend调用forwardingTargetForSelector方法,并接收到返回的对象,调用对象同名的方法。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSString *selectorName = NSStringFromSelector(aSelector);
    if ([selectorName isEqualToString:@"selector"]) {
        return object;
    }
    return [super forwardingTargetForSelector:aSelector];
}

需要注意的是,如果是处理类方法的调用,需要实现类方法的forwardingTargetForSelector方法。也就是加号开头的方法,在NSObject.h中虽然未声明,但方法是存在且可以使用的。

methodSignatureForSelector

根据消息转发的汇编代码实现,当forwardingTargetForSelector:方法未做出任何响应的话,会来到methodSignatureForSelector:方法,在方法内部生成NSMethodSignature类型的方法签名对象。在生成签名对象时,可以指定targetSEL,可以将这两个参数换成其他参数,将消息转发给其他对象。

// 自定义方法签名
[otherObject methodSignatureForSelector:otherSelector];

在方法签名的过程中,可以将未实现的方法转发给其代理。如果不实现则用默认的方法签名,如果返回nil则表示方法签名异常,直接导致crash

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(test)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}

forwardInvocation

生成NSMethodSignature签名对象后,就会调用forwardInvocation方法,这是消息转发中最后一步了,如果在这步还没有对消息进行处理,则会导致崩溃。

系统会调用响应者的forwardInvocation方法,并传入一个NSInvocation对象,NSInvocation对象中包含原始消息及参数。这个方法只有方法未实现的时候才会调用。可以实现forwardInvocation方法,将消息转发给另一个对象。forwardInvocation方法是一个动态方法,在响应者无法响应方法时,会调用forwardInvocation方法,可以重写这个方法实现消息转发。

消息转发中forwardInvocation需要做的是,确认消息将发送到哪里,以及用原始参数发送消息。可以通过invokeWithTarget方法,向target发送被转发的消息。调用invokeWithTarget方法后,原方法的返回值将被返回给调用方。

// object是接收消息的对象
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([object respondsToSelector:[anInvocation selector]]) {
        [anInvocation invokeWithTarget:object];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

forwardInvocation方法和forwardingTargetForSelector的区别在于,后者只能返回一个对象并且调用对象的同名方法。而forwardInvocation方法,可以在里面做任何事情,也可以不做任何处理,只进行一行打印,都代表对消息转发做出了响应。

也可以理解为,forwardInvocation中的处理,就代表了方法中处理了selector原本的事件。也可以调用当前对象的其他方法,selectortarget都不是readonly的,随后调用invoke即可。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(test:)) {
        anInvocation.selector = @selector(run);
        [anInvocation invoke];
        return;
    }

    [super forwardInvocation:anInvocation];
}

类方法

同样的,如果是处理类方法的转发,也需要实现下面两个类方法。这两个类方法在NSObject.h中是没有声明的,也没有代码提示,需要自己写。

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
+ (void)forwardInvocation:(NSInvocation *)anInvocation;

NSInvocation

NSInvocation可以通过属性target获取被调用对象,通过selector获取被调用的方法,这两个属性都是可读可写的,也就是可以手动改变被调用的对象和方法。并且,可以通过下面方法,对参数进行处理。

// 根据索引,设置和获取参数
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

// 设置和获取返回值
- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

可以通过getArgument:atIndex:获取对应下标的参数,并对参数进行处理。需要注意的是,传入的是一个地址,所以需要对基础数据类型取地址传进去。参数对应的下标,第零个是target、第一个是selector,从第二个开始才是外面传入的参数。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(test:)) {
        int value = 0;
        [anInvocation getArgument:&value atIndex:2];
    }

    [super forwardInvocation:anInvocation];
}

doesNotRecognizeSelector

如果消息转发都没有处理未实现的方法,则会由doesNotRecognizeSelector方法抛出异常。当执行到这一步,就已经不能对未实现的方法进行任何处理了。如果想改写异常信息,可以重写此方法。

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSString *method = NSStringFromSelector(aSelector);
    if ([method isEqualToString:@"test:"]) {
        NSLog(@"未实现test:方法,请相关开发关注");
    }
}

应用

崩溃拦截

在项目中经常会出现因为调用未实现的方法,导致程序崩溃的情况。在学习消息转发后,就可以通过消息转发来解决这个问题。

所有的类的基类都是NSObject类(NSProxy除外),可以将NSObject类的消息转发流程拦截,然后做一些统一的处理,这样就可以解决方法未实现导致的崩溃。根据Category可以将原类方法“覆盖”的特点,可以在Category中实现相应的拦截方法。

// 接收消息的IMP
void dynamicResolveMethod(id self, SEL _cmd) {
    NSLog(@"method forward");
}

// 对NSObject创建的Category
@implementation NSObject (ExceptionForward)

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    const char *types = sel_getName(sel);
    class_addMethod([self class], sel, (IMP)dynamicResolveMethod, types);
    return YES;
}

#pragma clang diagnostic pop

@end

我们的拦截方案是在resolveInstanceMethod方法中,动态创建未实现的方法,并将IMP统一设置为dynamicResolveMethod函数进行处理。这样所有未实现的方法都会执行dynamicResolveMethod函数,而不崩溃,在dynamicResolveMethod函数中可以做崩溃统计等操作。

多继承

可以通过消息转发机制来模拟多继承,两个类中虽然不存在继承关系,但是却由另一个类处理了Warrior的事件。

由上面的例子可以看出,分属两个继承分支的类,通过消息转发机制实现了继承的关系。Warriornegotiate消息由其“父类”Diplomat来实现。

通过消息转发实现的多重继承相对于普通继承来说更有优势,消息转发可以将消息转发给多个对象,这样就可以将代码按不同职责封装为不同对象,并通过消息转发给不同对象处理。

需要注意的是,Warrior虽然通过消息转发机制可以响应negotiate消息,但如果通过respondsToSelector:isKindOfClass:方法进行判断的话,依然是返回NO的。如果想让这两个方法可以在判断negotiate方法时返回YES,需要重写这两个方法并在其中加入判断逻辑。

- (BOOL)respondsToSelector:(SEL)aSelector {
    if ([super respondsToSelector:aSelector]) {
        return YES;
    } else {
        // 
    }
    return NO;
}

OC中是不支持多继承的,但是可以通过消息转发模拟多继承。在子类中实例化多个父类,当消息发送过来的时候,在消息转发的方法中,将调用重定向到父类的实例对象中,以实现多继承的效果。

下面是多继承的例子,创建两个父类CatDog,并将需要子类继承的方法都定义到Protocol中,在CatDog中实现Protocol中的方法。

@protocol CatProtocol <NSObject>
- (void)eatFish;
@end

@interface Cat : NSObject <CatProtocol>
@end

@implementation Cat
- (void)eatFish {
    NSLog(@"Cat Eat Fish");
}
@end

@protocol DogProtocol <NSObject>
- (void)eatBone;
@end

@interface Dog : NSObject
@end

@implementation Dog
- (void)eatBone {
    NSLog(@"Dog Eat Bone");
}
@end

子类直接通过遵守父类的协议,来表示自己“继承”哪些类,并在内部实例化对应的父类对象。在外界调用协议方法时,子类其实是没有实现这些父类的方法的,所以通过转发方法将消息转发给响应的父类。

@interface TestObject : NSObject <CatProtocol, DogProtocol>
@end

@interface TestObject()
@property (nonatomic, strong) Cat *cat;
@property (nonatomic, strong) Dog *dog;
@end

@implementation TestObject

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.cat respondsToSelector:aSelector]) {
        return self.cat;
    } else if ([self.dog respondsToSelector:aSelector]) {
        return self.dog;
    } else {
        return self;
    }
}

// 忽略Cat和Dog的初始化过程
@end

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ydWP9s9qFyICfXfqLPkQBg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237270次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录