objc_msgSend探索

objc_msgSend探索

在上一篇cache_t探索文章中,我们探索了cache_t作用,是去进行方法缓存,其目的就是当方法再次调用时能更快的进行响应.,接下来我们探究一下如何从cache_t中读取方法.我们在源码中 obcj-cache.m中会找到这样一个注释

* Cache readers (PC-checked by collecting_in_critical()) * objc_msgSend* * cache_getImp

这说明cache读取时通过objc_msgSendcache_getImp这两个函数进行读取.

我们都知道OC是一门动态的语言,动态语言就是指我们的程序运行的过程中可以对于我们的类、对象、属性、方法、变量进行修改,可以改变他们的数据结构,可以添加或者删除一些函数,可以去改变一些变量的值等等的操作.我们经常提到的runtime就是可以实现语言动态的一组api,runtime所有都是围绕两个核心
1:类的各方面的动态配置(使用runtime的api动态的修改我们的类或者对象的信息,为类添加属性方法,修改成员变量的值)
2:消息传递(消息的发送和消息的转发,消息的发送就是runtime通过sel找imp,然后实现对应的方法)

我们的消息发送在编译的时候,编译器就会把这个方法转换成为objc_msgSend这么一个函数,为了验证这件事情,我们在main中加入熟悉的person

int main(int argc, char * argv[]) {    NSString * appDelegateClassName;    @autoreleasepool {        WTPerson *p = [WTPerson alloc];        [p say:@"hello"];        [p run];    }    return UIApplicationMain(argc, argv, nil, appDelegateClassName);}

然后在终端中输入 xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc main.m,我们获取到.cpp文件,在.cpp文件中我们可以找到相应的编译器编译后的代码

int main(int argc, char * argv[]) {    NSString * appDelegateClassName;    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;         WTPerson *p = ((WTPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("WTPerson"), sel_registerName("alloc"));        ((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)p, sel_registerName("say:"), (NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_main_bd8849_mi_0);        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));    }    return UIApplicationMain(argc, argv, __null, appDelegateClassName);}

我们发现,alloc这种类方法和sayrun这种对象方法都被包装成了((void (*)(id, SEL))(void *)objc_msgSend)(receiver, sel)格式.
当我们的方法都没有参数,可转换成objc_msgSend后都会带有两个默认的参数消息的接收者receiver、消息的方法名sel.类方法的接收者是类对象,他就会通过类对象的isa指针找到我们的元类,从元类中找对应的方法;实例对象方法的接收者是实例对象,他就会通过实例对象的isa指针找到我们的类对象,从类对象中找对应的方法.
当我们的方法有参数时,我们的参数会放在objc_msgSend的第三个参数及以后中,第一个和第二个参数是不变的,也就是说,objc_msgSend的参数是两个默认的参数加上方法本身的参数.

下面我们试一下直接调用objc_msgSend函数发现同样调用类say方法,也就是说我们可以直接手动去调用objc_msgSend函数.
需要注意:手动调用需要导入头文件#import <objc/message.h>

在.cpp文件中,我们可以看到objc_msgSend其实是有很多种类的

__OBJC_RW_DLLIMPORT void objc_msgSend(void); //__OBJC_RW_DLLIMPORT void objc_msgSendSuper(void); //给父类发消息__OBJC_RW_DLLIMPORT void objc_msgSend_stret(void); //返回值是结构体__OBJC_RW_DLLIMPORT void objc_msgSendSuper_stret(void); //__OBJC_RW_DLLIMPORT void objc_msgSend_fpret(void); //返回值是浮点类型

接下来我们详细了解一下objc_msgSendSuperobjc_msgSend区别

objc_msgSendSuper

我们创建一个WTStudent类继承WTPerson,重写其init方法

- (instancetype)init {    if (self = [super init]) {        NSLog(@"%@", [self class]);        NSLog(@"%@", [super class]);    }    return self;}

面试的摧残让我们都知道两个打印的都是WTStudent,但是为什么呢?让我们来研究一下selfsuper这两个关键字有什么区别:
我们用clang命令编译WTStudent.m,得到如下编译代码

static instancetype _I_WTStudent_init(WTStudent * self, SEL _cmd) {    if (self = ((WTStudent *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("WTStudent"))}, sel_registerName("init"))) {        NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_WTStudent_8d6cff_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));        NSLog((NSString *)&__NSConstantStringImpl__var_folders_x6_75ftxbbd4t97nl_2t2sh7kww0000gn_T_WTStudent_8d6cff_mi_1, ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("WTStudent"))}, sel_registerName("class")));    }    return self;}

我们发现self编译后是objc_msgSend,super编译后是objc_msgSendSuper,objc_msgSendSuper的第一个参数不再是self,而是__rw_objc_super的结构体指针,包括消息的接收者:self和开始搜索方法实现的超类super:(id)class_getSuperclass(objc_getClass("WTStudent")).
我们看到objc_msgSendSuper的消息的接收者仍然是self,所以[super class]打印出来的一样是self这个对象所指的类,也就是WTStudent.

通过这里,我们可以得出结论:objc_msgSendobjc_msgSendSuper唯一的区别只有他们查找方法的出发点不同,objc_msgSendSuper是从方法实现的超类super也就是类的父类开始查找,objc_msgSend是从本身开始查找.我们可以直接实现super关键字

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 */};

我们可以看到,我们成功的调用了superrun方法,如果super_class改成WTStudent,调用的方法是study方法的话,不会因为super没有study方法崩溃,而会因为一直调用[WTStudent study]死循环崩溃.如果super_class改成改成NSObject则会因为NSObject没有run方法崩溃.

objc_msgSend

接下来我们来进行探索一下objc_msgSend的流程,在源码我们找到了各种系统的实现,最后我们锁定到了objc-msg-arm64.s文件中,以.s结尾的都是汇编写的文件,而arm64是我们的真机架构,objc_msgSend为什么使用汇编实现,因为汇编比C语言更快,可以免去局部变量的copy操作,参数直接被存放寄存器中,可以直接使用

//进入objc_msgSend流程ENTRY _objc_msgSend//流程开始,无需frameUNWIND _objc_msgSend, NoFrame//判断p0(消息接受者)是否存在,不存在则重新开始执行objc_msgSendcmp p0, #0 // nil check and tagged pointer check//如果支持小对象类型。返回小对象或空#if SUPPORT_TAGGED_POINTERS//b是进行跳转,b.le是小于判断,也就是小于的时候LNilOrTaggedb.le LNilOrTagged //  (MSB tagged pointer looks negative)#else//等于,如果不支持小对象,就LReturnZerob.eq LReturnZero#endif//通过p13取isaldr p13, [x0] // p13 = isa//通过isa取class并保存到p16寄存器中GetClassFromIsa_p16 p13, 1, x0 // p16 = class//LGetIsaDone是一个入口LGetIsaDone:// calls imp or objc_msgSend_uncached//进入到缓存查找或者没有缓存查找方法的流程CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached#if SUPPORT_TAGGED_POINTERSLNilOrTagged:// nil check判空处理,直接退出b.eq LReturnZero // nil checkGetTaggedClassb LGetIsaDone// SUPPORT_TAGGED_POINTERS#endifLReturnZero:// x0 is already zeromov x1, #0movi d0, #0movi d1, #0movi d2, #0movi d3, #0retEND_ENTRY _objc_msgSend

这一部分代码,实际上就是先检测我们调用_objc_msgSend的调用方,是否为空,如果为空,则直接去进行方法的调用,直接返回。否则就将调用方的isa指针存到p13寄存器里面去,其中比较重要的一个方法是GetClassFromIsa_p16他是通过isa来获取class,下面我们来重点看下他

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */#if SUPPORT_INDEXED_ISA// Indexed isamovp16, \src// optimistically set dst = srctbzp16, #ISA_INDEX_IS_NPI_BIT, 1f// done if not non-pointer isa// isa in p16 is indexedadrpx10, _objc_indexed_classes@PAGEaddx10, x10, _objc_indexed_classes@PAGEOFFubfxp16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract indexldrp16, [x10, p16, UXTP #PTRSHIFT]// load class from array1:// **因为是真机环境,所以走这里**#elif __LP64__//如果needs_auth参数等于0,暂时不用管他的含义,在objc_msgSend中,这里传入的是1.if \needs_auth == 0 // _cache_getImp takes an authed class already        // ** 将src,也就是我们的入参isa指针,存放到寄存器P16中**movp16, \src.else// 64-bit packed isa        //**调用ExtractISA**ExtractISA p16, \src, \auth_address.endif#else// 32-bit raw isamovp16, \src#endif.endmacro//**这个方法有两个实现,一个是针对A12芯片以上的手机,我们这里看A12以下的**.macro ExtractISA        //**实际上就是将传入的参数,对象的isa与isa_mask按位与,也就是得到Class**and    $0, $1, #ISA_MASK.endmacro

这一段内容,简单来说,就是将对象的isa传入GetClassFromIsa_p16然后,这个方法针对不同的isa类型做了不同的处理,最终得到了类对象Class。在下面就是CacheLookup方法了,我们继续往下看

//在cache中通过sel查找imp的核心流程.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant//从x16中取出class移到x15中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表示p16寄存器存储的值,当前是Class//#数值表示一个值,这里的CACHE经过全局搜索发现是2倍的指针地址,也就是16个字节//#define CACHE (2 * __SIZEOF_POINTER__)//经计算,p10就是cacheldr p10, [x16, #CACHE] // p10 = mask|bucketslsr p11, p10, #48 // p11 = maskand p10, p10, #0xffffffffffff // p10 = bucketsand w12, w1, w11 // x12 = _cmd & mask//真机64位看这个#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16//CACHE 16字节,也就是通过isa内存平移获取cache,然后cache的首地址就是 (bucket_t *)ldr p11, [x16, #CACHE] // p11 = mask|buckets#if CONFIG_USE_PREOPT_CACHES//获取buckets#if __has_feature(ptrauth_calls)tbnz p11, #0, LLookupPreopt\Functionand p10, p11, #0x0000ffffffffffff // p10 = buckets#else//and表示与运算,将与上mask后的buckets值保存到p10寄存器and p10, p11, #0x0000fffffffffffe // p10 = buckets//p11与#0比较,如果p11不存在,就走Function,如果存在走LLookupPreopttbnz p11, #0, LLookupPreopt\Function#endif//按位右移7个单位,存到p12里面,p0是对象,p1是_cmdeor p12, p1, p1, LSR #7and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask#elseand p10, p11, #0x0000ffffffffffff // p10 = buckets//LSR表示逻辑向右偏移//p11, LSR #48表示cache偏移48位,拿到前16位,也就是得到mask//这个是哈希算法,p12存储的就是搜索下标(哈希地址)//整句表示_cmd & mask并保存到p12and p12, p1, p11, LSR #48 // x12 = _cmd & mask#endif // CONFIG_USE_PREOPT_CACHES

这一段主要是获取到我们缓存也就是之前讲过的cache_t的首地址,并且获取到bucketsmask。接着继续往下走

//去除掩码后bucket的内存平移//PTRSHIFT经全局搜索发现是3//LSL #(1+PTRSHIFT)表示逻辑左移4位,也就是*16//通过bucket的首地址进行左平移下标的16倍数并与p12相与得到bucket,并存入到p13中add p13, p10, p12, LSL #(1+PTRSHIFT)// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))// do {//ldp表示出栈,取出bucket中的imp和sel分别存放到p17和p91: ldp p17, p9, [x13], #-BUCKET_SIZE //     {imp, sel} = *bucket--//cmp表示比较,对比p9和p1,如果相同就找到了对应的方法,返回对应imp,走CacheHitcmp p9, p1 //     if (sel != _cmd) {//b.ne表示如果不相同则跳转到2fb.ne 3f //         scan more//     } else {2: CacheHit \Mode // hit:    call or return imp//     }//向前查找下一个bucket,一直循环直到找到对应的方法,循环完都没有找到就调用_objc_msgSend_uncached3: cbz p9, \MissLabelDynamic //     if (sel == 0) goto Miss;//通过p13和p10来判断是否是第一个bucketcmp p13, p10 // } while (bucket >= buckets)b.hs 1b#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 4b

这一段是查找缓存中最核心的代码,通过循环来查找方法在缓存中的位置,如果找到了则调用CacheHit \Mode,否则调用MissLabelDynamic。接下来我们看看CacheHit的实现

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa.macro CacheHit//**objs_msgSend中$0==NORMAL**.if $0 == NORMAL        //调用找到的方法TailCallCachedImp x17, x10, x1, x16// authenticate and call imp.elseif $0 == GETIMPmovp0, p17cbzp0, 9f// don't ptrauth a nil impAuthAndResignAsIMP x0, x10, x1, x16// authenticate imp and re-sign as IMP9: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 IMPcmpx16, x15cincx16, x16, ne// x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)ret// return imp via x17.else.abort oops.endif.endmacro//**依旧有两个实现,我们看A12以下的**.macro TailCallCachedImp// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa        //**$0(imp) ^ $3(isa)**        //**实际上就是一个解码的过程**eor$0, $0, $3        //**跳转到$0(imp)的地址,就是调用IMP**br$0.endmacro

比较简单的实现,就是去调用查找到的SEL对应的IMP,实现方法的调用。到此,objc_msgSend调用方法,在缓存中查找方法的流程就全部结束了,这个流程我们也称之为方法的快速查找流程,和我们之前的找到cache_t中方法的流程是一样的,只是我们之前用的是lldb进行的查找,objc_msgSend直接使用编译器方法进行的查找

_lookUpImpOrForward

快速查找未找到会跳转__objc_msgSend_uncached,在其中会跳转到MethodTableLookup,在MethodTableLookup中执行了_lookUpImpOrForward,这里进行慢速查找流程

STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSavesMethodTableLookupTailCallFunctionPointer x17END_ENTRY __objc_msgSend_uncachedSTATIC_ENTRY __objc_msgLookup_uncachedUNWIND __objc_msgLookup_uncached, FrameWithNoSavesMethodTableLookupretEND_ENTRY __objc_msgLookup_uncachedSTATIC_ENTRY _cache_getImpGetClassFromIsa_p16 p0, 0CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant.macro MethodTableLookupSAVE_REGS MSGSEND// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)// receiver and selector already in x0 and x1mov x2, x16mov x3, #3bl _lookUpImpOrForward// IMP in x0mov x17, x0RESTORE_REGS MSGSEND

在源码objc-runtime-new.m中,我们找到了他的实现

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior){    // 定义消息转发的imp    const IMP forward_imp = (IMP)_objc_msgForward_impcache;    IMP imp = nil;    Class curClass;    runtimeLock.assertUnlocked();    // 判断类是否初始化    if (slowpath(!cls->isInitialized())) {        behavior |= LOOKUP_NOCACHE;    }    runtimeLock.lock();    // 是否是已知的类,是否已经被加载过    checkIsKnownClass(cls);    // 确定父类继承链的关系    cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);    runtimeLock.assertLocked();    curClass = cls;    // 死循环,只有达到条件才会退出循环,    for (unsigned attempts = unreasonableClassCount();;) {        if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {            // 再一次从cache中找imp            // 目的: 防止多线程操作时,刚好调用函数,此时缓存进来了#if CONFIG_USE_PREOPT_CACHES            imp = cache_getImp(curClass, sel);            if (imp) goto done_unlock;            curClass = curClass->cache.preoptFallbackClass();#endif        } else {            method_t *meth = getMethodNoSuper_nolock(curClass, sel);            if (meth) {                imp = meth->imp(false);                goto done;            }            if (slowpath((curClass = curClass->getSuperclass()) == 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)) {            break;        }        if (fastpath(imp)) {            goto done;        }    }    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_CACHES        while (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;}static method_t *getMethodNoSuper_nolock(Class cls, SEL sel){    runtimeLock.assertLocked();    auto const methods = cls->data()->methods();    for (auto mlists = methods.beginLists(),              end = methods.endLists();         mlists != end;         ++mlists)    {        method_t *m = search_method_list_inline(*mlists, sel);        if (m) return m;    }    return nil;}findMethodInSortedMethodList(SEL key, const method_list_t *list){    if (list->isSmallList()) {        if (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS && objc::inSharedCache((uintptr_t)list)) {            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSEL(); });        } else {            return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.getSmallNameAsSELRef(); });        }    } else {        return findMethodInSortedMethodList(key, list, [](method_t &m) { return m.big().name; });    }}

其实现就是从先判断是否缓存过,缓存过再次查找cache,未缓存就从当前类的方法列表中查找方法的实现,最后根据findMethodInSortedMethodList这个二分查找流程查找方法实现.

static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName){    ASSERT(list);    auto first = list->begin();    auto base = first; // 0    decltype(first) probe;    uintptr_t keyValue = (uintptr_t)key;    uint32_t count;    // count 16    // 小了的情况    // 第一次 probe = 0 + count >> 1 = 8 base = 9 count-- = 15     // 第二次 probe = 12 base = 13 count >>= 1 = 7 count >> 1 = 3 count-- = 6     // 第三次 probe = 14 count >>=1 = 3 count >> 1 = 1        // 大了的情况    // 第一次 probe = 0 + count >> 1 = 8 base = 0 count = 16     // 第二次 probe = 4 base = 0 count >>= 1 = 8 count >> 1 = 4     // 第三次 probe = 2 count >>=1 = 2 count >> 1 = 0    for (count = list->count; count != 0; count >>= 1) {        probe = base + (count >> 1);        uintptr_t probeValue = (uintptr_t)getName(probe);        if (keyValue == probeValue) {            while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) { // 查找第一次出现的地方,为了调用分类的方法                probe--;            }            return &*probe;        }        if (keyValue > probeValue) {            base = probe + 1;            count--;        }    }    return nil;}

当我们在慢速查找找到方法时,会调用log_and_fill_cache,其内部调用cls->cache.insert(sel, imp, receiver);将方法存入cache中,下次调用会进行快速查找流程,子类调用父类方法,缓存仍然会缓存到子类,因为在for循环中,如果子类没找到时curClass = curClass->getSuperclass()会将curClass更新成父类,log_and_fill_cache的写入与curClass无关,而是写入cls的cache,cls一直是不变的.

以上是找到了imp,当未找到imp,则会将最初定义消息转发的imp赋值给当前imp,进行消息转发流程

总结

objc_msgSend其具体实现如下:

1.receiver是否存在
2.reciver - isa - class
3.class - 内存偏移 - cache
4.cache - buckets - 对应sel
5.buckets 有对应的sel - cacheHit - 调用imp - 方法的快速查找流程
6.buckets 没有对应的sel - __objc_msgSend_uncached - 方法的慢速查找流程
7._lookUpImpOrForward - 先找当前类的methodlist - 再找父类的cache - 父类的methodlist - 父类为nil - forward的imp

免责声明:本网信息来自于互联网,目的在于传递更多信息,并不代表本网赞同其观点。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,并请自行核实相关内容。本站不承担此类作品侵权行为的直接责任及连带责任。如若本网有任何内容侵犯您的权益,请及时联系我们,本站将会在24小时内处理完毕。
相关文章
返回顶部