iOS 底层原理 - 运行时、方法调用及快速方法查找

60 阅读9分钟

1.运行时和编译时

  • 编译时 顾名思义就是正在编译时候,就是编译器帮你把源代码翻译成机器能识别代码,这个过程做了类型检查、语法分析和词法分析;
  • 运行时 就是代码跑起来了,被装载到内存中去了。

2.Runtime调用

1.png

  • Objective-C Code代码中,比如对象方法的调用, [user sayHello];

  • Framework&Service中,接口的调用,比如isKindOfClassisMemberOfClass等;

  • Runtime API中,c/c++源码方法的使用,比如objc_msgSend, objc_msgSendSuper。 上图中:

  • Compiler为编译器层,会将代码翻译成某个中间状态的语⾔,同时会做一些LLVM编译器的优化,比如将alloc方法优化执行objc_alloc方法。

  • runtime system libarary 就是底层库。

3.方法调用的本质

1.探索本质

使用clangoc代码编译成c/c++代码,查看本质。

((void (*)(id, SEL))(void *)objc_msgSend)((id)user, sel_registerName("saySomething1"));

Objective-C中,JHSPerson对象saySomething1方法的调用,最终是通过c/c++objc_msgSend来实现。objc_msgSend很眼熟,因为我们在上一篇章,cache_t结构分析与底层探索中,发现过他的踪迹!

回顾一下我们探索cache_t的思路和流程:

  • cache_t结构体中,有一个方法void incrementOccupied();,增加占用,内部实现为:_occupied++;,很容易理解:向cache_t中插入内容,占用数加1

  • 全局搜索incrementOccupied()方法,只有一个地方用到了该方法,向cache_t中插入数据,cache_t::insert方法。

  • 继续全局搜索cache_t::insert方法找到了一段非常重要的注释,解读注释:cache_t分为cache读取cache写入两个点。见下图:

2.png

这里给了我们很大的启发,结合上面的分析,方法的调用最终都是转为了消息发送,也就是objc_msgSend,此过程中应该会通过cache_getImp方法从缓存cache_t中获取方法实现,从而完成方法的一个查找流程,最终完成函数的执行。

2.OC和c/c++源码转换

OC代码做个转换,使用objc/message.h c/c++源码来实现方法调用。例如:

        JHSPerson *person = [JHSPerson alloc];
        [person saySomething2];
        objc_msgSend(person, @selector(saySomething2));

运行发现调用结果是一致的。需要注意的是需要将objc_msgSend严厉的检查机制关掉。见下图:

3.png

3.objc_msgSendSuper

引入一个案例

JHSPerson继承自JHSTeacherJHSPerson只进行了saySomething1方法的声明,并没有实现saySomething1方法,JHSTeacher实现了saySomething1方法。在main中,JHSPerson调用了方法saySomething1。参考下面的示例:

@interface JHSTeacher : NSObject

- (void)saySomething1;

@end

@implementation JHSTeacher

- (void)saySomething1{

    NSLog(@"666 %s",__func__);

}

@end

@interface JHSPerson : JHSTeacher

- (void)saySomething1;

- (void)saySomething2;

@end

@implementation JHSPerson

- (void)saySomething2{

    NSLog(@"666");

}

@end

打印结果是:

666 [JHSTeacher saySomething1] 虽然JHSPerson没有实现saySomething1方法,但程序运行后并没有异常,而是调用了父类的saySomething1方法。说明消息的接受者虽然是自己,自己没有实现,调用到了父类的方法。我们可以使用objc_msgSendSuper直接调用父类的方法。

objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

该方法的两个参数:

  • 参数1:结构体objc_super指针;
  • 参数2:方法编号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 */

};

代码润霞

JHSPerson *person = [JHSPerson alloc];
// 子类是不是没有 -> 父类
[person saySomething1];
struct objc_super jhs_objc_super;
jhs_objc_super.receiver = person;
jhs_objc_super.super_class = JHSPerson.class;
objc_msgSendSuper(&jhs_objc_super,@selector(saySomething1));

Something1 -[JHSTeacher saySomething1] Something1 -[JHSTeacher saySomething1]

4.快速方法查找

objc_msgSend的快速方法查找流程是通过汇编实现的,使用汇编的原因是速度快。在libobjc.A.dylib全局搜索_objc_msgSend,汇编核心实现在objc_msg_arm64.s文件中。

1._objc_msgSend函数(关于寄存器了解)

ENTRY _objc_msgSend

UNWIND _objc_msgSend, NoFrame
//1. p0 和空对比,即判断接收者是否存在,其中p0是objc_msgSend的第一个参数-消息接收者receiver
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
// 2.获取isa
ldr p13, [x0] // p13 = isa 
// 3. p16 = class 获取对应的类
GetClassFromIsa_p16 p13, 1, x0 //

LGetIsaDone:

// 4. CacheLookup
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS

LNilOrTagged:

b.eq LReturnZero // nil check

GetTaggedClass

b LGetIsaDone

// SUPPORT_TAGGED_POINTERS

#endif

流程:

  1. 首先对方法接受者p0,判断是否为空,如果为空直接返回;
  2. 如果不为空,获取isa指针,放到p13中,也即是对象的首地址;
  3. GetClassFromIsa_p16是定义的一个宏,在实现中,通过isa找到对应的类;ExtractISA也是个宏定义,将传入的isa&isaMask,得到class,并将class赋给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

// 宏定义ExtractISA
.macro ExtractISA

and    $0, $1, #ISA_MASK

.endmacro
  1. 类找到后,进入宏CacheLookUp缓存查找流程。

4.png

2.宏CacheLookUp

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

mov x15, x16 // stash the original isa

LLookupStart\Function:

// p1 = SEL, p16 = isa

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS

ldr p10, [x16, #CACHE] // p10 = mask|buckets

lsr p11, p10, #48 // p11 = mask

and p10, p10, #0xffffffffffff // p10 = buckets

and w12, w1, w11 // x12 = _cmd & mask

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16

ldr p11, [x16, #CACHE] // p11 = mask|buckets

#if CONFIG_USE_PREOPT_CACHES

#if __has_feature(ptrauth_calls)

tbnz p11, #0, LLookupPreopt\Function

and p10, p11, #0x0000ffffffffffff // p10 = buckets

#else

and p10, p11, #0x0000fffffffffffe // p10 = buckets

tbnz p11, #0, LLookupPreopt\Function

#endif

eor p12, p1, p1, LSR #7

and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask

#else

and p10, p11, #0x0000ffffffffffff // p10 = buckets

and p12, p1, p11, LSR #48 // x12 = _cmd & mask

#endif // CONFIG_USE_PREOPT_CACHES

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4

ldr p11, [x16, #CACHE] // p11 = mask|buckets

and p10, p11, #~0xf // p10 = buckets

and p11, p11, #0xf // p11 = maskShift

mov p12, #0xffff

lsr p11, p12, p11 // p11 = mask = 0xffff >> p11

and p12, p1, p11 // x12 = _cmd & mask

#else

#error Unsupported cache mask storage for ARM64.

#endif

add p13, p10, p12, LSL #(1+PTRSHIFT)

// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

// do {

1: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--

cmp p9, p1 //     if (sel != _cmd) {

b.ne 3f //         scan more

//     } else {

2: CacheHit \Mode // hit:    call or return imp

//     }

3: cbz p9, \MissLabelDynamic //     if (sel == 0) goto Miss;

cmp p13, p10 // } while (bucket >= buckets)

b.hs 1b

\


// wrap-around:

//   p10 = first bucket

//   p11 = mask (and maybe other bits on LP64)

//   p12 = _cmd & mask

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS

add p13, p10, w11, UXTW #(1+PTRSHIFT)

// p13 = buckets + (mask << 1+PTRSHIFT)

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16

add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))

// p13 = buckets + (mask << 1+PTRSHIFT)

// see comment about maskZeroBits

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4

add p13, p10, p11, LSL #(1+PTRSHIFT)

// p13 = buckets + (mask << 1+PTRSHIFT)

#else

#error Unsupported cache mask storage for ARM64.

#endif

add p12, p10, p12, LSL #(1+PTRSHIFT)

// p12 = first probed bucket

// do {

4: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--

cmp p9, p1 //     if (sel == _cmd)

b.eq 2b //         goto hit

cmp p9, #0 // } while (sel != 0 &&

ccmp p13, p12, #0, ne //     bucket > first_probed)

b.hi 4b

省略。。。。

流程简述,只分析CACHE_MASK_STORAGE_HIGH_16环境:

1.获取类对象地址后,进行指针平移16个字节,得到cache_t的首地址;因为objc_class中,isa8个字节superclass8个字节;平移16个字节,即可获取cache_t首地址,赋值给p11,也就是_bucketsAndMaybeMask;源码如下:

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        ldr	p11, [x16, #CACHE]  // p11 = mask|buckets

2.在arm64环境下,maskbuckets放在一起共占用8个字节64位;其中mask高16位buckets低48位。通过掩码运算&#0x0000fffffffffffe高16位抹零获取buckets;将buckets赋值给p10

#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
tbnz	p11, #0, LLookupPreopt\Function
and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else 
// 走该流程获取buckets
and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
tbnz	p11, #0, LLookupPreopt\Function
#endif 
// 此部分就位cache_hash算法
eor	p12, p1, p1, LSR #7
and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES

  1. 在缓存插入的时候,是以hash下标的形式储存的,而下标的算法是:
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}

  • 汇编实现采用同样的算法,现将_cmd右移7位,并异或运算eor,赋值给p12
  • p11右移48位获取mask,然后p12mask通过and运算获取下标,再赋值给p12,这样就获得了_cmd的下标! 所以可知:
  • p11 等于 _bucketsAndMaybeMask
  • p10 等于 buckets,也就是首个bucket_t地址;
  • p12 等于 要查找的方法的hash下标。 4.hash下标找到了,buckets首个元素的地址也找到了,那么怎么找到_cmd的位置呢,没错地址平移,平移多少单位呢?16个字节的倍数,因为bucket_t中的两个属性是impsel两个指针地址,8+8=16个字节
   add	p13, p10, p12, LSL #(1+PTRSHIFT)
   // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

根据buckets首地址偏移下标 index * 16个单位,p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) ,其中PTRSHIFT = 3(_cmd & mask) << (1+PTRSHIFT) 相当于下标左移,即乘以16,相对于16的倍数进行平移。所以最后加上buckets首地址的话,就获得了当前_cmd对应的bucket地址 5.开启一个循环,[x13]取寄存器x13里的值,p17指向一个地址,这个指令是向这个地址中赋值,而ldp是一个出栈指令,出栈后地址自动平移。进行sel比对,将获取的当前_cmdbucket,也就是p13赋值给p17p9p17=impp9=sel

                                                // do {
    1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
            cmp	p9, p1				//     if (sel != _cmd) {
            b.ne	3f				//         scan more
                                                    //     } else {
    2:	CacheHit \Mode				// hit:    call or return imp
                                                    //     }
    3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
            cmp	p13, p10			// } while (bucket >= buckets)
            b.hs	1b

  • cmp p9, p1,如果当前获取的sel与要查找的sel相同,则缓存命中,CacheHit

  • 如果不相等,则进入3流程中,判断当前获取的selp9是否为空,如果为空,则Miss,缓存没有命中;

  • 如果获取的sel不为空,说明存在下标冲突;则以当前获取的bucket的地址与首个bucket的地址进行比较;

  • 果获取地址,大于等于首地址,继续比较流程,向前查找,循环下去!

  • 直到查询到首地址位置。

6.如果上面的循环结束依然没有找到,则会进入下面的流程,CACHE_MASK_STORAGE_HIGH_16环境下,同样p11右移48位获取mask,而mask等于开辟的总空间容量减1,所以获取最后一个存储空间所在的位置,也即是首地址的基础上,添加mask*16的位置,所以这里p13就是当前最大的那个存储空间,也就是最后一个存储空间

    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
        add	p13, p10, w11, UXTW #(1+PTRSHIFT)
                                                // p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
                                                // p13 = buckets + (mask << 1+PTRSHIFT)
                                                // see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
        add	p13, p10, p11, LSL #(1+PTRSHIFT)
                                                // p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
     

7.重新设定p12的值,上面已经知道p12是要查找方法_cmd存储下标,首地址添加index*16,即可获取当前要查找的方法_cmd对应bucket地址,并赋值给p12

  add	p12, p10, p12, LSL #(1+PTRSHIFT)
  // p12 = first probed bucket

8.再开启一个循环,此次循环是从最后一个位置,向要查找的_cmd对应位置,进行向前查找。

                                                // do {
    4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
            cmp	p9, p1				//     if (sel == _cmd)
            b.eq	2b				//         goto hit
            cmp	p9, #0				// } while (sel != 0 &&
            ccmp	p13, p12, #0, ne		//     bucket > first_probed)
            b.hi	4b

  • cmp p9, p1,如果当前获取的sel与要查找的sel相同,跳转至流程2,即缓存命中,CacheHit
  • 如果不相等,判断sel是否为空,如果不为空,并且循环获取的地址大于p12的位置,继续循环流程。

9.如果以上流程均未能命中缓存,则进入MissLabelDynamic流程,未能命中缓存。

3.缓存命中CacheHit

CacheLookup中,Mode传入的为NORMAL,会执行TailCallCachedImp。见下面源码:

 // CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
 .macro CacheHit
 .if $0 == NORMAL
         TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
 .elseif $0 == GETIMP
         mov	p0, p17
         cbz	p0, 9f			// don't ptrauth a nil imp
         AuthAndResignAsIMP x0, x10, x1, x16	// 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, x10, x1, x16	// authenticate imp and re-sign as IMP
         cmp	x16, x15
         cinc	x16, x16, ne			// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
         ret				// return imp via x17
 .else
 .abort oops
 .endif
 .endmacro

 // 调用imp
 .macro TailCallCachedImp
         // $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
         eor	$0, $0, $3
         br	$0
 .endmacro

TailCallCachedImp实现中进行了位异或运算,获取imp。因为在存储imp时,对imp进行了编码处理,取出执行调用时,需要进行解码操作

4.静态函数__objc_msgSend_uncached

如果缓存没有命中,则会进入MissLabelDynamic流程。全局搜索MissLabelDynamic,发现MissLabelDynamic即为CacheLookUp的第三个参数:

5.png

也就是_objc_msgSend中传入的__objc_msgSend_uncached。见下图:

6.png

全局搜索__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,继续跟踪MethodTableLookup。在MethodTableLookup的汇编实现中,我们可以看到最重要的是_lookUpImpOrForward的方法,然后全局搜索_lookUpImpOrForward发现搜不到实现方法, 说明该方法并不是汇编实现的,需要去C/C++源码中查找。

c/c++中调动汇编,去查找汇编时, 需要将需要搜索的方法多加一个下划线。 汇编中调用c/c++方法,去查找c/c++方法时,需要将需要查找的方法去掉一个下划线。