iOS 底层探索篇 ——Runimte 运行时&方法的本质

239 阅读7分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

何时进行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_msgSendcache_getImp

在这里插入图片描述

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

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的实现,让LGPerson继承自LGTeacher。

在这里插入图片描述

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 汇编源码

在这里插入图片描述 图片来自链接: Style_月月.

我们在方法调用的地方打个端点,因为方法调用的地方会调用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,点进去,看到了这样一段代码:

//---- 消息发送 -- 汇编入口--objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend 
//---- 无窗口
    UNWIND _objc_msgSend, NoFrame 
    
//---- p0 和空对比,即判断接收者是否存在,其中p0是receiver,也就是person
    cmp p0, #0          // nil check and tagged pointer check 
//---- le小于 --支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative) 
#else
//---- p0 等于 0 时,直接返回 空
    b.eq    LReturnZero 
#endif 
//---- p0即receiver存在的流程
//---- 根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器,这里也注释了得知p13是isa。
    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 
    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    ret

    END_ENTRY _objc_msgSend

参考自Style_月月.

我们看一下这个汇编:

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

这里做的是把x0(receiver)的首地址也就是isa存入p13,然后进入GetClassFromIsa_p16,将p13,1,xo作为参数传入。

这里会判断是否 SUPPORT_INDEXED_ISA。这里是不支持SUPPORT_INDEXED_ISA的。

在这里插入图片描述

所以走到下面__LP64__。在__LP64__流程中,因为传过来的needs_auth ==1 所以会走到 ExtractISA p16, \src, \auth_address

.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(得到class)后存到$0里面,也就是p16里面。

所以

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

这段代码的作用就是将xo的首地址也就是isa放入到p13的寄存器里面,然后根据p13去获取class放入到p16。为什么要获取class呢?因为cache里面的insert在objc_msgSend之前,而cache在class里面 ,所以我们要获取class。