在上一篇文章:objc_class 中 cache 原理分析中,分析了cache的写入流程,在写入流程之前,还有一个cache读取流程,即objc_msgSend 和 cache_getImp

在分析之前,首先了解什么是Runtime

Runtime 介绍

runtime 即我们常说的 运行时,是 OC 底层的一套 C/C++ 的 API,编译器最终都会将 OC 代码转化为运行时代码,我们通过
clang -rewrite-objc xxx.m可以看到编译后的 .cpp 文件。

OC 语言将尽可能多的决策从编译时链接时****延迟到运行时。只要有可能,它就动态地做事情。这意味着该语言不仅需要一个编译器,还需要一个运行时系统来执行编译后的代码。运行时系统作为Objective-C语言的一种操作系统;它使语言起作用。

运行时 是代码跑起来,被装载到内存中的过程,如果此时出错,则程序会崩溃,是一个动态阶段

编译时 是源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段

交互方式

runtime的使用有以下三种方式

  • Objective-C 代码:例如 [person funcTest];
  • NSObject 方法: 例如 isKindOfClass、isMemberOfClass
  • Runtime API:例如 class_getInstanceSize

  • compiler 就是我们熟知的编译器,即 LLVM,
  • runtime system libarary 就是底层库

编译时:

  • 1、对我们的代码进行语法词法等分析,并给出waring、error等提示。类似一个扫描过程,将代码扫一遍;
  • 2、将编写的高级语言(C C++ OC等)编译成机器识别的机器语言(汇编、机器语言01等),编译得到相应的二进制目标文件。
  • 编译时并没有进行加载分配内存等操作。

链接时:

将编译得到的二进制文件和库函数等进行连接。

运行时:

代码已被装载到内存中,已运行起来了。

方法的本质

准备环境

  • 1.创建一个 ZMPerson 类继承自 NSObject,添加一个实例方法

// .h 文件@interface ZMPerson : NSObject- (void)messageSent;@end// .m 文件
@implementation ZMPerson- (void)messageSent {NSLog(@"消息收到了");
}
  • 2.在 main 函数中创建 LCPerson 的实例,并调用实例方法
#import <UIKit/UIKit.h>
#import "ZMPerson.h"
#import <objc/message.h>int main(int argc, char * argv[]) {@autoreleasepool {ZMPerson *person = [ZMPerson new];[person messageSent];}return 0;
}
  • 3.通过 Clang 命令,将 main.m 文件编译成 main.cpp 文件,找到 main 函数的实现代码
int main(int argc, char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; ZMPerson *person = ((ZMPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZMPerson"), sel_registerName("new"));((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("messageSent"));}return 0;
}

可以看到,ZMPerson 类调用 new 类方法以及对象调用messageSent 实例方法,最终都会转化为 objc_msgSend 消息发送,由此,我们可以判断方法的本质就是消息发送。

验证(方法的本质就是消息发送)

通过调用 objc_msgSend 方式调用,查看与对象直接调用结果是否一致

 ZMPerson *person = [ZMPerson new];[person messageSent];objc_msgSend(person, sel_registerName("messageSent"));

[person messageSent] 与objc_msgSend(person, sel_registerName(“messageSent”)) 调用结果一样

注意:
直接调用 objc_msgSend 的方法会报错,需要导入头文件 #import
<objc/message.h>,此时编译还是会报错,需要将严厉检查机制设置为 NO

通过调用父类方法

1.我们再创建一个 ZMSon 类,继承自 ZMPerson 类,并声明 - (void)messageSent 实例方法,不实现


#import "ZMPerson.h"NS_ASSUME_NONNULL_BEGIN@interface ZMSon : ZMPerson// 与父类方法一样没有实现
- (void)messageSent;@end
  • 2.通过调用 objc_msgSendSuper 方式调用,查看与子类对象直接调用结果
int main(int argc, char * argv[]) {@autoreleasepool {// 子类没有实现 messageSentZMSon *son = [ZMSon new];[son messageSent];struct objc_super lcsuper;lcsuper.receiver = son;lcsuper.super_class = [ZMSon class];objc_msgSendSuper(&lcsuper, sel_registerName("messageSent"));}return 0;
}

我们发现 [son messageSent] 和 objc_msgSendSuper 执行的都是父类中 messageSent
的实现
,由此我们可以猜测,方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。

拓展-objc_msgSendSuper

上面我们用到了 objc_msgSendSuper 调用,我们看下它的源码定义

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

有两个参数(objc_super 结构体,sel 方法名),其结构体类型是objc_super定义的结构体对象

  • 从源码我们可以知道 objc_super 有两个成员 receiver(消息接收者)以及 super_class (父类)。
  • 这个是告诉编译器,优先从父类中查找方法实现;直接调用方法是优先从子类中找方法实现。

objc_msgSend

  • 在 objc-781 源码中查找 objc_msgSend,发现都是用汇编实现的,汇编的特性
  • 快:更容易被机器识别
  • 参数的动态性:汇编调用函数时传递的参数是不确定的

消息查找机制

  • 快速查找:通过 cache 缓存查找
  • 慢速查找:methodList 中查找,查找不到会走消息转发流程

源码分析

objc_msgSend 流程

  • 消息接收者 和 sel:
  • 消息接受者 --> 对象 --> isa --> 方法(类/元类) --> cache_t --> methodliss(bits中)

所以需要在arm64.s后缀的文件中查找objc_msgSend源码实现,发现是汇编实现,其汇编整体执行的流程图如下

objc_msgSend源码整体流程

    ENTRY _objc_msgSendUNWIND _objc_msgSend, NoFrame//p0 是传入的第一个参数:消息的接收者。//cmp(compare) p0与nil比较,如果p0为空,那么就直接返回cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS//小对象类型b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else//消息接收者(p0)为空,返回空b.eq    LReturnZero
#endif//从x0寄存器指向的地址 取出消息接收者的 isa,存入 p13寄存ldr p13, [x0]       // p13 = isa//在 64 位架构下,p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息GetClassFromIsa_p16 p13     // p16 = class
LGetIsaDone:// calls imp or objc_msgSend_uncached//如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程CacheLookup NORMAL, _objc_msgSend#if SUPPORT_TAGGED_POINTERS
LNilOrTagged://等于空,返回空b.eq    LReturnZero     // nil check// taggedadrp    x10, _objc_debug_taggedpointer_classes@PAGEadd x10, x10, _objc_debug_taggedpointer_classes@PAGEOFFubfx    x11, x0, #60, #4ldr x16, [x10, x11, LSL #3]adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEadd x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFFcmp x10, x16b.ne    LGetIsaDone// ext taggedadrp    x10, _objc_debug_taggedpointer_ext_classes@PAGEadd x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFFubfx    x11, x0, #52, #8ldr x16, [x10, x11, LSL #3]b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endifLReturnZero:// x0 is already zeromov x1, #0movi    d0, #0movi    d1, #0movi    d2, #0movi    d3, #0retEND_ENTRY _objc_msgSend

消息接收者(receiver)是否为空

  • 如果支持 tagged pointer,跳转至 LNilOrTagged
  • 如果小对象为空,直接返回空(LReturnZero)

不为空,继续下面流程

  • 如果不是小对象
  • 消息接收者(p0)为空,返回空(LReturnZero) 从 receiver 中取出 isa 存入 p13 寄存器
  • 通过 GetClassFromIsa_p16 得到 class 信息

GetClassFromIsa_p16 得到 class 信息

获取isa

GetClassFromIsa_p16 的汇编代码如下

.macro GetClassFromIsa_p16 /* src */
//此处用于watchOS
#if SUPPORT_INDEXED_ISA // Indexed isa
//将isa的值存入p16寄存器mov p16, $0         // optimistically set dst = src //判断是否是 nonapointer isatbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa // isa in p16 is indexed
//将_objc_indexed_classes所在的页的基址 读入x10寄存器adrp    x10, _objc_indexed_classes@PAGE
//x10 = x10 + _objc_indexed_classes(page中的偏移量) x10基址 根据 偏移量 进行 内存偏移add x10, x10, _objc_indexed_classes@PAGEOFF
//从p16的第ISA_INDEX_SHIFT位开始,提取 ISA_INDEX_BITS 位 到 p16寄存器,剩余的高位用0补充ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:#elif __LP64__ // 64-bit packed isa
//p16 = class = isa & ISA_MASK(位运算 & 即获取isa中的shiftcls信息)and p16, $0, #ISA_MASK #else// 32-bit raw isamov p16, $0#endif.endmacro

CacheLookup 缓存查找

CacheLookup 的汇编源码

.macro CacheLookup//// Restart protocol:////   As soon as we're past the LLookupStart$1 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$1,//   then our PC will be reset to LLookupRecover$1 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//
LLookupStart$1:// p1 = SEL, p16 = isa//#define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16//p11 = mask|buckets,从 x16(isa)中平移16字节,得到 cache,存入p11。objc_class 结构,偏移 isa(8字节)+ superClass(8字节)就是 cache(mask高16位 + buckets低48位)ldr p11, [x16, #CACHE]              // p11 = mask|buckets#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//p11(cache) & 0x0000ffffffffffff ,高16位抹零,得到 buckets,存入p10寄存器and p10, p11, #0x0000ffffffffffff   // p10 = buckets//p11(cache)右移48位,得到 mask,mask&p1(sel,msgSend的第二个参数 cmd),就会得到 sel-imp的下标 index,存入p12and p12, p1, p11, LSR #48       // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4and 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.
#endif//define PTRSHIFT 3//p12是下标,p10是 buckets 的首地址,下标左移16位(1<<4)得到实际内存的偏移量,通过buckets的首地址偏移,获取bucket存入p12寄存器//LSL #(1+PTRSHIFT)-- 实际含义就是得到一个bucket占用的内存大小 -- 相当于mask = occupied -1-- _cmd & mask -- 取余数add p12, p10, p12, LSL #(1+PTRSHIFT)// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))//从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)ldp p17, p9, [x12]      // {imp, sel} = *bucket
//比较 sel 与 p1
1:  cmp p9, p1          // if (bucket->sel != _cmd)//不相等,跳转到2fb.ne    2f          //     scan more//相等,即 cacheHit 缓存命中,直接返回impCacheHit $0         // call or return imp2:  // not hit: p12 = not-hit bucket//如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncachedCheckMiss $0            // miss if bucket->sel == 0//判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)cmp p12, p10        // wrap if bucket == buckets//如果等于,则跳转至第3fb.eq    3f//从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket//跳转至第1步,继续对比 sel 与 cmdb   1b          // loop3:  // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//人为设置到最后一个元素,p11(mask)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4add p12, p12, p11, LSL #(1+PTRSHIFT)// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif// Clone scanning loop to miss instead of hang when cache is corrupt.// The slow path may detect any corruption and halt later.//再查找一遍缓存,拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9ldp p17, p9, [x12]      // {imp, sel} = *bucket
//比较 sel 与 p1(传入的参数cmd)
1:  cmp p9, p1          // if (bucket->sel != _cmd)//如果不相等,即走到2fb.ne    2f          //     scan more//如果相等 即 CacheHit,直接返回impCacheHit $0         // call or return imp//如果一直找不到,即CheckMiss
2:  // not hit: p12 = not-hit bucketCheckMiss $0            // miss if bucket->sel == 0//判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)表示前面已经没有了,但是还是没有找到cmp p12, p10        // wrap if bucket == buckets如果等于,跳转至第3步b.eq    3f//从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket//跳转至第1步,继续对比 sel 与 cmdb   1b          // loopLLookupEnd$1:
LLookupRecover$1:
3:  // double wrap//跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncachedJumpMiss $0.endmacro

1.获取 cache

通过 isa 首地址平移 16 字节(在 objc_class 结构中,isa 占8字节,superClass 占8字节) ,即 p11 =
cache

2.计算下标

通过上一步获取的 cache &(与上)掩码(0x0000ffffffffffff)的到 buckets,(cache 首地址存储的是 ‘高16位存mask,低48位存buckets’ ),
即 p10 = buckets
将 cache 右移48位,得到 mask,即 p11 = mask
p1 是 objc_msgSend 的第二个参数(_cmd),p1 & mask,通过哈希算法,得到需要查找存储 sel-imp 的 bucket 下标 index(在存储 sel-imp 时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取)

3.根据 下标 和 buckets 去除对应的 bucket

一个 bucket 实际占用16字节(sel 占8字节,imp 占8字节),左移4位(2^4 = 16)
通过 首地址 + 实际偏移量,获取哈希下标 index 对应的 bucket

4.根据获取的 bucket,取出其中的 imp 存入p17,即 p17 = imp,取出 sel 存入 p9,即 p9 = sel

5.第一次递归循环

比较获取的 bucket 中 sel 与 objc_msgSend 的第二个参数的 _cmd (即p1)是否相等
相等,直接跳转至 CacheHit,即缓存命中,返回 imp
不相等,分两种情况
一直都找不到,直接跳转至 CheckMiss,进入慢速查找流程
如果根据 index 获取的bucket 等于buckets 的第一个元素,则人为的将当前 bucket 设置为 buckets 的最后一个元素(通过buckets首地址+mask右移44位(等同于左移4位)直接定位到 bucker 的最后一个元素),然后继续进行递归循环(第一个递归循环嵌套第二个递归循环)
如果当前 bucket 不等于 buckets 的第一个元素,则继续向前查找,进入第一次递归循环

6.第二次递归循环

重复 5 的操作,与第一次递归唯一区别是,如果当前的 bucket 还是等于 buckets 的第一个元素,则直接跳转至 JumpMiss,进入慢速查找流程

objc_msgSend流程分析之缓存查找相关推荐

  1. iOS-底层原理 12:objc_msgSend流程分析之快速查找

    iOS 底层原理 文章汇总 本文的主要目的是理解objc_msgSend的方法查找流程 在上一篇文章iOS-底层原理 11:objc_class 中 cache 原理分析中,分析了cache的写入流程 ...

  2. 【Android 逆向】整体加固脱壳 ( DexClassLoader 加载 dex 流程分析 | 查找 DexFile 对应的C代码 | dalvik_system_DexFile.cpp 分析 )

    文章目录 前言 一.查找 DexFile 对应的 C++ 代码 1.根据 Native 文件命名惯例查找 C++ 代码 2.根据方法名查找 二.dalvik_system_DexFile.cpp 源码 ...

  3. Glide系列(四) — Glide缓存流程分析

    文章目录 一.概述 1.1 背景 1.2 系列文章 二.准备知识 2.1 Glide 的缓存分层结构 2.2 Glide 缓存相关类的关联关系 三.缓存的获取流程 3.1 缓存获取的入口 3.2 内存 ...

  4. VLC架构及流程分析

    0x00 前置信息 VLC是一个非常庞大的工程,我从它的架构及流程入手进行分析,涉及到一些很细的概念先搁置一边,日后详细分析. 0x01 源码结构(Android Java相关的暂未分析) # bui ...

  5. 【Android 逆向】整体加固脱壳 ( DexClassLoader 加载 dex 流程分析 | RawDexFile.cpp 分析 | dvmRawDexFileOpen函数读取 DEX 文件 )

    文章目录 前言 一.RawDexFile.cpp 中 dvmRawDexFileOpen() 方法分析 前言 上一篇博客 [Android 逆向]整体加固脱壳 ( DexClassLoader 加载 ...

  6. igmpproxy_Linux IGMP PROXY 学习笔记 之二 igmp proxy的处理流程分析

    上一节中我们分析了linux kernel中igmp proxy相关的数据结构与实现需求分析,本节我们分析kernel中对组播数据流和组播数据的处理流程. 对于目的ip地址为组播地址的数据,可以分为两 ...

  7. c++builder启动了怎么停止_App 竟然是这样跑起来的 —— Android App/Activity 启动流程分析...

    在我的上一篇文章: AJie:按下电源键后竟然发生了这一幕 -- Android 系统启动流程分析​zhuanlan.zhihu.com 我们分析了系统在开机以后的一系列行为,其中最后一阶段 AMS( ...

  8. Elasticsearch 5.x segments merge 流程分析

    2019独角兽企业重金招聘Python工程师标准>>> Elasticsearch 5.x  segments merge 流程分析 这两周主要看了下 Elasticsearch(其 ...

  9. Android 源码 PackageManagerService 启动流程分析

    <Android 源码 installPackage 流程分析>一节着重分析了 apk 安装流程,接下来我们分析 PackageManagerService 启动时都做了些什么? 执行 P ...

最新文章

  1. 有关 HashMap 面试会问的一切
  2. php能做的事,PHP也能干大事 随机函数
  3. mysql数据库utf-8编码
  4. 【渝粤教育】国家开放大学2018年春季 0025-21T数据结构 参考试题
  5. datagridview列 值提取_提取符合条件的多个记录,VLOOKUP:盘他!
  6. macos复制粘贴快捷键 快速_探究Mac OS十大键盘快捷键
  7. 李航统计学习方法 Chapter1 统计学习方法概论
  8. 视频流媒体服务器的作用是什么?流媒体服务器功能介绍
  9. 2022最新淘宝天猫商品sku精准库存(sku库存200)
  10. linux虚拟串口控制器实现---适用于无开发板学习tty driver
  11. KUKA力控软件使用问题介绍
  12. 为何中华武术不堪一击?武学大师临终前解密搏击格斗的残酷真相
  13. 快速部署OpenStack的操作笔记(珍藏版)
  14. 短信链接复制搜索公众号,短信如何推广公众号?
  15. 如何自动识别视频语音内容并生成字幕
  16. 狂飙高启兰好飒,你看狂飙了吗?
  17. Docker镜像-Docker
  18. word设置每页50行
  19. 单反数码相机(百科名片)
  20. 电力电子矢量输出总结

热门文章

  1. JSP页面传值到JSP页面
  2. Flask后端开发(二) - Flask的练习(入门)
  3. 哈工大深圳计算机研究生院导师,哈工大深圳研究生院学科设置及合作导师情况.doc...
  4. R语言基础教程(1)
  5. 云计算机技术的,云计算机技术简介
  6. 调和分析笔记3:卡尔德隆-济格蒙德分解
  7. 软件安全测试有哪些方法?
  8. phonegap mysql_phoneGap-Android开发环境搭建
  9. 叶小天的功课忙了许多
  10. mac的safari浏览器如何开启开发者模式