一、OC runtime运行时

在探索objc_msgSend的时候,我们需要先了解OC的runtime机制。

(一)runtime简介

runtime 是 OC底层的一套C/C++的API(引入 <objc/runtime.h> 或<objc/message.h>),编译器最终都会将OC代码转化为运行时代码。

(二)runtime 交互的三种方式


Objective-C语言与runtime系统的交互主要通过三个不同的层次:

  • 通过Objective-C 源码
    比如直接调用方法[self say]
  • 通过定义在Foundation framework 中的NSObject类方法
    比如NSSelectorFromStringisKindeofClassisMemberOfClass等方法。
  • 通过直接调用runtime函数
    比如sel_registerNameclass_getInstanceSize等底层方法。

二、探索OC方法本质

(一)先手准备

我们先常规的去创建一个类:

@interface Person : NSObject+ (void)whoAmI;
- (void)sayHello;
- (void)sayMarvelous;@end

在main.m文件中:

Person *person = [Person alloc];
[person sayHello];

接下来看一下编译后的c++代码:

int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));}return 0;
}

(二)仔细分析

1. cpp代码:

  • 在调用allocsayHello时,都调用了objc_msgSend方法,意思是objc消息发送
  • objc_getClass(“Person”)获取Person
  • sel_registerName(“XXX”)调用方法,类似于@selectorNSSelectorFromString()

2. 使用objc_msgSend方法:

int main(int argc, const char * argv[]) {@autoreleasepool {//        Person *person = [Person alloc];
//        [person sayHello];Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));}return 0;
}

输出:

Hello!

证明成功调用了sayHello方法。

(三)继续探究

我们创建一个新类,继承于Person,叫它Student。让它调用父类的方法:

Student *stu = [Student alloc];
[stu sayHello];

转成源码:

Student *stu = ((Student *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Student"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)stu, sel_registerName("sayHello"));

实际还是调用了objc_msgSend。那么,有没有方法区分调用的是父类方法还是自己的方法呢?通过查阅资料,我知道了objc_msgSendSuper方法,立即去试一试:

Student *stu = [Student alloc];
//[stu sayHello];struct objc_super lsuper;
lsuper.receiver = stu;
lsuper.super_class = [Person class];
((void (*)(id, SEL))(void *)objc_msgSendSuper)((__bridge id)(&lsuper), sel_registerName("sayHello"));

结果:

Hello!

(四)小结

  • 方法的本质:发送消息
  • OC调用方法等价于runtimeobjc_msgSendobjc_msgSendSuper消息发送

三、objc_msgSend

在objc4源码中我们会发现objc_msgSend是使用汇编实现的,汇编主要的特性是:

  • 速度快:汇编更容易被机器识别。
  • 方法参数的动态性:汇编调用函数时传递的参数是不确定的,那么发送消息时,直接调用一个函数就可以发送所有的消息。

(一)消息查找机制

  • 快速查找:
    cache中查找
  • 慢速查找:
    a. methodList中查找
    b. 消息转发

(二)在cache中快速查找

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

在前面我们也看到了,objc_msgSend需要传入两个参数。此外,如果方法本身有参数,会把本身的参数拼接到这两个参数后面。
在objc源码的objc-msg-arm64.s中,可以看到下面部分:

 ENTRY _objc_msgSendUNWIND _objc_msgSend, NoFrame//p0 是传入的第一个参数:消息的接收者。//cmp p0与nil比较,如果p0为空,那么就直接返回。cmp    p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS//小对象类型(是否是taggedPointer类型的指针)b.le   LNilOrTagged        //  (MSB tagged pointer looks negative)
#else//消息接收者为空,返回空b.eq   LReturnZero
#endif//消息接收者为不空//p13 是获取消息接收者的isaldr   p13, [x0]       // p13 = isa//p16 是根据isa p13获取到ClassGetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:// calls imp or objc_msgSend_uncached// 在cache中开始找imp// 如果有就调用,如果没有走objc_msg_uncached分支CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

接下来看下CacheLookup源码,我们在objc-msg-arm64.s中找.macro CacheLookup

.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.//  一旦我们越过LLookupStartFunction标签,我们可能已经加载了无效的缓存指针或掩码。//   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://   当我们通过LLookupEndFunction之前调用task_restartable_ranges_synchronize//  (或当信号击中我们时),我们的PC将被重置为LLookupRecoverFunction,//   该函数会强制跳转到具有以下要求的缓存未命中代码路径://   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 conventionsmov  x15, x16            // stash the original isa 藏匿原始的isa
LLookupStart\Function:// p1 = SEL, p16 = isa
//便于观察。将前面的#define搬过来了,对照着看一下:
/*#define CACHE_MASK_STORAGE_OUTLINED 1#define CACHE_MASK_STORAGE_HIGH_16 2#define CACHE_MASK_STORAGE_LOW_4 3#define CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 4*/
/*#if defined(__arm64__) && __LP64__#if TARGET_OS_OSX || TARGET_OS_SIMULATOR#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS#else#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16#endif#elif defined(__arm64__) && !__LP64__#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4#else#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED#endif*/
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRSldr p10, [x16, #CACHE]          // p10 = mask|bucketslsr   p11, p10, #48           // p11 = maskand   p10, p10, #0xffffffffffff   // p10 = bucketsand    w12, w1, w11            // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 64位真机⚠️⚠️⚠️ldr  p11, [x16, #CACHE]          // p11 = mask|buckets isa 平移16字节得到 cache_t,cache首地址是mask_buckets
#if CONFIG_USE_PREOPT_CACHES        // p11 = _bucketsAndMaybeMask,即cache的第一个8字节
#if __has_feature(ptrauth_calls)tbnz    p11, #0, LLookupPreopt\Functionand  p10, p11, #0x0000ffffffffffff   // p10 = buckets
#elseand    p10, p11, #0x0000fffffffffffe   // p10 = bucketstbnz   p11, #0, LLookupPreopt\Function
#endifeor   p12, p1, p1, LSR #7and  p12, p12, p11, LSR #48      // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else// _bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位) p10 = bucketsand p10, p11, #0x0000ffffffffffff   // p10 = bucketsand    p12, p1, p11, LSR #48       // x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4ldr   p11, [x16, #CACHE]              // p11 = mask|bucketsand   p10, p11, #~0xf         // p10 = bucketsand    p11, p11, #0xf          // p11 = maskShiftmov  p12, #0xfffflsr p11, p12, p11           // p11 = mask = 0xffff >> p11and    p12, p1, p11            // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endifadd   p13, p10, p12, LSL #(1+PTRSHIFT)// p12 逻辑左移4位即扩大16倍,指针平移到对应的bucket位置上// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // p13 指向哈希下标对应的bucket
// insert bucket的时候,do-while写入,哈希和二次哈希,读取的时候也是do-while读取cache// do { // p17 = imp, p9 = sel,bucket中imp和sel分别赋给p17和p9
1:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket-- //赋值完成后p13 -= BUCKET_SIZE,指向前一个bucketcmp p9, p1              //     if (sel != _cmd) {//获取的sel和_cmd,如果不相等,调整到3fb.ne   3f              //         scan more//     } else {2:  CacheHit \Mode              // hit:    call or return imp //获取的sel和_cmd,如果相等,缓存命中,call or return imp//     }
3:  cbz p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;// 如果取出的sel位nil,则goto Misscmp   p13, p10            // } while (bucket >= buckets) //如果bucket >= buckets,即没有到最前面b.hs 1b      // 则继续比较前一个bucket,如果到最前面了,就继续执行后续代码// 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.// 当CACHE_ALLOW_FULL_UTILIZATION时可能会发生完全缓存。// 因此,当我们循环回到第一个探测的bucket时停止,而不是再次击中第一个bucket时。// 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_ADDRSadd  p13, p10, w11, UXTW #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))//p11 = _bucketsAndMaybeMask,逻辑右移44位,相当于mask逻辑左移4位// p13 = buckets + (mask << 1+PTRSHIFT)// see comment about maskZeroBits// 再左移4位获取到mask指向的bucket,相当于p11右移了44位// bucket >= buckets,再次从最后到最前面进行一次do-while循环查找
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4add   p13, p10, p11, LSL #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endifadd   p12, p10, p12, LSL #(1+PTRSHIFT)// p12 = first probed bucket// do {4:  ldp p17, p9, [x13], #-BUCKET_SIZE   //     {imp, sel} = *bucket-- // 这里重复1:标签,从mask--->0 查找,从后到前查找cmp p9, p1              //     if (sel == _cmd) // 汇编里没有do-while,p13指向最后再重复一次1:标签的逻辑b.eq   2b              //         goto hitcmp  p9, #0              // } while (sel != 0 &&ccmp    p13, p12, #0, ne        //     bucket > first_probed)b.hi    4bLLookupEnd\Function:
LLookupRecover\Function:b   \MissLabelDynamic....endmacro

我也不会汇编语言,只能看着源码给的注释和别人的注释理解。主要分为下面几步:

1. 流程:

1.1 获取到指向 cache 和 _bucketsAndMaybeMask

在前面类的结构分析文章中我们清楚的知晓objc_class的属性为:isasuperClasscache等:

struct objc_class : objc_object {// Class ISA;Class superclass;cache_t cache; // formerly cache pointer and vtableclass_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags...
}

通过p16 = class = isa ,首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa首地址 占8字节,superClass占8字节),获取cahcep11指向cache中第一个8字节_bucketsAndMaybeMask_bucketsAndMaybeMask中高16位存mask,低48位存buckets(高16位 | 低48位 = mask | buckets), _bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位),即p11 = _bucketsAndMaybeMask

1.2 从 _bucketsAndMaybeMask 中分别取出 buckets 和 mask,并由 mask 根据哈希算法计算出哈希下标

p10 = _bucketsAndMaybeMask & 0x0000ffffffffffff = buckets
_bucketsAndMaybeMask >> 48 = mask
p12 = _cmd & mask = 哈希下标,记作 begin
objc_msgSend的参数p1(即第二个参数_sel)& mask,通过哈希算法,得到需要查找存储sel-impbucket下标begin,即p12 = begin = _sel & mask,因为在存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储

static inline mask_t cache_hash(SEL sel, mask_t mask)
{return (mask_t)(uintptr_t)sel & mask;
}

1.3 根据所得的哈希下标 begin 和 buckets 首地址,取出哈希下标对应的 bucket

PTRSHIFT是一个宏定义,固定值为3:

#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE

根据计算的哈希下标begin 乘以单个bucket占用的内存大小,得到buckets首地址距离begin下标指向的bucket在实际内存中的偏移量。通过首地址 + 实际偏移量,获取哈希下标begin对应的bucketbucket是有selimp两个属性组成,每个属性都是8个字节的大小,所以bucket的大小是16

1.4 进入 do-while 循环,根据 bucket 中的 sel 查找

  1. bucket中的属性属性impsel分别赋值为p17 和 p9。
1:   ldp p17, p9, [x13], #-BUCKET_SIZE//{imp, sel} = *bucket--
//赋值完成后p13 -= BUCKET_SIZE,指向前一个bucket
  1. bucket中的属性属性impsel分别赋值为p17 和 p9。
cmp  p9, p1
//p9 == p1,缓存命中执行CacheHit
//不相等,执行下面的逻辑
  1. p9是否为nil
cbz  p9, \MissLabelDynamic       //     if (sel == 0) goto Miss;
// 如果p9 == nil,则指向goto Miss,默认没找到,
// 这里忽略了哈希冲突后二次哈希可能导致begin下标和真实写入的index之间存在差异
// 而且初始化或扩容后,里面的bucket都是空的,sel和imp都是nil,直接简单粗暴,即p9指向的sel为nil,则认为丢失,也是为了更快
  1. p9 != nil,判断p13是否 已经执行到最前面了
cmp  p13, p10// } while (bucket >= buckets)
// 如果bucket >= buckets,则跳转到第一步,while循环开始,while (bucket < buckets)
// while循环结束,依然没有找到,则跳转到最后的bucket,即mask下标所指向的bucket,从后到前再次查找一遍
  1. begin --> 0,依然没有找到,跳转到最后,mask指向的bucket
add  p13, p10, p11, LSR #(48 - (1+PTRSHIFT))// p13 = buckets + (mask << 1+PTRSHIFT)
// p11 = _bucketsAndMaybeMask,逻辑右移44位,相当于mask逻辑左移4位
// p13 = buckets + (mask << 1+PTRSHIFT) 指向最后的bucket
// 正常是p11右移48位获取到mask,再左移4位,相当于_bucketsAndMaybeMask右移44位
// 此时p13,指向最后的bucket,while循环,跳转到第一步

小结

  • 第一次do-while循环,从begin ---> 0 查找一遍,如果没命中,p9不为nil,开始第二次do-while循环
  • 第二次do-while循环,从mask ---> 0再次查找一遍,
  • 依然如此,则执行__objc_msgSend_uncached ---> MethodTableLookup ---> _lookUpImpOrForward开始查找方法列表

2. CacheHit

下面我们看一下CacheHit源码:

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMALTailCallCachedImp x17, x10, x1, x16   // authenticate and call imp//调用imp
.elseif $0 == GETIMPmov   p0, p17cbz  p0, 9f          // don't ptrauth a nil impAuthAndResignAsIMP x0, x10, x1, x16  // authenticate imp and re-sign as IMP
9:  ret             // return IMP//返回imp
.elseif $0 == LOOKUP// 执行__objc_msgSend_uncached,开始方法列表查找// No nil check for ptrauth: the caller would crash anyway when they// jump to a nil IMP. We don't care if that jump also fails ptrauth.AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMPcmp   x16, x15cinc    x16, x16, ne            // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)ret              // return imp via x17
.else
.abort oops
.endif
.endmacro

[OC学习笔记]objc_msgSend(一):方法快速查找相关推荐

  1. Servlet和HTTP请求协议-学习笔记01【Servlet_快速入门-生命周期方法、Servlet_3.0注解配置、IDEA与tomcat相关配置】

    Java后端 学习路线 笔记汇总表[黑马程序员] Servlet和HTTP请求协议-学习笔记01[Servlet_快速入门-生命周期方法.Servlet_3.0注解配置.IDEA与tomcat相关配置 ...

  2. Redis学习笔记①基础篇_Redis快速入门

    若文章内容或图片失效,请留言反馈.部分素材来自网络,若不小心影响到您的利益,请联系博主删除. 资料链接:https://pan.baidu.com/s/1189u6u4icQYHg_9_7ovWmA( ...

  3. JDBC学习笔记01【JDBC快速入门、JDBC各个类详解、JDBC之CRUD练习】

    黑马程序员-JDBC文档(腾讯微云)JDBC笔记.pdf:https://share.weiyun.com/Kxy7LmRm JDBC学习笔记01[JDBC快速入门.JDBC各个类详解.JDBC之CR ...

  4. Python学习笔记--10.Django框架快速入门之后台管理admin(书籍管理系统)

    Python学习笔记--10.Django框架快速入门之后台管理 一.Django框架介绍 二.创建第一个Django项目 三.应用的创建和使用 四.项目的数据库模型 ORM对象关系映射 sqlite ...

  5. matlab修改变量名称_MATLAB学习笔记1:如何快速创建多个仅有数字变化变量名?...

    一直以来,本人用MATLAB都是想用什么功能就搜索什么功能,或者查看MATLAB帮助文档.(不得不说MATLAB的帮助文档做得真好) 由于没有系统学习过MATLAB,所以代码都很水-- 好吧,开个文章 ...

  6. OGG学习笔记04-OGG复制部署快速参考

    OGG学习笔记04-OGG复制部署快速参考 源端:Oracle 10.2.0.5 RAC + ASM 节点1 Public IP地址:192.168.1.27 目标端:Oracle 10.2.0.5 ...

  7. 深度学习笔记:优化方法总结(BGD,SGD,Momentum,AdaGrad,RMSProp,Adam)

    深度学习笔记(一):logistic分类  深度学习笔记(二):简单神经网络,后向传播算法及实现  深度学习笔记(三):激活函数和损失函数  深度学习笔记:优化方法总结  深度学习笔记(四):循环神经 ...

  8. OpenCV学习笔记(十七):查找并绘制轮廓:findContours(),drawContours(),approxPolyDP()

    OpenCV学习笔记(十七):查找并绘制轮廓:findContours() 1.findContours() 函数 该函数使用Suzuki85算法从二值图像中检索轮廓.轮廓线是一种用于形状分析.目标检 ...

  9. 2020-4-5 深度学习笔记17 - 蒙特卡罗方法 3 ( 马尔可夫链蒙特卡罗方法MCMC-先验分布/后验分布/似然估计,马尔可夫性质)

    第十七章 蒙特卡罗方法 中文 英文 2020-4-4 深度学习笔记17 - 蒙特卡罗方法 1 (采样和蒙特卡罗方法-必要性和合理性) 2020-4-4 深度学习笔记17 - 蒙特卡罗方法 2 ( 重要 ...

最新文章

  1. HDU 3507:Print Article
  2. 实体链接(Entity Linking)、依存句法分析、成分句法树、词袋模型、文本向量空间模型(TF-IDF)、
  3. golang 读取 ini配置信息
  4. 安卓 sharedpreferences可以被其它activity读取_【安卓逆向】“一份礼物”之我要o泡逆向分析...
  5. es6变量赋值重命名
  6. python 窗口 网页 访问_同事用Python操控浏览器运行,引的妹子围观不止!
  7. java memcmp_C 库函数
  8. Android已申请动态权限报错,Android 读取或者写入U盘时,报错:Permission denied
  9. SAP License:SAP中的报表查询
  10. Prometheus 轻松实现集群监控
  11. python-成都Python课程
  12. HttpHandler和ashx要实现IRequiresSessionState接口才能访问Session信息(转载)
  13. Android View框架总结(九)KeyEvent事件分发机制
  14. fiddler中文乱码解决
  15. 云和恩墨大讲堂 | 基于PCIE 闪存卡的 Oracle 数据库使用
  16. 【电脑开机没反应的常见原因和解决方法】
  17. cad版本怎么在线转换?软件操作更高效
  18. 通过后台数据在百度地图标记多个点
  19. Spring Security系列(10)- 微服务权限方案及Oauth2介绍
  20. 预约直播 | 2023年STM32峰会线上直播开启报名!

热门文章

  1. JAX-RS 2.0 REST 客户端
  2. 自然语言理解,什么是“理解”?
  3. C++:了解SGI-STL空间配置器
  4. struts 国际化 中文编码问题
  5. ubuntu server 10.4下Apache2的三种虚拟主机的实现
  6. 移动端手机上传图片处理
  7. android 中间凹背景_中间凸出的电视背景墙造型设计图 客厅中间凹陷两边凸出的电视背景墙装修效果图...
  8. 数字营销加速进入“下半场”,如何应对虚假流量“顽疾”?
  9. 【汇正财经】指数分化,科创50、创业板强势
  10. 服务器共享文件夹迁移,如何进行共享文件夹权限设置迁移复制?