runtime - 理解OC的消息和消息轉(zhuǎn)發(fā)機(jī)制

您將了解到了runtime是如何通過objc_msgSend在運(yùn)行時(shí)把方法和方法實(shí)現(xiàn)進(jìn)行動(dòng)態(tài)綁定的忆矛;
也將了解到runtime下動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)的機(jī)制是怎樣的健提。

消息

本章描述了代碼的消息表達(dá)式如何轉(zhuǎn)換為對(duì)objc_msgSend函數(shù)的調(diào)用砚嘴,如何通過名字來指定一個(gè)方法聪铺,以及如何使用objc_msgSend函數(shù)亥揖。

獲得方法地址

避免動(dòng)態(tài)綁定的唯一辦法就是取得方法的地址妙同,并且直接象函數(shù)調(diào)用一樣調(diào)用它。
當(dāng)一個(gè)方法會(huì)被連續(xù)調(diào)用很多次矢门,而且您希望節(jié)省每次調(diào)用方法都要發(fā)送消息的開銷時(shí)盆色,使用方法地址來調(diào)用方法就顯得很有效。
利用NSObject類中的methodForSelector:方法祟剔,您可以獲得一個(gè)指向方法實(shí)現(xiàn)的指針隔躲,并可以使用該指針直接調(diào)用方法實(shí)現(xiàn)。methodForSelector:返回的指針和賦值的變量類型必須完全一致物延,包括方法的參數(shù)類型和返回值類型都在類型識(shí)別的考慮范圍中宣旱。

下面的例子展示了怎么使用指針來調(diào)用setFilled:的方法實(shí)現(xiàn):

void (*setter)(id, SEL, BOOL);
int i;

setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];

for ( i = 0; i < 1000, i++ )
 setter(targetList[i], @selector(setFilled:), YES);

方法指針的第一個(gè)參數(shù)是接收消息的對(duì)象(self),第二個(gè)參數(shù)是方法選標(biāo)(_cmd)教届。這兩個(gè)參數(shù)在方法中是隱藏參數(shù)响鹃,但使用函數(shù)的形式來調(diào)用方法時(shí)必須顯示的給出。
使用methodForSelector:來避免動(dòng)態(tài)綁定將減少大部分消息的開銷案训,但是這只有在指定的消息被重復(fù)發(fā)送很多次時(shí)才有意義买置,例如上面的 for 循環(huán)。
注意强霎,methodForSelector:是 Cocoa 運(yùn)行時(shí)系統(tǒng)的提供的功能忿项,而不是 Objective-C 語言本身的功 能。

objc_msgSend

在objective-C中城舞,消息時(shí)知道運(yùn)行時(shí)才會(huì)與方法實(shí)現(xiàn)進(jìn)行綁定的轩触。編譯器會(huì)把一個(gè)消息表達(dá)式:

[receiver message]

轉(zhuǎn)換成一個(gè)對(duì)消息函數(shù)objc_msgSend的調(diào)用。該函數(shù)有兩個(gè)主要參數(shù):消息接收者和消息對(duì)應(yīng)的方法名字---即方法選標(biāo)家夺。

objc_msgSend(receive, selector)

同時(shí)接收消息中的任意數(shù)目的參數(shù):

objc_msgSend(receive, select, arg1, arg2, ...)

該消息函數(shù)做了動(dòng)態(tài)綁定所需要的一切:
它首先找到選標(biāo)所對(duì)應(yīng)的方法實(shí)線脱柱。因?yàn)椴煌念悓?duì)同一方法可能會(huì)有不同的實(shí)現(xiàn),所以找到的方法實(shí)線依賴于消息接收者的類型拉馋。
然后將消息接受者對(duì)象(指向消息接受者對(duì)象的指針)以及方法中指定的參數(shù)傳給找到的方法實(shí)現(xiàn)榨为。
最后,將方法實(shí)現(xiàn)的返回值作為該函數(shù)的返回值返回煌茴。

注意:objc_msgSend方法看起來好像返回了數(shù)據(jù)随闺,其實(shí)objc_msgSend從不返回?cái)?shù)據(jù),而是你的方法在運(yùn)行時(shí)方法實(shí)現(xiàn)被調(diào)用后才會(huì)返回?cái)?shù)據(jù)蔓腐。下面詳細(xì)敘述消息發(fā)送的步驟(如下圖):

消息框架:


消息框架

這樣就能解釋objc_msgSend工作原理了矩乐,當(dāng)對(duì)象收到消息時(shí),為了匹配消息的接收者和選擇子:

  1. 消息函數(shù)首先根據(jù)對(duì)象的isa指針找到該對(duì)象所對(duì)應(yīng)的類的方法列表objc_method_list回论,并從方法列表中尋找該消息對(duì)應(yīng)的方法選標(biāo)散罕。如果能找到就可以直接跳轉(zhuǎn)到相關(guān)的具體實(shí)現(xiàn)中去調(diào)用。
  2. 如果找不到傀蓉,將會(huì)通過super_class指針沿著繼承樹向上去搜索笨使,直到繼承樹根部(通常為NSObject類)。一旦找到了方法選標(biāo)僚害,objc_msgSend則以消息接收者對(duì)象為參數(shù)調(diào)用硫椰,調(diào)用該選標(biāo)對(duì)應(yīng)的方法實(shí)現(xiàn)。
  3. 如果到了繼承樹根部還沒有找到萨蚕,就會(huì)進(jìn)行消息轉(zhuǎn)發(fā)靶草,還有三次機(jī)會(huì)來處理。(消息轉(zhuǎn)發(fā)在下文有介紹)

這就是在運(yùn)行時(shí)系統(tǒng)中選擇方法實(shí)現(xiàn)的方式岳遥。在面向?qū)ο缶幊讨修认瑁话惴Q作方法和消息動(dòng)態(tài)綁定的過程

為了加快消息的處理過程浩蓉,運(yùn)行時(shí)系統(tǒng)通常會(huì)將使用過的方法選標(biāo)和方法實(shí)現(xiàn)的地址放入緩存中派继。每個(gè)類
都有一個(gè)獨(dú)立的緩存宾袜,同時(shí)包括繼承的方法和在該類中定義的方法。消息函數(shù)會(huì)首先檢查消息接收者對(duì)象
對(duì)應(yīng)的類的緩存(理論上驾窟,如果一個(gè)方法被使用過一次庆猫,那么它很可能被再次使用)。如果在緩存中已經(jīng)
有了需要的方法選標(biāo)绅络,則消息僅僅比函數(shù)調(diào)用慢一點(diǎn)點(diǎn)月培。如果程序運(yùn)行了足夠長(zhǎng)的時(shí)間,幾乎每個(gè)消息都
能在緩存中找到方法實(shí)現(xiàn)恩急。程序運(yùn)行時(shí)杉畜,緩存也將隨著新的消息的增加而增加。

使用隱藏的參數(shù)

疑問:
我們經(jīng)常用到關(guān)鍵字self衷恭,但是self是如何獲取當(dāng)前方法的對(duì)象呢此叠?

其實(shí),這也是runtime系統(tǒng)的作用随珠,self是在方法運(yùn)行時(shí)被動(dòng)態(tài)傳入的拌蜘。

當(dāng)objc_msgSend找到方法對(duì)應(yīng)實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn)牙丽,并將消息中所有參數(shù)都傳遞給方法實(shí)現(xiàn)简卧,同時(shí),她還將傳遞兩個(gè)隱藏參數(shù)

  • 接收消息的對(duì)象(self所指向的內(nèi)容烤芦,當(dāng)前方法的對(duì)象指針)
  • 方法選擇器(_cmd指向的內(nèi)容举娩,當(dāng)前方法的SEL指針)

這些參數(shù)幫助方法實(shí)現(xiàn)獲得了消息表達(dá)式的信息。它們被認(rèn)為是“隱藏”的是因?yàn)樗鼈儾]有在定義方法的源代碼中聲明构罗,而是在代碼編譯時(shí)插入方法實(shí)現(xiàn)中的铜涉。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?/p>

這兩個(gè)參數(shù)中遂唧,self更實(shí)用芙代。它是在方法實(shí)現(xiàn)中訪問消息接收者對(duì)象的實(shí)例變量的途徑。

這時(shí)我們可能會(huì)想到另一個(gè)關(guān)鍵字super盖彭,實(shí)際上super關(guān)鍵字接收到消息時(shí)纹烹,編譯器會(huì)創(chuàng)建一個(gè)objc_super結(jié)構(gòu)體:

struct objc_super { id receiver; Class class;}

這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類。receiver仍然是self本身召边,當(dāng)我們想通過[super class]獲取父類時(shí)铺呵,編譯器其實(shí)是將指向selfid指針和classSEL傳遞給objc_msgSendSuper函數(shù)。只有在NSObject類中才能找到class方法隧熙,然后class方法底層被轉(zhuǎn)換為object_getClass()片挂,接著底層編譯器將代碼轉(zhuǎn)換為objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個(gè)參數(shù)是指向selfid指針,與調(diào)用[self class]相同音念,所以我們得到的永遠(yuǎn)都是self的類型沪饺。因此你會(huì)發(fā)現(xiàn):

// 這句話并不能獲取父類的類型,只能獲取當(dāng)前類的類型名
NSLog(@"%@", NSStringFromClass([super class]));

消息轉(zhuǎn)發(fā)

消息轉(zhuǎn)發(fā)機(jī)制基本分為三個(gè)步驟:
1闷愤、動(dòng)態(tài)方法解析
2整葡、備用接收者
3、完整轉(zhuǎn)發(fā)

整個(gè)消息轉(zhuǎn)發(fā)流程如下圖所示:

image

1肝谭、所屬類動(dòng)態(tài)方法解析

首先掘宪,如果沿著繼承樹沒有搜索到相關(guān)方法則會(huì)向接受者所屬的類進(jìn)行一次請(qǐng)求蛾扇,調(diào)用所屬類的類方法 +resolveInstanceMethod:(實(shí)例方法) 或者 +resolveClassMethod:(類方法)攘烛。在這個(gè)方法中,我們有機(jī)會(huì)為該未知消息新增一個(gè)”處理方法“镀首。不過使用該方法的前提是我們已經(jīng)實(shí)現(xiàn)了該”處理方法”坟漱,只需要在運(yùn)行時(shí)通過class_addMethod函數(shù)動(dòng)態(tài)添加到類里面就可以了。

+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;

舉個(gè)例子:

// Person.m
#import "Person.h"
#import <objc/runtime.h>

@interface Person()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end

@implementation Person

- (instancetype)init {
    if (self = [super init]) {
    }
    return self;
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));
    if (sel == @selector(appendString:)) {
        class_addMethod([self class], sel, (IMP)dynamicAdditonMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    NSLog(@"resolveClassMethod: %@", NSStringFromSelector(sel));
    return [super resolveClassMethod:sel];
}

void dynamicAdditonMethodIMP(id self, SEL _cmd) {
    NSLog(@"dynamicAdditonMethodIMP");
}
@end


// viewController.m
id *p = [[Person alloc] init];
[p appendString:@""];

輸出結(jié)果:

2018-06-12 14:38:54.461050+0800 getIP[12036:607027] resolveInstanceMethod: appendString:
2018-06-12 14:38:54.461230+0800 getIP[12036:607027] dynamicAdditonMethodIMP

首先創(chuàng)建了一個(gè)Person的實(shí)例對(duì)象更哄,一定要用id類型來聲明芋齿,否則會(huì)在編譯器就報(bào)錯(cuò),因?yàn)檎也坏较嚓P(guān)函數(shù)的聲明(這里是appendString:)成翩。id類型由于可以指向任何類型的對(duì)象觅捆,因此編譯時(shí)能夠找到NSString類的相關(guān)方法聲明就不會(huì)報(bào)錯(cuò)。

由于Person類沒有聲明和定義appendString:方法麻敌,所以運(yùn)行時(shí)應(yīng)該會(huì)報(bào)unrecognized selector錯(cuò)誤栅炒,但是并沒有,因?yàn)槲覀冎貙懥祟惙椒?code>+ (BOOL)resolveInstanceMethod:(SEL)sel术羔,當(dāng)找不到相關(guān)實(shí)例方法的時(shí)候就會(huì)調(diào)用該類方法去詢問是否可以動(dòng)態(tài)添加赢赊,如果返回YES就會(huì)再次執(zhí)行相關(guān)方法,如何給一個(gè)類動(dòng)態(tài)添加一個(gè)方法级历,那就是調(diào)用runtime庫中的class_addMethod方法释移,該方法原型是:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

第一個(gè)參數(shù)是需要添加方法的類;
第二個(gè)參數(shù)是一個(gè)selector寥殖,也就是實(shí)例方法的名字玩讳;
第三個(gè)參數(shù)是一個(gè)IMP類型的變量也是函數(shù)實(shí)現(xiàn),需要傳入一個(gè)C函數(shù)嚼贡,這個(gè)函數(shù)至少兩個(gè)參數(shù)锋边,一個(gè)是id self 一個(gè)是SEL _cmd
第四個(gè)參數(shù)是函數(shù)類型编曼,更多含義見:Type Encodings豆巨;

2、備用接收者

動(dòng)態(tài)方法解析無法處理消息時(shí)掐场,則會(huì)走備用接收者往扔。這個(gè)備用接收者只能是一個(gè)新的對(duì)象贩猎,不能是self本身,否則就會(huì)出現(xiàn)無線循環(huán)萍膛。如果我們沒有指定相應(yīng)的對(duì)象來處理aSelector吭服,則應(yīng)該調(diào)用父類的實(shí)現(xiàn)來返回結(jié)果。

Person類聲明兩個(gè)方法:

@interface Person : NSObject
- (void)hello;
+ (Person *)hi;
@end

實(shí)現(xiàn)在Person.m中實(shí)現(xiàn)新的接收對(duì)象_helper和forwardingTargetForSelector:方法:


@interface Person()
{
    RuntimeMethodHelper *_helper;
}
@end


- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector");
    NSString *selectorString = NSStringFromSelector(aSelector);
    // 將消息交給_helper來處理
    if ([selectorString isEqualToString:@"hello"]) {
        return _helper;
    }
    return [super forwardingTargetForSelector:aSelector];
}

RuntimeMethodHelper類需要實(shí)現(xiàn)轉(zhuǎn)發(fā)的方法:

#import "RuntimeMethodHelper.h"

@implementation RuntimeMethodHelper

- (void)hello {
    NSLog(@"%@, %p", self, _cmd);
}

@end

最后在viewController.m中調(diào)用:

id p = [[Person alloc] init];
[p hello];

輸出結(jié)果:

2018-06-12 16:54:26.113808+0800 getIP[13842:768645] forwardingTargetForSelector
2018-06-12 16:54:26.114031+0800 getIP[13842:768645] <RuntimeMethodHelper: 0x60400000e270>, 0x10ed5f93b

3蝗罗、消息重定向

如果動(dòng)態(tài)方法解析和備用接收者都沒有處理這個(gè)消息艇棕,就只剩最后一次機(jī)會(huì),那就是消息重定向串塑。這個(gè)時(shí)候runtime會(huì)將未知消息的所有細(xì)節(jié)都封裝為NSInvocation對(duì)象沼琉,然后調(diào)用下述方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation;

forwardInvocation:消息給這個(gè)問題提供了一個(gè)更特別的,動(dòng)態(tài)的解決方案:當(dāng)一個(gè)對(duì)象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法響應(yīng)某消息時(shí)桩匪,運(yùn)行時(shí)系統(tǒng)將通過forwardInvocation:消息通知該對(duì)象打瘪。每個(gè)對(duì)象都從 NSObject類中繼承了forwardInvocation:方法。然而傻昙,NSObject 中的方法實(shí)現(xiàn)只是簡(jiǎn)單地調(diào)用了 doesNotRecognizeSelector:闺骚。通過實(shí)現(xiàn)您自己的forwardInvocation:方法,您可以在該方法實(shí)現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對(duì)象妆档。

要轉(zhuǎn)發(fā)消息給其他對(duì)象時(shí)僻爽,forwardInvocation:方法所必須做的有:

  • 決定將消息轉(zhuǎn)發(fā)給誰
  • 并且,將消息和原來的參數(shù)一塊轉(zhuǎn)發(fā)出去

注意:forward意思是“轉(zhuǎn)寄”贾惦,forwardingTargetForSelector:和forwardInvocation:都是把消息轉(zhuǎn)發(fā)給一個(gè)新的接收對(duì)象胸梆。

這里消息可以通過invokeWithTarget:方法來轉(zhuǎn)發(fā):

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation");
    if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:_helper];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        if ([RuntimeMethodHelper instancesRespondToSelector:aSelector]) {
            signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
        }
    }
    return signature;
}

運(yùn)行結(jié)果:

2018-06-12 17:21:22.255362+0800 getIP[14454:801341] forwardInvocation
2018-06-12 17:21:22.255588+0800 getIP[14454:801341] <RuntimeMethodHelper: 0x604000016fd0>, 0x10c80f8e9

轉(zhuǎn)發(fā)消息后的返回值將返回給原來的消息發(fā)送者。你可以返回任何類型的返回值纤虽,包括id乳绕,結(jié)構(gòu)體,浮點(diǎn)數(shù)等逼纸。

總結(jié)

至此洋措,我們了解到了runtime是如何通過objc_msgSend在運(yùn)行時(shí)把方法和方法實(shí)現(xiàn)進(jìn)行動(dòng)態(tài)綁定的;也了解到如果沿繼承樹找不到IMP杰刽,如何進(jìn)行動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)的菠发。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市贺嫂,隨后出現(xiàn)的幾起案子滓鸠,更是在濱河造成了極大的恐慌,老刑警劉巖第喳,帶你破解...
    沈念sama閱讀 211,743評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件糜俗,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)悠抹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門珠月,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人楔敌,你說我怎么就攤上這事啤挎。” “怎么了卵凑?”我有些...
    開封第一講書人閱讀 157,285評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵庆聘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我勺卢,道長(zhǎng)伙判,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評(píng)論 1 283
  • 正文 為了忘掉前任值漫,我火速辦了婚禮澳腹,結(jié)果婚禮上织盼,老公的妹妹穿的比我還像新娘杨何。我一直安慰自己,他們只是感情好沥邻,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,581評(píng)論 6 386
  • 文/花漫 我一把揭開白布危虱。 她就那樣靜靜地躺著,像睡著了一般唐全。 火紅的嫁衣襯著肌膚如雪埃跷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評(píng)論 1 290
  • 那天邮利,我揣著相機(jī)與錄音弥雹,去河邊找鬼。 笑死延届,一個(gè)胖子當(dāng)著我的面吹牛剪勿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播方庭,決...
    沈念sama閱讀 38,960評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼厕吉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了械念?” 一聲冷哼從身側(cè)響起头朱,我...
    開封第一講書人閱讀 37,719評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎龄减,沒想到半個(gè)月后项钮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,516評(píng)論 2 327
  • 正文 我和宋清朗相戀三年烁巫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鳖敷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,650評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡程拭,死狀恐怖定踱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情恃鞋,我是刑警寧澤崖媚,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站恤浪,受9級(jí)特大地震影響畅哑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜水由,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,936評(píng)論 3 313
  • 文/蒙蒙 一荠呐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧砂客,春花似錦泥张、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至彤恶,卻和暖如春钞钙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背声离。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評(píng)論 1 266
  • 我被黑心中介騙來泰國打工芒炼, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人术徊。 一個(gè)月前我還...
    沈念sama閱讀 46,370評(píng)論 2 360
  • 正文 我出身青樓本刽,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親弧关。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盅安,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,527評(píng)論 2 349

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