OC源碼分析之方法的解析與轉發原理

前言

想要成爲一名iOS開發高手,免不了閱讀源碼。如下是筆者在OC源碼探索中梳理的一個小系列——類與對象篇,歡迎你們閱讀指正,同時也但願對你們有所幫助。html

  1. OC源碼分析之對象的建立
  2. OC源碼分析之isa
  3. OC源碼分析之類的結構解讀
  4. OC源碼分析之方法的緩存原理
  5. OC源碼分析之方法的查找原理
  6. OC源碼分析之方法的解析與轉發原理

OC中方法的調用是經過objc_msgSend(或objc_msgSendSuper,或objc_msgSend_stret,或objc_msgSendSuper_stret)函數,向調用者發送名爲SEL的消息,找到具體的函數地址IMP,進而執行該函數。若是找不到IMP,會進行方法的解析,這至關於提供一次容錯處理;方法解析以後,若是依然找不到IMP,還有最後一次機會,那就是消息的轉發。git

方法的查找流程盡在 OC源碼分析之方法的查找原理 一文中,文接此文,本文將深刻剖析方法的解析與轉發。github

下面進入正題。緩存

須要注意的是,筆者用的源碼是 objc4-756.2架構

1 方法的解析

方法的解析,即method resolver(又名消息的解析,也叫方法決議),其創建在方法的查找的失敗結果上,入口源碼以下:app

// 在【類...根類】的【緩存+方法列表】中都沒找到IMP,進行方法解析
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        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;
    }
複製代碼

它主要是調用了resolveMethod函數。resolveMethod函數處理完畢以後,還要從新執行一次retry(再走一遍方法的查找流程)。其中,triedResolver這個變量使得消息的解析只進行一次。ide

1.1 resolveMethod

且看resolveMethod函數源碼:函數

static void resolveMethod(Class cls, SEL sel, id inst) {
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            resolveInstanceMethod(cls, sel, inst);
        }
    }
}
複製代碼

這裏有兩個分支,主要是對cls作個是否元類的判斷:源碼分析

  • 不是元類,意味着調用的是實例方法,那麼執行resolveInstanceMethod函數
  • 是元類,說明調用的是類方法,執行resolveClassMethod函數,以後若是依然沒找到IMP,則再去執行resolveInstanceMethod函數;

先看實例方法的狀況post

1.2 實例方法解析

resolveInstanceMethod源碼以下:

static void resolveInstanceMethod(Class cls, SEL sel, id inst) {
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());

    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // 若是你沒有實現類方法 +(BOOL)resolveInstanceMethod:(SEL)sel
        // NSObject也有實現,因此通常不會走這裏
        // 注意這裏傳入的第一個參數是:cls->ISA(),也就是元類
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 調用類方法: +(BOOL)resolveInstanceMethod:(SEL)sel
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // 再找一次imp(此次是sel,而不是resolveInstanceMethod)
    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));
        }
    }
}
複製代碼

resolveInstanceMethod函數前後調用了兩次lookUpImpOrNil

  • 第一次的調用是判斷類(包括其父類,直至根類)是否實現了+(BOOL)resolveInstanceMethod:(SEL)sel類方法
    • SEL_resolveInstanceMethod至關於@selector(resolveInstanceMethod:)NSObject類中有實現這個類方法(返回的是NO,會影響是否打印),因此通常會接着往下走。
  • 第二次的調用的目的是檢測是否有sel對應的IMP。假如你在+(BOOL)resolveInstanceMethod:(SEL)sel中添加了sel的函數地址IMP,此時再次去查找這個IMP就能找到。

注意到這兩次調用中,resolver都是NO,所以在其調用lookUpImpOrForward時不會觸發 消息的解析,僅僅是從「類、父類、...、根類」的緩存中和方法列表中找IMP,沒找到會觸發 消息轉發

lookUpImpOrNil函數源碼:

IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) {
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}
複製代碼

這裏會判斷IMP是不是消息轉發而來的,若是是,就不返回。

1.3 類方法解析

類方法的解析首先是調用resolveClassMethod函數,其源碼以下:

// 這裏的cls是元類,由於類方法存儲在元類
static void resolveClassMethod(Class cls, SEL sel, id inst) {
    runtimeLock.assertUnlocked();
    assert(cls->isRealized());
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // 若是你沒有實現類方法 +(BOOL)resolveClassMethod:(SEL)sel
        // NSObject也有實現,因此通常不會走這裏
        // 注意這裏的第一個參數是cls,是元類
        return;
    }

    Class nonmeta;
    {
        mutex_locker_t lock(runtimeLock);
        // 獲取 元類的對象,即類。換句話說,nonmeta 也就是 inst
        nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
        // +initialize path should have realized nonmeta already
        if (!nonmeta->isRealized()) {
            _objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
                        nonmeta->nameForLogging(), nonmeta);
        }
    }
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 調用類方法: +(BOOL)resolveClassMethod:(SEL)sel
    bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel);

    // 再找一次imp
    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 resolveClassMethod:%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函數大致相同,須要留意的是,此次判斷類(包括其父類,直至根類)是否實現的是+(BOOL)resolveClassMethod:(SEL)sel類方法。

讓咱們回顧一下resolveMethod函數對類方法的解析

// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst, 
        NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
{
    // 此時的cls爲元類,也就是 NSObject 調用 resolveInstanceMethod:
    resolveInstanceMethod(cls, sel, inst);
}
複製代碼

在通過resolveClassMethod的處理以後,若是依然沒有找到類方法的IMP,就會再次執行resolveInstanceMethod函數!不一樣於實例方法的是,此時的cls是元類,所以msg(cls, SEL_resolveInstanceMethod, sel);便是向元類內部發送resolveInstanceMethod:消息,也就意味着是根類調用resolveInstanceMethod:方法(此次只能在根類的分類中補救了),同時緩存查找類方法的IMP僅發生在根元類和根類中,而方法列表中查找類方法的IMP則分別在「元類、元類的父類、...、根元類、根類」中進行。

簡而言之,當咱們調用一個類方法時,若是在類中沒有實現,同時在resolveClassMethod中也沒有處理,那麼最終會調用根類(NSObject)的同名實例方法

1.4 舉個栗子

經過上述的分析,相信你們對方法的解析有了必定的認知,下面咱們來整個簡單的例子消化一下。

@interface Person : NSObject

+ (void)personClassMethod1;
- (void)personInstanceMethod1;

@end

@implementation Person

@end
複製代碼

一個簡單的Person類,裏面分別有一個類方法和一個實例方法,可是都沒有實現。

接着添加對這兩個方法的解析:

- (void)unimplementedMethod:(SEL)sel {
    NSLog(@"沒實現?不要緊,毫不崩潰");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"動態實例方法解析:%@", NSStringFromSelector(sel));
    if (sel == @selector(personInstanceMethod1)) {
        IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
        Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
        const char *methodType = method_getTypeEncoding(method);
        return class_addMethod(Person.class, sel, methodIMP, methodType);
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"動態類方法解析:%@", NSStringFromSelector(sel));
    if (sel == @selector(personClassMethod1)) {
        IMP methodIMP = class_getMethodImplementation(self, @selector(unimplementedMethod:));
        Method method = class_getInstanceMethod(Person.class, @selector(unimplementedMethod:));
        const char *methodType = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("Person"), sel, methodIMP, methodType);
    }
    return [super resolveClassMethod:sel];
}
複製代碼

看看打印:

經過對類方法解析的源碼分析,咱們知道,也能夠把對Person類方法的處理放在NSObject分類的resolveClassMethod:resolveInstanceMethod:中,都能達到相同的效果(記得把Person類中的resolveClassMethod:處理去掉)。這裏略過不提。

2 消息轉發

方法的調用通過了查找、解析,若是仍是沒有找到IMP,就會來到消息轉發流程。它的入口在lookUpImpOrForward函數靠後的位置

// No implementation found, and method resolver didn't help. 
// Use forwarding.
imp = (IMP)_objc_msgForward_impcache;
cache_fill(cls, sel, imp, inst);
複製代碼

2.1 消息的轉發起始和結束

_objc_msgForward_impcache是彙編函數,以arm64架構爲例,其源碼以下:

STATIC_ENTRY __objc_msgForward_impcache

// No stret specialization.
b	__objc_msgForward

END_ENTRY __objc_msgForward_impcache
複製代碼

__objc_msgForward_impcache內部調用了__objc_msgForward

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函數的地址並調用。

說明:

  • adrp是以頁爲單位的大範圍的地址讀取指令,這裏的p就是page的意思
  • ldr相似與movmvn,噹噹即數(__objc_msgForward中是[x17, __objc_forward_handler@PAGEOFF]PAGEOFF是頁地址偏移值)大於movmvn能操做的最大數時,就使用ldr

OBJC2中,_objc_forward_handler實際上就是objc_defaultForwardHandler函數,其源碼以下:

// Default forward handler halts the process.
__attribute__((noreturn)) void 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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
複製代碼

是否是很熟悉?當咱們調用一個沒實現的方法時,報的錯就是「unrecognized selector sent to ...」

可是問題來了,說好的消息轉發流程呢?這纔剛開始怎麼就結束了?不急,憋慌,且看下去。

2.2 消息轉發的調用棧

回顧方法解析時舉的例子,不妨把解析的內容去掉,Let it crash!

發如今崩潰以前與消息轉發相關的內容是,調用了_CF_forwarding_prep_0___forwarding___這兩個函數。遺憾的是這兩個函數並未開源。

既然崩潰信息不能提供幫助,只好打印具體的調用信息了。

在方法的查找流程中,log_and_fill_cache函數就跟打印有關,跟蹤其源碼:

static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
#if SUPPORT_MESSAGE_LOGGING
    if (slowpath(objcMsgLogEnabled && implementer)) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill(cls, sel, imp, receiver);
}

bool objcMsgLogEnabled = false;

// Define SUPPORT_MESSAGE_LOGGING to enable NSObjCMessageLoggingEnabled
#if !TARGET_OS_OSX
# define SUPPORT_MESSAGE_LOGGING 0
#else
# define SUPPORT_MESSAGE_LOGGING 1
#endif
複製代碼

打印的關鍵函數就是logMessageSend,可是它受SUPPORT_MESSAGE_LOGGINGobjcMsgLogEnabled控制。

繼續跟進SUPPORT_MESSAGE_LOGGING

#if !DYNAMIC_TARGETS_ENABLED
    #define TARGET_OS_OSX 1
    ...
#endif
    
#ifndef DYNAMIC_TARGETS_ENABLED
 #define DYNAMIC_TARGETS_ENABLED 0
#endif
複製代碼

從源碼不難看出TARGET_OS_OSX的值是1,所以,SUPPORT_MESSAGE_LOGGING也爲1!

若是能把objcMsgLogEnabled改爲true,顯然就能夠打印調用信息了。經過全局搜索objcMsgLogEnabled,咱們找到了instrumentObjcMessageSends這個關鍵函數

void instrumentObjcMessageSends(BOOL flag) {
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}
複製代碼

接下來就好辦了!來到main.m,添加如下代碼

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        instrumentObjcMessageSends(true);
        [Person personClassMethod1];
        instrumentObjcMessageSends(false);
    }
    return 0;
}
複製代碼

運行工程,直到再次崩潰。此時已打印函數調用棧,日誌文件位置在logMessageSend函數中有標註

// Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }
複製代碼

打開Finder(訪達),cmd + shift + G快捷鍵,輸入/tmp/msgSends,找到最新的一份日誌文件(數字最大)

打印結果以下:

從這份日誌能夠看出,與轉發相關的方法是forwardingTargetForSelectormethodSignatureForSelector,分別對應了消息的快速轉發流程和慢速轉發流程,接下來開始分析這兩個方法。

2.3 消息的快速轉發

forwardingTargetForSelector:對應的就是消息的快速轉發流程,它在源碼中只是簡單的返回nil(可在子類或分類中重寫)

+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}
複製代碼

不過咱們能夠在開發文檔中找到說明(cmd + shift + 0快捷鍵)

歸納地說,forwardingTargetForSelector:主要是返回一個新的receiver,去處理sel這個當前類沒法處理的消息,若是處理不了,會轉到效率低下的forwardInvocation:。在效率方面,forwardingTargetForSelector:領先forwardInvocation:一個數量級,所以,最好不要用後者的方式處理消息的轉發邏輯。

關於forwardingTargetForSelector:返回的新的receiver,須要注意一下幾點:

  • 絕對不能返回self,不然會陷入無限循環;
  • 不處理的話,能夠返回nil,或者[super forwardingTargetForSelector:sel](非根類的狀況),此時會走methodSignatureForSelector:慢速轉發流程;
  • 若是有這個receiver,此時至關於執行objc_msgSend(newReceiver, sel, ...),那麼它必須擁有和被調用的方法相同方法簽名的方法(方法名、參數列表、返回值類型都必須一致)。

2.3.1 舉個栗子

咱們能夠實驗一下,準備工做以下

@interface ForwardObject : NSObject

@end

@implementation ForwardObject

+ (void)personClassMethod1 {
    NSLog(@"類方法轉發給%@,執行%s", [self className], __FUNCTION__);
}

- (void)personInstanceMethod1 {
    NSLog(@"實例方法轉發給%@,執行%s", [self className], __FUNCTION__);
}

@end

@interface Person : NSObject

+ (void)personClassMethod1;
- (void)personInstanceMethod1;

@end

@implementation Person

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"實例方法開始轉發");
    return [ForwardObject alloc];
}

+ (id)forwardingTargetForSelector:(SEL)sel {
    NSLog(@"類方法開始轉發");
    return [ForwardObject class];
}

@end
複製代碼

顯然,ForwardObject做爲消息轉發後的處理類,擁有Person類的同名類方法和實例方法。如今開始驗證,結果以下:

事實證實確實有效!接下來看消息的慢速轉發流程。

2.4 消息的慢速轉發

若是forwardingTargetForSelector:沒有處理消息(如返回nil),就會啓動慢速轉發流程,也就是methodSignatureForSelector:方法,一樣須要在子類或分類中重寫

// Replaced by CF (returns an NSMethodSignature)
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("+[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}

// Replaced by CF (returns an NSMethodSignature)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    _objc_fatal("-[NSObject methodSignatureForSelector:] "
                "not available without CoreFoundation");
}
複製代碼

經過閱讀官方文檔,咱們得出如下結論:

  • methodSignatureForSelector:方法是跟forwardInvocation:方法搭配使用的,前者須要咱們根據sel返回一個方法簽名,後者會把這個方法簽名封裝成一個NSInvocation對象,並將其做爲形參。
  • 若是有目標對象能處理Invocation中的selInvocation能夠指派這個對象處理;不然不處理。
    • Invocation能夠指派多個對象處理

注意:消息的慢速轉發流程性能較低,若是能夠的話,你應該儘量早地處理掉消息(如在方法解析時,或在消息的快速轉發流程時)。

2.4.1 舉個栗子

針對慢速流程,一樣能夠驗證。這裏把快速轉發例子中的Person類修改一下:

@implementation Person

// MARK: 慢速轉發--類方法

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"類方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(personClassMethod1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = [anInvocation selector];
    NSLog(@"類方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    id target = [ForwardObject class];
    if ([target respondsToSelector:aSelector]) [anInvocation invokeWithTarget:target];
    else [super forwardInvocation:anInvocation];
}

// MARK: 慢速轉發--實例方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"實例方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    if (aSelector == @selector(personInstanceMethod1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL aSelector = [anInvocation selector];
    NSLog(@"實例方法慢速轉發:%s, sel:%@", __FUNCTION__, NSStringFromSelector(aSelector));
    ForwardObject *obj = [ForwardObject alloc];
    if ([obj respondsToSelector:aSelector]) [anInvocation invokeWithTarget:obj];
    else [super forwardInvocation:anInvocation];
}

@end
複製代碼

其結果以下圖所示,顯然也沒有崩潰。

對方法簽名類型編碼不熟悉的能夠查看 蘋果官方的類型編碼介紹

3 總結

綜上所述,當咱們調用方法時,首先進行方法的查找,若是查找失敗,會進行方法的解析,此時OC會給咱們一次對sel的處理機會,你能夠在resolveInstanceMethod:(類方法對應resolveClassMethod:)中添加一個IMP;若是你沒把握住此次機會,也就是解析失敗時,會來到消息轉發階段,這個階段有兩個機會去處理sel,分別是快速轉發的forwardingTargetForSelector:,以及慢速轉發的methodSignatureForSelector:。固然,若是這些機會你都放棄了,那OC只好讓程序崩潰。

下面用一副圖總結方法的解析和轉發流程

4 問題討論

4.1 爲何引入消息轉發機制?

在一個方法被調用以前,咱們是沒辦法肯定它的實現地址的,直到運行時,這個方法被調用的時候,咱們才能真正知道它是否有實現,以及其具體的實現地址。這也就是所謂的「動態綁定」。

在編譯期,若是編譯器發現方法不存在,會直接報錯;一樣,在運行時,也有doesNotRecognizeSelector的處理。

在拋出doesNotRecognizeSelector這個異常信息以前,OC利用其動態綁定的特性,引入了消息轉發機制,給予了咱們額外的機會處理消息(解析 or 轉發),這樣的作法顯然更加周全合理。

5 PS

相關文章
相關標籤/搜索