美文网首页
Cache分析及objc_msgSend初探-消息快速查找流程

Cache分析及objc_msgSend初探-消息快速查找流程

作者: Wayne_Wang | 来源:发表于2021-07-13 11:36 被阅读0次
1.jpeg

前面的一篇文章Cache分析我们对cache_t进行了一个探索,从方法读取到方法存储都有了解。我们都知道存储主要是调用了一个insert方法。并且我们也分析了这个方法。

之前我们读取cache都是直接利用指针平移cache然后去读取内容。但是在真是情况下是什么样的呢?在真实情况是什么情况下或者说是谁发起了cache的调用然后进行读取插入呢?

但是我们没有去探究过它是在什么情况下insert,或者说在insert前它干了什么事呢?使得insert发起cache缓存形成一个闭环流程,因为我们之前都是直接从类的结构去分析然后平移指针得到的cache,然后从cache中看到一个insert方法然后探索的。但是在实际过程中是从哪儿开始到哪一步insert再到哪一步结束的呢?然后下面我们一起来探究下。

探索思路

我们先看看insert方法:
cache_t里的入口

struct cache_t {
    void insert(SEL sel, IMP imp, id receiver);
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
}
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    runtimeLock.assertLocked();

    // Never cache before +initialize is done
    if (slowpath(!cls()->isInitialized())) {
        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
//41B645
    ASSERT(sel != 0 && cls()->isInitialized());

    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1;
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    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) {
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    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)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        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));

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

我们直接简单粗暴地在这个方法里打上一个断点查看堆栈信息:

1.png

从上图的第6步我们看到在调用insert方法前调用的方法是log_and_fill_cache。也就是实际调起insert方法的上层方法:我们进去看看。如图:

2.png

发现这个方法就是简单的调用了下insert。所以我们需要继续往上查找。在哪儿调用了log_and_fill_cache。我们在当前文件全局搜索。发现在lookUpImpOrForward调用了一次,其他地方没有。这也跟我们之前查看栈信息的时候看到的调用顺序一致。这个方法我们发现是c++的消息发送和转发的流程方法。这样我们直接查看这个lookUpImpOrForward方法和更上一步的objc_msgSend

ps小技巧:我们其实在查找insert的流程的时候还可以利用去查看insert的注释来了解它的闭环流程。我们直接去左侧全局搜索栏搜索objc-cache。如图:

3.png

我们从上图从下往上看可以看到cache_t销毁插入、以及各种锁的处理这些就是cachereaders/writers(读写流程)。再往上我们看到 cache_getImpobjc_msgSend*。到这里我们就知道如果想要发起cache的读写流程就必须发起cache_getImp ,也就是获取到cache的指针地址imp。再之前就是利用消息机制发送了消息,调用了objc_msgSend*。在往上看就是他的读取流程了。也就是开头流程。(流程倒推思路)

所以,我们想要形成一个闭环就必须把我们还没有探究的步骤objc_msgSend补充完整。接下来我们就来看看objc_msgSend

objc_msgSend初探

在这之前我们补充下Runtime的部分知识,这段我直接放在文章末尾补充处。

在我们的开发过程中有三种方式来调起苹果的底层runtime。

第一种:从oc层面调起相关方法;
第二种:从NSObject层面调起提供的相关的API
第三种:从底层objc调起提供的相关API;

RunTime调起三种方式.png
objc_msgSend-消息缓存/快速查找流程

接下来我们用代码探索下:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>

@interface ZYPerson : NSObject

@property (nonatomic, copy) NSString *hobby;

- (void)sayHello;

- (void)sayHai:(NSString *)word;

- (void)sayHappy;

@end

@implementation ZYPerson

- (void)sayHello{
    NSLog(@"%s",__func__);
}

- (void)sayHai:(NSString *)word
{
    NSLog(@"%@",word);
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        ZYPerson *person = [ZYPerson alloc];
        
        [person sayHello];
        [person sayHai:@"hai"];
        [person sayHappy];
   
        NSLog(@"Hello, World!");
    }
    return 0;
}

我们在ZYPerson类定义了两个方法一个sayHello.m实现了,一个sayHappy.m里没有实现。这个时候我们command+B编译是可以通过的,但是让我们command+R去运行的时候就会报错。这样也体现了我们经常说的OC层面的运行时

接下来我们把上面的代码文件编译成c++文件查看下底层这些方法到底以什么形式存在和运行的。利用clang命令:clang -rewrite-objc main.m -o main.cpp

然后我们打开main.cpp文件全局搜索int main(函数代码如下:

int main(int argc, const char * argv[]) {
   /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

       ZYPerson *person = ((ZYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZYPerson"), sel_registerName("alloc"));

       ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
       ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("sayHai:"), (NSString *)&__NSConstantStringImpl__var_folders_wq_kwvzjp9x2hd8qmtb3wkyklmc0000gn_T_main_716503_mi_2);
       ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHappy"));

       NSLog((NSString *)&__NSConstantStringImpl__var_folders_wq_kwvzjp9x2hd8qmtb3wkyklmc0000gn_T_main_716503_mi_3);
   }
   return 0;
}

从上面的代码我们就可以看到OC代码编译后在底层就是利用我们上面说的第三种方式(sel_registerName)调起了runtime。并且我们的方法都变成了如下类型的存在

ZYPerson *person = ((ZYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZYPerson"), sel_registerName("alloc"));

 ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
    
 ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("sayHai:"), (NSString *)&__NSConstantStringImpl__var_folders_wq_kwvzjp9x2hd8qmtb3wkyklmc0000gn_T_main_716503_mi_2);
     
 ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHappy"));

所以我们在上层的方法在底层就是一个方法调用的过程。objc_msgSend就是函数的发起者,并且携带N参数。在我们的alloc方法中一个参数为ZYPerson,一个alloc;在其他的两个方法中则是一个为person,一个如:sayHappy;或者是一个为person,一个如:sayHai:以及一个 NSString的字符串。
这其实就是消息发送的过程,在发送过程中携带两个参数一个是消息的接收者(receiver)一个是消息主体(body)。

所以我们是否可以直接利用这种消息机制来调用方法?
如果我们直接这么写代码会运行不起来那是因为xcode 的设置问题。我们进入buildSetting 修改一个参数如图:

4.png

把默认的YES 改成NO,这个设置是限制我们调用下方方法的时候传参的个数。接下来我们用代码来实现看看上面的猜想,如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       
        ZYPerson *person = [ZYPerson alloc];

        //第一种方式 OC 层面
        [person sayHello];
        [person sayHai:@"hai"];
        
        //第二种方式 NSObject 层面
        objc_msgSend(person, @selector(sayHello));
        //第三种方式 objc 层面
        objc_msgSend(person, sel_registerName("sayHello"));
        
        [person sayHappy];
        
        NSLog(@"Hello, World!");
    }
    return 0;
}
2021-07-01 15:54:54.753213+0800 ZYProjectSeventh000[32392:824661] -[ZYPerson sayHello]
2021-07-01 15:54:54.753683+0800 ZYProjectSeventh000[32392:824661] hai
2021-07-01 15:54:54.753805+0800 ZYProjectSeventh000[32392:824661] -[ZYPerson sayHello]
2021-07-01 15:54:54.753850+0800 ZYProjectSeventh000[32392:824661] -[ZYPerson sayHello]
2021-07-01 15:54:54.753985+0800 ZYProjectSeventh000[32392:824661] -[ZYPerson sayHappy]: unrecognized selector sent to instance 0x105a6d540

从上面的分析就知道方法的调用在底层就是消息的发送。

objc_msgSendSuper探索

发现可以正常调用,我们每次打开mian.cpp文件的时候在开头总能看到这样的代码:

__OBJC_RW_DLLIMPORT void objc_msgSend(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void);
__OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void);
__OBJC_RW_DLLIMPORT struct objc_class *objc_getClass(const char *);
__OBJC_RW_DLLIMPORT struct objc_class *class_getSuperclass(struct objc_class *);
__OBJC_RW_DLLIMPORT struct objc_class *objc_getMetaClass(const char *);
__OBJC_RW_DLLIMPORT void objc_exception_throw( struct objc_object *);
__OBJC_RW_DLLIMPORT int objc_sync_enter( struct objc_object *);
__OBJC_RW_DLLIMPORT int objc_sync_exit( struct objc_object *);
__OBJC_RW_DLLIMPORT Protocol *objc_getProtocol(const char *);

里面有一个objc_msgSendSuper,我们知道我们上面调用person自己的方法是用objc_msgSend,那如果我们调用一个类的父类方法会不会用这个objc_msgSendSuper呢?下面我们再次探究下修改下代码如下:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <objc/message.h>

@interface ZYPerson : NSObject

- (void)sayHello;

@end

@implementation ZYPerson

- (void)sayHello{
    NSLog(@"%s",__func__);
}

@end

@interface ZYIoser : ZYPerson
- (void)sayHello;
- (void)sayHappy;

@end

@implementation ZYIoser

- (void)sayHappy{
    NSLog(@"%s",__func__);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        ZYPerson *person = [ZYPerson alloc];
        ZYIoser *ioser = [ZYIoser alloc];
        //第一种方法
        [ioser sayHello];
        [ioser sayHappy];
        //第二种方式
//        objc_msgSend(ioser, @selector(sayHappy));
        //第三种方式
//        objc_msgSend(ioser, sel_registerName("sayHello"));
        
        NSLog(@"Hello, World!");
    }
    return 0;
}

2021-07-01 16:21:50.618188+0800 ZYProjectSeventh000[32665:846315] -[ZYPerson sayHello]
2021-07-01 16:21:50.618614+0800 ZYProjectSeventh000[32665:846315] -[ZYIoser sayHappy]
2021-07-01 16:21:50.618651+0800 ZYProjectSeventh000[32665:846315] Hello, World!
Program ended with exit code: 0

这时候我们再次把.m编译成c++文件看看。

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        ZYPerson *person = ((ZYPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZYPerson"), sel_registerName("alloc"));
        ZYIoser *ioser = ((ZYIoser *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZYIoser"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)ioser, sel_registerName("sayHello"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)ioser, sel_registerName("sayHappy"));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_wq_kwvzjp9x2hd8qmtb3wkyklmc0000gn_T_main_370a95_mi_2);
    }
    return 0;
}

发现在底层并没有按照我们的预想直接利用objc_msgSendSuper调用sayHello方法。那我们不禁思考那这个方法是干嘛的?我们是否可以模仿用objc_msgSend的方法直接调用父类方法呢?我们到这儿就需要去查看objc_msgSendSuper这个方法的实现需要哪些参数了。我们回到objc源码搜索。

OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

发现要传一个struct objc_super *和后面的参数等。那我们再看看struct objc_super *的结构是什么样的。

/// Specifies the superclass of an instance. 
struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};
#endif

搜索发现是上面这样 因为我们是__OBJC2_所以可以简化为:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    
    __unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */
};

那我们回到我们的main.m尝试模仿objc_msgSend的方法调用我们的sayHappy

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        ZYPerson *person = [ZYPerson alloc];
        ZYIoser *ioser = [ZYIoser alloc];
        //第一种方法
        [ioser sayHello];
        [ioser sayHappy];
        //第二种方式
        struct objc_super zy_objc_super;
        zy_objc_super.receiver = ioser;
        zy_objc_super.super_class = ZYIoser.class;
        objc_msgSendSuper(&zy_objc_super, @selector(sayHello));
        
        struct objc_super zy_objc_super2;
        zy_objc_super2.receiver = ioser;
        zy_objc_super2.super_class = ZYPerson.class;
        objc_msgSendSuper(&zy_objc_super2, @selector(sayHello));
        
//        objc_msgSend(ioser, @selector(sayHappy));
        //第三种方式
//        objc_msgSend(ioser, sel_registerName("sayHello"));
        
        NSLog(@"Hello, World!");
    }
    return 0;
}
2021-07-01 17:01:23.734669+0800 ZYProjectSeventh000[33054:870577] -[ZYPerson sayHello]
2021-07-01 17:01:23.735207+0800 ZYProjectSeventh000[33054:870577] -[ZYIoser sayHappy]
2021-07-01 17:01:23.735247+0800 ZYProjectSeventh000[33054:870577] -[ZYPerson sayHello]
2021-07-01 17:01:23.735270+0800 ZYProjectSeventh000[33054:870577] -[ZYPerson sayHello]
2021-07-01 17:01:23.735311+0800 ZYProjectSeventh000[33054:870577] Hello, World!
Program ended with exit code: 0

发现完美调用。看细节我们发现我用了两个对象设置super_class。但是都实现了调用 那 super_class的设置的作用是什么呢?其实在结构体objc_super里注释有解释:

__unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */

所以,super_class的作用是作为第一查找对象。意思就是首先查找你传的类有没有该方法,没有才去其他类。而不是从最底层的子类一层一层找父类的方法。

objc_msgSend底层汇编分析

前面我们初步了解到了objc_msgSend但是它的底层实现是在哪里呢?我们不得而知,那么我么利用断点的方式来看看objc_msgSend在哪里(方法在文章最后的补充里)。

这样我们就知道objc_msgSend是在libobjc.A.dylib里面了,所以我们回到objc源码搜索。

10.png

我们知道objc_msgSend是汇编写的,所以我们直接找.s文件我们选择一个来看比如:arm64我们常用的架构。点开查找入口:

11.png 12.png

为了更好地去解释和分析汇编代码,我对我们走的主流程 汇编 进行了单句注释的方式来分析。我们这次主要查看 真机模式下的汇编所以我们遇到一些环境变量判断的时候 我们选择真机模式,比如CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16CONFIG_USE_PREOPT_CACHES__has_feature(ptrauth_calls)这些在前面的文章都有解释和用到这里就不做过多赘述了。下面我们遇到这些就选这些进就好。

ps:我会在主流程每一句或者每一段汇编下面加上注释和对应的objc 源码解释。部分对象或者类等为了方便理解 用常用的person对象 和 ZYPerson类 表述。

_objc_msgSend:

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

    cmp    p0, #0            // nil check and tagged pointer check
    /*
     *比较p0(接收者/receiver person)是否为0(即为空)不为空继续往下走
     */
  
    /*
     * 是否为SUPPORT_TAGGED_POINTERS类型 小对象类型 是则走if
     */
#if SUPPORT_TAGGED_POINTERS
    
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    /*
     *如果p0和空比较一致则跳转LNilOrTagged 否则往下走
     */
#else
    b.eq    LReturnZero
    /*
     *如果p0和空比较一致则跳转LReturnZero 否则往下走
     */
#endif
    ldr    p13, [x0]        // p13 = isa
    /*
     *从x0寄存器中取出地址赋值给p13 即把接收者person地址isa给p13
     */
    GetClassFromIsa_p16 p13, 1, x0    // p16 = class
    /*
     * 给GetClassFromIsa_p16 方法传入三个参数(isa、1、x0/receiver)得到一个类class(ZYPerson) 然后赋值给p16
     */
    
/*
 * LGetIsaDone 方法
 */
LGetIsaDone:
    // calls imp or objc_msgSend_uncached
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
    /*
     *CacheLookup 方法根据得到的类class 去获取到类的cache 并且根据sel 查找 imp
     */
    
    
/*
 *是否为SUPPORT_TAGGED_POINTERS类型 小对象类型
 */
#if SUPPORT_TAGGED_POINTERS
    
    /*
     *这个方法是判空后的处理
     */
LNilOrTagged:
    b.eq    LReturnZero        // nil check
    /*
     *如果p0和空比较一致则跳转LReturnZero 否则往下走
     */
    
    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

GetClassFromIsa_p16:


//宏定义
/*
 *这个方法就是ISA & mask -> class
 */
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

/*
 *判断是否是一个 indexed isa 32位走if 所以我们走下面的__LP64__
 */
#if SUPPORT_INDEXED_ISA

    // Indexed isa
    mov    p16, \src            // optimistically set dst = src
    tbz    p16, #ISA_INDEX_IS_NPI_BIT, 1f    // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add    x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr    p16, [x10, p16, UXTP #PTRSHIFT]    // load class from array
1:

/*
 *判断如果是64位 所以我们走__LP64__
 */
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
    /*
     * 判断 needs_auth 是否等于0 但是我们传入的是 1 所以我们走eles
     */
    mov    p16, \src
    /*
     *把person的isa 存到p16寄存器
     */
.else
    // 64-bit packed isa
    ExtractISA p16, \src, \auth_address
    /*
     *ExtractISA 方法传入isa、x0/recever 得到一个类class(ZYPerson) 存到 p16寄存器
     */
.endif
#else
    // 32-bit raw isa
    mov    p16, \src

#endif

.endmacro

SUPPORT_INDEXED_ISA:
补充一下宏定义SUPPORT_INDEXED_ISA

#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif

ExtractISA:

.macro ExtractISA
    and    $0, $1, #ISA_MASK
/*
 * $1(isa) & #ISA_MASK 得到类class 存到 $0(即$0存着class)
 */

CacheLookup:


/*
 *objc_msgSend - cache_getIMP
 */
/*
 *方法 -> person - sel -> imp
 */
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
    //
    // Restart protocol:
    //
    //   As soon as we're past the LLookupStart\Function label we may have
    //   loaded an invalid cache pointer or mask.
    //
    //   When task_restartable_ranges_synchronize() is called,
    //   (or when a signal hits us) before we're past LLookupEnd\Function,
    //   then our PC will be reset to LLookupRecover\Function which forcefully
    //   jumps to the cache-miss codepath which have the following
    //   requirements:
    //
    //   GETIMP:
    //     The cache-miss is just returning NULL (setting x0 to 0)
    //
    //   NORMAL and LOOKUP:
    //   - x0 contains the receiver
    //   - x1 contains the selector
    //   - x16 contains the isa
    //   - other registers are set as per calling conventions
    //

    mov    x15, x16            // stash the original isa
    /*
     * 将x16的指针赋值给x15
     */
LLookupStart\Function:
    // p1 = SEL, p16 = isa
    
/*
 *arm64 mac模拟器走这里
 */
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    ldr    p10, [x16, #CACHE]                // p10 = mask|buckets
    lsr    p11, p10, #48            // p11 = mask
    and    p10, p10, #0xffffffffffff    // p10 = buckets
    and    w12, w1, w11            // x12 = _cmd & mask
/*
 * arm64 64位非模拟器走这里走这里
 */
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16

    ldr    p11, [x16, #CACHE]            // p11 = mask|buckets
/*
 * #define CACHE            (2 * __SIZEOF_POINTER__) = 16 (全局搜索找到的)
 * x16/class 和 #CACHE/16 做 + 操作 即 class地址+0x10 即将class的首地址左移了8字节
 * 从类的结构知道这时候得到的是cache_t 存到p11 -> p11 = cache_t
 */
    
/*
 *arm64 64位非模拟器非mac走这里
 */
#if CONFIG_USE_PREOPT_CACHES
/*
 * A12仿生芯片iphoneX及以上走if __has_feature
 */
#if __has_feature(ptrauth_calls)
    tbnz    p11, #0, LLookupPreopt\Function
    /*
     * 比较p11的第0号位是否为0 不为0就走LLookupPreopt 否则往下走
     */
    and    p10,  p11, #0x0000ffffffffffff    // p10 = buckets
    /*
     * 将p11/cache_t 和掩码#0x0000ffffffffffff(mask) 做&操作 得到buckets存到 p10
     */
#else
    and    p10, p11, #0x0000fffffffffffe    // p10 = buckets
    tbnz    p11, #0, LLookupPreopt\Function
#endif
    eor    p12, p1, p1, LSR #7
    /*
     * p1/sel 右移7位 然后 & p1/sel 得到一个值存到 p12
     * 这句汇编对应到源码 cache_hash 方法中的 value ^= value >> 7; 这就是一个异或算法 到时候取值的时候也会做同样的操作
     * 这里的p1是sel的一个uintptr_t类型的值并不是直接的sel指针
     */
    and    p12, p12, p11, LSR #48        // x12 = (_cmd ^ (_cmd >> 7)) & mask
    /*
     * p11/cache_t/_bucketsAndMaybeMask 右移48位得到mask然后 & p12 得到一个值存到 p12
     * 这个值就是哈希算法算出的bucket的index
     * 这句汇编对应到源码cache_hash 方法中的 return (mask_t)(value & mask),即得到 begin
     */
    
    /*
     * 以上两句汇编对应objc源码如下:
         static inline mask_t cache_hash(SEL sel, mask_t mask)
         {
             uintptr_t value = (uintptr_t)sel;
         #if CONFIG_USE_PREOPT_CACHES
             value ^= value >> 7; //真机走这里
         #endif
             return (mask_t)(  & mask);
         }
     */
#else
    and    p10, p11, #0x0000ffffffffffff    // p10 = buckets
    and    p12, p1, p11, LSR #48        // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
    
/*
 * arm64 32位非模拟器走这里走这里
 */
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4

    ldr    p11, [x16, #CACHE]                // p11 = mask|buckets
    and    p10, p11, #~0xf            // p10 = buckets
    and    p11, p11, #0xf            // p11 = maskShift
    mov    p12, #0xffff
    lsr    p11, p12, p11            // p11 = mask = 0xffff >> p11
    and    p12, p1, p11            // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif

    add    p13, p10, p12, LSL #(1+PTRSHIFT)
                        // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
    /*
     * PTRSHIFT = 3 在__LP64__下
     * 将p12/index 左移4位 得到一个值
     * 将p10/buckets + (p12/index << 4)  表示将p10/buckets 的首地址往前移动 n 个步长(bucket的大小16),
     * 因为 一个bucket存sel和imp总共16字节,左移4位就是刚好一个bucket 大小。p13 = 当前要查找的bucket
     */
    /*
     * 相当于源码  b[i]
     */
                        // do {
1:    ldp    p17, p9, [x13], #-BUCKET_SIZE    //     {imp, sel} = *bucket--
    /*
     * 将x13的内容取出来 分别赋值给 p17,p19
     * x13是bucket arm64下存的顺序是 imp sel 所以 p17=imp p19=sel
     * 然后将 x13的bucket 的地址 减去一个bucket的大小 表示下次来遍历的bucket将是本次bucket的左边一个(左移)。
     * 先取值再进行左移 bucket--
     */
    cmp    p9, p1                //     if (sel != _cmd) {
    /*
     * 比较 p9和p1 即 比较上一步得到的(缓存的)sel和传入的_cmd
     * 如果两者一致就直接走 CacheHit 不一致就走 b.ne    3f
     */
    b.ne    3f                //         scan more
    
    
                        //     } else {
2:    CacheHit \Mode                // hit:    call or return imp
    /*
     * 表示直接命中,就是在缓存中找到了对应的方法。就不需要继续循环查找了
     **/
                        //     }
3:    cbz    p9, \MissLabelDynamic        //     if (sel == 0) goto Miss;
    /*
     * 判断p9/sel 是否存在 不存在 走MissLabelDynamic
     * 存在就接着往下走
     **/
    cmp    p13, p10            // } while (bucket >= buckets)
    /*
     * 比较p10/buckets 的地址 和 p13/bucket 的首地址
     * 如果p13首地址大于p10首地址(当前bucket不是buckets里的第一个)就继续走b.hs    1b
     * 不然就直接往下走
     **/
    b.hs    1b
    /*
     * 如果比较结果 是 大于0 即bucket不是buckets里的第一个则跳转回 1b 继续进行比较 (循环比较)
     **/

   /* 这里1,2,3 对应objc cache insert源码
        do {
            if (fastpath(b[i].sel() == 0)) {//该位置的bukcet 为空就插入
                incrementOccupied(); //0+1 = 1
                b[i].set<Atomic, Encoded>(b, sel, imp, cls());
                return;
            }
            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));
    */
    
    // wrap-around:
    //   p10 = first bucket
    //   p11 = mask (and maybe other bits on LP64)
    //   p12 = _cmd & mask
    //
    // A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
    // So stop when we circle back to the first probed bucket
    // rather than when hitting the first bucket again.
    //
    // Note that we might probe the initial bucket twice
    // when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
    add    p13, p10, w11, UXTW #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    add    p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                        // p13 = buckets + (mask << 1+PTRSHIFT)
                        // see comment about maskZeroBits
    /*
     * PTRSHIFT = 3 在__LP64__下
     * p11/cache_t/_bucketsAndMaybeMask 右移44位得到mask然后 + p10/buckets 将得到的存到 p13
     * 这句汇编的意思是 将mask 左移4位 就是 mask(buckets容量)*16(bucket大小)然后+buckets 首地址
     * 相当于 将地址 定位到了 buckets的 末尾位置
     */
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
    add    p13, p10, p11, LSL #(1+PTRSHIFT)
                        // p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
    add    p12, p10, p12, LSL #(1+PTRSHIFT)
                        // p12 = first probed bucket
    /*
     * 当上面的循环判断bucket < buckets 时候 即 查询到的bucket index/p12 比buckets 第一个bucket idnex还小的时候表示越界了,
     * 将p12/inex 左移4位 在和 p10/buckets 相加然后赋值给p12
     * 这句汇编对应到源码cache_hash 方法中的 return (mask_t)(value & mask),即得到 begin
     */

                        // do {
4:    ldp    p17, p9, [x13], #-BUCKET_SIZE    //     {imp, sel} = *bucket--
    /*
     * 将x13的内容取出来 分别赋值给 p17,p19
     * x13是bucket arm64下存的顺序是 imp sel 所以 p17=imp p19=sel
     * 然后将 x13的bucket 的地址 减去一个bucket的大小 表示下次来遍历的bucket将是本次bucket的左边一个(左移)。
     * 先取值再进行左移 bucket--
     * 这里意思就是从最后一个bucket 开始再次循环--往前查找
     */
    cmp    p9, p1                //     if (sel == _cmd)
    b.eq    2b                //         goto hit
    /*
     * 比较 p9和p1 即 比较上一步得到的(缓存的)sel和传入的_cmd
     * 如果两者一致就直接走 2b 命中缓存,不一致就往下走
     */
    cmp    p9, #0                // } while (sel != 0 &&
    ccmp    p13, p12, #0, ne        //     bucket > first_probed)
    /*
     * 比较 p9和0 即比较sel是否为空 不为空接着下面比较 为空走 b.hi 4b
     * 同时比较 p12/sel 是否大于 p13/first probed 首个bucket地址,并且p12/sel 是否为空
     * 如果满足上面条件 则继续循环 如果不满足则往下走 结束 查询
     */
    b.hi    4b

LLookupEnd\Function:
LLookupRecover\Function:
    b    \MissLabelDynamic


    
    
    
#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
    and    p10, p11, #0x007ffffffffffffe    // p10 = buckets
    autdb    x10, x16            // auth as early as possible
#endif

    // x12 = (_cmd - first_shared_cache_sel)
    adrp    x9, _MagicSelRef@PAGE
    ldr    p9, [x9, _MagicSelRef@PAGEOFF]
    sub    p12, p1, p9

    // w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
    // bits 63..60 of x11 are the number of bits in hash_mask
    // bits 59..55 of x11 is hash_shift

    lsr    x17, x11, #55            // w17 = (hash_shift, ...)
    lsr    w9, w12, w17            // >>= shift

    lsr    x17, x11, #60            // w17 = mask_bits
    mov    x11, #0x7fff
    lsr    x11, x11, x17            // p11 = mask (0x7fff >> mask_bits)
    and    x9, x9, x11            // &= mask
#else
    // bits 63..53 of x11 is hash_mask
    // bits 52..48 of x11 is hash_shift
    lsr    x17, x11, #48            // w17 = (hash_shift, hash_mask)
    lsr    w9, w12, w17            // >>= shift
    and    x9, x9, x11, LSR #53        // &=  mask
#endif

    ldr    x17, [x10, x9, LSL #3]        // x17 == sel_offs | (imp_offs << 32)
    cmp    x12, w17, uxtw

.if \Mode == GETIMP
    b.ne    \MissLabelConstant        // cache miss
    sub    x0, x16, x17, LSR #32        // imp = isa - imp_offs
    SignAsImp x0
    ret
.else
    b.ne    5f                // cache miss
    sub    x17, x16, x17, LSR #32        // imp = isa - imp_offs
.if \Mode == NORMAL
    br    x17
.elseif \Mode == LOOKUP
    orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
    SignAsImp x17
    ret
.else
.abort  unhandled mode \Mode
.endif

5:    ldursw    x9, [x10, #-8]            // offset -8 is the fallback offset
    add    x16, x16, x9            // compute the fallback isa
    b    LLookupStart\Function        // lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

这几段汇编里面在主流程有几个方法我没有注释即缓存命中:CacheHit \ModeMissLabelDynamic查找preopt:LLookupPreopt。第一个缓存命中分两种模式但是最后都是返回一个我们查找的结果。后面两个是处理流程中 异常情况的,犹豫篇幅和时间问题就不做过多的分析了。感兴趣的可以自己去查看下。

从上面的分析和我们之前对cache的源码分析 我们可以得出一下的一个流程。我做成了流程图:

objc_msgSend流程图:

objc_msgSend流程图.png

补充

1,Runtime

Runtime有两个版本 ⼀个Legacy版本(早期版本) ,⼀个Modern版本(现⾏版本)

  • 早期版本对应的编程接⼝:Objective-C 1.0
  • 现⾏版本对应的编程接⼝:Objective-C 2.0
  • 早期版本⽤于Objective-C 1.0, 32位的Mac OS X的平台上
  • 现⾏版本:iPhone程序和Mac OS X v10.5 及以后的系统中的 64 位程序

Objective-C Runtime Programming Guide

结构图:

13.png

调起 runtime 的三种方式

RunTime调起三种方式.png

2,查找objc_msgSend在哪里

首先回到我们自己的项目在自己的代码里打一个断点,如图:

5.png

然后到导航栏上找到debug->Debug Workflow->Always Show Disassembly进入汇编页面。

6.png

在汇编里找到objc_msgSend方法 并且在那一行打上一个断点

7.png

让代码运行到objc_msgSend这行汇编,然后按住control然后点击步骤调试进入到该方法:

8.png 9.png

相关文章

网友评论

      本文标题:Cache分析及objc_msgSend初探-消息快速查找流程

      本文链接:https://www.haomeiwen.com/subject/lbdkultx.html