OC底层-方法的本质探究之方法的快速查找

506 阅读9分钟

写在开始

  • 最近工作有点忙,博客和底层之路有点拉下了。之前不怎么写博客,拾起博客的时候虽然每次写的时候很花时间,但是写完的感觉还是很棒。探究的时候可能会枯燥,甚至抓狂,写出来的时候总觉得一切都值得了。也算是鼓励自己坚持下去。
  • 上篇cache_t的结构和写入已经分析了,本篇就看下cache是在哪里被需要的,有意思的是cache的取出流程和写入流程的照应,研究存取的时候可能没什么感觉,到取出呼应上的时候就会恍然大悟。

一、准备工作

Objective-C Runtime

Overview
The Objective-C Runtime module APIs define the base of the Objective-C language. These APIs include:

Types such as the NSObject class and the NSObjectProtocol protocol that provide the root functionality of most Objective-C classes

Functions and data structures that comprise the Objective-C runtime, which provides support for the dynamic properties of the Objective-C language

You typically don't need to use this module directly.


  • 从官方文档的概述我们可以看到

    • Runtime提供了OC的底层实现
    • OC提供运行时特性
  • 运行时和编译时

    • 编译时是代码翻译成机器语言的过程。OC的编译采用的是gcc作为编译器前端,llvm作为编译器后端。
      • gcc作为编译器前端的任务是进行:预处理词法分析语义分析生成AST抽象树静态分析非语法错误生成中间代码IR。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。
      • llvm作为编译器后端的任务是进行:生成字节码生成target汇编语言优化生成target相关的mac-olink连接
  • 运行时是编译后得到的代码被装载到内存的过程。运行时会对类型进行检查,此时若出错程序会崩溃

  • Runtime的使用方式

    • 自定义方法的调用,例如[person funcHello]
    • 通过NSObject调用,例如iskindOfClass
    • 通过Runtime API调用,例如class_getInstanceSize

arm64常用汇编指令

1.ret:返回

2.mov:全称move,把后面内容的移动到前面寄存器

mov x0, #0x1

3.add:将后面x0+x1,赋给x2

 mov x0, #0x1

mov x1, #0x2

add x2,  x0, x1

4.sub:将后面x0-x1,赋给第x2

 mov x0, #0x1

mov x1, #0x2

sub x2,  x0, x1

5.cmp:比较指令,x0 - x1,并根据结果设置CPSR的标志位

 mov x0, #0x1

mov x1, #0x2

cmp x0, x1

6.b:跳转指令,b直接跳转到mycode对应的地址

 b(条件) 目标地址

b mycode

mov x0, #0x5

mycode:

mov x1, #0x6

7.bl:带返回的跳转指令

 bl(条件) 目标地址 (类似函数调用)

执行的操作:(1)将下一条指令的地址存储到lr(x30)寄存器中(2)跳转到标记处,执行代码

8.ldr:从内存中读取数据

 ldr x0, [x1] ;(x1里面存的是地址 取出一定大小的数据  数据大小取决于x0的大小)

dr x0, [x1, #0x4]  (取x1中存的地址  加上立即数0x4  , 最终地址中的数据  赋给x0)

ldr x0, [x1, #0x4]!    ;(同上  多了一个 x1中的地址值  等于最终地址值)

9.ldur:去内存中的数据放到寄存器

 ldur x0, [x1, #-0x4]        ;立即数为负

10.ldp:从内存中读取数据,放到一对寄存器中(p是pair的简称,一对的意思)

  ldp w0, w1, [x1, #0x4] 

11.str:往内存中写入数据

 str w0,[x1, #0x4]      ;把w0寄存器中的数据放到x1内存中 

stur w0, [x1, #-0x4]

stp w0, w1, [x1, #0x4]

12.条件域(跟在指令后并没有空格)

 EQ:equal,相等  

NE:not equal, 不相等

GT:great than, 大于

GE:great or equal, 大于等于

LT:less than, 小于

LE:less or equal, 小于等于

beq mycode(找CPSR中Z位 1就跳, 不是1 就不跳,也就是之前结果为0就跳) 

二、方法在底层的实现

1.clang看源码实现

  • clang命令

clang -rewrite-objc main.m -o main.cpp

  • 编译后.cpp代码
//main.m中方法的调用
MuPerson *person = [MuPerson alloc];
[person funcHello];
[person funcWorld];

//clang编译后的底层实现截取
MuPerson *person = ((MuPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MuPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("funcHello"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("funcWorld"));
  • clang后的源码分析,我们可以看到方法的本质是通过消息发送objc_msgSendobjc_msgSend有两个参数名,一个是receiver消息接受者,一个是方法编号sel(cmd)

2.代码验证

  • 这里用代码代用objc_msgSend。有两个细节注意
    • 导入头文件 #import <objc/message.h>
    • target -> Build Setting -> enable strict checking of obc_msgSend callsYES 改为NO,把严格检查机制关掉,避免报错
MuPerson *person = [MuPerson alloc];
// 实例化方法调用
[person funcHello];
// runtime调用
objc_msgSend(person,sel_registerName("funcHello"));
  • 打印结果一致。上层的实例化方法等价于底层objc_msgSend的调用。

3.猜测方法的查找机制

  • 实例化方法查找,去当前类、当前类的父类,一直到继承链的顶端查找sel,直到找到为止。
  • 类方法查找,去当前类的isa指向即元类查找,一致到isa指向链的顶端查找sel,直到找到位置。
  • 底层方法实现不分实例化方法-和类方法+,因为类是它元类的类对象。
  • 底层objc_msgSend,查找应该先去当前指向类的cache中快速查找,如果查找没有命中cache,则去当前指向类的方法列表中慢速查找,查找到IMP即缓存。如果当前指向类的查找没有命中,就会查找当前指向类的父类递归查找IMP。如果都没有查找到,方法将进入方法转发阶段
  • ps:猜测也是基于自己对前面自己探究知识和宏观了解的OC层面的一些总结,后边都回到源码中一一验证。当前知识阶段出现错误,后续会更新博客。

三、objc源码探究

1.定位objc_msgSend

  • 环境仍然为objc_781,搜索objc_msgSend,这里有一个细节:objc_msgSend在汇编层面,原因猜测objc_msgSend是运行时装载到内存中,持有动态性不确定性,再有就是汇编效率高。 架构我们选择研究arm64真机环境。tip:command+左键收起左边全部目录。

2.objc_msgSend快速查找流程分析

  • objc_msgSend 源码流程
//---- 消息发送的汇编入口
ENTRY _objc_msgSend 
//---- 无窗口
    UNWIND _objc_msgSend, NoFrame 
    
//---- p0 的非空判断 p0 表示objc_msgSend的消息接收者receiver
    cmp p0, #0          // nil check and tagged pointer check 
//---- le小于 -> 支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative) 
#else
//---- p0 为空时,直接返回空
    b.eq    LReturnZero 
#endif 
//---- p0即receiver 存在的流程
//---- 根据对象拿出isa ,即从x0寄存器指向的地址 取出 isa,存入 p13寄存器
    ldr p13, [x0]       // p13 = isa 
//---- 在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出shiftcls信息,得到class信息
    GetClassFromIsa_p16 p13     // p16 = class 
LGetIsaDone:
    // calls imp or objc_msgSend_uncached 
//---- 如果有isa,走到CacheLookup 即缓存查找流程,也就是所谓的sel-imp快速查找流程
    CacheLookup NORMAL, _objc_msgSend

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
//---- 等于空,返回空
    b.eq    LReturnZero     // nil check 

    // tagged
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    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.判断receivertagged pointer

  • 判断receiver是否为空
    • receiver 不为空
    • 判断是否有tagged pointer特性
      • 有,则走LNilOrTagged。拿到持有tagged pointer特性receiverisa(这里不做重点分析),跳出进入LGetIsaDone
      • 没有,走GetClassFromIsa_p16,拿到对象的isa,继续向下。
    • receiver为空
      • 为空直接跳出执行LReturnZero

2.拿到当前的class

.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
  • and p16, $0, #ISA_MASK 64位系统通过isa & ISA_MASK 获取shiftcls位域的类信息,即class
  • 拿到class,进入CacheLookup NORMAL 流程
.macro CacheLookup
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart$1 label we may have loaded
	//   an invalid cache pointer or mask.
	//
	//   When task_restartable_ranges_synchronize() is called,
	//   (or when a signal hits us) before we're past LLookupEnd$1,
	//   then our PC will be reset to LLookupRecover$1 which forcefully
	//   jumps to the cache-miss codepath which have the following
	//   requirements:
	//
	//   GETIMP:
	//     The cache-miss is just returning NULL (setting x0 to 0)
	//
	//   NORMAL and LOOKUP:
	//   - x0 contains the receiver
	//   - x1 contains the selector
	//   - x16 contains the isa
	//   - other registers are set as per calling conventions
	//
LLookupStart$1:

	// p1 = SEL, p16 = isa
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	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	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
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	add	p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
					// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p12, p12, p11, LSL #(1+PTRSHIFT)
					// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif

	// 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

LLookupEnd$1:
LLookupRecover$1:
3:	// double wrap
	JumpMiss $0

.endmacro

3.class平移拿到cache

  • [x16, #CACHE] ,其中#define CACHE (2 * __SIZEOF_POINTER__)。将class的地址平移16位(isa8 字节+superclass 8字节),拿到类结构中的cachecachearm64maskAndBuckets

4.cache取出buckets

  • and p10, p11, #0x0000ffffffffffffcache高16位抹零(maskAndBuckets,高16位为mask,低48位为buckets),得到buckets。存入p10寄存器

5.buckets查找bucket

  • and p12, p1, p11, LSR #48 得到index
    • p11右移48位,得到mask
    • mask & p1(msgSend的第二个参数 sel) ,得到bucket的下标index,存入p12。这里要回顾到,cache_fill存取bucket 的时候正是用的cache_hash算法,即(mask_t)(uintptr_t) sel & mask
  • add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))得到bucket
    • PTRSHIFT = 3,左移4位,即偏移16位。16位刚好是一个bucket的大小。
    • (_cmd & mask)index,index左移4位即index *16,得到当前bucketbuckets里面的偏移量
    • 用首地址+偏移量,得到bucket

5.bucket递归循环对比sel返回imp

1.取出selimp

* 通过`bucket`,取出`sel``imp`

2.缓存命中

  • bucketsel和传入的sel对比,如果有则命中缓存,return imp

3.缓存未命中

  • 如果缓存没有命中,判断当前bucket 是否是buckets的第一位
    • 不是第一位,则向前--*bucket,拿到新的buckets回到第一步递归。
    • 是第一位,则add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)),将当前mask左移16位,mask = capacity - 1.相当于将当前指向首位的指针移动到位。拿到新的bucket,回到第一步递归。第二次递归如果当前bucketbuckets的第一位,结束递归。没有sel,则 CheckMiss $0 。因为$0normal,会跳转至__objc_msgSend_uncached方法的慢速查找流程。

4.存取算法对比

  • 在取的方法探究的时候,我一度茫然。
    • 为什么是向前查找?
    • 为什么是查找到最后一位?
  • 回头在理了一遍cacah_fill的流程,一切才清晰起来。
    • 存的时候index 在经过第一次cache_hash(mask & sel)后,会进入do while循环,防止查找出错,这里设计了cache_next算法。cache_next在真机__arm64__环境下,return i ? i-1 : mask; 如果有index,就index-1向前,i==0等于maskmask==capacity - 1mask就是最后一位元素,所以index指向了最后一位。

    • 算法的一致性提现的淋漓尽致,一个逻辑中存和取的对应保证算法的一致,让程序设计也变得清晰起来。可能这就是底层设计的魅力。

   do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(sel, imp, cls);
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));
  
  #elif __arm64__环境下,
// objc_msgSend has lots of registers available.
// Cache scan decrements. No end marker needed.
#define CACHE_END_MARKER 0
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}

四、探究过程的总结和思考

  • isa的重要性,isa的走位贯穿全局。
  • 前面知识的联想和呼应,isa -> 类 -> cache ->objc_msgsend
  • 知识串成线的过程虽然漫长,但还是要坚持下去。