
前面的一篇文章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
}
我们直接简单粗暴地在这个方法里打上一个断点查看堆栈信息:

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

发现这个方法就是简单的调用了下insert
。所以我们需要继续往上查找。在哪儿调用了log_and_fill_cache
。我们在当前文件全局搜索。发现在lookUpImpOrForward
调用了一次,其他地方没有。这也跟我们之前查看栈信息的时候看到的调用顺序一致。这个方法我们发现是c++
的消息发送和转发的流程方法。这样我们直接查看这个lookUpImpOrForward
方法和更上一步的objc_msgSend
。
ps小技巧:我们其实在查找insert
的流程的时候还可以利用去查看insert
的注释来了解它的闭环流程。我们直接去左侧全局搜索栏搜索objc-cache
。如图:

我们从上图从下往上看可以看到cache_t
的销毁
、插入
、以及各种锁的处理这些就是cache
的readers/writers
(读写流程)。再往上我们看到 cache_getImp
和 objc_msgSend*
。到这里我们就知道如果想要发起cache
的读写流程就必须发起cache_getImp
,也就是获取到cache
的指针地址imp
。再之前就是利用消息机制发送了消息,调用了objc_msgSend*
。在往上看就是他的读取流程了。也就是开头流程。(流程倒推思路)
所以,我们想要形成一个闭环就必须把我们还没有探究的步骤objc_msgSend
补充完整。接下来我们就来看看objc_msgSend
。
objc_msgSend初探
在这之前我们补充下Runtime的部分知识,这段我直接放在文章末尾补充处。
在我们的开发过程中有三种方式来调起苹果的底层runtime。
第一种:从oc
层面调起相关方法;
第二种:从NSObject
层面调起提供的相关的API
;
第三种:从底层objc
调起提供的相关API
;

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
修改一个参数如图:

把默认的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
源码搜索。

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


为了更好地去解释和分析汇编代码,我对我们走的主流程 汇编 进行了单句注释的方式来分析。我们这次主要查看 真机模式下的汇编所以我们遇到一些环境变量判断的时候 我们选择真机模式,比如CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
、CONFIG_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 \Mode
、MissLabelDynamic
、查找preopt:LLookupPreopt
。第一个缓存命中分两种模式但是最后都是返回一个我们查找的结果。后面两个是处理流程中 异常情况的,犹豫篇幅和时间问题就不做过多的分析了。感兴趣的可以自己去查看下。
从上面的分析和我们之前对cache的源码分析 我们可以得出一下的一个流程。我做成了流程图:
objc_msgSend
流程图:

补充
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
结构图:

调起 runtime 的三种方式

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

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

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

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


网友评论