iOS底层原理07: objc_msgSend分析上

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

cache拓展补充

内存平移

我们在上一篇文章中查找selimp是使用了下边这样的方式来查找数据

$12中无法找到数据时,我们采用了指针平移的方式,最终找到了数据:

(lldb) p $9.buckets()[4]
(bucket_t) $19 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 45536
    }
  }
}
复制代码

那么就有一个疑问:$9.buckets()[4]这种取值方式我们经常是在数组中这样通过索引获取数据,那么bucket_t是不是数组呢,我们查看源码发现,bucket_t是一个结构体:

那么通过$9.buckets()的打印数据中我们可以明白,$12是一个结构体指针,针对一个结构体指针的下标操作$9.buckets()[4],我们称为取内存平移$9.buckets()[4]等同于$12+4

_bucketsAndMaybeMask解析

在之前的操作中我们有这样的数据结构:

那么,这个_bucketsAndMaybeMask是什么东西呢?我们通过lldb来看一下:

查看buckets()方法的实现:

通过_bucketsAndMaybeMask获取到地址addr,然后和bucketsMask进行操作之后,强制转换为十六进制地址(还原原来地址)

_bucketsAndMaybeMask存储的就是buckets()这一段内存的首地址

cache读取流程分析

在介绍cache_t是我们知道了方法是通过insert操作进行插入缓存的,那么是什么时候进行的insert操作呢,我们在insert方法中打上断点,执行代码:

根据左边的调用栈分析,是log_and_fill_cache调用了insert方法进行缓存插入操作,我们看一下log_and_fill_cache的实现:

调用栈显示,是lookUpImpOrForward调用了log_and_fill_cache

这是C++底层的消息流程

runtime的运行时理解

runtime 概述

想要理解消息流程,那么必须先了解runtime的相关知识

编译时编译器会帮我们把源代码翻译成机器能识别的代码,或者是某个中间状态的代码。在这个过程中编译器会帮我们进行语法分析类型检查等工作;而运行时是我们的程序已经运行起来,被加载到内存中去的时候,此时所有的操作都是在内存中进行的。

runtime有两个版本:

  • Legacy版本为早期版本,对应Objective-C 1.0,用于32位的MacOS X系统中
  • Modern为现行版本,对应Objective-C 2.0,用于iPhoneMacOS X v10.5之后的64位系统中

Objective-C Runtime Programming Guide

调用runtime 的三种方式

  • OC的方法;比如 [peron run]
  • NSObject的接口;比如 isKindoOfClass
  • objc下层Api;比如class_getInstanceSize

我们来看下边代码

@interface Teacher : NSObject
- (void)say:(NSString *)string;
- (void)run;
@end

@implementation Teacher

- (void)say:(NSString *)string {
    NSLog(@"%s-->%@", __func__,string);
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Teacher *teacher = [Teacher alloc];
        [teacher say:@"hello"];
        [teacher run];
    }
    return 0;
}
复制代码

Teacher有两个方法say:runsay:方法有实现,而run方法没有实现;这段代码在编译时可以编译成功,但是在运行时就会崩溃,报错:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Teacher run]: unrecognized selector sent to instance 0x1007aa0a0'
terminating with uncaught exception of type NSException
复制代码

来看一下它在cpp文件文件中的代码:

Teacher *teacher = ((Teacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Teacher"), sel_registerName("alloc"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)teacher, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_7z_qx6zs9gj2s58dv3ps88mf9pr0000gn_T_main_388285_mi_1);
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("run"));
复制代码

为方便观看,简化如下:

Teacher *teacher = ((Teacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Teacher"), sel_registerName("alloc"));
objc_msgSend)(teacher, sel_registerName("say:"), __NSConstantStringImpl__var_folders_7z_qx6zs9gj2s58dv3ps88mf9pr0000gn_T_main_388285_mi_1);
objc_msgSend(teacher, sel_registerName("run"));
复制代码

我们常用的OC代码都会在底层使用runtime进行解释

OC方法的调用实质上是消息发送的流程

objc_msgSend(id self, SEL _cmd)
复制代码
  • id self 为消息的接收者
  • SEL _cmd 为消息的主体 sel+参数

objc_msgSend和objc_msgSendSuper

明白了其本质之后,那么我们就可以直接通过objc_msgSend实现方法调用:

需要将Build Settings中的Enable Strict Checking of objc_msgSend Calls改为NO,默认为YES

我们给Teacher类添加一个方法talk,并且实现:

- (void)talk;

- (void)talk {
    NSLog(@"-->%s", __func__);
}
复制代码

方法调用成功

我们创建一个Person类,让Teacher继承自Person类,在Person类中实现run方法,代码如下:

@interface Person : NSObject

@end

@implementation Person

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

@end

@interface Teacher : Person
- (void)say:(NSString *)string;
- (void)run;
- (void)talk;
@end

@implementation Teacher

- (void)say:(NSString *)string {
    NSLog(@"%s-->%@", __func__,string);
}

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

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Teacher *teacher = [Teacher alloc];
        [teacher say:@"hello"];
        [teacher run];
    }
    return 0;
}
复制代码

运行项目:

没有崩溃,虽然子类Teacher没有run方法的实现,但是直接找到了父类Personrun方法

此时生成cpp文件:

Teacher *teacher = ((Teacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Teacher"), sel_registerName("alloc"));
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)teacher, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_7z_qx6zs9gj2s58dv3ps88mf9pr0000gn_T_main_54da99_mi_3);
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("run"));
复制代码

cpp文件并没有什么异常,依然是通过objc_msgSend发送的消息,那么我们怀疑是不是objc_msgSend在底层会去找父类的方法呢,我们在cpp文件中发现还有一个向父类发送消息的方法objc_msgSendSuper;从objc源码中可以找到objc_msgSendSuper的方法定义:

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码

他有两个参数:

  • 结构体objc_super
  • SEL

objc源码中关于结构体objc_super的定义:

由于当前环境为满足__OBJC2__Objective-C 2.0,所以结构体可以简化为:

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;
    __unsafe_unretained _Nonnull Class super_class;
    /* super_class is the first class to search */
};
复制代码

我们在工程中,尝试使用objc_msgSendSuper去调用run方法:

解释:super_class是第一个需要查找的类,如果找不到对应的方法,那么向上查找父类,然后查找;因为NSObject没有run方法,而NSObject的父类是nil,所以此处不能传NSObject;

_objc_msgSend流程分析

我们来分析一下_objc_msgSend流程分析的底层实现,它是用汇编实现的,我们在objc的源码中去分析:

代码不多,解释如下:

_objc_msgSend

	ENTRY _objc_msgSend // 汇编常用方法进入ENTRY
	UNWIND _objc_msgSend, NoFrame // 常用语法
	cmp	p0, #0 // 寄存器p0是objc_msgSend的第一个参数`id self`,和0比较判断有没有消息接收者
#if SUPPORT_TAGGED_POINTERS // 判断条件是否支持 tagged pointer
	b.le	LNilOrTagged // 执行LNilOrTagged开始向下执行
#else
	b.eq	LReturnZero // p0 = 0,没有消息接收者,消息为空
#endif
	ldr	p13, [x0] // x0寄存器为消息接收者,将x0赋值给p13,p13 = 接收者的isa
	GetClassFromIsa_p16 p13, 1, x0	// GetClassFromIsa_p16的解释在下方,最终结果 p16 =接收者的class
LGetIsaDone: // 指令完成 其实是一个通过receiver找class的过程,因为class中有cache
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero: // return nil
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend // _objc_msgSend结束
复制代码

GetClassFromIsa_p16

// 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 // 不满足直接看else
	// 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__ // linux MacOS X的系统 此处看else分支arm64系统  
.if \needs_auth == 0 // needs_auth=1 不满足  _cache_getImp takes an authed class already
	mov	p16, \src
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address // 操作结束之后,p16是接收者的class
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
复制代码

ExtractISA

// ExtractISA p16, \src, \auth_address
.macro ExtractISA
	and    $0, $1, #ISA_MASK // $0=p16,$1=src=isa,解释:$1与ISA_MASK进行与操作得到的class,存到$0=p16
.endmacro
复制代码

下一章继续……

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享