「OC」源码学习—— 消息发送、动态方法解析和消息转发
「OC」源码学习—— 消息发送、动态方法解析和消息转发
前言
前面我们在学习alloc源码的时候,就在callAlloc
源码之中简单的探究过,类初始化缓存的问题,我们知道在一个类第一次被实例化的时候,会调用objc_msgSend
去二次调用alloc
方法,这篇文章就是探究我们在调用方法的时候,会需要经历一个什么流程。
方法的本质
我们可以通过clang编译main.m文件可以查看main函数之中方法的调用
//main.m中方法的调用
LGPerson *person = [LGPerson alloc];
[person sayNB];
[person sayHello];//👇clang编译后的底层实现
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayNB"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
我们之前在小蓝书
之中也学习过,方法的调用本质上就是进行消息发送
我们可以在编译器之中进行简单的尝试
1、直接调用
objc_msgSend
,需要导入头文件#import <objc/message.h>
2、需要将target --> Build Setting -->搜索msg
– 将enable strict checking of obc_msgSend calls
由YES
改为NO
,将严厉的检查机制关掉,否则objc_msgSend的参数会报错
GGObject *obj = [[GGObject alloc] init];
((void (*)(id, SEL))objc_msgSend)(obj, @selector(speak));
[obj speak];
[obj sayHello];//结果-[GGObject speak]
-[GGObject speak]
-[GGObject sayHello]
我们会发现,使用方法调用和使用它objc_msgSend
得到的结果也是一样的
objc_msgSend和objc_msgSendSuper的区别
来看看这么一个案例:
声明一个MyPerson类
#import <Foundation/Foundation.h>
@interface MyPerson : NSObject
-(void)study;
@end
#import "MyPerson.h"
@implementation MyPerson
-(void)study {NSLog(@"%s",__func__);
}
@end
声明一个MyTeacher类继承自MyPerson类
#import "MyPerson.h"
@interface MyTeacher : MyPerson
@end
#import "MyTeacher.h"
#import <objc/message.h>
@implementation MyTeacher
-(instancetype)init {if (self = [super init]) {NSLog(@"%@",[self class]);NSLog(@"%@",[super class]);}return self;
}
-(void)study {[super study];
}
@end
在ViewContriller里使用这个MyTeacher
#import "ViewController.h"
#import "MyTeacher.h"
@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];MyTeacher *t = [[MyTeacher alloc] init];[t study];
}
@end
完整消息流程
objc_msgSend
的汇编实现
我们点进Objc源码之中关于objc_msgSend
的相关内容,会发现它是汇编实现的,这里借用一下大佬月月的给出的函数注释。iOS-底层原理 12:消息流程分析之快速查找
//---- 消息发送 -- 汇编入口--objc_msgSend主要是拿到接收者的isa信息
ENTRY _objc_msgSend
//---- 无窗口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 // taggedadrp x10, _objc_debug_taggedpointer_classes@PAGEadd x10, x10, _objc_debug_taggedpointer_classes@PAGEOFFubfx x11, x0, #60, #4ldr x16, [x10, x11, LSL #3]adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEadd x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFFcmp x10, x16b.ne LGetIsaDone// ext taggedadrp x10, _objc_debug_taggedpointer_ext_classes@PAGEadd x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFFubfx x11, x0, #52, #8ldr x16, [x10, x11, LSL #3]b 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
是否为空。若不为空看这个receiver
是否支持tagged pointer
。- 如果支持再跳转到
LNilOrTagged
函数之中,处理这个小对象的isa
指针
- 如果支持再跳转到
- 如果不为空且指针不为
tagged pointer
,我们就直接将receiver
的isa
指针取出存入p13寄存器适中,然后再调用GetClassFromIsa_p16
,使用掩码将isa之中的shiftcls
位域的内容取出来。 - 将
isa
的信息提取出来之后,进入CacheLookup函数,进入快速流程查找
快速查找流程
// CacheLookup的汇编源码
.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 --- #define CACHE (2 * __SIZEOF_POINTER__),其中 __SIZEOF_POINTER__表示pointer的大小 ,即 2*8 = 16
//---- p11 = mask|buckets -- 从x16(即isa)中平移16字节,取出cache 存入p11寄存器 -- isa距离cache 正好16字节:isa(8字节)-superClass(8字节)-cache(mask高16位 + buckets低48位)ldr p11, [x16, #CACHE]
//---- 64位真机
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- p11(cache) & 0x0000ffffffffffff ,mask高16位抹零,得到buckets 存入p10寄存器-- 即去掉mask,留下bucketsand p10, p11, #0x0000ffffffffffff // p10 = buckets //--- p11(cache)右移48位,得到mask(即p11 存储mask),mask & p1(msgSend的第二个参数 cmd-sel) ,得到sel-imp的下标index(即搜索下标) 存入p12(cache insert写入时的哈希下标计算是 通过 sel & mask,读取时也需要通过这种方式)and p12, p1, p11, LSR #48 // x12 = _cmd & mask //--- 非64位真机
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4 and 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.
#endif//--- p12是下标 p10是buckets数组首地址,下标 * 1<<4(即16) 得到实际内存的偏移量,通过buckets的首地址偏移,获取bucket存入p12寄存器
//--- LSL #(1+PTRSHIFT)-- 实际含义就是得到一个bucket占用的内存大小 -- 相当于mask = occupied -1-- _cmd & mask -- 取余数add p12, p10, p12, LSL #(1+PTRSHIFT) // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) -- PTRSHIFT是3//--- 从x12(即p12)中取出 bucket 分别将imp和sel 存入 p17(存储imp) 和 p9(存储sel)ldp p17, p9, [x12] // {imp, sel} = *bucket //--- 比较 sel 与 p1(传入的参数cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不相等,即没有找到,请跳转至 2fb.ne 2f // scan more
//--- 如果相等 即cacheHit 缓存命中,直接返回impCacheHit $0 // call or return imp 2: // not hit: p12 = not-hit bucket
//--- 如果一直都找不到, 因为是normal ,跳转至__objc_msgSend_uncachedCheckMiss $0 // miss if bucket->sel == 0
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素,),如果等于,则跳转至第3步cmp p12, p10 // wrap if bucket == buckets
//--- 定位到最后一个元素(即第一个bucket)b.eq 3f
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳转至第1步,继续对比 sel 与 cmdb 1b // loop 3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//--- 人为设置到最后一个元素
//--- p11(mask)右移44位 相当于mask左移4位,直接定位到buckets的最后一个元素,缓存查找顺序是向前查找add p12, p12, p11, LSR #(48 - (1+PTRSHIFT)) // p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4add 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.
//--- 再查找一遍缓存()
//--- 拿到x12(即p12)bucket中的 imp-sel 分别存入 p17-p9ldp p17, p9, [x12] // {imp, sel} = *bucket //--- 比较 sel 与 p1(传入的参数cmd)
1: cmp p9, p1 // if (bucket->sel != _cmd)
//--- 如果不相等,即走到第二步b.ne 2f // scan more
//--- 如果相等 即命中,直接返回impCacheHit $0 // call or return imp 2: // not hit: p12 = not-hit bucket
//--- 如果一直找不到,则CheckMissCheckMiss $0 // miss if bucket->sel == 0
//--- 判断p12(下标对应的bucket) 是否 等于 p10(buckets数组第一个元素)-- 表示前面已经没有了,但是还是没有找到cmp p12, p10 // wrap if bucket == buckets b.eq 3f //如果等于,跳转至第3步
//--- 从x12(即p12 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
//--- 跳转至第1步,继续对比 sel 与 cmdb 1b // loop LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
//--- 跳转至JumpMiss 因为是normal ,跳转至__objc_msgSend_uncachedJumpMiss $0
.endmacro//以下是最后跳转的汇编函数
.macro CacheHit
.if $0 == NORMALTailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMPmov p0, p17cbz p0, 9f // don't ptrauth a nil impAuthAndResignAsIMP x0, x12, 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, x12, x1, x16 // authenticate imp and re-sign as IMPret // return imp via x17
.else
.abort oops
.endif
.endmacro.macro CheckMiss// miss if bucket->sel == 0
.if $0 == GETIMP
//--- 如果为GETIMP ,则跳转至 LGetImpMisscbz p9, LGetImpMiss
.elseif $0 == NORMAL
//--- 如果为NORMAL ,则跳转至 __objc_msgSend_uncachedcbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
//--- 如果为LOOKUP ,则跳转至 __objc_msgLookup_uncachedcbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro.macro JumpMiss
.if $0 == GETIMPb LGetImpMiss
.elseif $0 == NORMALb __objc_msgSend_uncached
.elseif $0 == LOOKUPb __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
前面我们已经通过objc_msgSend
获取了消息发送的receiver
的类信息,根据我们之前学习的内容可以知道,objc_class
的首地址偏移16字节就是cache_t
。
- 在
objc_class
偏移16字节获取cache_t
之中的内容 - 根据我们之前在cache_t的分析,在objc 838 的源码之中,
_bucketsAndMaybeMask
是一个指向64位内存的指针, 高16位存储掩码,低48位作为存储buckets
,在这个函数之中使用移位运算符将这两个内存取出 - 将我们
objc_msgSend
的第二个参数的选择子进行哈希运算,查找buckets
的下标的位置的选择子和我们传入的选择子是否一样,如果不一样就不断的往前遍历(因为要考虑哈希冲突,系统解决哈希冲突的方法是使用开放寻址法),而当我们遍历到bucket到第一个元素,我们再把指针指向buckets到最后一个元素- 如果找到匹配项,那么直接调用
cacheHit
方法 - 如果程序第二次到达bucket的首元素位置,则说明cache_t未存储相关信息,则直接跳转
_objc_msgSend_uncached
进入慢速查找流程
- 如果找到匹配项,那么直接调用
慢速查找流程
__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 searchMethodTableLookup // 开始查询方法列表
TailCallFunctionPointer x17END_ENTRY __objc_msgSend_uncached
此时这个MethodTableLookup
最终都会跳转到lookUpImpOrForward
,这里仍然借用月月的源码注释:iOS-底层原理 13:消息流程分析之慢速查找
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{// 定义的消息转发const IMP forward_imp = (IMP)_objc_msgForward_impcache; IMP imp = nil;Class curClass;runtimeLock.assertUnlocked();// 快速查找,如果找到则直接返回imp//目的:防止多线程操作时,刚好调用函数,此时缓存进来了if (fastpath(behavior & LOOKUP_CACHE)) { imp = cache_getImp(cls, sel);if (imp) goto done_nolock;}//加锁,目的是保证读取的线程安全runtimeLock.lock();//判断是否是一个已知的类:判断当前类是否是已经被认可的类,即已经加载的类checkIsKnownClass(cls); //判断类是否实现,如果没有,需要先实现,此时的目的是为了确定父类链,方法后续的循环if (slowpath(!cls->isRealized())) { cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);}//判断类是否初始化,如果没有,需要先初始化if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) { cls = initializeAndLeaveLocked(cls, inst, runtimeLock);}runtimeLock.assertLocked();curClass = cls;//----查找类的缓存// unreasonableClassCount -- 表示类的迭代的上限//(猜测这里递归的原因是attempts在第一次循环时作了减一操作,然后再次循环时,仍在上限的范围内,所以可以继续递归)for (unsigned attempts = unreasonableClassCount();;) { //---当前类方法列表(采用二分查找算法),如果找到,则返回,将方法缓存到cache中Method meth = getMethodNoSuper_nolock(curClass, sel);if (meth) {imp = meth->imp;goto done;}//当前类 = 当前类的父类,并判断父类是否为nilif (slowpath((curClass = curClass->superclass) == nil)) {//--未找到方法实现,方法解析器也不行,使用转发imp = forward_imp;break;}// 如果父类链中存在循环,则停止if (slowpath(--attempts == 0)) {_objc_fatal("Memory corruption in class list.");}// --父类缓存imp = cache_getImp(curClass, sel);if (slowpath(imp == forward_imp)) { // 如果在父类中找到了forward,则停止查找,且不缓存,首先调用此类的方法解析器break;}if (fastpath(imp)) {//如果在父类中,找到了此方法,将其存储到cache中goto done;}}//没有找到方法实现,尝试一次方法解析if (slowpath(behavior & LOOKUP_RESOLVER)) {//动态方法决议的控制条件,表示流程只走一次behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior);}done://存储到缓存log_and_fill_cache(cls, imp, sel, inst, curClass); //解锁runtimeLock.unlock();done_nolock:if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {return nil;}return imp;
}
流程:
- 进入
LookUpImpOrForward
函数,首先就是再次检查缓存,这一步是为了在多线程的情况下,当我们进入这个函数的时候,新的混粗已经存入cache_t
- 接着就是给程序加锁,防止我们在程序之中查看
buckets
是,因为重复添加缓存导致指针错误 - 查看是否是已知类,且类是否实现再查看类是否进行初始化
实现和初始化的区别
实现:
- 分配类的可读写数据(
class_rw_t
结构)。- 解析并注册类的方法列表、属性和协议。
- 确定类的父类及元类关系,确保继承链完整。
初始化:
- 初始化静态变量或全局状态。
- 执行一次性的配置代码(如注册通知、加载资源)
-
for循环遍历继承链,根据
getMethodNoSuper_nolock->getMethodFromListArray / getMethodFromRelativeList->search_method_list_inline
函数进行进行寻找-
对于无序的方法列表,我们使用
findMethodInUnsortedMethodList
,使用for循环遍历对比sel -
findMethodInSortedMethodList
查找有序的方法列表,通过二分查找对比sel
取出method_t
-
-
当遍历发现父类链之中存在循环则报错,终止循环,如果找到对应选择子,则直接返回imp,执行写入流程
-
判断
是否执行过
动态方法解析- 如果
没有
,执行动态方法解析
- 如果
执行过
一次动态方法解析,则走到消息转发流程
- 如果
findMethodInSortedMethodList
查找流程探究
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{ASSERT(list);const method_t * const first = &list->first;const method_t *base = first;const method_t *probe;uintptr_t keyValue = (uintptr_t)key; //key 等于 say666uint32_t count;//base相当于low,count是max,probe是middle,这就是二分for (count = list->count; count != 0; count >>= 1) {//从首地址+下标 --> 移动到中间位置(count >> 1 右移1位即 count/2 = 4)probe = base + (count >> 1); uintptr_t probeValue = (uintptr_t)probe->name;//如果查找的key的keyvalue等于中间位置(probe)的probeValue,则直接返回中间位置if (keyValue == probeValue) { // -- while 平移 -- 排除分类重名方法while (probe > first && keyValue == (uintptr_t)probe[-1].name) {//排除分类重名方法(方法的存储是先存储类方法,在存储分类---按照先进后出的原则,分类方法最先出,而我们要取的类方法,所以需要先排除分类方法)//如果是两个分类,就看谁先进行加载probe--;}return (method_t *)probe;}//如果keyValue 大于 probeValue,就往probe即中间位置的右边查找if (keyValue > probeValue) { base = probe + 1;count--;}}return nil;
}
从第一次查找开始就取中间位置,与想查找的value
进行对比,如果相等,还需要使用while循环,逐步向前搜索,目的是排除分类方法(因为分类方法会被插入到方法的最前端);若不相等,则继续进行二分查找,直到count为0
消息转发流程
在快慢两次查找都没有找到方法实现的情况下,我们会进行以下操作
动态方法决议
:慢速查找流程未找到后,会执行一次动态方法决议消息转发
:如果动态方法决议仍然没有找到实现,则进行消息转发
动态方法解析resolveMethod_locked
我们在lookUpImpOrForward
函数里,可以看到动态方法解析的入口逻辑:
if (slowpath(behavior & LOOKUP_RESOLVER)) {//动态方法决议的控制条件,表示流程只走一次behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior);}
当本类和本类继承链下的cache
和method list
都查找不到imp
,imp
被赋值成了_objc_msgForward_impcache
但是它没有调用,会进入动态方法解析流程resolveMethod_locked
,并且只会执行一次。
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); // inst:类对象 cls: 元类if (!lookUpImpOrNilTryCache(inst, sel, cls)) {resolveInstanceMethod(inst, sel, cls);}}// chances are that calling the resolver have populated the cache// so attempt using itreturn lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
- 如果是
类
,执行实例方法
的动态方法决议resolveInstanceMethod
- 如果是
元类
,执行类方法
的动态方法决议resolveClassMethod
,如果在元类中没有找到其对应的类方法,则在元类
的实例方法
的动态方法决议resolveInstanceMethod
中查找,主要是因为类方法在元类中是实例方法
,所以还需要查找元类中实例方法的动态方法决议 - 在动态方法决议消息前,需要查找cls
类
中是否有该方法的实现
,即通过lookUpImpOrNil
方法又会进入lookUpImpOrForward
慢速查找流程查找resolveInstanceMethod
方法- 如果没有,则直接返回
- 如果有,则发送
resolveInstanceMethod
消息(为了防止程序死循环,所以我们在一整个消息流程之中只会运行一次动态消息转发)
以下是如何实现动态方法决议
+ (BOOL)resolveInstanceMethod:(SEL)sel{if (sel == @selector(say666)) {NSLog(@"%@ 来了", NSStringFromSelector(sel));//获取sayMaster方法的impIMP imp = class_getMethodImplementation(self, @selector(sayMaster));//获取sayMaster的实例方法Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));//获取sayMaster的丰富签名const char *type = method_getTypeEncoding(sayMethod);//将sel的实现指向sayMasterreturn class_addMethod(self, sel, imp, type);}return [super resolveInstanceMethod:sel];
}
消息转发流程
在慢速查找的流程中,我们了解到,如果快速+慢速没有找到方法实现,动态方法决议也不行,就使用消息转发
,这个流程同样被分为快速转发和慢速转发
快速转发forwardingTargetForSelector
首先我们要知道快速转发进入了一个什么函数
- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(sayHello)) {return [LGPerson new]; // 返回其他类中实现过这个方法的方法.}return [super forwardingTargetForSelector:aSelector];
}
- 作用:将消息转发给
LGPerson
,由它响应消息。 - 特点:
- 无需生成方法签名或
NSInvocation
对象,性能较高。 - 转发后,消息的接收者变为LGPerson,对外透明
- 无需生成方法签名或
适用场景
- 伪多继承:通过转发实现多个对象的功能组合,例如将不同职责的消息分发给不同对象处理
- 代理模式:将消息转发给代理对象,简化主对象逻辑
慢速转发——methodSignatureForSelector
和forwardInvocation
首先我们一定要重写methodSignatureForSelector
,返回一个方法签名。在上一步我们返回了一个方法签名,那么在运行时系统就会创建一个NSInvocation 对象,传入forwardInvocation
,再次进行转发。
// 生成方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if (aSelector == @selector(unimplementedMethod)) {return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 返回方法签名}return [super methodSignatureForSelector:aSelector];
}// 处理转发逻辑
- (void)forwardInvocation:(NSInvocation *)anInvocation {if ([_backupObject respondsToSelector:anInvocation.selector]) {[anInvocation invokeWithTarget:_backupObject]; // 调用备用对象} else {[super forwardInvocation:anInvocation];}
}
参考文章
iOS 消息发送、动态方法解析和消息转发 objc4-838.1源码
手撕iOS底层13 – 手摸手的助你理清objc_msgSend
汇编源码
面试遇到Runtime的第三天-消息转发
iOS-底层原理 12:消息流程分析之快速查找
iOS-底层原理 14:消息流程分析之 动态方法决议 & 消息转发
相关文章:
「OC」源码学习—— 消息发送、动态方法解析和消息转发
「OC」源码学习—— 消息发送、动态方法解析和消息转发 前言 前面我们在学习alloc源码的时候,就在callAlloc源码之中简单的探究过,类初始化缓存的问题,我们知道在一个类第一次被实例化的时候,会调用objc_msgSend去二次调用alloc…...
MySQL数据库下篇
#作者:允砸儿 #日期:乙巳青蛇年 四月十四 今天笔者将会把MySQL数据库的知识完结,再者笔者会浅写一下sql注入的内容。在后面笔者会逐渐的将网安世界徐徐展开。 php与mysql联动 编程接口 笔者在前面的文章写了php的内容,现在我…...
Linux之进程概念
目录 一、冯诺依曼体系结构 二、操作系统(Operator System) 2.1、概念 2.2、设计OS的目的 2.3、系统调用和库函数概念 三、进程 3.1、基本概念 3.1.1、描述进程-PCB 3.1.2、task_struct 3.1.3、查看进程 3.1.4、通过系统调用获取进程标识符 3.1.5、两种杀掉进程的方…...
numpy模块综合使用
一、numpy模块的综合使用方法 # 使用矩阵的好处,矩阵对于python中列表,字典等数据类型一个一个拿来计算是会方便计算很多的,底层使用的是c语言 # 在数据分析和数据处理的时候也经常常用 import numpy as np array np.array([[1,2,3],[2,3,4…...
嵌入式硬件篇---SPI
文章目录 前言1. SPI协议基础1.1 物理层特性四线制(标准SPI)SCKMOSIMISONSS/CS 三线制(半双工模式)通信模式 1.2 通信时序(时钟极性CPOL和相位CPHA)常用模式Mode 0Mode 3 1.3 典型通信流程 2. STM32F103RCT…...
【大模型】AI智能体Coze 知识库从使用到实战详解
目录 一、前言 二、知识库介绍 2.1 coze 知识库功能介绍 2.2 coze 知识库应用场景 2.3 coze 知识库类型 2.4 coze 知识库权限说明 2.5 coze 知识库与记忆对比 2.6 知识库的使用流程 三、知识库创建与使用 3.1 创建知识库入口 3.2 创建文本知识库 3.2.1 上传文件 3.…...
深度学习:系统性学习策略(二)
深度学习的系统性学习策略 基于《认知觉醒》与《认知驱动》的核心方法论,结合深度学习的研究实践,从认知与技能双重维度总结以下系统性学习策略: 一、认知觉醒:构建深度学习的思维操作系统 三重脑区协同法则 遵循**本能脑(舒适区)-情绪脑(拉伸区)-理智脑(困难区)**的…...
TikTok 推广干货:AI 加持推广效能
TikTok 推广是提升账号影响力、吸引更多关注的关键一环。其中,巧妙利用热门话题标签是增加视频曝光的有效捷径。运营者需要密切关注当下流行趋势,搜索与账号定位紧密相关的热门标签。例如,对于美妆账号而言,带上 “# 美妆教程 #热…...
滑动窗口——将x减到0的最小操作数
题目: 这个题如果我们直接去思考方法是很困难的,因为我们不知道下一步是在数组的左还是右操作才能使其最小。正难则反,思考一下,无论是怎么样的,最终这个数组都会分成三个部分左中右,而左右的组合就是我们…...
无侵入式弹窗体验_探索 Chrome 的 Close Watcher API
1. 引言 在网页开发中,弹窗(Popup)是一种常见的交互方式,用于提示用户进行操作、确认信息或展示关键内容。然而,传统的 JavaScript 弹窗方法如 alert()、confirm() 和 prompt() 存在诸多问题,包括阻塞主线程、样式不可定制等。 为了解决这些问题,Chrome 浏览器引入了 …...
牛客周赛 Round 92 题解 Java
目录 A 小红的签到题 B 小红的模拟题 C 小红的方神题 D 小红的数学题 E 小红的 ds 题 F 小红的小苯题 A 小红的签到题 直接构造类似于 a_aaaa,a_aaaaaaaa 这种 即可 // github https://github.com/Dddddduo // github https://github.com/Dddddduo/acm-java…...
DAY 17 训练
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 DAY 17 训练 聚类算法聚类评估指标介绍1. 轮廓系数 (Silhouette Score)2. CH 指数 (Calinski-Harabasz Index)3. DB 指数 (Davies-Bouldin Index) 1. KMeans 聚类算法原理确定…...
多源最短路径(Floyed)
#include <iostream> #include <vector> #include <stack> using namespace std; class Graph{ private: int vertex; //顶点数 //int** matrix; //有向图关系矩阵 int** path; //存储关系矩阵 int** pre; //存储中间节点k public: con…...
基于去中心化与AI智能服务的web3钱包的应用开发的背景描述
Web3代表了下一代互联网模式,其核心特征包括去中心化、数据主权、智能合约和区块链技术的广泛应用。根据大数据调查显示,用户希望拥有自己的数据控制权,并希望在去中心化网络中享受类似Web2的便捷体验。DeFi(去中心化金融) 生态日趋成熟的背景…...
LabVIEW车牌自动识别系统
在智能交通快速发展的时代,车牌自动识别系统成为提升交通管理效率的关键技术。本案例详细介绍了基于 LabVIEW 平台,搭配大恒品牌相机构建的车牌自动识别系统,该系统在多个场景中发挥着重要作用,为交通管理提供了高效、精准的解决方…...
C# Newtonsoft.Json 使用指南
Newtonsoft.Json (也称为 Json.NET) 是一种适用于 .NET 的常用高性能 JSON 框架,用于处理 JSON 数据。它提供了高性能的 JSON 序列化和反序列化功能。 安装 通过 NuGet 安装 基本用法 1. 序列化对象为 JSON 字符串 using Newtonsoft.Json;var product new Prod…...
Python_day22
DAY 22 复习日 复习日 仔细回顾一下之前21天的内容,没跟上进度的同学补一下进度。 作业: 自行学习参考如何使用kaggle平台,写下使用注意点,并对下述比赛提交代码 kaggle泰坦里克号人员生还预测 一、Kaggle 基础使用步骤 注册与登录…...
浏览器的B/S架构和C/S架构
浏览器的B/S架构和C/S架构 概述拓展 欢迎来到 Shane 的博客~ 心有猛虎,细嗅蔷薇。 概述 C/S架构? Client/Server架构。但是缺少通用性、系统维护、升级需要重新设计和开发,并且需要开发不同的操作系统,增加了维护和管理的难度。&…...
【C++】内存管理 —— new 和 delete
文章目录 一、C/C 内存分布二、C 语言中动态内存管理方式1. malloc / calloc / realloc / free 三、C 内存管理方式1. new / delete2. operator new 与 operator delete 函数3. new 和 delete 的实现原理(1) new 的原理(2) delete 的原理(3) new T[N] 的原理(4) delete[] 的原理…...
springboot3整合SpringSecurity实现登录校验与权限认证
一:概述 1.1 基本概念 (1)认证 系统判断身份是否合法 (2)会话 为了避免每次操作都进行认证可将用户信息保存在会话中 session认证 服务端有个session,把 session id给前端,每次请求cookie都带着…...
【东枫科技】使用LabVIEW进行深度学习开发
文章目录 DeepLTK LabVIEW深度学习工具包LabVIEW中的深度神经网络**功能与特性****功能亮点:** **支持的网络层****支持的网络架构****参考示例** 授权售价 DeepLTK LabVIEW深度学习工具包 LabVIEW中的深度神经网络 功能亮点: 在 LabVIEW 中创建、配置…...
《智能网联汽车 自动驾驶系统通用技术要求》 GB/T 44721-2024——解读
目录 一、核心框架与适用范围 二、关键技术要求 1. 总体要求 2. 动态驾驶任务执行 3. 动态驾驶任务后援 4. 人机交互(HMI) 5. 说明书要求 三、附录重点 附录A(规范性)——功能安全与预期功能安全 附录B(资料性…...
同一个虚拟环境中conda和pip安装的文件存储位置解析
文章目录 存储位置的基本区别conda安装的包pip安装的包 看似相同实则不同的机制实际路径示例这种差异带来的问题如何检查包安装来源最佳实践建议 总结 存储位置的基本区别 conda安装的包 存储在Anaconda(或Miniconda)目录下的pkgs和envs子目录中: ~/anaconda3/en…...
《Hadoop 权威指南》笔记
Hadoop 基础 MapReduce Hadoop 操作 Hadoop 相关开源项目...
每日一题洛谷P8615 [蓝桥杯 2014 国 C] 拼接平方数c++
P8615 [蓝桥杯 2014 国 C] 拼接平方数 - 洛谷 (luogu.com.cn) #include<iostream> #include<string> #include<cmath> using namespace std; bool jud(int p) {int m sqrt(p);return m * m p; } void solve(int n) {string t to_string(n);//int转换为str…...
【C++】AVL树实现
目录 前言 一、AVL树的概念 二、AVL树的实现 1.基本框架 2.AVL树的插入 三、旋转 1.右单旋 2.左单旋 3.左右双旋 4.右左双旋 四、AVL树的查找 五、AVL树的平衡检测 六、AVL树的删除 总结 前言 本文主要讲解AVL树的插入,AVL树是在二叉搜索树的基础上&a…...
49.EFT测试与静电测试环境和干扰特征分析
EFT测试与静电测试环境和干扰特征分析 1. EFT/B电快速瞬变脉冲群测试及干扰特征分析2. EFT的干扰特征分析与滤波方法3. ESD静电测试及干扰特征分析 1. EFT/B电快速瞬变脉冲群测试及干扰特征分析 EFT测试是模拟在大的感性设备断开瞬间产生的快速瞬变脉冲群对被测设备的影响。 E…...
html body 设置heigth 100%,body内元素设置margin-top出滚动条(margin 重叠问题)
今天在用移动端的时候发现个问题,html,body 设置 height:100% 会出现纵向滚动条 <!DOCTYPE html> <html> <head> <title>html5</title> <style> html, body {height: 100%; } * {margin: 0;padding: 0; } </sty…...
1688 API 自动化采集实践:商品详情实时数据接口开发与优化
在电商行业竞争日益激烈的当下,实时获取 1688 平台商品详情数据,能够帮助商家分析市场动态、优化选品策略,也能助力数据分析师洞察行业趋势。通过 API 自动化采集商品详情数据,不仅可以提高数据获取效率,还能保证数据的…...
Transformer Decoder-Only 参数量计算
Transformer 的 Decoder-Only 架构(如 GPT 系列模型)是当前大语言模型的主流架构,其参数量主要由以下几个部分组成: 嵌入层(Embedding Layer)自注意力层(Self-Attention Layers)前馈…...
苍穹外卖(数据统计–Excel报表)
数据统计(Excel报表) 工作台 接口设计 今日数据接口 套餐总览接口 菜品总览接口 订单管理接口 编辑代码导入 功能测试 导出运营数据Excel报表 接口设计 代码开发 将模板文件放到项目中 导入Apache POI的maven坐标 在ReportCont…...
如何实现Flask应用程序的安全性
在 Flask 应用中,确保安全性非常关键,尤其是当你将应用部署到公网环境中时。Flask 本身虽然轻量,但通过组合安全策略、扩展库和最佳实践,可以构建一个非常安全的 Web 应用。 一、常见 Flask 安全风险(必须防护…...
【Redis】Redis的主从复制
文章目录 1. 单点问题2. 主从模式2.1 建立复制2.2 断开复制 3. 拓扑结构3.1 三种结构3.2 数据同步3.3 复制流程3.3.1 psync运行流程3.3.2 全量复制3.3.3 部分复制3.3.4 实时复制 1. 单点问题 单点问题:某个服务器程序,只有一个节点(只搞一个…...
趣味编程:四叶草
概述:在万千三叶草中寻觅,只为那一抹独特的四叶草之绿,它象征着幸运与希望。本篇博客主要介绍四叶草的绘制。 1. 效果展示 绘制四叶草的过程是一个动态的过程,因此博客中所展示的为绘制完成的四叶草。 2. 源码展示 #define _CR…...
HTTP 响应状态码总结
一、引言 HTTP 响应状态码是超文本传输协议(HTTP)中服务器对客户端(通常是 Web 浏览器)请求的响应指示。这些状态码是三位数字代码,用于告知客户端请求的结果,包括请求是否成功。响应被分为五个类别&#…...
C语言常见的文件操作函数总结
目录 前言 一、打开和关闭 1.fopen 细节 2.fclos 基本用法示例 二、读写 1.fputc和fgetc 1)fputc 细节 基本用法示例 2)fgetc 细节 基本用法示例 2.fputs和fgets 1)fputs 细节 基本用法示例 2)fgets 细节 基本用法示例 3)puts的使用,以及为什…...
卫宁健康WiNGPT3.0与WiNEX Copilot 2.2:医疗AI创新的双轮驱动分析
引言:医疗AI的双翼时代 在医疗信息化的浪潮中,人工智能技术的深度融入正在重塑整个医疗行业。卫宁健康作为国内医疗健康和卫生领域数字化解决方案的领军企业,持续探索AI技术在医疗场景中的创新应用。2025年5月10日,在第29届中国医院信息网络大会(CHIMA2025)上,卫宁健康…...
【GPT入门】第38课 RAG评估指标概述
这里写自定义目录标题 一、RAG评估指标二、ragas 评估三、trulens 一、RAG评估指标 二、ragas 评估 2.1 ragas介绍 开源地址:https://github.com/explodinggradients/ragas 官方文档:https://docs.ragas.io/en/stable/从文本生成和文本召回两个维度&am…...
深度剖析多模态大模型中的视频编码器算法
写在前面 随着多模态大型语言模型(MLLM)的兴起,AI 理解世界的能力从静态的文本和图像,进一步拓展到了动态的、包含丰富时空信息的视频。视频作为一种承载了动作、交互、场景变化和声音(虽然本文主要聚焦视觉部分)的复杂数据形式,为 MLLM 提供了理解真实世界动态和因果关…...
【递归、搜索与回溯算法】导论
📝前言说明: 本专栏主要记录本人递归、搜索与回溯算法的学习以及LeetCode刷题记录,按专题划分每题主要记录:(1)本人解法 本人屎山代码;(2)优质解法 优质代码ÿ…...
《智能网联汽车 自动驾驶功能道路试验方法及要求》 GB/T 44719-2024——解读
目录 1. 适用范围 2. 关键术语 3. 试验条件 3.1 试验道路 3.2 试验车辆 3.3 试验设备 3.4 试验时间 4. 试验方法及要求 4.1 功能激活 4.2 动态驾驶任务执行 4.3 动态驾驶任务后援 4.4 状态提示 5. 附录A(核心环境要素) 6. 实施要点 原文链接…...
path环境变量满了如何处理,分割 PATH 到 Path1 和 Path2
要正确设置 Path1 的值,你需要将现有的 PATH 环境变量 中的部分路径复制到 Path1 和 Path2 中。以下是详细步骤: 步骤 1:获取当前 PATH 的值 打开环境变量窗口: 按 Win R,输入 sysdm.cpl,点击 确定。在 系…...
实战项目1(02)
目录 任务场景一 【sw1和sw2的配置如下】 任务场景二 【sw3的配置】 【sw4-6的配置】 任务场景一 某公司有生产、销售、研发、人事、财务等多个部门,这些部门分别连接在两台交换机(SW1和SW2)上,现要求给每个部门划分相应的V…...
m1 安装 Elasticsearch、ik、kibana
一、下载安装ES 1、下载地址 ES|download 2、安装 将下载的安装包解压到 要安装的文件目录 关闭 ES 的安全模式 本地文本编辑器打开elasticsearch.yml配置文件,将红箭头指的地方 改为 false3、启动 ES 启动命令 进入 ES 的安装目录,进入bin文件目…...
游戏引擎学习第273天:动画预览
回顾并为一天的内容定下基调 。目前我们正在编写角色的移动代码,实际上,我们已经在昨天完成了一个简单的角色跳跃的例子。所以今天的重点是,开始更广泛地讨论动画,因为我们希望对现有的动画进行调整,让它看起来更加令…...
JVM中的安全点是什么,作用又是什么?
JVM中的安全点(Safepoint) 是Java虚拟机设计中的一个关键机制,主要用于协调所有线程的执行状态,以便进行全局操作(如垃圾回收、代码反优化等)。它的核心目标是确保在需要暂停所有线程时,每个线程…...
游戏引擎学习第271天:生成可行走的点
回顾并为今天的内容设定背景 我们昨天开始编写一些游戏逻辑相关的内容,虽然这部分不是最喜欢的领域,更偏好底层引擎开发,但如果要独立完成一款游戏,游戏逻辑也必须亲自处理。所以我们继续完善这部分内容。事实上,接下…...
FlySecAgent:——MCP全自动AI Agent的实战利器
最近,出于对人工智能在网络安全领域应用潜力的浓厚兴趣,我利用闲暇时间进行了深入研究,并成功开发了一款小型轻量化的AI Agent安全客户端FlySecAgent。 什么是 FlySecAgent? 这是一个基于大语言模型和MCP(Model-Contr…...
DAMA车轮图
DAMA车轮图是国际数据管理协会(DAMA International)提出的数据管理知识体系(DMBOK)的图形化表示,它以车轮(同心圆)的形式展示了数据管理的核心领域及其相互关系。以下是基于用户提供的关键词对D…...
使用vue3-seamless-scroll实现列表自动滚动播放
vue3-seamless-scroll组件支持上下左右无缝滚动,单步滚动,并且支持复杂图标的无缝滚动。 核心特性 多方向无缝滚动 支持上下、左右四个方向的自动滚动,通过 direction 参数控制(默认 up),适用于新闻轮播、…...