您好,欢迎访问代理记账网站
移动应用 微信公众号 联系我们

咨询热线 -

电话 15988168888

联系客服
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

iOS 底层探索篇 ——Runtime

iOS 底层探索篇 —— Runtime

    • 何时进行insert
    • Runtime 介绍
      • objc_msgSend 汇编源码

何时进行insert

上文讲了insert这个方法,那么到底什么时候进行插入呢。我们在源码中搜索->insert,看看insert方法什么时候被调用。
在这里插入图片描述
发现都不是要找的方法。那么现在我们就需要去cache_t的insert方法的实现里面打断点,然后运行一下,就可以得到insert是被谁调用的了。
在这里插入图片描述
这里可以看到insert的上一步是log_and_fill_cache,那么insert大概率就是在这个里面被调用的,我们点进去看一下。
在这里插入图片描述
果然发现了 cls->cache.insert(sel, imp, receiver).
这里也可以在lldb输入bt进行输出查找,但是不推荐,因为太丑了太难寻找了。
在这里插入图片描述
那么又是那里调用的log_and_fill_cache这个方法的呢。偷懒点击一下log_and_fill_cache下面那一行,就可以跳到调用log_and_fill_cache的地方了,也就是lookUpImpOrForward方法。
在这里插入图片描述

在写入流程之前,还有一个cache读取流程,即objc_msgSend 和 cache_getImp
在这里插入图片描述

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

Runtime 介绍

Runtime称为运行时有两个版本 一个Legacy版本(早期版本) ,一个Modern版本(现行版本),它区别于编译时

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

  • 编译时 顾名思义就是正在编译的时候 . 那什么叫编译呢?就是编译器帮你把
    源代码翻译成机器能识别的代码 . 是源代码翻译成机器能识别的代码的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态的阶段

runtime的使用有以下三种方式,其三种实现方法与编译层和底层的关系如图所示

  • 通过OC代码,例如 [person sayNB]

  • 通过NSObject借口,例如isKindOfClass

  • 通过Runtime API,例如class_getInstanceSize

我们定义一个LGPerson类,在类中添加两个方法,实现其中一个方法。
在这里插入图片描述
生成一个对象,并调用这两个方法,我们可以看到在编译时候是没有报错的。
在这里插入图片描述
运行一下。
在这里插入图片描述
报错了。这就是编译时和运行时的区别。
clang 一下 .m 文件去看一下 这三个方法底层是怎么实现的。
在这里插入图片描述
这里可以看出,方法的本质就是objc_msgSend消息发送,并且消息发送需要两个非常重要的参数。一个就是消息接收者(receiver),第二个就是消息的主体。
我们为方法添加参数看看会有什么变化。
在这里插入图片描述
在这里插入图片描述
重新clang一下。
在这里插入图片描述
发现方法名多了个:,后面多加了一个参数。这就说明,方法的主体 等于 方法名加参数。
既然调用方法等同于objc_msgSend,那么直接调用objc_msgSend的效果是不是一样的呢。
验证一下:
在这里插入图片描述
运行一下:
在这里插入图片描述
发现方法跑了两次,说明[person sayNB]等价于objc_msgSend(person,sel_registerName(“sayNB”))

这里也可以用@selector来传入sel
在这里插入图片描述
结果输出两次,说明是有效的。
在这里插入图片描述

注:
1、直接调用objc_msgSend,需要导入头文件#import <objc/message.h>
2、需要将target --> Build Setting -->搜索msg – 将enable strict checking of obc_msgSend calls由YES 改为NO,将严厉的检查机制关掉,否则objc_msgSend的参数会报错。

那么如果子类调用父类的方法,会是什么情况呢?一起来看一下
先添加LGTeacher类,并添加sayNB的实现。
在这里插入图片描述
clang编译一下:
在这里插入图片描述
我们看到开头是有objc_msgSendSuper方法的,但是没有调用。
在这里插入图片描述
这里去查找一下objc_msgSendSuper是如何实现的
在这里插入图片描述
发现objc_msgSendSuper需要的参数有

  • struct objc_super * super : 一个objc_super的结构体
  • SEL op: 一个sel
  • … : 参数(可能没有)

SEL和参数我们知道是什么,我们去找一下objc_super是个什么东西组成的。
在这里插入图片描述
我们压根就不看__!OBJC2__里面的东西,所以只需要看receiver 和 super_class.
我们尝试在main 中调用objc_msgSendSuper这个方法。
在这里插入图片描述

运行一下:
在这里插入图片描述
发现两者都成功调用了父类LGTeacher中的sayHello方法。所以这里,我们可以作一个猜测:方法调用,首先是在类中查找,如果类中没有找到,会到类的父类中查找。
接下来,我们来探索objc_msgSend的源码实现

objc_msgSend 汇编源码

我们在方法调用的地方打个端点,因为方法调用的地方会调用objc_msgSend方法,如何运行。
在这里插入图片描述
程序运行到这里,然后打开Xcode 工具栏的debug - debug workflow - always showdisassembly。
在这里插入图片描述
在objc_msgSend的那一行打下断点 ,继续运行,然后按住control点击step in。
在这里插入图片描述
看到objc_msgSend在libobjc.A.dylib里面,接着去源码里面搜索objc_msgSend。

在这里插入图片描述
因为我们要找汇编,所以我们看.s文件。我们看到有不同架构下的objc-msg文件,这里我们只看真机情况下也就是objc-msg-arm64.s文件。
在这里插入图片描述
看到这个ENTRY _objc_msgSend,点进去,看到了这样一段代码:
在这里插入图片描述
我们一行一行来看这个汇编:

cmp是英文compare 的缩写,也就是对比,p0的话就是p0的地址,也就是objc_msgSend的第一个参数-消息接收者receiver person的地址,判断person的地址是否为0。

cmp p0, #0

le小于,支持taggedpointer(小对象类型)的流程

#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)

p0 等于 0 时,直接返回 空。

#else
	b.eq	LReturnZero

接下来就是p0(receiver)存在的流程,来看第一行。

	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class

源码已经注释了,p13是isa,p16是class。这里的x0是person,我们搜索一下GetClassFromIsa_p16,找到他的宏。
这里会判断是否 SUPPORT_INDEXED_ISA,这里传过来person 的地址,person对象的首地址是isa,所以传过来的是isa的地址。这里是不支持的,所以走到下面__LP64__。在__LP64__流程中,mov p16, \src,做的是把地址存入到p16中。

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#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:

#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

接下来搜索一下ExtractISA,
在这里插入图片描述
这里就是将传过来的地址$1 & ISA_MASK(得到classclass)后存到$0里面,也就是p16里面。

所以

	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class

这段代码的作用就是去获取class。为什么要获取class呢?因为cache里面的insert在objc_msgSend之前,而cache在class里面 ,所以我们要获取class。


分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进