我这篇文章呢,主要来分析一下objc_msgSend,关于他的一个执行流程和快速查找的过程,那首先我需要了解一下Runtime是怎么调起底层的呢?也就是Runtime是怎么发起的呢? ,我进行任何的方法的调用,他在底层的名字都叫做objc_msgSend,直观的翻译过来就是消息的发送。 所以我通过这种objc_msgSend的方式,也是一样能够实现OC的方法调用的。这里Runtime的三种方式就用了两种,还一种方式是什么呢? 那问题就来了,objc_msgSend是如何让消息传递到父类的呢?难道我要去看他的底层源码吗?先不管这么多,我先全局搜索一下试试。 报了个错,根据之前调用objc_msgSend的经验,也一样的需要强转一下。
/message.h> [obj sayHello]; objc_msgSend(obj,@selector(sayHello)); objc_msgSend(obj,sel_registerName 需要配置让 objc_msgSend 支持多个参数。 3. objc_msgSend源码解析 objc_msgSend 打断点发现是 libobjc.A.dylib 库的: ? 汇编相关的指令可以参考我之前的文章汇编-循环、选择、判断 3.1 _objc_msgSend 源码解读如下: //入口 ENTRY _objc_msgSend UNWIND _objc_msgSend ,__objc_msgSend_uncached) CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached #if SUPPORT_TAGGED_POINTERS
objc_msgSend 的任务是把对象和选择器传入并查找相应方法的函数指针,然后跳转到这个函数指针指向的位置。 查找方法的过程是很复杂的。 0x002 cbz x9, __objc_msgSend_uncached x9 包含了从 bucket 中读取的选择器,这条命令比较了它和0并且如果它不是0的话会跳转到 __objc_msgSend_uncached 这就是 `objc_msgSend` 主体的结尾。剩下的是对 nil 和 标记指针特殊的处理。 objc_msgSend 不能清除这块内存,因为它不知道返回值到底有多大。为了解决这个问题,编译器生成的代码会在调用 objc_msgSend 之前用 0 填满这块内存。 这就是 nil 处理的结尾,也是整个 objc_msgSend 的结尾。 对于 objc_msgSend() 代码有一点不够清晰就是缓存中也包含了 "负" 的缓存值来记录 misses 的缓存。
objc_msgSend方法内部会访问和使用到的数据成员。 objc_msgSend函数的内部实现 objc_msgSend函数是所有OC方法调用的核心引擎,它负责查找真实的类或者对象方法的实现,并去执行这些方法函数。 }; /* objc_msgSend的C语言版本伪代码实现. receiver: 是调用方法的对象 op: 是要调用的方法名称字符串 */ id objc_msgSend(id receiver 是指传递给objc_msgSend的OC方法中的参数。 else return objc_msgSend_uncached(receiver, op, cls, ...); } /* 方法未命中缓存处理函数:objc_msgSend_uncached
上一篇里面,我从OC层面来探索了objc_msgSend如何进行消息的发送,对普通开发者来说也是比较容易理解的,那很多人都知道,Runtime是由C或者C++以及汇编语言写的一套底层的API。 然后我打开objc源码,全局搜索objc_msgSend。 竟然有644个!看哪一个呢?我之前已经剧透了,objc_msgSend是用汇编写的,所以我来找汇编文件就可以了,按住command。 展开arm64.s文件之后,也会看到很多的objc_msgSend,如果稍微有点熟悉的话,你就会知道,在汇编里面,经常有这么一个方法,ENTRY。 这里先插一个问题,为什么objc_msgSend是用汇编写的?而不是用C/C++写呢?我刚刚随便一搜索就搜到了很多的objc_msgSend。 也就是说,源码里面包含了多个版本的objc_msgSend方法,他们是根据返回值的类型和调用者的类型分别处理的,如果说用C或者C++来实现。
由于首次调用或者缓存扩容等问题导致的缓存查找失败,就需要进入慢速查找流程. objc_msgSend慢速查找 慢速查找入口-汇编部分 在快速查找流程无法找到对应缓存的时候,会跳到CheckMiss\JumpMiss 这个macro中并且走到__objc_msgSend_uncached这个函数中。 STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS 点击下一步来到__objc_msgSend_uncached ? 最终调用lookUpImpOrForward,而且给出了c++函数的位置 ? objc_msgSend慢速流程.png cache_getImp没有发生递归 STATIC_ENTRY _cache_getImp GetClassFromIsa_p16 p0 CacheLookup
前言 Runtime 消息发送与转发流程总是大家关注的重点,却常常忽略方法缓存机制这个显著提升 objc_msgSend 性能的幕后功臣。 一、从 objc_msgSend 谈起 注意:arm64 汇编代码会出现很多p字母,实际上是一个宏,64 位下是x,32 位下是w,p就是寄存器。 objc_msgSend objc_msgSend 代码如下: ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFram ...// 处理对象是 STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves MethodTableLookup 那么在前面分析的__objc_msgSend_uncached方法就仍然会调用这个IMP,接下来就是真正的消息转发阶段了。
上一篇文章分析了 objc_msgSend 的汇编实现,这边文章继续分析 objc_msgSend 中缓存的查找逻辑以及汇编代码是如何进入 c/c++ 代码的。 1. CacheLookup 查找缓存 1.1 CacheLookup源码分析 传递的参数是 NORMAL, _objc_msgSend, __objc_msgSend_uncached: //NORMAL, _objc_msgSend, __objc_msgSend_uncached .macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant __objc_msgSend_uncached() } 2. __objc_msgSend_uncached 在缓存没有命中的情况下会走到 __objc_msgSend_uncached() 的逻辑: STATIC_ENTRY __objc_msgSend_uncached
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello")); //简化: objc_msgSend 方法快速查找 objc_msgSend源码流程 想要了解方法是如何查找的,就需要查看objc_msgSend的方法实现。(前提:方法快速查找流程使用汇编编写) ? //objc_msgSend(id,Sel); ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame // p0表示该方法的第一个参数。 CacheLookup NORMAL, _objc_msgSend objc_msgSend快速查找源码流程 想要看懂这部分内容需要对:类的结构、cacae_t的结构有一个清楚的理解。 ... .endmacro 查找失败后都会进入__objc_msgSend_uncached STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached
因此我们可以将所有静态库字符串表中的objc_msgSend统一替换为另外一个长度相同的字符串:hook_msgSend(名字任意只要长度一致并唯一)即可。 然后在主工程源代码中实现一个名字为hook_msgSend的函数即可。 这个函数必须要和objc_msgSend的函数签名保持一致,这样在链接时所有静态库中的objc_msgSend调用都会统一转化为hook_msgSend调用。 下面的是具体的实现步骤: 1. 在主工程中编写hook_msgSend的实现。 hook_msgSend的函数签名要和objc_msgSend保持一致,并且要在主工程代码中实现,而且必须要用汇编代码实现。 三)、将字符串表中的objc_msgSend字符串替换为hook_msgSend字符串。 四)、保存并关闭静态库.a文件。 5. 编译、链接并运行你的主工程程序。
(v31, (const char *)&unk_190A2D5CF); v138 = v32; v33 = objc_msgSend(v32, "rmxhgtexxxYJsr { v37 = (void *)objc_alloc(&OBJC_CLASS___xxYuxCNxkKxxZe); v134 = objc_msgSend (v37, (const char *)&unk_190A2D5CF); v133 = objc_msgSend(v134, "GlvxhXtNyNxYxb::", v137, (v4, "init"); v5 = (void *)objc_alloc(&OBJC_CLASS___NSDictionary); *(_QWORD *)&v386 = objc_msgSend (v5, "init"); v6 = (void *)objc_alloc(&OBJC_CLASS___xxxNxNxxfRxxxx); v385 = objc_msgSend(v6, "init
objc_msgSend函数是runtime中核心的函数,为什么会崩溃在这,怎么处理这种crash? 2、objc_msgSend原理 每一个OC对象有一个类,每一个OC类都有一个方法列表。 另一个原因是objc_msgSend必须够快。 当然,谁都不会想要用汇编写下整个复杂的消息查找过程。这没必要。 因此,objc_msgSend主要有以下几个步骤: 获取传入的对象的类 获取这个类的方法缓存 通过传入的selector,在缓存中查找方法 如果缓存中没有,调用C代码 跳到这个方法的IMP 3、objc_msgSend 使用符号断点,我们可以查看objc_msgSend的符号指令 libobjc.A.dylib`objc_msgSend: 0x1931bb6a0 <+0>: cmp x0, #0x0 4、objc_msgSend crash原因 如上图,对象在堆内存区,在还没有被销毁之前,isa指针会指向其Class对象的内存地址,此时objc_msgSend是没有问题的,而对象被销毁之后,堆内存被回收
追根溯源找到了objc_msgSend,下面探究下objc_msgSend。 既然方法调用都是通过objc_msgSend的,那么我可不可以直接通过objc_msgSend发消息呢。 在用objc_msgSend方式发送消息。 objc_msgSend汇编探究 探究objc_msgSend首先找到objc_msgSend所在的底层库。怎么找呢? 必须拿出yysd-汇编 汇编显示objc_msgSend在libobjc.A.dylib系统库,实际上看objc_msgSend前缀是objc猜测应该在 objc源码中。
编译器会将一个下面的一个消息表达式 [receiver message] 转变成一个消息函数 objc_msgSend,这个函数将接收者和消息中提到的方法的名称(即方法selector)作为其两个主要参数 : objc_msgSend(receiver, selector) 消息中传递的其他参数也在 objc_msgSend被处理 objc_msgSend(receiver, selector, arg1 所以,利用objc_msgSend也可以达到混编的目的 假设我们有一个OC对象NewObject继承自NSObject: @interface NewObject : NSObject - (void ((id)objc_getClass("NewObject"), sel_registerName("alloc"), sel_registerName("init")); objc_msgSend ((id)myobj, sel_registerName("doSomethingWith:"), (char *)"abc"); 如果是类方法则更简单了: objc_msgSend((id)objc_getClass
)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc (eat)); 上面方法调用的意思就是:给p对象发送名为eat的消息,所以OC中给对象发消息本质上都是调用objc_msgSend方法,接着看下苹果官方文档对这个方法的定义(我是用的Dash查看的): 和objc_msgSend_stret。 接下来我们再看下objc_msgSend的底层实现,objc 源码,发现底层是用汇编代码实现的(表示很蛋疼): ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE
objc_retainAutoreleasedReturnValue(v5); 85 CFRelease(v69); 86 CFRelease(v68); 87 v6 = objc_msgSend (v63, (const char *)&unk_195F9F96E); 96 month = objc_msgSend(v63, (const char *)&unk_195F9F973); 97 day = objc_msgSend(v63, (const char *)&unk_195F9F979); 98 hour = objc_msgSend(v63, (const char *)&unk_195F34810); 99 minute = objc_msgSend(v63, (const char *)&unk_195F5F105); 100 second = objc_msgSend (v37, "setKey:", v46); 196 objc_msgSend(v37, "setClazz:", v40); 197 objc_msgSend
说道Objective-C里面的消息机制,大部分人都知道是调用方法其实就是发送消息,一个叫objc_msgSend的东西负责的。 这个实现的函数就是objc_msgSend,该函数定义如下: void objc_msgSend(id self, SEL cmd, ...) 函数调用)中的那些参数 举例来说: id return = [git commit:parameter]; 上面的Objective-C方法在运行时会转换成如下函数: id return = objc_msgSend (git, @selector(commit), parameter); objc_msgSend函数会在接收者所属的类中搜寻其方法列表,如果能找到这个跟选择子名称相同的方法,就跳转到其实现代码,往下执行 说过了OC的函数调用实现,你会觉得消息转发要处理很多,尤其是在搜索上,幸运的是objc_msgSend在搜索这块是有做缓存的,每个OC的类都有一块这样的缓存,objc_msgSend会将匹配结果缓存在快速映射表
、理解objc_msgSend的作用 对象上调用方法用OC的术语,叫做“传递消息”。消息有名称或选择子,可以接受参数,而且可能还有返回值。 先理解C语言的函数调用方式。 ,其“原型”如下: void objc_msgSend(id self, SEL cmd, ...) ); objc_msgSend函数会依据接受者与选择子的函数来调用适当的方法。 objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和objc_msgSend函数很像。 这项优化对objc_msgSend非常关键,如果不这么做的话,那么每次调用Objective-C方法之前,都需要为调用objc_msgSend函数准备“栈帧”,大家在“栈踪迹”中可以看到这种“栈帧”。
BUG:使用objc_msgSend时报错 原因:Xcode默认设置是禁用Runtime objc_msgSend call方法的 解决办法: 将 objc_msgSend设置成no.不让禁用即可 所有用objc_msgSend肯定可以上架的。 ?
bl _objc_msgSend dateComponents.month = 2022; bl _objc_msgSend objc_msgSend 方法是汇编实现的,它的函数定义是 Id objc_msgSend(id self, SEL _cmd, ...) : id 表示当前对象,sel 表示这个对象的所有方法。 前面已经提到调用 objc_msgSend 需要指令(例如上图中,使用 bl 汇编指令跳转 _objc_msgSend 函数)。 不过我们仍然需要调用真正的 objc_msgSend函数。 同样,objc_msgSend有另一个不同的间接方式来加载函数本身的地址并调用它。 但带来的负面影响是这将连续进行两次调用(_objc_msgSend$dateFromComponents 和 _objc_msgSend),这对性能来说并不理想。