方法的本質(zhì)

探索方法的本質(zhì)

一個(gè)最基本的方法調(diào)用代碼

void run(){
    NSLog(@"%s",__func__);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [[LGPerson alloc] init];;
        [person sayNB];
    }
    return 0;
}

方法的調(diào)用底層到底是個(gè)什么東西呢
我們可以利用clang的一些命令 clang -rewrite-objc main.m -o main.cpp,將main文件編譯成c之后的代碼
cd /Users/caoxiang/Desktop/dealloc/dealloc

  • xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m
    擴(kuò)展一下

  • 指定真機(jī)

xcrun -sdk iphoneos clang -rewrite-objc ViewController.m

  • 指定模擬器

xcrun -sdk iphonesimulator clang -rewrite-objc ViewController.m

  • 指定SDK版本

xcrun -sdk iphonesimulator10.3 clang -rewrite-objc ViewController.m

clang之后的代碼

其中有個(gè)問(wèn)題
run()方法直接調(diào)用,而我們的oc方法被編譯成一個(gè)objc_msgSend函數(shù)(runtime里的消息發(fā)送機(jī)制)
run函數(shù)在編譯器就確定了函數(shù)的調(diào)用與實(shí)現(xiàn),
因此,oc的方法的本質(zhì)就是objc_msgSend(或者objc_msgSendSuper等函數(shù))的調(diào)用
objc_msgSend兩個(gè)參數(shù),第一個(gè)參數(shù)是對(duì)象是哪個(gè)對(duì)象的操作,第二個(gè)參數(shù)就是sel也就是方法,通過(guò)sel找到imp的實(shí)現(xiàn),完成方法的調(diào)用.就叫做消息發(fā)送機(jī)制.

objc_msgSend流程

方法的查找流程分為兩種

  • 快速查找:利用匯編直接從緩存中找
  • 慢速查找:快速查找沒(méi)有命中,從方法表中查找
    現(xiàn)在方法調(diào)用處打一個(gè)斷點(diǎn)


    打斷點(diǎn)

然后debug->debug WorkFlow ->Always Show Disassembly

查看匯編

然后進(jìn)入?yún)R編
匯編

然后按住cotrol + stepIn
源碼位置

可以看到objc_msgSendlibobjc

打開(kāi)源碼搜索objc_msgSend,我們直接看匯編,找到.s文件,現(xiàn)在架構(gòu)大部分都是arm64,所以我們直接看objc-msg-arm64.s文件
看匯編重要的一點(diǎn)事看ENTRY表示入口,如下圖

找到文件

image.png
// person - isa - 類
    ldr p13, [x0]       // p13 = isa
GetClassFromIsa_p16 p13     // p16 = class

p13為isa,因?yàn)閤0為首地址,[]就是首地址的值,首地址就是isa指針
GetClassFromIsa_p16是一個(gè)宏,下邊是實(shí)現(xiàn)

.macro GetClassFromIsa_p16 /* src */

#if SUPPORT_INDEXED_ISA
    // Indexed isa
    mov p16, $0         // optimistically set dst = src
    tbz p16, #ISA_INDEX_IS_NPI_BIT, 1f  // done if not non-pointer isa
    // isa in p16 is indexed
    adrp    x10, _objc_indexed_classes@PAGE
    add x10, x10, _objc_indexed_classes@PAGEOFF
    ubfx    p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
    ldr p16, [x10, p16, UXTP #PTRSHIFT] // load class from array
1:

#elif __LP64__
    // 64-bit packed isa
    and p16, $0, #ISA_MASK

#else
    // 32-bit raw isa
    mov p16, $0

#endif

.endmacro

SUPPORT_INDEXED_ISAindexIsa一般不常用,watchos開(kāi)發(fā)是indexIsa
and p16, $0, #ISA_MASK這句才是重點(diǎn),$0是傳進(jìn)來(lái)的參數(shù),也就是p13 isa 拿$0與isa_mask進(jìn)行&運(yùn)算得到類,在前兩篇文章中介紹了對(duì)象和類之間是怎么聯(lián)系起來(lái)的,所以我們的p16是一個(gè)類
拿到isa之后進(jìn)行下邊操作

LGetIsaDone:
    CacheLookup NORMAL  

CacheLookup是一個(gè)宏定義,下邊是實(shí)現(xiàn)代碼

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro
1:   cmp    p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp

b.ne 2f如果bucket的sel != _cmd找2,否則直接命中返回$0
快速查找流程結(jié)束,如果沒(méi)有命中就進(jìn)入慢速查找流程

2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f              //進(jìn)行3主要是為了保存一份方便下次查找
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop

3:  // wrap: p12 = first bucket, w11 = mask   
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp p17, p9, [x12]      // {imp, sel} = *bucket

CheckMiss也是一個(gè)宏

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

我們這里傳進(jìn)來(lái)的是normal,那么找__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 search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

來(lái)看一下MethodTableLookup是什么東西
他也是一個(gè)宏定義

.macro MethodTableLookup
...
bl  __class_lookupMethodAndLoadCache3
...

這里只展示了一個(gè)最重要的一行代碼,bl跳轉(zhuǎn)到__class_lookupMethodAndLoadCache3
我們根據(jù)以往經(jīng)驗(yàn),匯編會(huì)自動(dòng)在前邊加一個(gè)_,那么我們?nèi)サ粢粋€(gè)下劃線全局搜_class_lookupMethodAndLoadCache3

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

來(lái)一個(gè)demo


@interface LGPerson : NSObject

- (void)sayNB;
+ (void)sayHappay;

@end
@implementation LGPerson

- (void)sayNB{
    NSLog(@"%s",__func__);
}

+ (void)sayHappay{
    NSLog(@"%s",__func__);
}
@end
@interface LGStudent : LGPerson
- (void)sayHello;
+ (void)sayObjc;
@end
@implementation LGStudent
- (void)sayHello{
    NSLog(@"%s",__func__);
}

+ (void)sayObjc{
    NSLog(@"%s",__func__);
}
@end
@interface NSObject (LG)
- (void)sayMaster;
+ (void)sayEasy;
@end
@implementation NSObject (LG)

- (void)sayMaster{
    NSLog(@"%s",__func__);
}
+ (void)sayEasy{
    NSLog(@"%s",__func__);
}

@end

LGStuednt繼承與LGPerson,LGPerson繼承與NSObject
然后調(diào)用

LGStudent *student = [[LGStudent alloc] init];
        // 對(duì)象方法
        // 自己有 - 返回自己
        [student sayHello];
        // 自己沒(méi)有 - 老爸 -
        // [person sayNB]; // CACHE
        
        [student sayNB];
        // 自己沒(méi)有 - 老爸沒(méi)有 - NSObject
        [student sayMaster];
        // 自己沒(méi)有 - 老爸沒(méi)有 - NSObject 沒(méi)有
        // unrecognized selector sent to instance 0x103000450
       [student performSelector:@selector(saySomething)];
  • 自己有的時(shí)候返回自己的方法
  • 自己沒(méi)有的時(shí)候找父類
  • 老爸沒(méi)有時(shí)找NSObject
  • 都沒(méi)有,拋出異常
    實(shí)例方法的查找會(huì)根據(jù)繼承連去一層一層的找

類方法的調(diào)用

// 類方法
        // 自己有 - 返回自己
        [LGStudent sayObjc];
        // 自己沒(méi)有 - 老爸 -
        [LGStudent sayHappay];
        // 自己沒(méi)有 - 老爸沒(méi)有 - NSObject
        [LGStudent sayEasy];
        // 自己沒(méi)有 - 老爸沒(méi)有 - NSObject 沒(méi)有

這里會(huì)有一個(gè)問(wèn)題如果我這樣調(diào)用會(huì)不會(huì)崩潰

 [LGStudent performSelector:@selector(sayMaster)];

會(huì)打印

-[NSObject(LG) sayMaster]

發(fā)現(xiàn)不會(huì)蹦,因?yàn)轭惙椒ù嬖谠惱镞?我們調(diào)用類方法

  • 首先去元類里邊找,
  • 元類里邊沒(méi)有找到就招父元類,
  • 沒(méi)有找到繼續(xù)根據(jù)繼承鏈找,
  • 最后找到根元類(NSObject的元類),根元類又繼承與NSObject

isa流程圖.png

最后找到sayMaster,所以不會(huì)崩潰
看下源碼來(lái)驗(yàn)證下到底是不是這個(gè)流程
我們上邊看匯編,如果緩存沒(méi)有命中就來(lái)到慢速查找流程,也就是_class_lookupMethodAndLoadCache3方法

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
  • 當(dāng)調(diào)用實(shí)例方法的時(shí)候,cls就是當(dāng)前類,sel就是調(diào)用的方法,obj就是當(dāng)前實(shí)例
  • 當(dāng)調(diào)用類方法的時(shí)候,cls則代表的是當(dāng)前類的元類(MetaClass),因?yàn)殪o態(tài)方法是存放在元類里邊的
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // 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();
    checkIsKnownClass(cls);

    if (!cls->isRealized()) {
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
 done:
    runtimeLock.unlock();
    return imp;
}

因?yàn)槭锹俨檎?這里傳進(jìn)來(lái)的cache為NO,所以下邊這幾行代碼不看,

if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

這里加了一把鎖,是防止多線程情況下產(chǎn)生錯(cuò)亂,比如同時(shí)調(diào)用a方法和b方法,那么這里在調(diào)用a的時(shí)候返回一個(gè)b的imp,會(huì)產(chǎn)生問(wèn)題
checkIsKnownClass,是判斷類是否合法

 if (!cls->isRealized()) {
        realizeClass(cls);
    }

這行代碼是拿到父類元類,以及類的data里邊的rw里的ro里的methodlist等等一系列信息,是為方法查找做準(zhǔn)備條件,這里不做重點(diǎn)研究.

// Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

先在本類里邊找,getMethodNoSuper_nolock

getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

拿到類的data里的methodList進(jìn)行循環(huán)遍歷,
search_method_list進(jìn)行二分法查找,查找速度更快,下邊為查找算法,這里不做過(guò)多研究

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;
    uint32_t count;
    
    for (count = list->count; count != 0; count >>= 1) {
        probe = base + (count >> 1);
        
        uintptr_t probeValue = (uintptr_t)probe->name;
        
        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)probe[-1].name) {
                probe--;
            }
            return (method_t *)probe;
        }
        
        if (keyValue > probeValue) {
            base = probe + 1;
            count--;
        }
    }
    
    return nil;
}

通過(guò)一些列算法找到方法,然后看

if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }

如果找到了method那么下次還慢速查找么,蘋果爸爸肯定不會(huì)這樣蠢的,如果找到就調(diào)用log_and_fill_cache進(jìn)行緩存,下次直接利用匯編快速查找,緩存的操作跟我們研究cache_t結(jié)構(gòu)時(shí)是一模模一樣樣的.
goto done 然后查找結(jié)束

如果找不到呢,那么就去找父類Try superclass caches and method lists.根據(jù)代碼注釋我們也可以看出來(lái)接下來(lái)要找父類了.
如果找父類,就不開(kāi)始跳匯編了,因?yàn)閯傞_(kāi)始父類元類等條件我們已經(jīng)準(zhǔn)備好了,那么我們現(xiàn)在直接找父類的cache.

// Superclass cache.
            imp = cache_getImp(curClass, sel);

如果找到imp,直接goto done直接返回,如果找不到,然后找到父類的方法列表進(jìn)行查找(流程同在本類中查找流程)
如果都找不到方法呢,直接報(bào)錯(cuò)我們很熟悉的一個(gè)錯(cuò)誤+[LGStudent sayLove]: unrecognized selector sent to class 0x1000012e8.
imp = (IMP)_objc_msgForward_impcache;看這句代碼.發(fā)現(xiàn)點(diǎn)不進(jìn)去.那么按照國(guó)際慣例,全局搜索
然后選擇objc_msg_arm64.s沒(méi)錯(cuò)又是惡心人的匯編,為什么找他呢,因?yàn)槠渌胤蕉际钦{(diào)用,沒(méi)有實(shí)現(xiàn),按照經(jīng)驗(yàn)STATIC_ENTRY __objc_msgForward_impcache ,是不是很熟悉

STATIC_ENTRY __objc_msgForward_impcache

    // No stret specialization.
    b   __objc_msgForward

    END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward

    adrp    x17, __objc_forward_handler@PAGE
    ldr p17, [x17, __objc_forward_handler@PAGEOFF]
    TailCallFunctionPointer x17
    
    END_ENTRY __objc_msgForward

__objc_forward_handler是個(gè)啥玩意兒啊,來(lái)搜一下(經(jīng)驗(yàn)告訴我搜不到的時(shí)候去掉一個(gè)下劃線),

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

發(fā)現(xiàn)他是個(gè)這么個(gè)玩意兒objc_defaultForwardHandler,點(diǎn)進(jìn)去看一下我的天啊,出現(xiàn)了好熟悉的代碼

objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

這不就是經(jīng)常出現(xiàn)的報(bào)錯(cuò)原因么
但是找不到方法直接報(bào)錯(cuò),體驗(yàn)很不好,有沒(méi)有什么方法補(bǔ)救一下呢?
蘋果爸爸告訴我們,可以的,在給你一次機(jī)會(huì).可以利用消息轉(zhuǎn)發(fā)機(jī)制
http://www.itdecent.cn/p/03383d2d395d

補(bǔ)充:為什么要設(shè)計(jì)meteClass

1、首先會(huì)再一次的從類中尋找需要調(diào)用方法的緩存,如果能命中緩存直接返回該方法的實(shí)現(xiàn),如果不能命中則繼續(xù)往下走。
2、從類的方法列表中尋找該方法,如果能從列表中找到方法則對(duì)方法進(jìn)行緩存并返回該方法的實(shí)現(xiàn),如果找不到該方法則繼續(xù)往下走。
3、從父類的緩存尋找該方法,如果父類緩存能命中則將方法緩存至當(dāng)前調(diào)用方法的類中(注意這里不是存進(jìn)父類),如果緩存未命中則遍歷父類的方法列表,之后操作如同第2步,未能命中則繼續(xù)走第3步直到尋找到基類。
4、如果到基類依然沒(méi)有找到該方法則觸發(fā)動(dòng)態(tài)方法解析流程。
5、還是找不到就觸發(fā)消息轉(zhuǎn)發(fā)流程
走到這里一套方法發(fā)送的流程就都走完了,那這跟元類的存在有啥關(guān)系?我們都知道類方法是存儲(chǔ)在元類中的,那么可不可以把元類干掉,在類中把實(shí)例方法和類方法存在兩個(gè)不同的數(shù)組中?
答:行是肯定可行的,但是在lookUpImpOrForward執(zhí)行的時(shí)候就得標(biāo)注上傳入的cls到底是實(shí)例對(duì)象還是類對(duì)象,這也就意味著在查找方法的緩存時(shí)同樣也需要判斷cls到底是個(gè)啥。
倘若該類存在同名的類方法和實(shí)例方法是該調(diào)用哪個(gè)方法呢?這也就意味著還得給傳入的方法帶上是類方法還是實(shí)例方法的標(biāo)識(shí),SEL并沒(méi)有帶上當(dāng)前方法的類型(實(shí)例方法還是類方法),參數(shù)又多加一個(gè),而我們現(xiàn)在的objc_msgSend()只接收了(id self, SEL _cmd, ...)這三種參數(shù),第一個(gè)self就是消息的接收者,第二個(gè)就是方法,后續(xù)的...就是各式各樣的參數(shù)。
通過(guò)元類就可以巧妙的解決上述的問(wèn)題,讓各類各司其職,實(shí)例對(duì)象就干存儲(chǔ)屬性值的事,類對(duì)象存儲(chǔ)實(shí)例方法列表,元類對(duì)象存儲(chǔ)類方法列表,完美的符合6大設(shè)計(jì)原則中的單一職責(zé),而且忽略了對(duì)對(duì)象類型的判斷和方法類型的判斷可以大大的提升消息發(fā)送的效率,并且在不同種類的方法走的都是同一套流程,在之后的維護(hù)上也大大節(jié)約了成本。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容