objc對象調(diào)用方法詳細過程

在調(diào)用對象的方法之前我們首先弄清楚對象的方法存在哪里.

實例方法存在類對象中
類方法存在元類對象中(元類其實也是一個類對象)

我們先看下類對象的結構布局

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

我們看到一個類對象就是一個結構體繼承與objc_object結構體,我們以前的文章中分析過objc_object結構體,這里簡單的說一下objc_object, objc_object里面有一個isa是一個共用體.里面有一個結構體使用位域來存儲更多的信息.

superclass是指向父類的指針
cache方法的緩存列表
bits& FAST_DATA_MASK的到class_rw_t結構體

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

在class_rw_t中 methods就是方法的緩存類表
我們以前討論分類的的時候也分析過method_array_t methods;結構

methods是一個二維數(shù)組, 數(shù)組里面的元素是分類的的方法類表
[
[分類方法1a方法,分類方法1b方法],
[分類方法2a方法,分類方法2b方法]
]

我們給一個對象發(fā)消息的時候就會找到methods這個數(shù)組里面然后緩存到cache中,就算給一個對象調(diào)用父類的方法也會緩存到cache中的_buckets, 當往_buckets緩存bucket_t的時候_buckets會檢查是否需要擴容,需要擴容就會清空所有元素然后,在緩存進來.(這個時候以前的緩存就沒有了)

struct cache_t {
    struct bucket_t *_buckets;
    mask_t _mask; //散列表的長度
    mask_t _occupied; //已經(jīng)緩存方法的個數(shù)
}

struct bucket_t {
private:
    cache_key_t _key;
    IMP _imp;
}

我看到cache結構體中_buckets是一個數(shù)組里面是bucket_t結構體_key就是方法名字_buckets就相當于一個離散列表(類似字典)

image.png
image.png
image.png

源碼分析調(diào)用方法過程

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone
    
LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

    END_ENTRY _objc_msgSend

x0, 寄存器里面存的是receiver消息接受者
b.le LNilOrTagged //如果receiver為nil跳轉到LNilOrTagged
b.eq LReturnZero // nil check
ret (表示return)
如果對象為nil就return

如果對象不為nil就走到CacheLookup 在緩存中尋找

.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)

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

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)

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

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

3:  // double wrap
    JumpMiss $0
    
.endmacro

CacheHit 命中緩存 結果是直接調(diào)用方法或者返回imp指針

.macro CacheHit
.if $0 == NORMAL
    MESSENGER_END_FAST
    br  x17         // call imp
.elseif $0 == GETIMP
    mov x0, x17         // return imp
    ret
.elseif $0 == LOOKUP
    ret             // return imp via x17
.else
.abort oops
.endif
.endmacro

CheckMiss在緩存中沒有找到

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

因為CacheLookup NORMAL傳的值是NORMAL 這里我們暫時只分析__objc_msgSend_uncached

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
    MethodTableLookup
    br  x17

END_ENTRY __objc_msgSend_uncached

我們再分析MethodTableLookup 發(fā)現(xiàn)是個宏

.macro MethodTableLookup
    
    // push frame
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3

    // imp in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16

.endmacro

bl __class_lookupMethodAndLoadCache3 意思是跳轉到__class_lookupMethodAndLoadCache3這個方法.

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

接下來就來到了lookUpImpOrForward

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) { //cache為NO在_objc_msgSend匯編代碼中已經(jīng)在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.read();
    
    //判斷類是否已經(jīng)實現(xiàn)過了
    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);//如果類沒有實現(xiàn)過,就去實現(xiàn)類

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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.assertReading();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);//再次從緩存中檢查
    if (imp) goto done;

    // Try this class's method lists.從自己的類中查找方法找到就把imp返回了
    {
        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)
        {
            //curClass是父類 curClass != nil;curClass = curClass->superclass一直便利,父類
            // Halt if there is a cycle in the superclass chain.如果超類鏈中存在循環(huán)裙士,則停止究珊。
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.從父類的緩存中找imp
            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;
            }
        }
    }

    // No implementation found. Try method resolver once.
    //自己類的緩存,方法列表,所有父類的緩存,方法類表都沒找到 進入動態(tài)方法解析階段 
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;   //在動態(tài)解析后就會 返回到retry 從新走一遍消息發(fā)送流程(如果動態(tài)解析階段我們動態(tài)的添加了方法.就會找到imp 如果沒有動態(tài)解析就會走到最后一個階段消息轉發(fā))
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache; //消息轉發(fā)
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

我們分析一下_class_resolveMethod

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

如果是實例方法則會調(diào)用 _class_resolveInstanceMethod.

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    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));
        }
    }
}

我們分析一下_class_resolveInstanceMethod方法SEL_resolveInstanceMethod 是+ (BOOL)resolveInstanceMethod:(SEL)sel 通過objc_msgSend調(diào)用這個類方法, 我們可以重寫這個類方法, 并且在類方法中 動態(tài)添加方法

-(void)test1{
    NSLog(@"---");
}

+(void)test1{
    NSLog(@"---");
}

+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(test)) {
        Class class = object_getClass(self);
        Method method = class_getInstanceMethod(class, @selector(test1));
        IMP imp = method_getImplementation(method);
        const char * types =  method_getTypeEncoding(method);
        class_addMethod(class, sel,imp,types);
    }
    return [super resolveClassMethod:sel];
}


+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(test)) {
        Method method = class_getInstanceMethod(self, @selector(test1));
        IMP imp = method_getImplementation(method);
        const char * types =  method_getTypeEncoding(method);
        class_addMethod(self, sel,imp,types);
    }
    return [super resolveInstanceMethod:sel];
}

如果我們動態(tài)解析沒有做事情 就會來到消息轉發(fā)_objc_msgForward_impcache這個imp我們發(fā)現(xiàn)在匯編中找到,但是經(jīng)過分析是沒有源碼的,這里暫不分析匯編
下面列出動態(tài)轉發(fā)的幾個方法.

- (id)forwardingTargetForSelector:(SEL)aSelector {
    
    if (aSelector == @selector(test)) {
        return [LC_Tool new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(test)) {
        NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:""];
        return methodSignature;
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //這里拿到 anInvocation可以為所欲為
    //可以任何事情都不做
    //可以轉發(fā)給一個對象調(diào)用方法
    //[anInvocation invokeWithTarget:[NSObject new]];
    
    //可以改變參數(shù)
    int a = 10;
    [anInvocation setArgument:&a atIndex:2]; //第0個參數(shù)是消息接受者.第1個參數(shù)是_cmd
    [anInvocation invokeWithTarget:[NSObject new]];
    
}

在消息轉發(fā)階段如果-forwardingTargetForSelector沒有實現(xiàn),就會調(diào)用- methodSignatureForSelector方法自己返回方法簽名,
然后調(diào)用-forwardInvocation返回一個NSInvocation對象

補充一點如果 消息轉發(fā)階段這個消息是類方法就會調(diào)用+forwardingTargetForSelector,+ methodSignatureForSelector
,+ forwardInvocation (雖然沒有暴露出api)

不管是類方法還是對象方法在消息轉發(fā)階段, 其實都是消息接受者調(diào)用以上的方法.(這樣就可以理解為啥 ,對象方法調(diào)用-號類方法調(diào)用+號了 因為消息接受者不同)

objc_msgSend的執(zhí)行流程可以分為3大階段

1消息發(fā)送
2動態(tài)方法解析
3消息轉發(fā)

image.png
image.png
image.png
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嘹履,一起剝皮案震驚了整個濱河市碍脏,隨后出現(xiàn)的幾起案子猾警,更是在濱河造成了極大的恐慌芒率,老刑警劉巖症副,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件启泣,死亡現(xiàn)場離奇詭異决摧,居然都是意外死亡亿蒸,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門掌桩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來边锁,“玉大人,你說我怎么就攤上這事波岛∶┨常” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵则拷,是天一觀的道長贡蓖。 經(jīng)常有香客問我曹鸠,道長,這世上最難降的妖魔是什么斥铺? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任彻桃,我火速辦了婚禮,結果婚禮上仅父,老公的妹妹穿的比我還像新娘叛薯。我一直安慰自己,他們只是感情好笙纤,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布耗溜。 她就那樣靜靜地躺著,像睡著了一般省容。 火紅的嫁衣襯著肌膚如雪抖拴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天腥椒,我揣著相機與錄音阿宅,去河邊找鬼。 笑死笼蛛,一個胖子當著我的面吹牛洒放,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播滨砍,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼往湿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了惋戏?” 一聲冷哼從身側響起领追,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎响逢,沒想到半個月后绒窑,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡舔亭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年些膨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钦铺。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡订雾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出职抡,到底是詐尸還是另有隱情,我是刑警寧澤误甚,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布缚甩,位于F島的核電站谱净,受9級特大地震影響,放射性物質發(fā)生泄漏擅威。R本人自食惡果不足惜壕探,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望郊丛。 院中可真熱鬧李请,春花似錦、人聲如沸厉熟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽揍瑟。三九已至白翻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绢片,已是汗流浹背滤馍。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留底循,地道東北人巢株。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像熙涤,于是被迫代替她去往敵國和親阁苞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • Objective-C語言是一門動態(tài)語言灭袁,它將很多靜態(tài)語言在編譯和鏈接時期做的事放到了運行時來處理猬错。這種動態(tài)語言的...
    有一種再見叫青春閱讀 585評論 0 3
  • 本文轉載自:http://southpeak.github.io/2014/10/25/objective-c-r...
    idiot_lin閱讀 935評論 0 4
  • runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的茸歧, 在面試過程中是經(jīng)常會被問到的倦炒, ...
    made_China閱讀 1,210評論 0 7
  • 本文詳細整理了 Cocoa 的 Runtime 系統(tǒng)的知識,它使得 Objective-C 如虎添翼软瞎,具備了靈活的...
    lylaut閱讀 800評論 0 4
  • 姓名:魏正君《六項精進》第270期感謝2組 公司:綿陽大北農(nóng)農(nóng)牧科技有限公司 【日精進打卡第77天】 【知~學習】...
    莫心莫肺閱讀 225評論 0 0