消息發(fā)送機(jī)制和運(yùn)行時(shí)

消息發(fā)送機(jī)制定義

OC的函數(shù)調(diào)用稱為消息發(fā)送袱贮。
OC的消息發(fā)送屬于動(dòng)態(tài)調(diào)用過程腮鞍。即在編譯的時(shí)候并不能決定真正調(diào)用哪個(gè)函數(shù)南用,只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱找到對應(yīng)的函數(shù)來調(diào)用甜奄。(事實(shí)證明吆寨,在編譯階段,OC可以調(diào)用任何函數(shù)拧廊,即使這個(gè)函數(shù)并未實(shí)現(xiàn),只要聲明過就不會(huì)報(bào)錯(cuò)晋修。而C語言在編譯階段就會(huì)報(bào)錯(cuò)吧碾。

OC是如何實(shí)現(xiàn)動(dòng)態(tài)調(diào)用

假如在OC中寫了這樣的一個(gè)代碼:
[obj makeText];
其中obj是一個(gè)對象,makeText是一個(gè)函數(shù)名稱墓卦。對于這樣一個(gè)簡單的調(diào)用倦春。在編譯時(shí)RunTime會(huì)將上述代碼轉(zhuǎn)化成
objc_msgSend(obj,@selector(makeText));
首先我們來看看obj這個(gè)對象,iOS中的obj都繼承于NSObject落剪。
@interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY; }
在NSObjcet中存在一個(gè)Class的isa指針睁本。然后我們看看Class:

struct objc_class {
  Class isa; // 指向metaclass
  Class super_class ; // 指向其父類
  const char *name ; // 類名
  long version ; // 類的版本信息,初始化默認(rèn)為0忠怖,可以通過runtime函數(shù)class_setVersion和class_getVersion進(jìn)行修改呢堰、讀取
  long info; // 一些標(biāo)識信息,如CLS_CLASS (0x1L) 表示該類為普通 class ,其中包含對象方法和成員變量;CLS_META (0x2L) 表示該類為 metaclass凡泣,其中包含類方法;
  long instance_size ; // 該類的實(shí)例變量大小(包括從父類繼承下來的實(shí)例變量);
  struct objc_ivar_list *ivars; // 用于存儲(chǔ)每個(gè)成員變量的地址
  struct objc_method_list **methodLists ; // 與 info 的一些標(biāo)志位有關(guān),如CLS_CLASS (0x1L),則存儲(chǔ)對象方法枉疼,如CLS_META (0x2L),則存儲(chǔ)類方法;
  struct objc_cache *cache; // 指向最近使用的方法的指針鞋拟,用于提升效率骂维;
  struct objc_protocol_list *protocols; // 存儲(chǔ)該類遵守的協(xié)議
    }
  • 在ObjC中,每個(gè)Class實(shí)際上都有兩個(gè)Class贺纲,the Class object (類對象)和meta Class(元類)航闺, the Class object 定義了instance method,metaClass 定義了class method猴誊,所以潦刃,每個(gè)Class對象實(shí)際上是metaClass的一個(gè)單例。其實(shí)類對象是編譯器為每個(gè)類生成的有且只有一個(gè)的單例稠肘。而這個(gè)單例的isa指針指的是metaClass福铅。因此,對應(yīng)類的內(nèi)存布局的理解是:
    1项阴、每個(gè)類實(shí)際上都有類對象和元類兩個(gè)概念
    2滑黔、類對象是編譯器為每個(gè)類生成的有且只有一個(gè)的保存關(guān)于實(shí)例方法的信息的單例
    3笆包、元類則是保存類方法信息的
    4、類對象時(shí)元類對象的一個(gè)單例 略荡,類對象的isa指針指向元類
    5庵佣、類對象和元類都是基于objc_class結(jié)構(gòu)的
  • Class isa:指向metaclass,也就是靜態(tài)的Class汛兜。一般一個(gè)Obj對象中的isa會(huì)指向普通的Class巴粪,這個(gè)Class中存儲(chǔ)普通成員變量和對象方法(“-”開頭的方法),同時(shí)這個(gè)Class中的isa指針指向靜態(tài)Class粥谬,也就是metaclass肛根,metaclass中存儲(chǔ)static類型成員變量和類方法(“+”開頭的方 法)
    Class類中其他的成員這里就先不做過多解釋了,下面我們來看看:
  • @selector (makeText)**:這是一個(gè)SEL方法選擇器漏策。對于一個(gè)類中每一個(gè)方法包含兩個(gè)隱式參數(shù):1派哲、方法所屬的對象 2、方法的編號(SEL)掺喻。SEL其本身是一個(gè)Int類型的存放著方法名字的地址芭届,作用是快速的通過方法名(makeText)查找到對應(yīng)方法的函數(shù)指針,然后調(diào)用對應(yīng)方法感耙。
    所以ios類中不能存在2個(gè)名稱相同的方法褂乍,即使參數(shù)類型不同。因?yàn)镾EL是根據(jù)方法名字生成的即硼,相同的方法名稱只能對應(yīng)一個(gè)SEL逃片。

最后,消息發(fā)送之后動(dòng)態(tài)查找對應(yīng)的方法

1 .編譯器將代碼[obj makeText];轉(zhuǎn)化為objc_msgSend(obj, @selector (makeText));
2 .通過obj的isa指針找到obj對應(yīng)的class只酥。
3 .在Class中先去cache中通過SEL查找對應(yīng)函數(shù)method(猜測cache中method列表是以SEL為key通過hash表來存儲(chǔ)的题诵,這樣能提高函數(shù)查找速度),若 cache中未找到层皱。再去methodList中查找性锭,若methodlist中未找到,則取superClass中查找叫胖。
4 .若能找到草冈,通過method中的函數(shù)指針跳轉(zhuǎn)到對應(yīng)的函數(shù)中去執(zhí)行。最后將method加入到cache中瓮增,以方便下次查找怎棱。
5 .當(dāng)objc_msgSend找到方法對應(yīng)的實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn)绷跑,并將消息中所有的參數(shù)都傳遞給 方法實(shí)現(xiàn)拳恋,同時(shí),它還將傳遞兩個(gè)隱藏的參數(shù):1砸捏、接收消息的對象 2谬运、方法選標(biāo)
objc_msgSend(receiver, selector, arg1, arg2, ...)

那么隙赁,消息發(fā)送機(jī)制能夠給我們的編程帶來些什么新鮮血液呢?

  • 獲得方法地址
    取得方法的地址梆暖,并且直接象函數(shù)調(diào)用一樣調(diào)用它伞访,這樣就避免了動(dòng)態(tài)綁定。
    如果一個(gè)方法會(huì)被連續(xù)調(diào)用很多次轰驳,多次調(diào)用方法時(shí)發(fā)送消息的開銷就會(huì)比較大厚掷,使用方法地址來調(diào)用方法就會(huì)節(jié)省開銷。但是這只有在指定的消息被重 復(fù)發(fā)送很多次時(shí)才有意義级解。
 setter(targetList[i], @selector(setFilled:), YES);
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];

方法指針的第一個(gè)參數(shù)是接收消息的對象(self)冒黑,第二個(gè)參數(shù)是方法選標(biāo)(_cmd)。這兩個(gè)參數(shù)在方法中是隱藏參數(shù)勤哗,但使用函數(shù)的形式來調(diào)用方法時(shí)必須顯示的給出薛闪。

  • 動(dòng)態(tài)方法解析
    需要?jiǎng)討B(tài)地提供一個(gè)方法的實(shí)現(xiàn)的時(shí)候。
    @dynamic propertyName;
    即表示編譯器須動(dòng)態(tài)地生成該屬性對應(yīng)地方法俺陋。
    可以通過 class_addMethod 方法將一個(gè)函數(shù)加入到類的方法中。
    void dynamicMethodIMP(id self, SEL _cmd) { // implementation .... }
    可以通過實(shí)現(xiàn) resolveInstanceMethod:和 resolveClassMethod:來動(dòng)態(tài)地實(shí)現(xiàn)給定選標(biāo)的對象方法或者類方法昙篙。
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
        if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
        return YES;
}
        return [super resolveInstanceMethod:aSEL];
}
@end

在進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制之前腊状,respondsToSelector:和instancesRespondToSelector: 會(huì)被首先調(diào)用。您可以在這兩個(gè)方法中為傳進(jìn)來的選標(biāo)提供一個(gè)IMP苔可。如果您實(shí)現(xiàn)了resolveInstanceMethod:方法但是仍然希望正常的消息轉(zhuǎn)發(fā)機(jī)制進(jìn)行缴挖,您只需要返回NO就可以了。
例如焚辅,摘自網(wǎng)上的一段示例代碼:

@interface SomeClass : NSObject
@property (assign, nonatomic) float objectTag;
@end

@implementation SomeClass
@dynamic objectTag;  //聲明為dynamic
//添加setter實(shí)現(xiàn)
void dynamicSetMethod(id self,SEL _cmd,float w){
    printf("dynamicMethod-%s\n",[NSStringFromSelector(_cmd)
                                 cStringUsingEncoding:NSUTF8StringEncoding]);
    printf("%f\n",w);
    objc_setAssociatedObject(self, ObjectTagKey, [NSNumber numberWithFloat:w], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//添加getter實(shí)現(xiàn)
void dynamicGetMethod(id self,SEL _cmd){
    printf("dynamicMethod-%s\n",[NSStringFromSelector(_cmd)
                                 cStringUsingEncoding:NSUTF8StringEncoding]);
   [objc_getAssociatedObject(self, ObjectTagKey) floatValue];
}

//解析selector方法
+(BOOL) resolveInstanceMethod: (SEL) sel{

    NSString *methodName=NSStringFromSelector(sel);
    BOOL result=NO;
    //動(dòng)態(tài)的添加setter和getter方法
    if ([methodName isEqualToString:@"setObjectTag:"]) {
        class_addMethod([self class], sel, (IMP) dynamicSetMethod,
                        "v@:f");
        result=YES;  
    }else if([methodName isEqualToString:@"objectTag"]){
        class_addMethod([self class], sel, (IMP) dynamicGetMethod,
                        "v@:f");
        result=YES;
    }
    return result;
}
  • 動(dòng)態(tài)加載
    Objective-C 程序可以在運(yùn)行時(shí)鏈接和載入新的類和范疇類映屋。新載入的類和在程序啟動(dòng)時(shí)載入的類并沒有區(qū) 別。
  • 消息轉(zhuǎn)發(fā)
    通常同蜻,給一個(gè)對象發(fā)送它不能處理的消息會(huì)得到出錯(cuò)提示棚点,然而,Objective-C 運(yùn)行時(shí)系統(tǒng)在拋出錯(cuò)誤之前湾蔓, 會(huì)給消息接收對象發(fā)送一條特別的消息來通知該對象瘫析。forwardInvocation:消息,該消息的唯一參數(shù)是個(gè) NSInvocation 類型的對象——該對象封裝了原始的消息和消息的參數(shù)默责。
    所以贬循,我們可以通過實(shí)現(xiàn) forwardInvocation:方法來對不能處理的消息做一些默認(rèn)的處理,也可以以其它的某種方式來避免錯(cuò)誤被拋出桃序。比如杖虾,它通常可以用來將消息轉(zhuǎn)發(fā)給其它的對象媒熊。
    假設(shè)奇适,您需要設(shè)計(jì)一個(gè)能夠響應(yīng) negotiate 消息的對象坟比,那么就可以通過在 negotiate 方法的實(shí)現(xiàn)中將 negotiate 消息轉(zhuǎn)發(fā)給其它的對象來達(dá)到這一目的。
    更進(jìn)一步滤愕,假設(shè)您希望您的對象和另外一個(gè)類的對象對negotiate的消息的響應(yīng)完全一致温算。一種可能的方式就是讓您的類繼承其它類的方法實(shí)現(xiàn)。 然而间影,有時(shí)候這種方式不可行注竿,因?yàn)槟念惡推渌惪赡苄枰?br> 在不同的繼承體系中響應(yīng) negotiate 消息。雖然您的類無法繼承其它類的negotiate方法魂贬,您仍然可以提供一個(gè)方法實(shí)現(xiàn)巩割,這個(gè)方法實(shí)現(xiàn)只是簡單
    的將 negotiate 消息轉(zhuǎn)發(fā)給其他類的對象,就好像從其它類那兒“借”來的一樣付燥。
    如下所示:
- negotiate
{
  if ( [someOtherObject respondsTo:@selector(negotiate)] )
  return [someOtherObject negotiate];
  return self;
}

以上方法的缺陷是:如果有很多消息都希望傳遞給其它對象時(shí)宣谈,您必須為每一種消息提供方法實(shí)現(xiàn)。此外键科,這種方式不能處理未知的消息闻丑。
解決辦法就是:使用forwardInvocation:方法。
因?yàn)楫?dāng)一個(gè)對象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法響應(yīng)某消息時(shí)勋颖,運(yùn)行時(shí)系統(tǒng)將通過 forwardInvocation:消息通知該對象嗦嗡。通過實(shí)現(xiàn)您自己的 forwardInvocation: 方法,您可以在該方法實(shí)現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對象饭玲。要轉(zhuǎn)發(fā)消息給其它對象侥祭,forwardInvocation:方法所必須做的有:1、決定將消息轉(zhuǎn)發(fā)給誰 2茄厘、將消息和原來的參數(shù)一塊轉(zhuǎn)發(fā)出去矮冬。轉(zhuǎn)發(fā)消息后的返回值將返回給原來的消息發(fā)送者。
如下所示:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
[anInvocation selector]])
    //消息可以通過 invokeWithTarget:方法來轉(zhuǎn)發(fā)
    [anInvocation invokeWithTarget:someOtherObject];
else
    [super forwardInvocation:anInvocation];
 }

所以次哈,forwardInvocation:方法就像一個(gè)不能識別的消息的分發(fā)中心胎署,將這些消息轉(zhuǎn)發(fā)給不同接收對象。 或者它也可以象一個(gè)運(yùn)輸站將所有的消息都發(fā)送給同一個(gè)接收對象窑滞。它可以將一個(gè)消息翻譯成另外一個(gè)消息硝拧,或者簡單的"吃掉“某些消息,因此沒有響應(yīng)也沒有錯(cuò)誤葛假。forwardInvocation:方法也可以對不 同的消息提供同樣的響應(yīng)障陶,這一切都取決于方法的具體實(shí)現(xiàn)。該方法所提供是將不同的對象鏈接到消息鏈的能力聊训。

注意: forwardInvocation:方法只有在消息接收對象中無法正常響應(yīng)消息時(shí)才會(huì)被調(diào)用抱究。 所以, 如果您希望您的對象將 negotiate 消息轉(zhuǎn)發(fā)給其它對象带斑,您的對象不能有 negotiate 方法鼓寺。否則勋拟, forwardInvocation:將不可能會(huì)被調(diào)用。

  • 消息轉(zhuǎn)發(fā)和多重繼承
    消息轉(zhuǎn)發(fā)很象繼承妈候,并且可以用來在Objective-C程序中模擬多重繼承敢靡。
    5BD4A9E5-0D9B-4DF6-A5EA-CB10F1C382E3.png

    在上圖中,Warrior 類的一個(gè)對象實(shí)例將 negotiate 消息轉(zhuǎn)發(fā)給 Diplomat 類的一個(gè)實(shí)例苦银⌒ル剩看起來,Warrior 類似乎和 Diplomat 類一樣幔虏, 響應(yīng) negotiate 消息纺念,并且行為和 Diplomat 一樣(盡管實(shí)際上是 Diplomat 類響應(yīng)了該消息)。
    消息轉(zhuǎn)發(fā)提供了多重繼承的很多特性想括。然而陷谱,兩者有很大的不同:多重繼承是將不同的行為封裝到單個(gè)的對象中,有可能導(dǎo)致龐大的瑟蜈,復(fù)雜的對象烟逊。而消息轉(zhuǎn)發(fā)是將問題分解到更小的對象中,但是又以一種對消息發(fā)送對象來說完全透明的方式將這些對象聯(lián)系起來铺根。
  • 消息代理對象
    我理解為一個(gè)輕量級的對象(消息代理對象)代表更多的對象進(jìn)行消息處理宪躯。
  • 消息轉(zhuǎn)發(fā)和類繼承
    盡管消息轉(zhuǎn)發(fā)很“象”繼承,但它不是繼承夷都。例如在 NSObject 類中,方法 respondsToSelector: 和 isKindOfClass:只會(huì)出現(xiàn)在繼承鏈中予颤,而不是消息轉(zhuǎn)發(fā)鏈中囤官。例如,如果向一個(gè) Warrior 類的對象詢問它能否響應(yīng) negotiate 消息蛤虐,
if ( [aWarrior respondsToSelector:@selector(negotiate)] )

返回值是NO党饮,盡管該對象能夠接收和響應(yīng)negotiate。大部分情況下驳庭,NO 是正確的響應(yīng)刑顺。但不是所有時(shí)候都是的。例如饲常,如果您使用消息轉(zhuǎn)發(fā)來創(chuàng)建一個(gè)代理對象以擴(kuò)展某個(gè)類的能力蹲堂,這兒的消息轉(zhuǎn)發(fā)必須和繼承一樣,盡可能的對用戶透明贝淤。如果您希望您的代理對象看起來就象是繼承自它代表的對象一樣柒竞,您需要重新實(shí)現(xiàn) respondsToSelector:和 isKindOfClass:方法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
   if ( [super respondsToSelector:aSelector] )
         return YES;
   else {
       /* Here, test whether the aSelector message can *
       * be forwarded to another object and whether                 that *
       * object can respond to it. Return YES if it can. */
   }
   return NO;
}
除了 respondsToSelector:和 isKindOfClass:外,instancesRespondToSelector: 方法也必須重新實(shí)現(xiàn)播聪。如果您使用的是協(xié)議類朽基,需要重新實(shí)現(xiàn)的還有 conformsToProtocol:方法布隔。 類似地,如果對象需要轉(zhuǎn)發(fā)遠(yuǎn)程消息稼虎,則 methodSignatureForSelector:方法必須能夠返回實(shí)際 響應(yīng)消息的方法的描述衅檀。例如,如果對象需要將消息轉(zhuǎn)發(fā)給它所代表的對象霎俩,您可能需要如下的 methodSignatureForSelector:實(shí)現(xiàn):
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
     NSMethodSignature* signature = [super methodSignatureForSelector:selector];
     if (!signature) {
     signature = [surrogate methodSignatureForSelector:selector];
     }
     return signature;
}

您也可以將消息轉(zhuǎn)發(fā)的部分放在一段私有的代碼里哀军,然后從 forwardInvocation:調(diào)用它。
其實(shí)茸苇,這一段我還沒有摸索透排苍。。学密。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淘衙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子腻暮,更是在濱河造成了極大的恐慌彤守,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哭靖,死亡現(xiàn)場離奇詭異具垫,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)试幽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進(jìn)店門筝蚕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人铺坞,你說我怎么就攤上這事起宽。” “怎么了济榨?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵坯沪,是天一觀的道長。 經(jīng)常有香客問我擒滑,道長腐晾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任丐一,我火速辦了婚禮藻糖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘库车。我一直安慰自己颖御,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著潘拱,像睡著了一般疹鳄。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上芦岂,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天瘪弓,我揣著相機(jī)與錄音,去河邊找鬼禽最。 笑死腺怯,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的川无。 我是一名探鬼主播呛占,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼懦趋!你這毒婦竟也來了晾虑?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤仅叫,失蹤者是張志新(化名)和其女友劉穎帜篇,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體诫咱,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡笙隙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坎缭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片竟痰。...
    茶點(diǎn)故事閱讀 40,030評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖掏呼,靈堂內(nèi)的尸體忽然破棺而出坏快,到底是詐尸還是另有隱情,我是刑警寧澤哄尔,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布假消,位于F島的核電站柠并,受9級特大地震影響岭接,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜臼予,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一鸣戴、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧粘拾,春花似錦窄锅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽追驴。三九已至,卻和暖如春疏之,著一層夾襖步出監(jiān)牢的瞬間殿雪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工锋爪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留丙曙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓其骄,卻偏偏與公主長得像亏镰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子拯爽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評論 2 355

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