iOS Runtime底层之消息传递

Runtime是C,C++汇编一起写成的API,有两个版本Modern和Legacy,OC2.0之后用的是Modern Version版本,可以运行在iOS2.0和macOS 10.5之后的系统中。

都说Objective-C是一门动态运行时语言,那什么是运行时?什么是编译时?
运行时:代码跑起来之后会装载到内存中,提供运行时功能
编译时:正在编译的时间,就是把源代码(高级语言)翻译成机器能识别的语言->机器语言->二进制

Runtime可以做的事情有很多

  • 黑魔法:动态交换方法、KVO实现等
  • 关联对象:给分类添加属性等
  • 消息转发:项目中一些防崩溃处理等

它可以做的事情有很多,在这里就探索Runtime的底层是如何实现的,也便深入理解Objective-C这门语言。

Runtime的消息传递过程

在OC调用方法时,我们clang一下,看看对应的C++实现是什么

clang -rewrite-objc main.m -o main.cpp
Person *p = [Person new];
[p sayHello];

//C++实现
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));

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

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

以上就是Runtime的消息传递过程,我们从上往下慢慢分析。

isa指针

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

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指针中时,用散列表去计数
  • extra_rc:表示该对象的引用计数,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数⼤于 10, 则需要使⽤到has_sidetable_rc
objc_msgSend的方法调用本质

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

objc_msgSend的方法调用分为两部分,汇编和C语言。
一、汇编部分
1、ENTRY _objc_msgSend
2、进入LNilOrTagged,当数据类型是NSTaggedPointer或消息名为空时,直接END_ENTRY _objc_msgSend,不为空则继续执行下一步,NSTaggedPointer类型的对象采用和isa一样的联合体位域的方式,可直接从地址中读取出想要的值,一般当数据类型的"value"足够小时,系统会自动转换为NSTaggedPointer类型,比如NSString转换为NSTaggedPointerString

#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif

3、LGetIsaDone 处理完isa,这一步就是isa指针已经处理完,要么从缓存中找到IMP,要么执行objc_msgSend_uncached从方法列表中获取IMP

LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

4、CacheLookup NORMAL 去缓存中找IMP,会有以下2种情况
1)CacheHit在缓存中找到了则直接calls imp
2)CheckMiss没找到则执行objc_msgSend_uncached
CacheLookup执行如下,主要看关键字就行。

//代码剔除了混淆理解的部分
.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

CacheHit执行如下

.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

CheckMiss执行如下

.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

5、objc_msgSend_uncached,缓存中没找到则去MethodTableLookup到方法列表中寻找

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

6、MethodTableLookup中会到objc-runtime-new.mm中调用_class_lookupMethodAndLoadCache3方法,从这里开始便从汇编到C语言执行了

.macro MethodTableLookup
    bl  __class_lookupMethodAndLoadCache3
.endmacro

二、C语言部分

1、执行_class_lookupMethodAndLoadCache3方法,该方法调用了lookUpImpOrForward函数

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

2、lookUpImpOrForward 查找imp,主要分为以下几步

  • 1)getMethodNoSuper_nolock(cls, sel);method_list中找,如果找到了method,则把它存入缓存;如果没找到,则向上遍历父类,步骤也是执行1)、2)步,直到查到根部(root)NSObject。
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)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
  • 2)log_and_fill_cache存入缓存
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 {//满了就扩大缓存容量
        cache->expand();
    }

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

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

三、消息转发
Part1、动态方法解析
调用_class_resolveMethod方法,判断当前类是否为元类,

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    //不是元类的话会调用`_class_resolveInstanceMethod`,如果这个方法能找到`imp`的话,会再次`objc_msgSend`发送消息
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
  • 1)不是元类的话会调用_class_resolveInstanceMethod,如果这个方法能找到imp的话,会再次objc_msgSend发送消息。
  • 2)是元类的话分两步,先调用_class_resolveClassMethod、再调用_class_resolveInstanceMethod
    注意:为什么元类要调用先调用_class_resolveClassMethod,再调用_class_resolveInstanceMethod呢,因为类方法在元类中是以实例的形式存在的,所以查找步骤如下Person(类方法)->元类(实例方法)->根元类(实例方法)->NSObject(实例方法)
    实现resolveInstanceMethodresolveClassMethod如下
//调用对象的实例方法时,先执行resolveInstanceMethod方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        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(walk)) {
        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(object_getClass(self), sel, readImp, type);
    }
    
    return [super resolveClassMethod:sel];
}

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

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(消息转发)

举例如下:
//寻找备用接收者,让别的类来提供同名的方法

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(walk)) {
        return [LGStudent new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

在forwardingTargetForSelector方法中我们可以坐哪些操作?
1.找备用接收者处理当前类没有实现的方法(注意:备用接收者的方法只能是实例方法)
2.crash收集
3.防止崩溃

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

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

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

//若返回函数签名,则继续调用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博大精深,未完待续~

你可能感兴趣的:(iOS Runtime底层之消息传递)