iOS底层-方法的本质&Runtime方法快速查找(上)

377 阅读5分钟

前言

在上一篇的探究类的cache文章中,我们在查找调用过程时找到了objc_msgSend,也就是我们常说的Runtime(运行时),我们先来了解下Runtime文档,接下来我们去分析Runtime

Runtime简介

Runtime我们知道是运行时,提到运行时我们会想到还有个编译时,他们之前的区别是什么呢?

  • 编译时:编译就是编译器帮你把源代码翻译成机器能识别的代码,编译时只是对语言进行最基本的检查报错,包括词法分析、语法分析等等。编译通过并不意味着程序就可以成功运行。可以说编译时是一个静态的阶段,类型错误很明显可以直接检查出来。
  • 运行时代码跑起来,被装载到内存中。,这个时候会具体对类型进行检查,而不仅仅是对代码简单扫描分析,此时若出错,程序会崩溃。运行时则是动态的阶段,开始具体与运行环境结合起来。 我们可以用三个方式调用Runtime
    1. OC的方法,例如:[person sayNB]
    1. NSObject的方法,例如:isKindOfClass
    1. RuntimeApi,例如:class_getInstanceSize 调用Runtime的三种方式:

截屏2021-06-30 11.07.03.png

方法的本质

方法的本质

    1. 代码准备: 定义一个WSPerson类,然后调用方法,最后用clang编译成C++代码:
@interface WSPerson : NSObject

- (void)sayNB;
- (void)sayGood:(NSString *)job;

@end

@implementation WSPerson

- (void)sayNB {
    NSLog(@"%s", __func__);
}

// 调用:
WSPerson *person = [WSPerson alloc];
[person sayNB];
[person sayGood: @"coder"];

// C++
WSPerson *person = ((WSPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WSPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)person, sel_registerName("sayGood:"), (NSString *)&__NSConstantStringImpl__var_folders_7t_93mn_rbs0p1d7bkzy4z_f0s40000gn_T_main_e9af44_mi_1);

@end

通过代码分析,我们知道方法的本质是objc_msgSend(消息发送),而objc_msgSend调用需要消息的接收者sel+参数。那么我们直接用模仿C++中的objc_msgSend来调用方法能成功吗?接下来我们来验证

  • 2. objc_msgSend直接调用方法

注意:
1. 导入头文件#import <objc/message.h>
2.关闭objc_msgSend的检查机制:target->Build Setting->搜索msg->Enable strict Checking of obc_msgSend Calls改为NO。 然后模仿调用:

WSPerson *person = [WSPerson alloc];
[person sayNB];
objc_msgSend(person, sel_registerName("sayNB"));

打印结果如下:

截屏2021-06-30 14.17.58.png

  • 得出结论:[person sayNB]objc_msgSend(person, sel_registerName("sayNB"))是等价的。

调用父类方法

  • 我们调用父类方法再试试:
@interface WSPerson : NSObject
- (void)sayNB;
- (void)sayGood;
@end

@implementation WSPerson
- (void)sayNB {
    NSLog(@"%s", __func__);
}
- (void)sayGood {
    NSLog(@"父类的方法:%s", __func__);
}
@end

@interface WSTeacher : WSPerson
@end

@implementation WSTeacher
- (void)sayGood {
    [super sayGood];
}

@end

编译成C++后,发现多了个objc_msgSendSuper

static void _I_WSTeacher_sayGood(WSTeacher * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("WSTeacher"))}, sel_registerName("sayGood"));
}
  • objc_msgSendSuper应该是给父类发送消息,我们去objc4-818.2查看源码:
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

objc_msgSendSuper有两个参数,第二个是SEL,第一个是个objc_super结构体,结构如下:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver; //接受者

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

我们现在用的是__OBJC2__,所以objc_super参数只有两个:receiversuper_class,这个super_class有个注释,是第一次查找的类,难道是查找不到会再查找父类?我们去试试。

  • 使用objc_msgSendSuper去调用父类方法:
struct objc_super ws_objc_super;
ws_objc_super.receiver = person;
ws_objc_super.super_class = teacher.class;
objc_msgSendSuper(&ws_objc_super, @selector(sayGood));

struct objc_super ws_objc_super1;
ws_objc_super1.receiver = person;
ws_objc_super1.super_class = person.class;
objc_msgSendSuper(&ws_objc_super1, @selector(sayGood));

// 结果
// 2021-07-01 10:51:36.953720+0800 RuntimeDemo[1977:2219575] 父类的方法:-[WSPerson sayGood]
// 2021-07-01 10:51:36.953777+0800 RuntimeDemo[1977:2219575] 父类的方法:-[WSPerson sayGood]
  • 说明super_class无论是teacher.class还是person.class,不影响结果。

总结

  • 从调用当前类和父类的方法,我们得出结论:方法的本质是消息发送。那么objc_msgSend的具体过程是怎样的,接下来我们进行汇编分析

objc_msgSend汇编

  • objc源码中搜索objc_msgSend,然后找到objc-msg-arm64.s汇编文件

截屏2021-07-01 14.09.51.png

_objc_msgSend

  • 首先objc_msgSend有两个参数消息的接收者sel
  • cmpcompare比较的意思,p0是消息的接收者,#00,这里是拿p00做对比。SUPPORT_TAGGED_POINTERS是小对象模式,它的定义是:
#if __arm64__
...
#define SUPPORT_TAGGED_POINTERS 1
...
// true arm64
#else
// arm64_32
#define SUPPORT_TAGGED_POINTERS 0
...
#endif

我们现在是模拟器,所以SUPPORT_TAGGED_POINTERS0,所以判断往下走。b.eq LReturnZero:这个是p00相等,也就是消息接受者不存在的意思,这时候返回LReturnZero:

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

也就是直接返回nil

GetClassFromIsa_p16

  • 继续往下走,这里p13=isa然后再看这个主要的方法GetClassFromIsa_p16,调用时传入了p13, 1, x0
// src=p13=isa, needs_auth=1, auth_address=x0
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA //arm64
	// 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 //将src赋值给p16
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address //得到p16
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

GetClassFromIsa_p16是一个宏定义,要传入三个参数,由于SUPPORT_INDEXED_ISAarm64中为1,则直接跳过看 __LP64__,这里传入的参数needs_auth=1,所以看ExtractISA这个方法:

.macro ExtractISA
	and    $0, $1, #ISA_MASK
.endmacro

ExtractISA

ExtractISA也是个宏定义,and&运算,这里是将$1 & ISA_MASK,再将得到的结果赋值给$0,而$0就是p16,所以这是根据isaISA_MASK拿到class的过程,然后回到最开始GetClassFromIsa_p16 p13, 1, x0,这个步骤就是先拿到class,所以这一行的注释上p16 = class

问题

    1. 为什么要先拿到class
    • 首先,我们分析objc_msgSend主要是为了寻找cache,但cache是在class里,所以我们必须先拿到class后才能取寻找class中的cache