OC底层原理(七):Runimte运行时&objc_msgSend快速查找的分析上

518 阅读9分钟

一、Runtime

编译时Runtime运行时

  • 编译时:顾名思义正在编译的时候,啥叫编译呢?就是编译器把源代码译成机器能够识别的代编译时会进行词法分析,语法分析主要是检查代码是否符合苹果的规范,这个检查的过程通常叫做静态类型检查
  • Runtime运行时代码跑起来,被装装载内存中运行时检查错误和编译时检查错误不一样,不是简单的代码扫描,而是在内存中做操作判断

Runtime版本

Runtime有两个版本: 一个Legacy版本(早期版本),一个Modern版本(现行版本)

  • Legacy早期版本对应的编程接口:Objective-C 1.0,用于32位Mac OS X的平台
  • Modern现行版本对应的编程接口:Objective-C 2.0,源码中经常看到的OBJC2,用于iPhone程序和Mac OS X v10.5以后的系统中的64位程序

Runtime调用三种方式

  • Objective-C方式: [penson sayNB]
  • Framework & Serivce方式:isKindOfClass
  • Runtime API方式: class_getInstanceSize

Screenshot 2021-07-05 at 6.41.40 PM.png

二、方法的本质

方法底层的实现

探究方法的底层两种方式。第一种汇编,第二种C++代码。汇编方式的方法的参数需要读寄存器不方便,所以采用第二种方式生成main.cpp文件。

  1. 首先自定义LGPerson类,在类中添加实例方法,在main函数中调用
  • 代码:
#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface LGPerson : NSObject
- (void)sayHello;
- (void)say:(NSString *)str;
@end

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

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

@interface LGTeacher: LGPerson
- (void)sayHello;
- (void)sayNB;
@end

@implementation LGTeacher
- (void)sayNB{
    NSLog(@"666");
}
@end

int main(int argc, char * argv[]) {
 
    @autoreleasepool {
        LGTeacher *teach = [LGTeacher alloc];
        [teach sayNB];
        [teach say:@"runtime -- msg"];
    }
    return 0;
}
  1. 在终端使用xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cppmain.m生成main64.cpp文件,查询main函数的实现:
  • c++代码:
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGTeacher *teach = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));

        ((void (*)(id, SEL))(void *)objc_msgSend)((id)teach, sel_registerName("sayNB"));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)teach, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_gy__p7f70ys54s2c4h_8hjgv8k80000gn_T_main_2163a1_mi_3);
    }
    return 0;
}

  • 结论:

所有的方法调用都是通过objc_msgSend发送的,所以方法本质就是消息发送

验证objc_msgSend发送消息

既然方法调用都是通过objc_msgSend的,那么我直接通过objc_msgSend发消息

  • 代码:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        LGTeacher *teach = [LGTeacher alloc];
        [teach sayNB];
        objc_msgSend((id)teach, sel_registerName("sayNB"));
        [teach say:@"runtime -- msg"];
        objc_msgSend((id)teach, sel_registerName("say:"),@"runtime -- msg2");
    }
    return 0;
}
  • llvm打印:
2021-07-05 23:16:26.051908+0800 001-运行时感受[7416:606713] 666
2021-07-05 23:16:26.052382+0800 001-运行时感受[7416:606713] 666
2021-07-05 23:16:26.052454+0800 001-运行时感受[7416:606713] ----say runtime -- msg---
2021-07-05 23:16:26.052491+0800 001-运行时感受[7416:606713] ----say runtime -- msg2---
  • 结论:
  1. 通过objc_msgSend[teach sayNB][teach say:@"runtime -- msg"]无论带不带输入参数,结果一样的;验证了方法的本质消息发送

  2. main函数里LGTeacher对象是直接调用objc_msgSend,传入的id也是一样的LGTeacher对象teach,说明objc_msgSend内部会自动判断当前类class是否有这个方法;没有就会找到该类的父类看看有没有,有就调用父类方法。

  3. 在用objc_msgSend方式发送消息,验证过程需要注意两点:

    • 导入消息发送的头文件#import <objc/message.h>
    • 关闭objc_msgSend检查机制:target --> Build Setting -->搜索objc_msgSend --> Enable strict checking of objc_msgSend calls设置为NO

父类方法[super function]

由于在objc的对象object是可以直接使用父类的方法,而调用本类中的方法实际是通过objc_msgSend发送的,那么在子类复写override方法时调用父类的方法[super function]消息发送是什么样的呢?

  • 自定义LGTeacher类,LGTeacher继承LGPerson类。在LGPerson类中自定义方法sayHello,子类LGTeacher对象调用sayHello方法。

父类的方法[super function]的底层

  1. LGTeacher复写调用父类LGPerson方法[super sayHello]
  • 代码:
#import <Foundation/Foundation.h>
#import <objc/message.h>

@interface LGPerson : NSObject
- (void)sayHello;
- (void)say:(NSString *)str;
@end

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

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

@interface LGTeacher : LGPerson
- (void)sayNB;
@end

@implementation LGTeacher
- (void)sayNB{
    NSLog(@"666");
}

- (void)sayHello {
    [super sayHello];
    NSLog(@"7777");
}
@end

int main(int argc, char * argv[]) {
    @autoreleasepool {
        LGTeacher * teach  = [LGTeacher alloc];
        [teach sayHello];
        NSLog(@"Hello, World!");
    }
    return 0;
}
  1. 使用xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cppmain.m生成mian-arm64.cpp文件。
  • c++main方法:
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        LGTeacher * teach = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)teach, sel_registerName("sayHello"));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_4);
    }
    return 0;
}

  • c++LGPersonLGPerson的函数:
#ifndef _REWRITER_typedef_LGPerson
#define _REWRITER_typedef_LGPerson
typedef struct objc_object LGPerson;
typedef struct {} _objc_exc_LGPerson;
#endif

struct LGPerson_IMPL {
	struct NSObject_IMPL NSObject_IVARS;
};
// - (void)sayHello;
// - (void)say:(NSString *)str;
/* @end */


// @implementation LGPerson

static void _I_LGPerson_sayHello(LGPerson * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_0,__func__);
}


static void _I_LGPerson_say_(LGPerson * self, SEL _cmd, NSString *str) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_1,str);
}
// @end
  • c++的LGTeacherLGTeacher函数:
#ifndef _REWRITER_typedef_LGTeacher
#define _REWRITER_typedef_LGTeacher
typedef struct objc_object LGTeacher;
typedef struct {} _objc_exc_LGTeacher;
#endif

struct LGTeacher_IMPL {
	struct LGPerson_IMPL LGPerson_IVARS;
};

// - (void)sayNB;
/* @end */


// @implementation LGTeacher

static void _I_LGTeacher_sayNB(LGTeacher * self, SEL _cmd) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_2);
}

static void _I_LGTeacher_sayHello(LGTeacher * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("sayHello"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9t_c60fhy096zg_yjphk3kd3ch40000l2_T_main_906af2_mi_3);
}
// @end
  • 结论:
  1. 子类对象可以通过objc_msgSendSuper方式调用父类的方法,方法的本质还是消息发送,只不过通过的不同发送流程,同样现在用objc_msgSendSuper向父类发消息。

objc_msgSendSuper底层实现

现在回到objc4.866.9源代码中搜索objc_msgSendSuper

  • objc源代码的message.h:
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
  • 结论:

objc_msgSendSuper的第一个参数是struct objc_super * _Nonnull super类型,继续在源码中查找objc_super 类型

objc_super结构体

  • objc源代码的message.h:
/// 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. 
    __unsafe_unretained _Nonnull Class super_class;
    
    /* super_class is the first class to search */

};
  • 结论:

objc_super结构体类型里面有两个参数:id receiverClass super_class

验证objc_msgSendSuper方法

在源代码工程中,屏蔽LGTeacher里的复写方法sayHello,声明一个objc_super结构体变量,赋值后验证方法。

  • 代码:
#import <objc/message.h>

int main(int argc, char * argv[]) {
    @autoreleasepool {
        LGTeacher *teach = [LGTeacher alloc];
        [teach sayHello];
//        struct objc_super {
//            /// Specifies an instance of a class.
//            __unsafe_unretained _Nonnull id receiver;
//            __unsafe_unretained _Nonnull Class super_class;
//        #endif
//            /* super_class is the first class to search */
//        };
        
        struct objc_super kc_objc_super;
        kc_objc_super.receiver = teach;
        kc_objc_super.super_class = LGPerson.class;

       objc_msgSendSuper(&kc_objc_super,@selector(sayHello));
    }
    return 0;
}
  • llvm打印:
2021-07-06 01:29:15.208080+0800 001-运行时感受[9801:834295] 666 -[LGPerson sayHello]
2021-07-06 01:29:15.208545+0800 001-运行时感受[9801:834295] 666 -[LGPerson sayHello]
  • 结论:
  1. [teach sayHello]和直接通过objc_msgSendSuper父类发消息的是一样的。子类的对象可以调用父类的方法

  2. 验证了一个原理: 方法调用,首先在本类中找,如果没有就到父类中找。

三、objc_msgSend汇编原理

首先找到objc_msgSend所在的libobic.A.dylib汇编库,按住control+单点调试进入objc_msgSend

Screenshot 2021-07-07 at 1.01.15 AM.png

Screenshot 2021-07-07 at 12.59.55 AM.png

  • 结论:

汇编显示objc_msgSendlibobjc.A.dylib系统库,在objc4源码中全局搜索objc_msgSend,找到真机的汇编objc-msg-arm64.s

objc_msgSend汇编入口

  1. 通过查找objc_msgSend的入口ENTERY发现了

截屏2022-12-29 上午12.38.09.png

  1. 通过p0的定义搜索头文件arm64-asm.h发现了

Screenshot 2021-07-07 at 1.48.29 AM.png

  • 结论:

下面的汇编会用到p0-p17;大家可能对汇编中x0x1比较熟悉,知道是寄存器p0-p17就是对x0-x17重新定义。

分析MSG_ENTRY _objc_msgSend

  • 汇编源代码:
	MSG_ENTRY _objc_msgSend // _objc_msgSend 入口,此时有两个输入参数:(isa)id receiver 还有一个是SEL _cmd
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// receiver 和 0 比较 nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS     //__LP64__ 64位系统 支持Taggedpointer类型
	b.le	LNilOrTagged		// <= 0时,支持Taggedpointer类型 走LNilOrTagged流程 (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero         // == 0时,直接返回nil 就是给空指针发送消息
#endif                          // 对象有值或者isa有值
	ldr	p14, [x0]		// p14 = isa 把x0寄存器里面的地址读取到p14寄存器,对象地址等于isa地址
	GetClassFromIsa_p16 p14, 1, x0	// p16 = class // p14,1,x0作为参数传到GetClassFromIsa_p16方法里
LGetIsaDone:    //这是一个标记符号,拿到isa操作完以后继续后面的操作
	// calls imp or objc_msgSend_uncached   //这个函数传递3个参数 NORMAL _objc_msgSend _objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached  //下一步调用CacheLookup

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    //== 0 直接返回nil
	b.eq	LReturnZero		// nil check
	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
    //结束进入_objc_msgSend
	END_ENTRY _objc_msgSend
  • 结论:
  1. 判断receiver是否等于nil,再判断是否支持Taggedpointer小对象类型

  2. 支持Taggedpointer小对象类型;小对象为空,返回nil;不为nil处理isa获取class跳转CacheLookup流程

  3. 不支持Taggedpointer小对象类型且receiver == nil,跳转LReturnZero流程返回nil

  4. 不支持Taggedpointer小对象类型且receiver != nil,通过GetClassFromIsa_p16把获取到class存放在p16的寄存器中,然后走CacheLookup流程

GetClassFromIsa_p16获取Class

  • 汇编源代码:
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#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:

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

  • 结论:

GetClassFromIsa_p16核心功能获取class存放在p16寄存器

ExtractISA

  • 在汇编arm-asm.h里:
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
   ...
#else
   ...
.macro ExtractISA
	and    $0, $1, #ISA_MASK  // and 表示 & 操作, $0 = $1(isa) & ISA_MASK  = class
.endmacro
// not JOP
#endif
  • 结论:

ExtractISA 主要功能 isa & ISA_MASK = class 存放到p16寄存器

CacheLookup流程

buckets和下标index

  • 汇编源代码:
.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  // x15 = x16的值是class; stash the original isa
LLookupStart\Function:
	// p0 = 接受对象, p1 = SEL, p16 = isa(这里的isa是类的isa)
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS // arm64的模拟器
	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
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // arm64真机
    //define CACHE      (2 * __SIZEOF_POINTER__)
    //[x16, #CACHE]     = isa + 0x10 = cache地址 = _bucketsAndMaybeMask地址
    //将_bucketsAndMaybeMask的地址存放到p11寄存器 p11 = mask|buckets
	ldr	p11, [x16, #CACHE]		// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES    // arm64真机
#if __has_feature(ptrauth_calls)    // A12以上
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
    //p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
    //tbnz    p11, #0; 表示若_bucketsAndMaybeMask第0位!=0,则跳转到LLookupPreopt\Function
	tbnz	p11, #0, LLookupPreopt\Function
#endif
    //p1 = _cmd, eor是异或,p12 = p1 ^(p1 >> 7) = _cmd ^ (_cmd >> 7)
	eor	p12, p1, p1, LSR #7

    //p11 >> 48 = _bucketsAndMaybeMask >> 48 = mask
    //p12 = p12 & (p11 >> 48) = _cmd ^ (_cmd >> 7) & mask
    //p12 = (_cmd ^ (_cmd >> 7)) & mask 这一步的作用就是hash求下标index
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#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
  • 结论:

首先是根据不同的架构判断,下面都是以真机为例。上面这段源码主要做了三件事:

  1. 获取_bucketsAndMaybeMask地址也就是cache的地址:p16 = isa(class)p16 + 0x10 = _bucketsAndMaybeMask = p11

  2. 获取buckets地址就是缓存内存的首地址:buckets =((_bucketsAndMaybeMask >> 48 )- 1 )

  3. 获取hash下标: p12 =(cmd ^ ( _cmd >> 7))& mask 这一步的作用就是获取hash下标index

流程如下:isa --> _bucketsAndMaybeMask --> buckets -->hash下标

遍历缓存

  • 汇编源代码:
// p12是哈希下标,p10是buckets的首地址
//((_cmd & mask) << (1+PTRSHIFT)),就是把p12(hash值下标)左移动4位(也就是*16),得到实际偏移大小,通过buckets(首地址)+偏移地址得到index所对应bucket,bucket的结构为{sel,imp}
add p13, p10, p12, LSL #(1+PTRSHIFT)
                    // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
                    // do {
1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket-- 先读取bucket,再减减
   cmp p9, p1              //     if (sel != _cmd) {
   b.ne 3f                 //         scan more
                           //     } else {
2: CacheHit \Mode                   // hit:    call or return imp
                           //     }
3: cbz p9, \MissLabelDynamic //   if (sel == 0) goto Miss;
   cmp p13, p10           // } while (bucket >= buckets)
   b.hs 1b
   
// 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
#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
                        // do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--
   cmp p9, p1              //     if (sel == _cmd)
   b.eq 2b                 //         goto hit
   cmp p9, #0              // } while (sel != 0 &&
   ccmp p13, p12, #0, ne //     bucket > first_probed)
   b.hi 4b
  • 结论:
  1. 根据hash下标index 找到index对应的bucketp13 = buckets + ((_cmd ^ (_cmd >> 7)) & mask) << (1+PTRSHIFT))

  2. 先获取对应的bucket然后取出impsel存放到p17p9,然后*bucket--向前移动

    • 1流程:p9 = sel和传入的参数_cmd进行比较。如果相等走2流程,如果不相等走3流程
    • 2流程:缓存命中直接跳转CacheHit流程
    • 3流程:判断sel == 0条件是否成?如果成立说明buckets里面没有传入的参数_cmd的缓存,没必要往下走直接跳转__objc_msgSend_uncached流程。如果sel = 0说明这个bucket被别的方法占用了,去找下一个位置看看是不是你需要的。然后在判断下个位置的bucket第一个bucket地址大小,如果大于第一个bucket的地址跳转1流程循环查找,如果小于等于则接继续后面的流程
  3. 如果循环到第1个bucket里都没有找到符合的_cmd,那么会接着离开查找缓存往下走__objc_msgSend_uncached

CacheHit流程

  • CacheHit \ModeMode = NORMAL
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
  • TailCallCachedImp是一个,宏定义如下:
// A12 以上 iPhone X 以上的
#if __has_feature(ptrauth_calls)
   ...
#else
.macro TailCallCachedImp
        // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	// $0 = cached imp, $1 = buckets, $2 = SEL, $3 = class类(也就是isa)
	eor	$0, $0, $3   // $0 = imp ^ class 这一步是对imp就行解码,获取运行时的imp地址
	br	$0           //调用 imp 执行方法
.endmacro
...
#endif
  • 结论:

缓存查询到以后直接对bucketimp进行解码操作。即imp = imp ^ class,然后调用解码后的imp

遍历缓存流程图

疑问:为什么要判断bucket中的sel = 0,等于0直接查找缓存流程就结束了?

  • 如果既没有hash冲突又没有目标方法的缓存,那么hash下标对应的bucket就是空的直接跳出缓存查找 不会出现中间是有空的bucket,两边有目标bucket这种情况。

mask向前遍历缓存

向前遍历缓存没有查询到就会跳转到mask对应的bucket继续向前查找i

#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 // arm64 真机,p11 = _bucketsAndMaybeMask,p10 = buckets
   add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))         // p11, LSR #(48 - (1+PTRSHIF)) = p11 >> 48 再 << 4位
                       // p11 >> 48 = mask, mask = capacity - 1 //开辟的内存的容量-1,buckets + mask << 4 找到最后一个bucket位置
   					// p13 = buckets + (mask << 1+PTRSHIFT) 找到最后一个bucket的位置
   					// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 // 32位老机型,不考虑
   add	p13, p10, p11, LSL #(1+PTRSHIFT)
   					// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
   //p12 = (_cmd ^ (_cmd >> 7)) & mask -> 下标index刚开始确认的下标,p10 = buckets
   add	p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = p10 + p12 >> 4 => buckets + ((_cmd ^ (_cmd >> 7)) & mask) << 4找到下标index对应的bucket的地址
   					// p12 = first probed bucket // 第一次定位的bucket地址
//首先先取出x13对应bucket的imp和sel存放到 p17 和 p9 寄存器
//然后x13=*buckets-- 最后一个bucket默认放的是imp = buckets sel = 1
   					// do {
4: ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
   cmp	p9, p1				//     if (sel == _cmd)
   b.eq	2b				//         goto hit // 如果相等就是找到缓存 跳转到2流程 缓存命中
   cmp	p9, #0				// } while (sel != 0 && // sel和0比较
   ccmp	p13, p12, #0, ne		//     bucket > first_probed) // bucket和第一次定位的bucket地址 比较且ne = (sel != 0)
   b.hi	4b              // hi表示无符号大于0 bucket 大于 第一次定位的bucket的地址 跳转4流程
   // 如果一直找到 直到bucket最后一个 还没有找到就跑下面代码
LLookupEnd\Function: // 查询结果
LLookupRecover\Function:
   b	\MissLabelDynamic // 没有找到,就走__objc_msgSend_uncached
  • 结论:
  1. 找到最后一个bucket的位置:p13 = buckets + (mask << 1+3) 找到最后一个bucket的位置

  2. 先获取对应的bucket然后取出impsel存放到p17p9,然后*bucket--向前移动

  3. p9 = sel和 传入的参数_cmd进行比较。如果相等走2流程

  4. 如果不相等再判断(sel != 0 && bucket > 第一次确定的hash下标bucket)接着循环缓存查找,如果整个流程循环完仍然没有查询到或者遇到空的bucket;说明该缓存中没有缓存)sel = _cmd的方法,缓存查询结束跳转__objc_msgSend_uncached流程

  5. mask向前遍历和前面的循环遍历逻辑基本一样

伪代码

  • 整个objc_msgsend消息发送的伪代码流程:
//
//  objc_msgSend伪代码实现.c
//  001-运行时感受
//
//  Created by cooci on 2021/3/18.
//

#include <stdio.h>

[person sayHello]  -> imp ( cache -> bucket (sel imp))

// 获取当前的对象
id person = 0x10000
// 获取isa
isa_t isa = 0x000000
// isa -> class -> cache
cache_t cache = isa + 16字节

// arm64
// mask|buckets 在一起的
buckets = cache & 0x0000ffffffffffff
// 获取mask
mask = cache LSR #48
// 下标 = mask & sel
index = mask & p1

// bucket 从 buckets 遍历的开始 (起始查询的bucket)
bucket = buckets + index * 16 (sel imp = 16)


int count = 0
// CheckMiss $0
do{

    if ((bucket == buckets) && (count == 0)){ // 进入第二层判断
        // bucket == 第一个元素
        // bucket人为设置到最后一个元素
        bucket = buckets + mask * 16
        count++;
        
    }else if (count == 1) goto CheckMiss
        
    // {imp, sel} = *--bucket
    // 缓存的查找的顺序是: 向前查找
    bucket--;
    imp = bucket.imp;
    sel = bucket.sel;
    
}while (bucket.sel != _cmd)  //  // bucket里面的sel 是否匹配_cmd

// CacheHit $0
return imp

CheckMiss:
    CheckMiss(normal)

缓存查询流程图

截屏2022-12-31 上午2.56.57.png

  • 流程图:
graph TB
A[objc_msgSend] -->B{cmp p0, #0<br>判断接受者是否存在?<br>}
    B -->|否| D[通过对象isa获取class]
    B -->|是| C{是否支持<br>taggegpointer对象}
    C -->|是| E{小对象或者空 <br>判断LNilOrTagged<br>}
    D --> G[获取isa完毕 LGetIsaDone]
    E -->|小对象Isa处理| G
    C -->|否| F[直接返回空LReturnZero]
    E -->|空| F
    G --> H[开去缓存查找流程 <br>CacheLookup NORMAL<br>]
    H --> I[通过类的指针平移16得到<br>cacheldr p11,x16 #CACHE<br>]
    I --> J[通过获取的mask_buckets掩码运算得到<br>bucketsand p10,p11, #0x0000ffffffffffff<br>]
    I --> K1[通过逻辑右移得到<br>mask p11, LSR #48<br>]
    K1 --> K2[通过and p12, p1 计算哈希函数<br>得到下标 _cmd &mask<br>]
    J --> K["p12=buckets+((_cmd & mask)<<(1+PTRSHIFT))<br>通过内存平移得到bucket<br>"]
    K2 --> K
    K -->|第一次通过下标向前查找| L["通过bucket结构体得到<br>{imp,sel}=*bucket--<br>"]
    L --> M{判断要查询sel和当前_cmd<br>是否相等<br>}
    M -->|是| N[命中缓存CacheHit NORMAL]
    M -->|否| Q{"sel是否为0(nil)"}
    Q -->|是| W["找不到缓存sel或者结束循环<br>进入慢速查找_objc_msgSend_uncache"]
    Q -->|否| O{"判断当前查询bucket是否为<br>第一个元素<br>bucket == buckets<br>"}
    O -->|是| P["把当前查询bucket设置为最后一个元素<br>p12 = buckets + (mask << 1+PTRSHIFT)<br>"]
    O -->|否| L
    P --> P1["计算出下标index所在bucket(first_probed)"]
    P1 -->|第二次从buckets最后开始向前查找| L1["通过bucket结构体得到  
{imp,sel}=*bucket--"]
    L1 --> M1{判断要查询sel和当前_cmd<br>是否相等<br>}
    M1 -->|是| N
    M1 -->|否| O1{"判断当前查询sel是存在<br>且当前bucket大于first_probed"}
    O1 -->|否| W
    O1 -->|是| L1
  • 总结:
  1. 由于上一节cache_t引入的objc_msgSend的分析,既然会有这么多内容,说明苹果底层是用了大量方式方法才保证了objc运行时发送消息的稳定性。

  2. 了解其中的实现方式有利于学习苹果架构原理.