iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析

Posted 卡卡西Sensei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析相关的知识,希望对你有一定的参考价值。

1. 回顾

在上篇博客iOS底层探索之Runtime:01—运行时&方法的本质中介绍了 运行时编译时的概念。同时也知道了OC方法的调用,本质上是发送消息,在底层通过objc_msgSend方法来实现。那么底层是如何实现的呢?

在这里插入图片描述

2. 消息发送底层如何实现

补充1

Runtime有两个版本 ⼀个是Legacy版本(早期版本) ,另一个是Modern版本(现⾏版本)

  • 早期版本对应的编程接⼝:Objective-C 1.0
  • 现⾏版本对应的编程接⼝:Objective-C 2.0
  • 早期版本⽤于Objective-C 1.0, 32位Mac OS X的平台上
  • 现⾏版本:iPhone程序和Mac OS X v10.5及以后的系统中的64 位程序

Objective-C Runtime Programming Guide

下面的代码已经不陌生了吧!调用效果都是一样的,一个是上层OC的对象调用方法,一个是下层消息的发送

JPStudent *stu = [[JPStudent alloc]init];
[stu test];
objc_msgSend(stu, sel_registerName("test"));

sel_registerName是一个C语言的方法,传入一个C语言的字符串(其实就是我们的方法名称)

objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)
sel_registerName(<#const char * _Nonnull str#>)
//C语言的函数,闯入一字符串

sel_registerName("test") 等价于 @selector(test),我们可以打印下它们的地址。

NSLog(@"%p---%p",sel_registerName("test"),@selector(test));

//打印输出
2021-06-29 12:58:50.610720+0800 方法的本质探索[42704:741799] 0x7fff7b9f5ddc---0x7fff7b9f5ddc

从打印结果来看,是一模摸一样样😁

666

补充2

OC中方法的调用,底层都是转换为消息发送objc_msgSend函数的调用,执行流程大概可以分为三大阶段。

  1. 消息发送流程
  2. 动态方法解析流程
  3. 消息转发流程

2.1 查找源码

既然要看objc_msgSend的底层,就得去苹果的源码里面去看看,必须要深入底层去探索。

源码工程查找objc_msgSend

源码工程查找
我的天哪!什么鬼👻啊?这么多文件,有汇编的,有C/C++的该看哪一个呢?而且架构还不一样。
我的天那

我们肯定是要找arm架构的,不要问为什么,问就是找它就对了,哈哈!因为我们手机的真机是arm架构的,加上OC的底层都是C、C++和汇编实现的,所以我们基本可以定位到objc-msg-arm64.s这个文件。

objc-msg-arm64.s

2.2 查看源码

既然找到了,就不要在外面停留了,进去看看。

汇编源码

偶买噶,我的天那!这是熟悉又陌生(大学学过)的汇编啊!恶魔😈,噩梦啊!大学学的时候就很懵!

苦涩

靓仔,稳住,挺住!
汇编确实是比较难啃,但也不是啃不动,一口吃不下,就慢慢啃!干,就完了!

加油

3. 分析汇编

汇编源码是从ENTRY _objc_msgSend开始,到END_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, 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

3.1 _objc_msgSend

  1. p0 和空对比,即判断接收者是否存在,其中p0objc_msgSend的第一个参数-消息接收者receiver
  2. if else 判断,如果支持tagged pointer,跳转至LNilOrTagged,如果小对象为空,则直接返回空,即LReturnZero。如果小对象不为空,则处理小对象的isa,走到 CacheLookup NORMAL
  3. GetClassFromIsa_p16是定义的一个宏,通过isa找到对应的类,ExtractISA也是个宏定义,将传入的isa&isaMask,得到class,并将class赋给p16
  • GetClassFromIsa_p1 的宏定义
// p13(isa), 1, x0(isa)
//GetClassFromIsa_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

3.2 CacheLookUp

  • CacheLookUp核心代码

// NORMAL, _objc_msgSend, __objc_msgSend_uncached ,  MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	//   As soon as we're past the LLookupStart\\Function 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\\Function,
	//   then our PC will be reset to LLookupRecover\\Function 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
	//
    
	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

//  p11 cache -> p10 = buckets
//  p11, LSR #48 -> mask
//  p1(_cmd) & mask = index -> p12
	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

// objc - 源码调试 + 汇编
//  p11 cache -> p10 = buckets
//  p1(_cmd) & mask = index -> p12
//  (_cmd & mask) << 4  -> int 1 2 3 4 5   地址->int
//  buckets +  内存平移 (1 2 3 4)
//  b[i] -> b + i
//  p13 当前查找bucket
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//  *bucket--  p17, p9
//  bucket 里面的东西 imp (p17) sel (p9)
//  查到的 sel (p9) 和我们 say1
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

LLookupEnd\\Function:
LLookupRecover\\Function:
	b	\\MissLabelDynamic

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \\Mode == GETIMP
	b.ne	\\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \\Mode == NORMAL
	br	x17
.elseif \\Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \\Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro
  1. 通过cache首地址平移16字节(因为在objc_class中,首地址距离cache正好16字节,即isa8字节,superClass8字节),获取cahcecache中高16位存mask,低48位存buckets,即p11 = mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
        ldr	p11, [x16, #CACHE]  // p11 = mask|buckets

  1. cache中分别取出bucketsmask,并由mask根据哈希算法计算出哈希下标。在arm64环境下,maskbuckets放在一起共占用8个字节,64位;其中mask在高16位,buckets在低48位。通过掩码(0x0000fffffffffffe)与运算(&)将高16位抹零获取buckets;将buckets赋值给p10。将cache右移48位,得到mask,即p10 = buckets
#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. objc_msgSend的参数p1(即第二个参数_cmd& msak,通过哈希算法,得到需要查找存储sel-impbucket下标index,即p12 = index = _cmd & mask。这是因为系统在存储sel-imp时,就是通过哈希计算得到下标,再去存储,所以读取也需要通过同样的方式。
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);
}

通过首地址 + 实际偏移量,获取哈希下标index对应的bucket

  1. 知道了下标,buckets的首地址也有了,那么怎么找到_cmd的位置呢?我们都知道可以通过内存地址平移,在bucket_t中存放的是impsel8+8=16个字节。
add	p13, p10, p12, LSL #(1+PTRSHIFT)
   // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

根据buckets首地址偏移下标 乘以16个单位,其中PTRSHIFT = 3,相当于下标左移4 就是以16的倍数进行平移,然后加上buckets首地址的话,就获得了当前_cmd对应的bucket地址。根据获取的bucket,取出其中的sel存入p17,即p17 = sel,取出imp存入p9,即p9 = imp

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
  1. cmp p9, p1,如果当前获取的sel与要查找的sel相同,则缓存命中,CacheHit
  2. 如果不相等,则进入3流程中,判断当前获取的sel,p9是否为空,如果为空,则Miss,缓存没有命中。
  3. 如果获取的sel不为空,说明存在下标冲突,则以当前获取的bucket的地址与首个bucket的地址进行比较如果获取地址,大于等于首地址,继续比较流程,向前查找,循环下去!直到查询到首地址位置。
  4. 如果上面的循环结束依然没有找到,则会进入下面的流程,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
     

上面已经知道p12是要查找方法_cmd的存储下标,只要把首地址加上偏移地址index*16,就可以知道_cmd对应bucket地址,并赋值给p12

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

此次循环是从最后一个位置,查找的_cmd对应位置,进行向前查找

#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

LLookupEnd\\Function:
LLookupRecover\\Function:
	b	\\MissLabelDynamic

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

3.3 CacheHit

下面是缓存命中(CacheHit)的分析

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

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

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

// NORMAL, _objc_msgSend, __objc_msgSend_uncached ,  MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

就是_objc_msgSend中传入的__objc_msgSend_uncached

_objc_msgSend
源码工程里面全局搜索__objc_msgSend_uncached
__objc_msgSend_uncached
分析:
在该函数中执行宏MethodTableLookup,继续跟踪MethodTableLookup,在MethodTableLookup的汇编实现中,我们可以看到最重要的_lookUpImpOrForward的方法,然后全局搜索_lookUpImpOrForward发现搜不到实现方法, 说明该方法并不是汇编实现的,需要去C/C++源码中查找。

到此,消息发送流程中汇编快速查找的分析就结束了,因为lookUpImpOrForward不是汇编实现的,是C/C++实现的,所以属于(慢速查找)。lookUpImpOrForward慢速查找下次再分析。

4. 总结

  1. 为什么底层不用C或者C++用汇编?
  • 汇编更接近机器语言,直接操作寄存器,查找效率高
  • 因为一些方法的参数未知,汇编可以处理未知的参数,更加动态化一点
  1. objc_msgSend函数的调用,执行流程大概可以分为三大阶段。
  • 消息发送流程(1.汇编快速查找,2.慢速查找)
  • 动态方法解析流程
  • 消息转发流程
  1. 消息发送流程流程图:
    发送消息流程图

补充

苹果2020 WWDC关于Runtime的优化

更多内容持续更新

🌹 喜欢就点个赞吧👍🌹

🌹 觉得学习到了的,可以来一波,收藏+关注,评论 + 转发,以免你下次找不到我😁🌹

🌹欢迎大家留言交流,批评指正,互相学习😁,提升自我🌹

以上是关于iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析的主要内容,如果未能解决你的问题,请参考以下文章

iOS底层探索之Runtime: 消息转发

iOS底层探索之Runtime:运行时&方法的本质

iOS底层探索之Runtime: objc_msgSend&汇编快速查找分析

iOS开发底层之RuntimeObjc_msgSend探究 - 08

iOS开发底层之RuntimeObjc_msgSend探究 - 08

iOS底层探索之多线程(十五)—@synchronized源码分析