当前位置: 首页 > news >正文

【iOS】探索消息流程

探索消息流程

  • Runtime介绍
  • `OC`三大核心动态特性
    • 动态类型
    • 动态绑定
    • 动态语言
  • 方法的本质
    • 代码转换
    • `objc_msgSend`
      • SEL
      • IMP
      • Method
    • 父类方法在子类中的实现
  • 消息查找流程
    • 开始查找
    • 快速查找流程
    • 慢速查找流程
      • 二分查找方法列表
      • 父类缓存查找
  • 动态方法解析
    • 动态方法决议
    • 实例方法
    • 类方法
    • 优化
  • 消息转发机制
    • 快速转发流程
    • 慢速转发

Runtime介绍

runtime是一套API,由C、C++、汇编一起写成的,为OC提供运行时,区别于编译时:

  • 运行时是指在代码跑起来,被装载到内存中的一个过程;在这个时候如果出错的话,程序就会崩溃,这是一个动态阶段
  • 编译时是源代码翻译成机器能识别的代码的一个过程,这个时候主要是对语言进行最基本的检查报错,即词法分析、语法分析等,这是一个静态的阶段

runtime的使用有以下三种方式,三种实现方法与编译层和底层的关系如图所示:

  • 通过OC代码,例如[person sayNB]
  • 通过NSObject方法,例如isKindOfClass
  • 通过Runtime API,例如class_getInstanceSize

请添加图片描述

其中的compiler就是我们了解的编译器,即LLVM,例如OC的alloc 对应底层的objc_allocruntime system libarary 就是底层库

OC三大核心动态特性

动态类型

OC中的对象是动态类型的,这就意味着在运行的时候可以发送消息给对象,而后对象可以根据收到的消息来执行相应的方法。与静态语言类型不同,静态类型在编译时就必须要确定引用哪种对象,而动态类型则更加广泛。

id someObject = [[NSString alloc] initWithString:@"hello"];
someObject = [[NSData alloc] init];//运行时someObject的类型转换成了NSDate

动态绑定

动态绑定是指方法调用可以在运行时解析,而不是在编译时。这就意味着OC运行时决定要执行对象的哪个方法,而不是在编译时。这种机制是通过消息传递实现的,这使得可以在程序运行期间改变对象的调用方法

动态语言

OC被称为动态语言的一个核心点就是消息转发机制,消息转发机制允许开发者截取并处理未被对象识别的消息。这使得即使某个方法或者函数没有被实现,编译的时候也不会报错,因为在运行时还可以动态的添加方法。

方法的本质

这里我们先使用Clang将mian.m编译成cpp文件

CJLPerson* person = [CJLPerson alloc];
CJLTeacher* teacher = [CJLTeacher alloc];
[person sayHello];//使用Clang编译
CJLPerson* person = ((CJLPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLPerson"), sel_registerName("alloc"));
CJLTeacher* teacher = ((CJLTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

通过上述代码我们可以看出,方法的本质就是objc_msgSend函数给对象发送消息。

代码转换

  • ((CJLTeacher *(*)(id, SEL))是类型强转,将objc_msgSend强制转换为特定返回类型和参数的函数指针
  • ((id)objc_getClass("CJLTeacher")是获取CJLTeacher对象
  • sel_registerName("alloc"))等同于@selector()

objc_msgSend

函数原型:

id objc_msgSend(id self, SEL op, ...);

SEL

在源码中,其定义如下所示:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

SEL一个不透明的类型,代表方法选择子,定义如下所示:

// GNU OC 中的 objc_selector
struct objc_selector {  void *sel_id;  const char *sel_types;  
};

实际上,SEL就是一个方法选择器,他负责告诉编译器当前我们想要调用哪一个方法:

在运行时,方法选择器用来表示方法的名字,一个方法选择器就预示一个C字符串,在OC运行的时候被注册,编译器生成选择器在类加载时由运行时自动映射。

可以在运行时 添加新的选择器,并使用sel_registerName函数检索现有的选择器。

获取SEL的三种方式:

SEL selA = @selector(study);
SEL selB = sel_registerName(@"study");
SEL selC = NSSelectorFromString(@"study");

注意:

OC在编译时会根据方法名字生成唯一一个区分的ID,这个ID是SEL类型的,只要方法名字相同,SEL返回就相同

Runtime中维护一个SEL的表,该表按照NSSet来存储,只要相同的SEL就会被看作是同一个方法并且被加载到表中,故而OC中需要避免方法重载

IMP

指向方法实现的首地址的指针,这里我们可以看到一个他的定义:

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
#endif

IMP的数据类型是指针,指向方法实现开始的位置

Method

这是一个不透明的类型,表示类中定义的方法,定义如下所示:

typedef struct objc_method *Method;struct objc_method {SEL _Nonnull method_name   OBJC2_UNAVAILABLE; //表示方法名的字符串char * _Nullable method_types   OBJC2_UNAVAILABLE; //char* 类型的,表示方法的类型,包含返回值和参数的类型IMP _Nonnull method_imp    OBJC2_UNAVAILABLE; //IMP类型,指向方法实现地址的指针
}                                                            OBJC2_UNAVAILABLE;

这里我们可以看出Method是一个结构体类型指针

当向对象发送消息的时候,调用SEL在对象的类以及父类方法列表中进行查找Method,由于Method结构体中包含IMP指针,故而一旦找到对应的Method就直接调用IMP去实现方法

父类方法在子类中的实现

这里我们定义两个类,一个类继承于另一个类

-(void)study {NSLog(@"%@ say: %s",[self class], __func__);NSLog(@"%@ say: %s",[super class], __func__);
//     [super study];
}

我们使用Clang将这段代码编译成cpp代码:

static void _I_MyTeacher_study(MyTeacher * self, SEL _cmd) {NSLog((NSString *)&__NSConstantStringImpl__var_folders_07_67glw_2n3csgg41v0tztm7w00000gn_T_MyTeacher_a75b57_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")), __func__);NSLog((NSString *)&__NSConstantStringImpl__var_folders_07_67glw_2n3csgg41v0tztm7w00000gn_T_MyTeacher_a75b57_mi_1,((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyTeacher"))}, sel_registerName("class")), __func__);((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MyTeacher"))}, sel_registerName("study"));
}

我们可以发现在编译期间[super class]转化成了objc_msgSendSuper的方式发送消息,其中消息的接受者依旧是self

消息查找流程

开始查找

这里我们主要研究arm64结构的汇编实现,来到objc-msg-arm64.s,这里先给出其汇编的整体流程图:

请添加图片描述

//----消息发送 -- 汇编入口 -- objc_msgSend主要是拿到接受者的isa信息	
ENTRY _objc_msgSend
//--- 流程开始,无需frame
UNWIND _objc_msgSend, NoFrame//---- p0 和空对比,即判断接收者是否存在,其中p0是objc_msgSend的第一个参数-消息接收者receivercmp	p0, #0			// nil check and tagged pointer check 
//---- le小于 --支持taggedpointer(小对象类型)的流程
#if SUPPORT_TAGGED_POINTERSb.le	LNilOrTagged		//  (MSB tagged pointer looks negative) 
#else
//---- p0 等于 0 时,直接返回 空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 GetTaggedClassb	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endifLReturnZero:// x0 is already zeromov	x1, #0movi	d0, #0movi	d1, #0movi	d2, #0movi	d3, #0retEND_ENTRY _objc_msgSend

判断objc_msgSend方法的第一个参数receiver是否为空

  • 如果支持taggedpointer(小对象类型),跳转到LNilOrTagged:
    • 如果小对象为空,则直接返回空,即LReturnZero
    • 如果小对象不为空,则处理小对象的isa,走到第二步
  • 如果既不是小对象,receiver也不为空,就有以下两步:
    • receiver中取出isa存入p13寄存器
    • 通过GetClassFromIsa_p16中,arm64架构下通过isa & ISA_MASK获取shiftcls位域的类信息,即class,而后走到第二步
.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */
//这里用于watchOS
#if SUPPORT_INDEXED_ISA// Indexed isa
//将isa的值存入p16寄存器mov	p16, \src			// optimistically set dst = srctbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa// isa in p16 is indexedadrp	x10, _objc_indexed_classes@PAGEadd	x10, x10, _objc_indexed_classes@PAGEOFFubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract indexldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:
//用于64位系统
#elif __LP64__
.if \needs_auth == 0 // _cache_getImp takes an authed class alreadymov	p16, \src
.else// 64-bit packed isaExtractISA p16, \src, \auth_address
.endif
#else// 32-bit raw isa即用于32位系统mov	p16, \src#endif
  • 获取isa结束,进入查找流程CacheLookup

快速查找流程

.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_ADDRSldr	p10, [x16, #CACHE]				// p10 = mask|bucketslsr	p11, p10, #48			// p11 = maskand	p10, p10, #0xffffffffffff	// p10 = bucketsand	w12, w1, w11			// x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//在64位真机//从x16(isa)中平移16个字节,取出cache存入p11寄存器 -- isa距离cache刚好16字节   ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)tbnz	p11, #0, LLookupPreopt\Functionand	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#elseand	p10, p11, #0x0000fffffffffffe	// p10 = bucketstbnz	p11, #0, LLookupPreopt\Function
#endifeor	p12, p1, p1, LSR #7and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
//and	p10, p11, #0x0000ffffffffffff	// p10 = bucketsand	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4ldr	p11, [x16, #CACHE]				// p11 = mask|bucketsand	p10, p11, #~0xf			// p10 = bucketsand	p11, p11, #0xf			// p11 = maskShiftmov	p12, #0xfffflsr	p11, p12, p11			// p11 = mask = 0xffff >> p11and	p12, p1, p11			// x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endifadd	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_ADDRSadd	p13, p10, w11, UXTW #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))// p13 = buckets + (mask << 1+PTRSHIFT)// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4add	p13, p10, p11, LSL #(1+PTRSHIFT)// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endifadd	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 hitcmp	p9, #0				// } while (sel != 0 &&ccmp	p13, p12, #0, ne		//     bucket > first_probed)b.hi	4bLLookupEnd\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 = bucketsautdb	x10, x16			// auth as early as possible
#endif// x12 = (_cmd - first_shared_cache_sel)adrp	x9, _MagicSelRef@PAGEldr	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_shiftlsr	x17, x11, #55			// w17 = (hash_shift, ...)lsr	w9, w12, w17			// >>= shiftlsr	x17, x11, #60			// w17 = mask_bitsmov	x11, #0x7ffflsr	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_shiftlsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)lsr	w9, w12, w17			// >>= shiftand	x9, x9, x11, LSR #53		// &=  mask
#endif// sel_offs is 26 bits because it needs to address a 64 MB buffer (~ 20 MB as of writing)// keep the remaining 38 bits for the IMP offset, which may need to reach// across the shared cache. This offset needs to be shifted << 2. We did this// to give it even more reach, given the alignment of source (the class data)// and destination (the IMP)ldr	x17, [x10, x9, LSL #3]		// x17 == (sel_offs << 38) | imp_offscmp	x12, x17, LSR #38.if \Mode == GETIMPb.ne	\MissLabelConstant		// cache misssbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2sub	x0, x16, x17        		// imp = isa - imp_offsSignAsImp x0ret
.elseb.ne	5f				        // cache misssbfiz x17, x17, #2, #38         // imp_offs = combined_imp_and_sel[0..37] << 2sub x17, x16, x17               // imp = isa - imp_offs
.if \Mode == NORMALbr	x17
.elseif \Mode == LOOKUPorr x16, x16, #3 // for instrumentation, note that we hit a constant cacheSignAsImp x17ret
.else
.abort  unhandled mode \Mode
.endif5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offsetadd	x16, x16, x9			// compute the fallback isab	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES.endmacro

总结:

  • 检查消息接受者是否存在,若为nil则不做任何的处理
  • 通过receiver的isa指针找到对应的class对象
  • 找到class类对象进行内存平移找到cache,而后从其中获取buckets
  • buckets中对比参数SEL,查看缓存中有没有同名的方法
  • 如果buckets中有对应的sel -->cacheHit --> 调用imp
  • 若在缓存中没有找到匹配的方法选择子sel,就执行慢速查找,调用_objc_msgSend_uncached函数,并进一步调用_lookUpImpOrForward函数进行全局方法查找

消息转发会先通过缓存进行查找方法实现,如果在缓存中没有找到方法实现,就会进入慢速查找过程,在类的方法列表以及父类链中进行循环查找

慢速查找流程

NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{//定义的消息转发const IMP forward_imp = (IMP)_objc_msgForward_impcache;IMP imp = nil;Class curClass;//检查当前线程是否未持有runtimeLock锁runtimeLock.assertUnlocked();if (slowpath(!cls->isInitialized())) {// The first message sent to a class is often +new or +alloc, or +self// which goes through objc_opt_* or various optimized entry points.//// However, the class isn't realized/initialized yet at this point,// and the optimized entry points fall down through objc_msgSend,// which ends up here.//// We really want to avoid caching these, as it can cause IMP caches// to be made with a single entry forever.//// Note that this check is racy as several threads might try to// message a given class for the first time at the same time,// in which case we might cache anyway.behavior |= LOOKUP_NOCACHE;}// runtimeLock is held during isRealized and isInitialized checking// to prevent races against concurrent realization.// runtimeLock is held during method search to make// method-lookup + cache-fill atomic with respect to method addition.// Otherwise, a category could be added but ignored indefinitely because// the cache was re-filled with the old value after the cache flush on// behalf of the category.//加锁,目的是保证读取线程安全runtimeLock.lock();// We don't want people to be able to craft a binary blob that looks like// a class but really isn't one and do a CFI attack.//// To make these harder we want to make sure this is a class that was// either built into the binary or legitimately registered through// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.//判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类checkIsKnownClass(cls);//判断类是否实现,如果没有,需要先实现;目的是为了确定父类链,方法后续的循环cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);// runtimeLock may have been dropped but is now locked againruntimeLock.assertLocked();curClass = cls;// The code used to lookup the class's cache again right after// we take the lock but for the vast majority of the cases// evidence shows this is a miss most of the time, hence a time loss.//// The only codepath calling into this without having performed some// kind of cache lookup is class_getInstanceMethod().//----查找类的缓存// unreasonableClassCount -- 表示类的迭代的上限,防止出现无限循环//(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限范围内,所以可以继续递归)for (unsigned attempts = unreasonableClassCount();;) {if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {//检查当前类的方法缓存是否是预优化常量
#if CONFIG_USE_PREOPT_CACHESimp = cache_getImp(curClass, sel);if (imp) goto done_unlock;curClass = curClass->cache.preoptFallbackClass();
#endif} else {// curClass method list.//---当前类方法列表(采用二分查找算法),若找到,则返回,将方法缓存到cache中method_t *meth = getMethodNoSuper_nolock(curClass, sel);if (meth) {imp = meth->imp(false);goto done;}if (slowpath((curClass = curClass->getSuperclass()) == nil)) {//当前类=当前类的父类,判断父类是否为nil// No implementation found, and method resolver didn't help.// Use forwarding.//--为找到方法实现,方法解析器也不行,使用转发imp = forward_imp;break;}}// Halt if there is a cycle in the superclass chain.//如果父类链中存在循环,则停止if (slowpath(--attempts == 0)) {_objc_fatal("Memory corruption in class list.");}// Superclass cache.//--父类循环imp = cache_getImp(curClass, sel);if (slowpath(imp == forward_imp)) {// Found a forward:: entry in a superclass.// Stop searching, but don't cache yet; call method// resolver for this class first.break;}//如果在父类中找到了forword,就停止查找,并且不缓存,首先调用此类的方法解析器if (fastpath(imp)) {// Found the method in a superclass. Cache it in this class.goto done;}//在父类中找到方法,存储在cache中}// No implementation found. Try method resolver once.--没有找到方法实现,尝试一次方法解析if (slowpath(behavior & LOOKUP_RESOLVER)) {//动态方法决议的控制条件,表示流程只走一次behavior ^= LOOKUP_RESOLVER;return resolveMethod_locked(inst, sel, cls, behavior);}done:if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHESwhile (cls->cache.isConstantOptimizedCache(/* strict */true)) {cls = cls->cache.preoptFallbackClass();}
#endif//存储到缓存log_and_fill_cache(cls, imp, sel, inst, curClass);}done_unlock://解锁runtimeLock.unlock();if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;
}

总结

  • cache缓存中进行查找,即快速查找,找到则直接返回imp

  • 判断cls:

    • 类是否实现,若没有则先实现,确定父类链。这时实例化的目的是为了确定父类链、ro、以及rw等内容
    • 是否初始化,若没有则进行初始化
  • 进行for循环,按照类继承链或者元类继承链的顺序去查找

    • 当前cls的方法列表中使用二分查找算法查找方法,若找到,就进入cache写入流程,并且返回imp,没有找到就返回nil

    • 父类链中若存在循环,直接报错终止循环

    • 父类缓存中查找方法没找到则返回nil,继续循环查找;找到的话,直接返回imp,执行写入流程

  • 判断是否执行过动态方法解析:

    • 若没有,执行动态方法解析
    • 若执行过一次,走消息转发流程

以上就是方法慢速查找流程,下面分别详细解释一下二分查找原理和父类缓存查找详细步骤

二分查找方法列表

查找方法列表流程

getMethodNoSuper_nolock->search_method_list_inline->findMethodInSortedMethodList

template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{ASSERT(list);auto first = list->begin();auto base = first;decltype(first) probe;uintptr_t keyValue = (uintptr_t)key;uint32_t count;//base相当于low,count是max,probe是middle,这就是一个二分算法for (count = list->count; count != 0; count >>= 1) {//从首地址+下标移动到中间位置probe = base + (count >> 1);uintptr_t probeValue = (uintptr_t)getName(probe);//如果查找的key的keyValue等于中间位置的probeValue,就直接返回中间位置if (keyValue == probeValue) {// `probe` is a match.// Rewind looking for the *first* occurrence of this value.// This is required for correct category overrides.while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {probe--;}//排除分类重名方法,如果是两个分类,就看谁先进行加载return &*probe;}if (keyValue > probeValue) {base = probe + 1;count--;}}return nil;
}

请添加图片描述

这里笔者附图详细说明二分查找方法列表的步骤

父类缓存查找

cache_getImp方法是通过汇编_cache_getImp实现,传入的$0GETIMP,如下所示 父类缓存查找流程

  • 如果父类缓存中找到了方法实现,则跳转至CacheHit即命中,则直接返回imp
  • 如果在父类缓存中,没有找到方法实现,则跳转至CheckMiss 或者 JumpMiss,通过判断$0 跳转至LGetImpMiss,直接返回nil

动态方法解析

当我们在使用objc_msgSend的快速查找与慢速查找都没有找到方法实现的一个情况下,Apple官方给出了两种建议:

  1. 动态方法决议:慢速查找结束后,会执行一次动态方法决议
  2. 消息转发:若使用动态方法决议韩式没有找到实现,就会进行消息转发

动态方法决议

当慢速查找流程未找到方法实现的时候,会先尝试一次动态方法决议

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{runtimeLock.assertLocked();ASSERT(cls->isRealized());runtimeLock.unlock();//对象 -- 类if (! cls->isMetaClass()) {//若不是元类则直接调用对象的解析方法// try [cls resolveInstanceMethod:sel]resolveInstanceMethod(inst, sel, cls);} else {//若是元类就调用类的解析方法, 类 -- 元类// try [nonMetaClass resolveClassMethod:sel]// and [cls resolveInstanceMethod:sel]resolveClassMethod(inst, sel, cls);if (!lookUpImpOrNilTryCache(inst, sel, cls)) {resolveInstanceMethod(inst, sel, cls);}}// chances are that calling the resolver have populated the cache// so attempt using it//如果方法解析中将其实现指向其他方法,则继续走方法查找流程return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}

动态决议方法的意义在于在运动时动态地为为实现方法提供实现

步骤详细:

  • 判断类是否为元类
    • 如果是类,执行实例方法的动态方法决议resolveInstanceMethod
    • 如果是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没找到或者为空,就在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 如果动态方法决议中,将其实现指向了其他方法,则继续指定的imp,即继续慢速查找lookUpImpOrForward

流程如下所示:

请添加图片描述

实例方法

针对实例方法的调用,当我们最后尝试动态方法决议的时候,会走到resolveInstanceMethod这一步:

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{runtimeLock.assertUnlocked();ASSERT(cls->isRealized());SEL resolve_sel = @selector(resolveInstanceMethod:);//这一步相当于发送消息前的容错处理if (!lookUpImpOrNilTryCache(cls, resolve_sel, cls->ISA(/*authenticated*/true))) {// Resolver not implemented.return;}BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;bool resolved = msg(cls, resolve_sel, sel);//发送resolve_sel消息// Cache the result (good or bad) so the resolver doesn't fire next time.// +resolveInstanceMethod adds to self a.k.a. cls//查找详细方法,例如sayHelloIMP imp = lookUpImpOrNilTryCache(inst, sel, cls);if (resolved  &&  PrintResolving) {if (imp) {_objc_inform("RESOLVE: method %c[%s %s] ""dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp);}else {// Method resolver didn't add anything?_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"", but no new implementation of %c[%s %s] was found",cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel));}}
}

如下步骤所示:

  • 在发送resolveInstanceMethod消息前,需要查找cls类中是否有该方法的实现,通过lookUpImpOrNil方法再次进入lookUpImpOrForward慢速查找流程查找resolveInstanceMethod方法
    • 若没有则直接返回
    • 若有,则发送resolveInstanceMethod消息
  • 再次慢速查找实例方法的实现,通过lookUpImpOrNil方法进入lookUpImpOrForward慢速查找流程查找实例方法

崩溃修改:

针对实例方法sayHello未实现的报错崩溃,可以通过类中重写resolveInstanceMethod类方法,将其指向其他方法的实现,在CJLPerson中重写resolveInstanceMethod将实例方法sayHello的实现指向say666方法实现就可以了,如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(sayHello)) {NSLog(@"%@哦no", NSStringFromSelector(sel));//获取sayMaster方法的impIMP imp = class_getMethodImplementation(self, @selector(say666));//获取sayMaster的实例方法Method sayMethod  = class_getInstanceMethod(self, @selector(say666));//获取sayMaster的丰富签名const char *type = method_getTypeEncoding(sayMethod);//将sel的实现指向sayMasterreturn class_addMethod(self, sel, imp, type);}return [super resolveClassMethod:sel];
}

这样就可以打印正确的结果了

类方法

对于类方法而言,与实例方法其实是相同的,同样通过重写resolveClassMethod类方法来解决问题,下面代码举例:

+ (BOOL)resolveClassMethod:(SEL)sel:(SEL)sel {if (sel == @selector(sayMethod)) {NSLog(@"%@哦no", NSStringFromSelector(sel));//获取sayMaster方法的impIMP imp = class_getMethodImplementation(objc_getMetaClass("CJLPerson"), @selector(say666));//获取sayMaster的实例方法Method sayMethod  = class_getInstanceMethod(objc_getMetaClass("CJLPerson"), @selector(say666));//获取sayMaster的丰富签名const char *type = method_getTypeEncoding(say666);//将sel的实现指向sayMasterreturn class_addMethod(self, sel, imp, type);}return [super resolveClassMethod:sel];
}

这里我们需要注意的是,在resolveClassMethod中传入cls不是类,而是元类,可以通过objc_getMetaClass来获取类的元类,这时由于类方法在元类中是实例方法

优化

我们通过方法慢速查找流程可以方法查找的两条路径:

  • 实例方法:类 – 父类 – 根类 – nil
  • 类方法:元类 – 根元类 – 根类 – nil

看这两者的路径,我们可以发现他们都会在跟类即NSObject中来查找,所以我们是不是可以将上述的两个方法统一整合,由于类方法在元类中是实例方法,这里我们就可以使用resolveInstanceMethod来实现:

+ (BOOL)resolveInstanceMethod:(SEL)sel{if (sel == @selector(say666)) {NSLog(@"%@ 来了", NSStringFromSelector(sel));IMP imp = class_getMethodImplementation(self, @selector(sayMaster));Method sayMethod  = class_getInstanceMethod(self, @selector(sayMaster));const char *type = method_getTypeEncoding(sayMethod);return class_addMethod(self, sel, imp, type);}else if (sel == @selector(sayNB)) {NSLog(@"%@ 来了", NSStringFromSelector(sel));IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));Method lgClassMethod  = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));const char *type = method_getTypeEncoding(lgClassMethod);return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);}return NO;
}

这个方法的实现,与源码中针对类方法的处理逻辑是一致的,完美的阐述了为什么调用类方法动态方法决议,还要调用对象方法动态方法决议,原因是因为类方法在元类中的实例方法

当然,上面这种写法还是会有其他的问题,比如系统方法也会被更改,针对这一点,是可以优化的,即我们可以针对自定义类中方法统一方法名的前缀,根据前缀来判断是否是自定义方法,然后统一处理自定义方法,例如可以在崩溃前pop到首页,主要是用于app线上防崩溃的处理,提升用户的体验。

消息转发机制

快速转发流程

若是在动态方法解析中仍没有找到方法实现,那么就会进入消息转发中快速转发(消息接受者替换),给开发者一个机会返回一个能够响应该方法的对象,该方法签名如下所示:

- (id)forwardingTargetForSelector:(SEL)aSelector;

这里我们需要返回一个实现了该方法的对象,使该对象能接收并且处理该消息,返回的对象用于接收消息,并且执行对应的方法。若是返回nil,则进入慢速转发

// 备用接收者
- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(say666)) {return [CJLTeacher new];}return [super forwardingTargetForSelector:aSelector];
}

慢速转发

若是快速转发中还是没有找到,就会进入最后一次挽救机会,在CJLPerson中重写methodSignatureForSelector,如下所示:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if (aSelector == @selector(sayBye)) {return [NSMethodSignature signatureWithObjCTypes:"v@:"];// 返回方法签名}return [super methodSignatureForSelector:aSelector];
}// 消息转发逻辑
- (void)forwardInvocation:(NSInvocation *)anInvocation {SEL selector = [anInvocation selector];// 检查备用对象是否响应方法if ([self.backupObject respondsToSelector:selector]) {[anInvocation invokeWithTarget:self.backupObject];} else {// 调用父类默认处理(抛出异常)[super forwardInvocation:anInvocation];}
}
  • 第一个方法中,为未实现的方法提供方法签名,避免运行时直接崩溃
    • v@:在这里表示方法返回值类型为void
  • 第二个方法中,将无法处理的消息转发给备用对象 _backupObject
  • 这里我们必须要注意,这两个方法必须顺序实现,仅调用第二个方法会产生报错

相关文章:

【iOS】探索消息流程

探索消息流程 Runtime介绍OC三大核心动态特性动态类型动态绑定动态语言 方法的本质代码转换objc_msgSendSELIMPMethod 父类方法在子类中的实现 消息查找流程开始查找快速查找流程慢速查找流程二分查找方法列表父类缓存查找 动态方法解析动态方法决议实例方法类方法优化 消息转发…...

院校机试刷题第六天:1134矩阵翻转、1052学生成绩管理、1409对称矩阵

一、1134矩阵翻转 1.题目描述 2.解题思路 很简单的模拟题&#xff0c;甚至只是上下翻转&#xff0c;遍历输出的时候先把最下面那一行输出即可。 3.代码 #include <iostream> #include <vector> using namespace std;int main() {int n;cin >> n;vector&l…...

DeepSeek在简历筛选系统中的深度应用

一、多模态解析引擎的技术突破 1.1 复杂格式的精准解析 针对简历格式多样性挑战,DeepSeek采用三级解析架构: 格式标准化层:基于Transformer的DocParser模型支持200+种文档格式转换视觉特征提取:使用改进的YOLOv8进行证书印章识别(mAP@0.5达93.7%)语义重构模块:通过注意…...

c++多线程debug

debug demo 命令行查看 ps -eLf|grep cam_det //查看当前运行的轻量级进程 ps -aux | grep 执行文件 //查看当前运行的进程 ps -aL | grep 执行文件 //查看当前运行的轻量级进程 pstree -p 主线程ID //查看主线程和新线程的关系 查看线程栈结构 pstack 线程ID 步骤&…...

【回溯 剪支 状态压缩】# P10419 [蓝桥杯 2023 国 A] 01 游戏|普及+

本文涉及知识点 C回溯 位运算、状态压缩、枚举子集汇总 P10419 [蓝桥杯 2023 国 A] 01 游戏 题目描述 小蓝最近玩上了 01 01 01 游戏&#xff0c;这是一款带有二进制思想的棋子游戏&#xff0c;具体来说游戏在一个大小为 N N N\times N NN 的棋盘上进行&#xff0c;棋盘…...

CUDA 纹理入门

一、什么是CUDA纹理 CUDA纹理是NVIDIA GPU提供的一种特殊内存访问机制,它允许高效地访问和过滤结构化数据。纹理内存最初是为图形渲染设计的,但在通用计算(GPGPU)中也很有用。 二、纹理内存的优势 缓存优化:纹理内存有专用的缓存,适合空间局部性好的访问模式 硬件过滤:支…...

大模型微调步骤整理

在对深度学习模型进行微调时,我通常会遵循以下几个通用步骤。 第一步是选择一个合适的预训练模型。PyTorch 的 torchvision.models 模块提供了很多经典的预训练模型,比如 ResNet、VGG、EfficientNet 等。我们可以直接使用它们作为模型的基础结构。例如,加载一个预训练的 Re…...

【GPT入门】第39课 OPENAI官方API调用方法

【GPT入门】第39课 OPENAI官方API调用方法 1. OPENAI 免费API2. openai调用最简单的API3.apiKey提取到环境变量 1. OPENAI 免费API 需要科学上网&#xff0c;可以调用 gpt-4o-mini 的 api, 使用其它旧的GPT&#xff0c;反而可能需要收费&#xff0c;例如 gpt-3.5-turbo 2. op…...

【DeepSeek论文精读】11. 洞察 DeepSeek-V3:扩展挑战和对 AI 架构硬件的思考

欢迎关注[【AIGC论文精读】](https://blog.csdn.net/youcans/category_12321605.html&#xff09;原创作品 【DeepSeek论文精读】1. 从 DeepSeek LLM 到 DeepSeek R1 【DeepSeek论文精读】7. DeepSeek 的发展历程与关键技术 【DeepSeek论文精读】11. 洞察 DeepSeek-V3&#xff…...

MySQL事务的一些奇奇怪怪知识

Gorm事务有error却不返回会发生什么 Gorm包是大家比较高频使用。正常的用法是&#xff0c;如果有失败返回error&#xff0c;整体rollback&#xff0c;如果不返回error则commit。下面是Transaction的源码&#xff1a; // Transaction start a transaction as a block, return …...

C语言内存函数与数据在内存中的存储

一、c语言内存函数 1、memcpy函数是一个标准库函数&#xff0c;用于内存复制。功能上是用来将一块内存中的内容复制到另一块内存中。用户需要提供目标地址、源地址以及要复制的字节数。例如结构体之间的复制。 memcpy函数的原型是&#xff1a;void* memcpy&#xff08;void* …...

Power BI Desktop运算符和新建列

1.运算符 运算符 含义 加 - 减 * 乘 / 除 ^ 幂 运算符 含义 等于 > 大于 < 小于 > 大于等于 < 小于等于 <> 不等于 运算符 含义 && 与 || 或 not 非 & 字符串连接 in 包含 not in 不包含 2.新建列 …...

windows 安装gdal实现png转tif,以及栅格拼接

windows 安装gdal实现png转tif&#xff0c;以及栅格拼接 一、安装gdal 网上有很多安装gdal的方法&#xff0c;此处通过osgeo4w安装gdal 1.下载osgeo4w 下载地址 https://trac.osgeo.org/osgeo4w/ 2、安装osgeo4w exe文件安装&#xff0c;前面部分很简单&#xff0c;就不再…...

【嵙大o】C++作业合集

​ 参考&#xff1a; C swap&#xff08;交换&#xff09;函数 指针/引用/C自带-CSDN博客 Problem IDTitleCPP指针CPP引用1107 Problem A编写函数&#xff1a;Swap (I) (Append Code)1158 Problem B整型数据的输出格式1163 Problem C时间&#xff1a;24小时制转12小时制1205…...

论信息系统项目的采购管理

论信息系统项目的采购管理 背景一、规划采购管理二、实施采购三、控制采购结语 背景 某市为对扶贫对象实施精确识别、精确帮扶、精确管理&#xff0c;决定由民政部门牵头&#xff0c;建设家庭经济状况分析及市、县&#xff08;区&#xff09;、镇&#xff08;街&#xff09;三级…...

创建型:单例模式

目录 1、核心思想 2、实现方式 2.1 饿汉式 2.2 懒汉式 2.3 枚举&#xff08;Enum&#xff09; 3、关键注意事项 3.1 线程安全 3.2 反射攻击 3.3 序列化与反序列化 3.4 克隆保护 4、适用场景 1、核心思想 目的&#xff1a;确保一个类仅有一个实例 功能&#xff1a;…...

职场方法论总结(4)-如何正确地汇报

一、明确汇报目标 区分类型&#xff1a;是项目进展汇报&#xff1f;数据总结&#xff1f;问题解决方案&#xff1f;还是资源申请&#xff1f;明确目标才能聚焦内容。听众需求&#xff1a; 所有人都希望你用最简短的语言把事情讲清楚&#xff0c;节省时间领导关注结果、风险和资…...

STM32SPI实战-Flash模板

STM32SPI实战-Flash模板 一&#xff0c;常用指令集&#xff08;部分&#xff09;二&#xff0c;组件库GD25QXX API 函数解析1,前提条件2,初始化与识别1, void spi_flash_init(void)2, uint32_t spi_flash_read_id(void) 3,擦除操作1, void spi_flash_sector_erase(uint32_t sec…...

CSS- 4.4 固定定位(fixed) 咖啡售卖官网实例

本系列可作为前端学习系列的笔记&#xff0c;代码的运行环境是在HBuilder中&#xff0c;小编会将代码复制下来&#xff0c;大家复制下来就可以练习了&#xff0c;方便大家学习。 HTML系列文章 已经收录在前端专栏&#xff0c;有需要的宝宝们可以点击前端专栏查看&#xff01; 点…...

【Retinanet】训练自己的数据集

目录 1.下载源码2.配置环境3.数据集准备4.训练自己的数据5.成功训练&#xff01; 1.下载源码 Retinanet代码&#xff1a;代码 下载到你的目录中&#xff0c;进行打开。 2.配置环境 这里就是cudapytorch&#xff0c;没有配置过的可以参考博客&#xff1a; 深度学习环境的搭建…...

微软将于 8 月 11 日关闭 Bing Search API 服务

微软宣布将于 2025 年 8 月 11 日正式关闭 Bing Search API 服务。届时&#xff0c;所有使用 Bing Search API 的实例将完全停用&#xff0c;同时不再接受新用户注册。 此次停用决定主要影响 Bing Search F1 及 S1 到 S9 资源的用户&#xff0c;以及 Custom Search F0 与 S1 到…...

探索 Python 的利器:help()、dir() 与 AI 工具的结合应用

引言 在编程世界中,Python 以其简洁的语法、强大的功能和丰富的库生态系统成为众多开发者的首选语言。无论是初学者还是资深工程师,在学习新模块、调试代码或探索未知功能时,常常需要有效的工具来帮助理解和解决问题。Python 提供了内置的 help() 和 dir() 函数,让开发者能…...

MySQL查询优化器底层原理解析:从逻辑优化到物理优化

MySQL查询优化器底层原理解析&#xff1a;从逻辑优化到物理优化 引言 在数据库系统中&#xff0c;SQL语句的执行效率直接影响着整个应用的性能表现。一条普通的SQL执行前会经历五个关键阶段&#xff1a;SQL输入、语法分析、语义检查、SQL优化、SQL执行。其中&#xff0c;SQL优…...

UI架构的历史与基础入门

本笔记的目的是通过一系列连贯的例子来探讨“事物-模型-视图-编辑器”这一隐喻。 这些例子都来自我的规划系统&#xff08;planning system&#xff09;&#xff0c;用于解释上述四个概念。所有例子都已实现&#xff0c;但并未在本文描述的清晰类结构中实现。 这些隐喻对应于《…...

(三)MMA(KeyCloak身份服务器/OutBox Pattern)

文章目录 项目地址一、KeyCloak二、OutBox Pattern2.1 配置Common模块的OutBox1. OutboxMessage2. 数据库配置OutboxMessageConfiguration3. 创建Save前的EF拦截器4. 创建Quartz后台任务5. 配置后台任务6. 注册服务2.2 创建OutBox的消费者项目地址 教程作者:教程地址:代码仓库…...

【通用智能体】Playwright:跨浏览器自动化工具

Playwright&#xff1a;跨浏览器自动化工具 一、Playwright 是什么&#xff1f;二、应用场景及案例场景 1&#xff1a;端到端&#xff08;E2E&#xff09;测试场景 2&#xff1a;UI 自动化&#xff08;表单批量提交&#xff09;场景 3&#xff1a;页面截图与 PDF 生成场景 4&am…...

单片机设计_停车场车位管理系统(AT89C52、LCD1602)

想要更多项目私wo!!! 一、电路设计 此电路由AT89C52单片机和LCD1602液晶显示模块等器件组成。 二、运行结果 三、部分代码 #include <reg52.h> //调用单片机头文件 #define uchar unsigned char //无符号字符型 宏定义 变量范围0~255 #define uint unsigned…...

【android bluetooth 协议分析 01】【HCI 层介绍 5】【SetEventMask命令介绍】

1. HCI_Set_Event_Mask 命令作用 项目内容命令名HCI_Set_Event_MaskOCF0x0001作用主机通过设置 Event Mask 告诉控制器&#xff1a;我只对某些事件感兴趣&#xff0c;屏蔽其他事件&#xff0c;以减少中断。事件来源事件是 HCI 与主机之间通信的反馈机制&#xff0c;控制器通过…...

python打卡day29

类的装饰器 知识点回顾 类的装饰器装饰器思想的进一步理解&#xff1a;外部修改、动态类方法的定义&#xff1a;内部定义和外部定义 回顾一下&#xff0c;函数的装饰器是 &#xff1a;接收一个函数&#xff0c;返回一个修改后的函数。类也有修饰器&#xff0c;类装饰器本质上确…...

【数据结构】树状数组

树状数组 假设一个数可以 x x x可以被二进制分解成 x 2 i 1 2 i 2 . . . 2 i m x 2^{i_1} 2^{i_2} ... 2^{i_m} x2i1​2i2​...2im​&#xff0c;不妨设 i 1 > i 2 > . . . > i m i_1 > i_2 > ... > i_m i1​>i2​>...>im​&#xff0c;进…...

Java虚拟机 - JVM与Java体系结构

Java虚拟机 JVM与Java体系结构为什么要学习JVMJava与JVM简介Java 语言的核心特性JVM&#xff1a;Java 生态的基石JVM的架构模型基于栈的指令集架构&#xff08;Stack-Based&#xff09;基于寄存器的指令集架构&#xff08;Register-Based&#xff09;JVM生命周期 总结 JVM与Jav…...

翻译:20250518

翻译题 文章目录 翻译题一带一路中国结 一带一路 The “One Belt and One Road” Initiative aims to achieve win-win and shared development. China remains unchanged in its commitment to foster partnerships. China pursues an independent foreign policy of peace, …...

SparkSQL基本操作

以下是 Spark SQL 的基本操作总结&#xff0c;涵盖数据读取、转换、查询、写入等核心功能&#xff1a; 一、初始化 SparkSession scala import org.apache.spark.sql.SparkSession val spark SparkSession.builder() .appName("Spark SQL Demo") .master("…...

Ansible模块——文件内容修改

修改文件单行内容 ansible.builtin.lineinfile 可以按行修改文件内容&#xff0c;一次修改一行&#xff0c;支持正则表达式。 选项名 类型 默认值 描述 attributesstrnull 设置目标文件的 Linux 文件系统属性&#xff08;attribute bits&#xff09;&#xff0c;作用类似于…...

基于单片机路灯自动控制仪仿真设计

标题:基于单片机路灯自动控制仪仿真设计 内容:1.摘要 本设计旨在解决传统路灯控制方式效率低、能耗大的问题&#xff0c;开展了基于单片机的路灯自动控制仪仿真设计。采用单片机作为核心控制单元&#xff0c;结合光照传感器、时钟模块等硬件&#xff0c;运用相关软件进行编程和…...

Spring Web MVC————入门(3)

今天我们来一个大练习&#xff0c;我们要实现一个登录界面&#xff0c;登录进去了先获取到登录人信息&#xff0c;可以选择计算器和留言板两个功能&#xff0c;另外我们是学后端的&#xff0c;对于前端我们会些基础的就行了&#xff0c;知道ajax怎么用&#xff0c;知道怎么关联…...

拓展运算符与数组解构赋值的区别

拓展运算符与数组解构赋值是ES6中用于处理数组的两种不同的特性&#xff0c;它们有以下区别&#xff1a; 概念与作用 • 拓展运算符&#xff1a;主要用于将数组展开成一系列独立的元素&#xff0c;或者将多个数组合并为一个数组&#xff0c;以及在函数调用时将数组作为可变参…...

【Linux】第二十章 管理基本存储

目录 1. 对 Linux 磁盘进行分区时有哪两种方案&#xff1f;分别加以详细说明。 2. 简单说下创建MBR磁盘分区涉及哪几个步骤&#xff1f; 3. 创建GPT分区与创建MBR分区有什么不同&#xff1f; 4. 在创建分区时就会在分区上创建文件系统吗&#xff1f; 5. 如何持久挂载文件系…...

DeepSeek本地部署全攻略:从零搭建到Web可视化及数据训练

目录 1. 环境准备与硬件要求2. 安装Ollama框架3. 部署DeepSeek模型4. Web可视化配置5. 数据投喂与模型训练6. 进阶技巧与常见问题1. 环境准备与硬件要求 硬件配置建议 基础配置:16GB内存 + RTX 3060显卡(流畅运行7B参数模型)进阶配置:32GB内存 + RTX 4090显卡(支持14B模型…...

JavaScript性能优化实战(12):大型应用性能优化实战案例

在前面的系列文章中,我们探讨了各种JavaScript性能优化技术和策略。本篇将聚焦于实际的大型应用场景,通过真实案例展示如何综合运用这些技术,解决复杂应用中的性能挑战。 目录 电商平台首屏加载优化全流程复杂数据可视化应用性能优化案例在线协作工具的实时响应优化移动端W…...

前缀和——中心数组下标

此题我们不应局限于前缀和的模板&#xff0c;因为该中心下标把数组分为两个部分且每个部分都要求和&#xff0c;我们就一个再创建一个”后缀和” 定义两个数组f&#xff0c;g。f[i]表示[0,i-1]所有元素的和 f[i]f[i-1]nums[i-1];g[i]表示[i1,n-1]的和 g[i]g[i1]nums[i1];因为依…...

Java——创建多线程的四种方式

一、继承Thread 步骤 1.定义一个类继承Thread 2.重写run方法&#xff0c;在方法中设置线程任务&#xff08;此线程具体执行的代码&#xff09; 3.创建自定义线程类对象 4.调用Thread中的start方法&#xff0c;开启线程&#xff0c;jvm自动调用run方法 常用方法 void sta…...

广域网学习

PPPoE技术&#xff08;拨号上网&#xff09; PPPoE &#xff08; PPP over Ethernet &#xff0c;以太网承载 PPP 协议&#xff09;是一种把 PPP 帧封装到以太网帧中的链路层协议。 PPPoE 可以使以太网网络中的多台主机连接到远端的宽带接入服务器。 应用场景 PPPoE 组网结构采…...

inverse-design-of-grating-coupler-3d

一、设计和优化3D光栅耦合器 1.1 代码讲解 通过预定义的环形间距参数(distances数组),在FDTD中生成椭圆光栅结构,并通过用户交互确认几何正确性后,可进一步执行参数扫描优化。 # os:用于操作系统相关功能(如文件路径操作) import os import sys# lumapi:Lumerical 的…...

渗透测试流程-中篇

#作者&#xff1a;允砸儿 #日期&#xff1a;乙巳青蛇年 四月廿一&#xff08;2025年5月18日&#xff09; 今天笔者带大家继续学习&#xff0c;网安的知识比较杂且知识面很广&#xff0c;这一部分会介绍很多需要使用的工具。会用各种工具是做网安的基础&#xff0c;ok咱们继续…...

2026武汉门窗门业移门木门铝艺门智能锁展会3月国博举办

展出面积&#xff1a;60000㎡ 观众&#xff1a;80000人次 参展企业&#xff1a;800 专业活动&#xff1a;20 2026武汉门窗门业移门木门铝艺门智能锁展会3月国博举办 2026第二届中国武汉整装定制家居暨门窗装饰材料博览会/2026武汉建博会 时间&#xff1a;2026年3月20-22日 …...

如何用mockito+junit测试代码

Mockito 是一个流行的 Java 模拟测试框架&#xff0c;用于创建和管理测试中的模拟对象(mock objects)。它可以帮助开发者编写干净、可维护的单元测试&#xff0c;特别是在需要隔离被测组件与其他依赖项时。 目录 核心概念 1. 模拟对象(Mock Objects) 2. 打桩(Stubbing) 3. 验…...

31、魔法生物图鉴——React 19 Web Workers

一、守护神协议&#xff08;核心原理&#xff09; 1. 灵魂分裂术&#xff08;线程架构&#xff09; // 主组件中初始化Workerconst workerRef useRef(null);​useEffect(() > {workerRef.current new Worker(new URL(./creatureWorker.js, import.meta.url));workerRef.…...

洛谷题目:P4052 [JSOI2007] 文本生成器 题解 本题(极难)

个人介绍: 题目传送门: P4052 [JSOI2007] 文本生成器 - 洛谷 (luogu.com.cn) 前言: 这道题要求计算长度为 m 的文章中,至少包含一个给定单词的可读文章的数量,并且结果需要对 10007 取模。下面是小亦为大家逐步分析解题思路: 题目整体思路: 为了方便计算…...

【Linux】命令行参数和环境变量

目录 一、命令行参数 二、环境变量 &#xff08;一&#xff09;PATH &#xff08;二&#xff09;查看环境变量 &#xff08;三&#xff09;获取环境环境变量 &#xff08;四&#xff09;为什么要环境变量 &#xff08;五&#xff09;环境变量特点总结 &#xff08;1&am…...