- iOS Objective -C alloc 调用流程
- iOS Object-C init&new
- iOS OC 对象的内存对齐原则
- iOS Objective-C isa
- iOS Objective-C isa 走位分析
- iOS OC 类原理
- iOS OC 方法的本质
1.Runtime
简介
1.1 Runtime
Runtime官方文档
作为一名iOS开发人员,说去Runtime
一定都很熟悉,Runtime
承载了Objective-C
的动态特性,也是使Objective-C
成为动态语言的根本。Runtime
是一套由C、C++和汇编编写的Api,其目的是为我们的OC提供动态特性,为了执行效率更高,稳定性更强,所以采用更接近于机器语言的C、C++和汇编。
1.2 Runtime Version
Runtime 两个版本分别是objc1(legecy
),objc2(Modern
),现行的版本主要是objc2
,主要就是在底层源码中-new
文件中的内容,同时也可以在宏__OBJC2__
中得以体现。
1.3 Runtime 的使用方式:
- Objective-C code @selector;
- NSObject 的方法 NSSelectorFromString();
- sel_registerName函数api
2. Objective-C
方法的本质
2.1 通过clang查看
执行clang
命令
clang -rewrite-objc main.m -o main.cpp
编译后我们可以看到,无论是
alloc
和init
还是我们的sayNB
方法,除去前面的类型强转都是objc_msgSend
,这个方法有两个参数,一个是消息的接收者为id
类型,另一个是方法的编号sel
。
那么如果我们直接调用一个函数呢,下面我们再次通过clang
编译后进行查看。
此时我们发现,如果直接调用函数则就是直接调用并没有出现objc_msgSend
。C语言函数的调用并没有通过objc_msgSend
进行消息发送。因为C语言函数是静态的,编译期间就已经确定了,函数地址也固定,所以函数的调用就是直接去找函数的地址调用即可。Objective-C
作为一门动态语言,需要在运行时去动态的查找方法,所以需要消息发送,至此我们应该能够大概清楚一点,OC方法的本质就是消息的发送。
2.2 OC方法发送的几种情况
注意一点:在OC中使用objc_msgSend
的时候,需要将Enbale Strict of Checking of objc_msgSend Calls
设置为NO
。这样才不会编译报错。
- 向对象
s
发送sayCode
消息,这是我们最常用的,init一个对象,然后调用该对象的方法。
LGStudent *s = [LGStudent alloc];
[s sayCode];
objc_msgSend(s, sel_registerName("sayCode"));
- 向
LGStudent
这个类的原类发送sayNB
消息,这个也是我们经常用到的一种方式,类调用它的类方法。
[LGStudent sayNB];
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("sayNB"));
- 向父类
LGPerson
发送sayHello
消息,这个也是我们平常应用很多的方式,就是调用父类的方法。
LGStudent *s = [LGStudent alloc];
struct objc_super lgSuper;
lgSuper.receiver = s;
lgSuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgSuper, @selector(sayHello));
- 向父类
LGPerson
的类,LGPerson
的原类发送sayNB
消息,这个就是平常我们调用父类的类方法的一种形式。
LGStudent *s = [LGStudent alloc];
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));
3.objc_msgSend
探索
注: 本文使用环境:
- objc4-779.1
- Xcode 11.5 (11E608c)
3.1 查找objc_msgSend
的实现
经过全局搜索,我们发现objc_msgSend
是用汇编实现的,而且根据不同架构有不同的版本,那么只能用比较抠脚的汇编知识去探索一下了,那么为什么要用汇编实现呢,应该是汇编更接近于机器语言容易被机器识别,OC 的动态特性导致参数和类型都是未知的,C语言中不可能通过写一个函数来保留未知的参数并且跳转到任意的函数指针,所以不能使用C
和C++
。
3.2 objc_msgSend
实现原理
3.2.1 objc_msgSend
开始的位置
注:本文分析的是objc4-779.1 arm64汇编的objc_msgSend的源码实现
首先通过搜索objc_msgSend
找到其实现的位置,在objc-msg-arm64.s
文件中,找到ENTRY _objc_msgSend
,ENTRY
是我们的汇编程序入口。
objc_msgSend源码
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
步骤分析:
cmp p0, #0 // nil check and tagged pointer check
首先对p0
进行非空校验,在汇编中我们通常会把第一个参数放入到第0个寄存器,objc_msgSend
第一个参数其实就是我们的id self
消息接收者。
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
非空校验通过后,将我们的x0
存储到p13
寄存器,然后调用GetClassFromIsa_p16
这个方法。根据名字我们应该能想到,该方法的用意是通过isa
获取类,方法存储在类中,要想找到方法就必须拿到类,无论是类还是原类。
GetClassFromIsa_p16源码
/********************************************************************
* GetClassFromIsa_p16 src
* src is a raw isa field. Sets p16 to the corresponding class pointer.
* The raw isa might be an indexed isa to be decoded, or a
* packed isa that needs to be masked.
*
* On exit:
* $0 is unchanged
* p16 is a class pointer
* x10 is clobbered
********************************************************************/
#if SUPPORT_INDEXED_ISA
.align 3
.globl _objc_indexed_classes
_objc_indexed_classes:
.fill ISA_INDEX_COUNT, PTRSIZE, 0
#endif
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // 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__
// 64-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
通过查看GetClassFromIsa_p16
源码我们可以发现,其主要的作用就是通过isa
与上ISA_MASK
拿到类信息。
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
最后获取到类后调用CacheLookup
,下面我们开始分析CacheLookup
。
3.2.2 CacheLookup
、CacheHit
、CheckMiss
、JumpMiss
等分析
话不多说,先上源码看看
CacheHit
、CheckMiss
、JumpMiss
、CacheLookup
源码
/********************************************************************
*
* CacheLookup NORMAL|GETIMP|LOOKUP
*
* Locate the implementation for a selector in a class method cache.
*
* Takes:
* x1 = selector
* x16 = class to be searched
*
* Kills:
* x9,x10,x11,x12, x17
*
* On exit: (found) calls or returns IMP
* with x16 = class, x17 = IMP
* (not found) jumps to LCacheMiss
*
********************************************************************/
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, x1 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x12, x1 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro CacheLookup
// p1 = SEL, p16 = isa
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask
#endif
and w12, w1, w11 // x12 = _cmd & mask
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro
__objc_msgSend_uncached
源码:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
MethodTableLookup
源码:
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
我们先从CacheLookup
入手,通过查看源码我们可以知道CacheLookup
有三种模式,分别是#define NORMAL 0
、#define GETIMP 1
和#define LOOKUP 2
,其中objc_msgSend
使用的是NORMAL
模式,从调用处传参就知道了。
源码分析:
CacheLookup
:首先将x16
也就是我们的类信息,偏移16字节,这个16字节来自一个宏定义#define CACHE (2 * __SIZEOF_POINTER__)
,通过偏移16字节就是我们的cache_t
的初始位置,所以p10
就是buckets
,p11
就是occupied|mask
,由于iOS
小端模式,低四位w11
就是mask
,高四位就是occupied
;将
w1
与上w11
就是sel
与上mask
就是通过哈希找出方法的位置;在
p10
也就是buckets
里面通过对p12
的累加不断寻找我们需要的方法,循环完毕后将结果放入p17
和p9
寄存器中;CacheHit
:比较p1
和p9
的值,如果找到了,则调用CacheHit
返回,这是在缓存cache
找到的结果,如果没找到就是bl not equal 2f
;CheckMiss
:跳转到2f
后调用CheckMiss
,我们使用的是NORMAL
模式,在CheckMiss
中NORMAL
对应的是__objc_msgSend_uncached
;__objc_msgSend_uncached
:中其主要就是继续调用了MethodTableLookup
;MethodTableLookup
:通过寄存器的各种操作准备查找所必须的条件,然后调用_lookUpImpOrForward
_lookUpImpOrForward
: 我们搜索该字符串并没有找到其实现的地方,这个时候我们想到C++方法的调用会在前面加一个,然后我们去掉在objc-runtime-new.mm
文件中就找到了它的实现,其实这就是方法查找流程中的慢速查找。此处的分析我们放在后面继续进行。下面我们回到
CacheLookup
,在调用CheckMiss
完成后比较p12
和p10
就是第一次查找后的位置是不是buckts
的首地址,如果是则跳转到3f
进行再次循环查找,其实就是在缓存中没找到,然后通过上述的慢速查找流程查找完毕后加入缓存,在次查找一遍。如果不是首地址就loop
循环到1f处重新执行上述操作。因为通过慢速查找后buckts
的地址可能通过扩容或者新增,致使首地址发生变化?(不太清楚)在跳转到
3f
处查找完毕后来到下面的1
处,继续判断查找到的是否是想要的,如果是则通过CacheHit
返回,如果不是继续跳转到2f
处继续CheckMiss
,完毕后继续比较bucket
和buckets
的地址,相同则跳转到3f
处调用JumpMiss
,不同则循环回到1f
处比较返回。
4.总结
- Objective-C方法的本质就是消息发送,消息发送司通过
objc_msgSend
函数实现的 -
objc_msgSend
为了执行的快速和稳定性,以及对参数和类型未知性的支持采用汇编实现 - 方法的查找优先从类的缓存进行查找,也就是我们类的
cache
,(cache_t类型),如果找到了直接返回,找不到则跳转到由C++实现的lookUpImpOrForward
函数进行查找。 - 方法查找有两种流程,一种是快速流程就是通过汇编在类的缓存里面查找,另一种就是慢速流程
lookUpImpOrForward
,下一篇文章我们将着重介绍lookUpImpOrForward