iOS底层学习——Runtime运行时&方法的本质&快速方法查找

1,186 阅读10分钟

1.运行时和编译时

  • 编译时:顾名思义就是正在编译的时候,编译时简单的作⼀些翻译⼯作,⽐如检查关键字是否错误,词法分析,语法分析之类的过程。

  • 运行时:就是代码跑起来了,被装载到内存中去了。如果出现错误会导致程序崩溃。

2.Runtime调用的三种途径

Runtime调用的三种途径下图:

Runtime调用的三种途径

  1. Objective-C Code代码中,比如对象方法的调用, [user sayHello];
  2. Framework&Service中,接口的调用,比如isKindOfClassisMemberOfClass等;
  3. Runtime API中,c/c++源码方法的使用,比如objc_msgSendobjc_msgSendSuper

上图中:

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

3.方法调用的本质

1.探索本质

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

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

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

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

思路:决定一个类功能的是函数,所以从cache_t的函数中去寻找突破口。

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

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

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

    image.png

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

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

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

        GFPerson * user = [GFPerson alloc];
        [user sayHello];
        objc_msgSend(user, sel_registerName(@"sayHello"));

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

image.png

3.objc_msgSendSuper

引入一个案例

Son继承自FatherSon只进行了sayHello方法的声明,并没有实现sayHello方法,father实现了sayHello方法。在main中,Son调用了方法sayHello。参考下面的示例:

@interface Father : NSObject
-(void)sayHello;
@end
@implementation Father
-(void)sayHello{
    NSLog(@"sayHello %s", __func__);
}
@end

@interface Son : Father
-(void)sayHello;
@end
@implementation Son
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Son *son = [[Son alloc]init];
        [son sayHello];
  }
  return 0;
}

打印结果:

sayHello调用结果

虽然Son没有实现sayHello方法,但程序运行后并没有异常,而是调用了父类的sayHello方法。说明消息的接受者虽然是自己,自己没有实现,调用到了父类的方法。我们可以使用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结构体如下:

/// Specifies the superclass of an instance. 
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 */
};

最终实现代码:

       Son *son = [[Son alloc]init];
       [son sayHello];
       struct objc_super obj;
       obj.receiver = son;
       obj.super_class = [father class];
       objc_msgSendSuper(&obj, sel_registerName("sayHello"));

打印结果为:

objc_msgSendSuper调用结果

同样可以调用成功。

4.快速方法查找

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

下面是_objc_msgSend汇编的核心代码

1._objc_msgSend函数

        ENTRY _objc_msgSend // 进入_objc_msgSend方法
	UNWIND _objc_msgSend, NoFrame

        // 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
	ldr	p13, [x0]		// p13 = isa 拿出ISA
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class 获取对应的类
LGetIsaDone:
	// 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:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend

流程简述:

  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
    
    .endmacro
    
    
    // 宏定义ExtractISA
    .macro ExtractISA
    and    $0, $1, #ISA_MASK
    .endmacro
    
    
  4. 类找到后,进入宏CacheLookUp缓存查找流程。

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
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#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

……省略

.endmacro

流程简述,只分析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赋值给p10cache_t结构分析与底层探索

    #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
    
  3. 在缓存插入的时候,是以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) 相当于下标左移4 ,即乘以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进行了编码处理,见下图:

image.png

取出执行调用时,需要进行解码操作。

4.静态函数__objc_msgSend_uncached

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

image.png

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

image.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++源码中查找。

5.知识点补充

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

5.慢速方法查找

进入慢速方法查找流程 lookUpImpOrForward