6、iOS底层分析 - 消息发送(objc_msgeSend)分析

2023-11-29 06:48

本文主要是介绍6、iOS底层分析 - 消息发送(objc_msgeSend)分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

调试分析

OC中的方法到底是怎么调用的呢?之前查资料知道oc的方法调用是通过消息发送的形式进行调用,那就来结合源码探究一下

     LGPerson *person = [[LGPerson alloc] init];Class pClass = [LGPerson class];[person sayNB]; 

汇编查看流程(注意使用真机调试)

Debug - Debug workflow - Always show Disassembly

    0x100000b4b <+27>:  movq   0x746(%rip), %rsi         ; (void *)0x00000001000012d8: LGPerson0x100000b52 <+34>:  movq   0x71f(%rip), %rcx         ; "alloc"0x100000b59 <+41>:  movq   %rsi, %rdi0x100000b5c <+44>:  movq   %rcx, %rsi0x100000b5f <+47>:  movq   %rax, -0x48(%rbp)0x100000b63 <+51>:  callq  *0x4a7(%rip)              ; (void *)0x0000000100344640: objc_msgSend0x100000b69 <+57>:  movq   0x710(%rip), %rsi         ; "init"0x100000b70 <+64>:  movq   %rax, %rdi0x100000b73 <+67>:  callq  *0x497(%rip)              ; (void *)0x0000000100344640: objc_msgSend0x100000b79 <+73>:  movq   %rax, -0x18(%rbp)0x100000b7d <+77>:  movq   0x714(%rip), %rax         ; (void *)0x00000001000012d8: LGPerson0x100000b84 <+84>:  movq   0x6fd(%rip), %rsi         ; "class"0x100000b8b <+91>:  movq   %rax, %rdi0x100000b8e <+94>:  callq  *0x47c(%rip)              ; (void *)0x0000000100344640: objc_msgSend0x100000b94 <+100>: movq   %rax, -0x20(%rbp)0x100000b98 <+104>: movq   -0x18(%rbp), %rax0x100000b9c <+108>: movq   0x6ed(%rip), %rsi         ; "sayNB"

通过汇编 Debug 可以发现LGPerson 调用的4个方法 alloc 、init、class、sayNB ,

其中init、sayNB 都可以看到 objc_msgSend。继续分析

找到 target 目录下,在终端使用clang -rewrite-objc main.m,编译生成main.cpp文件,对照二者的main函数

// oc
int main(int argc, const char * argv[]) {@autoreleasepool {LGPerson *person = [[LGPerson alloc] init];;[person sayNB];}return 0;
}// c++
int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")), sel_registerName("init"));;((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));}return 0;
}
  1. 对比分析可以发现,我们调用的 [person sayNB] 方法,在C++中则被编译成为了objc_msgSend((id)person, sel_registerName("sayNB"))
  2. 由以上两个地方可以知,方法在底层的本质其实就是 objc_msgSend  发送消息,调用方法其实就是调用  objc_msgSend 向特定的对象发送特定的消息
  3. objc_msgSend(id,sel)      id 消息接收者(对象)    sel 方法编号
  4. 得到的 key & mask 得到 index,然后找到对应 imp
  • 发送消息是找函数的过程,oc 底层封装的就是 c 就是通过消息发送找到真正函数实现的 imp
  • 如果直接调用 c 的函数,并不会有消息发送的过程,是直接调用的。
  • // 发送消息 : objc_msgSemd
    // 对象方法 - person - sel
    // 类方法  - 类 - sel
    // 父类 : objc_superMSgSend

control+in进入 进入objc_msgSend方法,找到其所在为libobjc.A.dylib,那么接下来,我们可以通过源码搜索对其进行探索

 

objc_msgSend 分析

在objc源码中全局搜索 objc_msgSend 方法,在 objc-msg-arm64.s 文件中找到 objc_msgSend 入口

	ENTRY _objc_msgSendUNWIND _objc_msgSend, NoFramecmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERSb.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#elseb.eq	LReturnZero
#endif// person - isa - 类ldr	p13, [x0]		// p13 = isaGetClassFromIsa_p16 p13		// p16 = class
LGetIsaDone:CacheLookup NORMAL		// calls imp or objc_msgSend_uncached

这个地方是用汇编写的,不是很好读。经过查询简单分析一下

  1. cmp    p0, #0            // nil check and tagged pointer check  空指针标记,也就是判空
  2. 判断  SUPPORT_TAGGED_POINTERS   操作(支持标记的指针)
  3. p13 相当于 isa
  4. 其次通过  GetClassFromIsa_p16  获取 当前的类 class
  5. 通过 CacheLookup 先到缓存中进行查找。
  6.  其实和类的结构基本上一样 ,就是通过汇编语言编写,对当前的类进行地地址偏移16个字节( isa 和 superClass 各占据8个字节),找到 cache 的结构体所在,然后找到 buckets 进行缓存方法的查找
  7. 如果没有查找到,则进行 CheckMiss
.macro CheckMiss// miss if bucket->sel == 0
.if $0 == GETIMPcbz p9, LGetImpMiss
.elseif $0 == NORMALcbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUPcbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

CheckMiss

根据传递参数的不同,返回不同的结果

objc_msgSend 刚才传递的类型为 NORMAL,所以接下来会调用__objc_msgSend_uncached 方法,并且内部只调用了MethodTableLookup 的方法以及回调一个方法指针

    STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSaves// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band p16 is the class to searchMethodTableLookupTailCallFunctionPointer x17END_ENTRY __objc_msgSend_uncached
  • 类的结构。先通过isa找到类,再找到类的 cache 的 buckets 中 ,当缓存之中全都没有找到之后,那么接下来就去类原来的存储空间,也就是  class - bits - rw - ro - methodList  中去找方法的定义。
  • 消息发送的汇编也是一样的在 MethodTableLookup 方法中,先是做了一系列准备工作,之后调用了__class_lookupMethodAndLoadCache3 方法,进行方法查找和加载缓存,
  • 下面汇编结束是 C/C++
macro MethodTableLookup// push frameSignLRstp fp, lr, [sp, #-16]!mov fp, sp// 一系列准备工作// save parameter registers: x0..x8, q0..q7sub sp, sp, #(10*8 + 8*16)stp q0, q1, [sp, #(0*16)]stp q2, q3, [sp, #(2*16)]stp q4, q5, [sp, #(4*16)]stp q6, q7, [sp, #(6*16)]stp x0, x1, [sp, #(8*16+0*8)]stp x2, x3, [sp, #(8*16+2*8)]stp x4, x5, [sp, #(8*16+4*8)]stp x6, x7, [sp, #(8*16+6*8)]str x8,     [sp, #(8*16+8*8)]// receiver and selector already in x0 and x1mov x2, x16// 前面都是准备工作 在一系列准备工作之后,进入到方法的查找以及缓存的加载     bl  __class_lookupMethodAndLoadCache3
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

注意 : 从汇编进入到C++阶段的方法,需要把汇编中的方法去掉一个下划线,才能在源码中搜索到。

  • __class_lookupMethodAndLoadCache3 之后就有些迷茫不知道下一步该干什么了,这个地方就查看了一下资料。
  • control+in 进入  objc_msgSend  方法之后,往下找发现有一个  __objc_msgSend_uncached  方法
  • 进入  __objc_msgSend_uncached  内部,在断点处,发现其跳转了一个位于  objc-runtime-new.mm  文件的C++方法

总结:

  • objc_msgSend 是使用汇编写的,主要是速度够快,够灵活(C语言做不到写一个函数来保留未知的参数并且跳转到任意的函数指针)
  • 汇编可以动态识别,性能更快更高。汇编更加容易被机器识别
  • objc_msgSend 首先通过快速路径对缓存进行查找,如果查找不到则进入 MethodTableLookup 方法列表查找,通过_class_lookupMethodAndLoadCache3 进行查找并且缓存


扩展

消息的发送LGStudent *s = [LGStudent new];[s sayCode];// 方法调用底层编译// 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)objc_msgSend(s, sel_registerName("sayCode"));// 类方法编译底层
//        id cls = [LGStudent class];
//        void *pointA = &cls;
//        [(__bridge id)pointA sayNB];objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));// 向父类发消息(对象方法)struct objc_super lgSuper;lgSuper.receiver = s;lgSuper.super_class = [LGPerson class];objc_msgSendSuper(&lgSuper, @selector(sayHello));//向父类发消息(类方法)struct objc_super myClassSuper;myClassSuper.receiver = [s class];myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

上边这个向父类发消息(类方法)

调用super_class 父类,然后去元类查找的原因是因为

类方法要到元类去找,元类找不到向元类的父类去找。

s -> s的元类 –> s父类的元类

直接取父类去发送消息的话就是直接

s的父类->s父类的元类

 

 

 

 

 

 

 

 

 

 

这篇关于6、iOS底层分析 - 消息发送(objc_msgeSend)分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/431851

相关文章

Redis连接失败:客户端IP不在白名单中的问题分析与解决方案

《Redis连接失败:客户端IP不在白名单中的问题分析与解决方案》在现代分布式系统中,Redis作为一种高性能的内存数据库,被广泛应用于缓存、消息队列、会话存储等场景,然而,在实际使用过程中,我们可能... 目录一、问题背景二、错误分析1. 错误信息解读2. 根本原因三、解决方案1. 将客户端IP添加到Re

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

锐捷和腾达哪个好? 两个品牌路由器对比分析

《锐捷和腾达哪个好?两个品牌路由器对比分析》在选择路由器时,Tenda和锐捷都是备受关注的品牌,各自有独特的产品特点和市场定位,选择哪个品牌的路由器更合适,实际上取决于你的具体需求和使用场景,我们从... 在选购路由器时,锐捷和腾达都是市场上备受关注的品牌,但它们的定位和特点却有所不同。锐捷更偏向企业级和专

Django中使用SMTP实现邮件发送功能

《Django中使用SMTP实现邮件发送功能》在Django中使用SMTP发送邮件是一个常见的需求,通常用于发送用户注册确认邮件、密码重置邮件等,下面我们来看看如何在Django中配置S... 目录1. 配置 Django 项目以使用 SMTP2. 创建 Django 应用3. 添加应用到项目设置4. 创建

Spring中Bean有关NullPointerException异常的原因分析

《Spring中Bean有关NullPointerException异常的原因分析》在Spring中使用@Autowired注解注入的bean不能在静态上下文中访问,否则会导致NullPointerE... 目录Spring中Bean有关NullPointerException异常的原因问题描述解决方案总结

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

python中的与时间相关的模块应用场景分析

《python中的与时间相关的模块应用场景分析》本文介绍了Python中与时间相关的几个重要模块:`time`、`datetime`、`calendar`、`timeit`、`pytz`和`dateu... 目录1. time 模块2. datetime 模块3. calendar 模块4. timeit

python-nmap实现python利用nmap进行扫描分析

《python-nmap实现python利用nmap进行扫描分析》Nmap是一个非常用的网络/端口扫描工具,如果想将nmap集成进你的工具里,可以使用python-nmap这个python库,它提供了... 目录前言python-nmap的基本使用PortScanner扫描PortScannerAsync异

Oracle数据库执行计划的查看与分析技巧

《Oracle数据库执行计划的查看与分析技巧》在Oracle数据库中,执行计划能够帮助我们深入了解SQL语句在数据库内部的执行细节,进而优化查询性能、提升系统效率,执行计划是Oracle数据库优化器为... 目录一、什么是执行计划二、查看执行计划的方法(一)使用 EXPLAIN PLAN 命令(二)通过 S

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置