閱讀源碼的樂(lè)趣

閱讀源碼的樂(lè)趣

閱讀源碼尤其是優(yōu)秀的源碼是一件很有樂(lè)趣的事情,可以拓寬視野,提高品位,鍛煉思維仗颈,就像間接地在跟作者溝通一樣。Quora 上有一個(gè)問(wèn)題是:TJ-Holowaychunk是如何學(xué)習(xí)編程的椎例,他的回答是

I don't read books, never went to school, I just read other people's code and always wonder how things work

如果有足夠的好奇心挨决,并且總想知道「How Things Work」,那么閱讀源碼就是個(gè)不錯(cuò)的途徑订歪。

源碼的復(fù)雜度不同脖祈,需要投入的時(shí)間、使用的方法也不同刷晋,以一個(gè)中等復(fù)雜度的項(xiàng)目為例盖高,簡(jiǎn)單分享下我閱讀源碼的一些經(jīng)驗(yàn)。

WWDC 2014掏秩,有一個(gè) Session 是講「Advanced User Interfaces with Collection Views」或舞,之所以選擇這個(gè),是因?yàn)樗俏覀冞€算熟悉的對(duì)象(Collection View)蒙幻,但蘋果用了一些「特殊」的架構(gòu)來(lái)做到代碼復(fù)用,并且減少 VC 的體積胆筒,而且使用了部分 iTunes Connect 的源碼邮破,而不是簡(jiǎn)單的演示代碼。所以決定一窺究竟仆救。

為了有一個(gè)大概的感受抒和,先看一遍視頻,不需要領(lǐng)會(huì)每個(gè)要點(diǎn)彤蔽,先記錄一些關(guān)鍵信息摧莽,方便到時(shí)翻源碼。

  • 這套結(jié)構(gòu)可以處理復(fù)雜的 DataSource
  • 可以同時(shí)適配 iPhone / iPad
  • 有一個(gè)統(tǒng)一的 loading indicator
  • 可以設(shè)置某個(gè) Header 是否置頂
  • 可以有一個(gè)全局的 Header
  • 通過(guò)聚合 DataSource 的方法來(lái)達(dá)到代碼復(fù)用顿痪,并且只有一個(gè) VC
  • 可以設(shè)置聚合形式為 Segmented / Composed
  • layout信息可以配置镊辕,且可以覆蓋
  • 使用了有限狀態(tài)機(jī)
  • 子 DataSource 在數(shù)據(jù)載入完成后會(huì)有一個(gè) block油够,所需的 DataSource 都載入完成時(shí),這些 block 會(huì)被統(tǒng)一執(zhí)行
  • Section Metrics 可以設(shè)置 Section 的具體表現(xiàn)
  • layout 的信息會(huì)在內(nèi)部被保存征懈,避免重復(fù)計(jì)算 (Snapshot Metrics)
  • Optional Layout Methods 會(huì)有意想不到的好效果

產(chǎn)生了一些疑問(wèn)石咬,比如

  • 多個(gè)子 DataSource 被組合成一個(gè) Composed DataSource 時(shí),如何通過(guò) IndexPath 找到對(duì)應(yīng)的 DataSource卖哎?
  • 找到之后如何處理鬼悠?
  • 是否置頂是如何實(shí)現(xiàn)的?
  • 如何通過(guò)有限狀態(tài)機(jī)來(lái)管理 Loading 狀態(tài)亏娜?
  • 如果有按鈕焕窝,那么按鈕的點(diǎn)擊事件如何處理?
  • Collection View 沒(méi)有 headerView维贺,這又是怎么實(shí)現(xiàn)的袜啃?
  • 數(shù)據(jù)是怎么載入的?

大概有了些概念和疑問(wèn)之后幸缕,就可以打開(kāi)源碼痛快看了群发,先來(lái)看看目錄結(jié)構(gòu) (可以在這里在線瀏覽)

|-Framework|-Categories|-DataSources|-Layouts|-ViewControllers|-Views|-Application

看來(lái)關(guān)鍵的信息都在 Framework 里了,那如何切入呢发乔?反其道而行之熟妓,先來(lái)看看這些 Framework 是怎么用的,最直接的就從 ViewController 入手栏尚。那就先來(lái)看看 AAPLCatListViewController 這個(gè)類吧起愈,如果沒(méi)猜錯(cuò)的話,應(yīng)該是展示喵咪列表(直觀的名字很重要)译仗。

果然很小抬虽,居然只有 140 行,如果不分離的話纵菌,1400 行也是可以輕松達(dá)到的阐污。看到定義了一個(gè) AAPLSegmentedDataSource咱圆,腦海里大概可以想象出是一個(gè)可以切換 Tag 的頁(yè)面笛辟,接著又看到了兩個(gè) DataSource,那這兩個(gè)頁(yè)面的數(shù)據(jù)源應(yīng)該就是它們了序苏。

@interfaceAPPLCatListViewController()@property(nonatomic,strong)AAPLSegmentedDataSource*segmentedDataSource;@property(nonatomic,strong)AAPLCatListDataSource*catsDataSource;@property(nonatomic,strong)AAPLCatListDataSource*favoriteCatsDataSource;@property(nonatomic,strong)NSIndexPath*selectedIndexPath;@property(nonatomic,strong)idselectedDataSourceObserver;@end

然后又看到這么一行

-(void)dealloc{[self.segmentedDataSourceaapl_removeObserver:self.selectedDataSourceObserver];}

看起來(lái)是蘋果自己實(shí)現(xiàn)了一個(gè) KVO Wrapper手幢,果然他們自己也無(wú)法忍受原生的KVO,哈哈忱详。接著到了 ViewDidLoad围来,新建了兩個(gè) DataSource,那新建的時(shí)候都干了些什么?

-(AAPLCatListDataSource*)newAllCatsDataSource{AAPLCatListDataSource*dataSource=[[AAPLCatListDataSourcealloc]init];dataSource.showingFavorites=NO;dataSource.title=NSLocalizedString(@"All",@"Title for available cats list");dataSource.noContentMessage=NSLocalizedString(@"All the big ...",@"The message to show when no cats are available");dataSource.noContentTitle=NSLocalizedString(@"No Cats",@"The title to show when no cats are available");dataSource.errorMessage=NSLocalizedString(@"A problem with the network ....",@"Message to show when unable to load cats");dataSource.errorTitle=NSLocalizedString(@"Unable To Load Cats",@"Title of message to show when unable to load cats");returndataSource;}

所以只是初始化监透,然后設(shè)置一些信息桶错,Nothing Special。然后看到了 AAPLLayoutSectionMetrics 才漆,看起來(lái)是設(shè)置 Layout 的一些顯示信息牛曹,如 height / backgroundColor 之類的。

最后創(chuàng)建了一個(gè) KVO 來(lái)監(jiān)測(cè) selectedDataSource 的變化醇滥,界面上做相應(yīng)的調(diào)整黎比。

接下來(lái)看看 AAPLCatListDataSource 的實(shí)現(xiàn),一進(jìn)去發(fā)現(xiàn)

@interfaceAAPLCatListDataSource:AAPLBasicDataSource/// Is this list showing the favorites or all available cats?@property(nonatomic)BOOLshowingFavorites;@end

看來(lái) AAPLBasicDataSource 一定做了很多事鸳玩,進(jìn)入到 AAPLBasicDataSource.m 文件阅虫,看到這個(gè)方法

-(void)setShowingFavorites:(BOOL)showingFavorites{if(showingFavorites==_showingFavorites)return;_showingFavorites=showingFavorites;[selfresetContent];[selfsetNeedsLoadContent];if(showingFavorites)[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(observeFavoriteToggledNotification:)name:AAPLCatFavoriteToggledNotificationNameobject:nil];}

注意到有一個(gè)setNeedsLoadContent方法,看起來(lái)數(shù)據(jù)的載入應(yīng)該是通過(guò)這個(gè)方法來(lái)觸發(fā)的不跟,進(jìn)去看看

-(void)setNeedsLoadContent{[NSObjectcancelPreviousPerformRequestsWithTarget:selfselector:@selector(loadContent)object:nil];[selfperformSelector:@selector(loadContent)withObject:nilafterDelay:0];}

第一個(gè)方法沒(méi)怎么接觸過(guò)颓帝,查一下文檔先,原來(lái)是可以取消之前通過(guò)performSelector:withObject:afterDelay:觸發(fā)的方法窝革,為了加深印象购城,順便 Google 一下這個(gè)方法,原來(lái)performSelector:withObject:afterDelay在方法被執(zhí)行前虐译,會(huì)持有 Object瘪板,方法執(zhí)行后在解除對(duì) Object 的持有,如果不小心多次調(diào)用這個(gè)方法就有可能導(dǎo)致內(nèi)存泄露漆诽,所以在調(diào)用此方法前先 cancel 一下是個(gè)好習(xí)慣侮攀。

再來(lái)看看這個(gè)loadContent都做了什么

-(void)loadContent{// To be implemented by subclasses…}

看來(lái)需要在子類實(shí)現(xiàn)這個(gè)方法,那就到 AAPLCatListDataSource 里看看這個(gè)方法都做了什么

-(void)loadContent{[selfloadContentWithBlock:^(AAPLLoading*loading){void(^handler)(NSArray*cats,NSError*error)=^(NSArray*cats,NSError*error){// Check to make certain a more recent call to load content hasn't superceded this one…if(!loading.current){[loadingignore];return;}if(error){[loadingdoneWithError:error];return;}if(cats.count)[loadingupdateWithContent:^(AAPLCatListDataSource*me){me.items=cats;}];else[loadingupdateWithNoContent:^(AAPLCatListDataSource*me){me.items=@[];}];};if(self.showingFavorites)[[AAPLDataAccessManagermanager]fetchFavoriteCatListWithCompletionHandler:handler];else[[AAPLDataAccessManagermanager]fetchCatListWithCompletionHandler:handler];}];}

使用了loadContentWithBlock:方法厢拭,進(jìn)去看看兰英,這個(gè)方法做了什么

-(void)loadContentWithBlock:(AAPLLoadingBlock)block{[selfbeginLoading];__weaktypeof(&*self)weakself=self;AAPLLoading*loading=[AAPLLoadingloadingWithCompletionHandler:^(NSString*newState,NSError*error,AAPLLoadingUpdateBlockupdate){if(!newState)return;[selfendLoadingWithState:newStateerror:errorupdate:^{AAPLDataSource*me=weakself;if(update&&me)update(me);}];}];// Tell previous loading instance it's no longer current and remember this loading instanceself.loadingInstance.current=NO;self.loadingInstance=loading;// Call the provided block to actually do the loadblock(loading);}

簡(jiǎn)單說(shuō)來(lái)就是生成了一個(gè) loading,然后把 loading 傳給 block供鸠,那loadingWithCompletionHandler:這個(gè)方法又做了什么

+(instancetype)loadingWithCompletionHandler:(void(^)(NSString*state,NSError*error,AAPLLoadingUpdateBlockupdate))handler{NSParameterAssert(handler!=nil);AAPLLoading*loading=[[selfalloc]init];loading.block=handler;loading.current=YES;returnloading;}

所以就是生成一個(gè) loading 實(shí)例畦贸,然后把 handler 存到 block 屬性里。既然存了回季,那將來(lái)某個(gè)時(shí)候一定會(huì)用到家制,從名字上來(lái)看,應(yīng)該是 loading 完成時(shí)會(huì)被調(diào)用泡一,搜索 block 關(guān)鍵字,發(fā)現(xiàn)只有在下面這個(gè)方法中 block 才會(huì)被調(diào)用

-(void)_doneWithNewState:(NSString*)newStateerror:(NSError*)errorupdate:(AAPLLoadingUpdateBlock)update{#if DEBUGif(!OSAtomicCompareAndSwap32(0,1,&_complete))NSAssert(false,@"completion method called more than once");#endifvoid(^block)(NSString*state,NSError*error,AAPLLoadingUpdateBlockupdate)=_block;dispatch_async(dispatch_get_main_queue(),^{block(newState,error,update);});_block=nil;}

既然是 _ 開(kāi)頭觅廓,那應(yīng)該是內(nèi)部方法鼻忠,對(duì)外封裝了幾種狀態(tài),如ignore,done,updateWithContent:等。

咦帖蔓,這里為什么要先把*block 賦給一個(gè)臨時(shí)變量 block矮瘟,然后再把 _block 設(shè)為 nil呢?看起來(lái)像是為了解決某種內(nèi)存問(wèn)題塑娇。如果直接 *block(newState, error, update) 會(huì)怎樣澈侠?哦,雖然這里沒(méi)有出現(xiàn) self埋酬,但 _block 是一個(gè) instance 變量哨啃,所以在 ^{} 里會(huì)對(duì) self 進(jìn)行強(qiáng)引用。而如果賦給一個(gè)臨時(shí)變量写妥,那么只會(huì)對(duì)這個(gè)臨時(shí)變量強(qiáng)引用拳球,就不會(huì)出現(xiàn)循環(huán)引用的情況。

AAPLLoading 看的差不多了珍特,再出來(lái)看loadContentWithBlock:祝峻,注意到在 CompletionHandler 里,有這么一段

[selfendLoadingWithState:newStateerror:errorupdate:^{AAPLDataSource*me=weakself;if(update&&me)update(me);}];

這里的 self 是 AAPLDataSource (Block嵌套多了扎筒,還真是容易暈袄痴摇),來(lái)看看endLoadingWithState:error:update這個(gè)方法都做了什么

-(void)endLoadingWithState:(NSString*)stateerror:(NSError*)errorupdate:(dispatch_block_t)update{self.loadingError=error;self.loadingState=state;if(self.shouldDisplayPlaceholder){if(update)[selfenqueuePendingUpdateBlock:update];}else{[selfnotifyBatchUpdate:^{// Run pending updates[selfexecutePendingUpdates];if(update)update();}];}self.loadingComplete=YES;[selfnotifyContentLoadedWithError:error];}

設(shè)置一些狀態(tài)嗜桌,然后在恰當(dāng)?shù)臅r(shí)機(jī)調(diào)用 update block奥溺,咦,這里有個(gè) dispatchblockt 沒(méi)怎么見(jiàn)過(guò)症脂,查了一下原來(lái)是一個(gè)內(nèi)置的空傳值和空返回的block谚赎。

看了下enqueuePendingUpdateBlock,會(huì)把現(xiàn)在的這個(gè) update 結(jié)合之前的 updateBlock诱篷,形成一個(gè)新的 updateBlock壶唤,應(yīng)該就是視頻里提到的當(dāng)所有的 DataSource 都載入完時(shí),統(tǒng)一執(zhí)行之前的 update block

notifyBatchUpdate:所做的是看一下 Delegate 是否響應(yīng)dataSource:performBatchUpdate:complete:如果響應(yīng)則走你棕所,不然挨個(gè)執(zhí)行 update / complete闸盔。

看完了loadContentWithBlock再來(lái)看看這個(gè) Block 里面都做了什么,大意是根據(jù) self.showingFavorites 來(lái)切換不同的數(shù)據(jù)源琳省,這里看到了一個(gè)新的類 AAPLDataAccessManager迎吵,看起來(lái)像是統(tǒng)一的數(shù)據(jù)層,瞄一眼

@classAAPLCat;@interfaceAAPLDataAccessManager:NSObject+(AAPLDataAccessManager*)manager;-(void)fetchCatListWithCompletionHandler:(void(^)(NSArray*cats,NSError*error))handler;-(void)fetchFavoriteCatListWithCompletionHandler:(void(^)(NSArray*cats,NSError*error))handler;-(void)fetchDetailForCat:(AAPLCat*)catcompletionHandler:(void(^)(AAPLCat*cat,NSError*error))handler;-(void)fetchSightingsForCat:(AAPLCat*)catcompletionHandler:(void(^)(NSArray*sightings,NSError*error))handler;@end

果然如此针贬,將來(lái)數(shù)據(jù)的載入形式有變化击费,或需要做緩存啥的,都可以在這一層處理桦他,其他部分不會(huì)感覺(jué)到變化蔫巩。

這一輪看下來(lái)已經(jīng)有不少信息量了,來(lái)簡(jiǎn)單捋一下:

[SegmentedDataSource setNeedsLoadContent]↓[CatListDataSource loadContent]↓[DataSource loadContentWithBlock:]↓
創(chuàng)建 loading,設(shè)置 loading 完成后要做的事 → 拿到數(shù)據(jù)后放到 updateQueue 里圆仔,等全部拿到再執(zhí)行 batchUpdate
                ↓
執(zhí)行 loadContentBlock → 使用 DataAccessManager 去獲取數(shù)據(jù)垃瞧,拿到后交給 loading

到這里,我們還沒(méi)有運(yùn)行 Project 看效果坪郭,因?yàn)槲矣X(jué)得代碼包含的信息會(huì)更豐富个从,而且這么看下來(lái)后,對(duì)于界面會(huì)長(zhǎng)啥樣也有個(gè)大概的了解歪沃。

這只是開(kāi)始嗦锐,繼續(xù)挖掘下去還會(huì)有不少好東西,比如 Favorite 按鈕的處理绸罗,它是通過(guò) Responder Chain 而不是 Delegate 來(lái)實(shí)現(xiàn)的意推,也是一個(gè)思路。通過(guò)有限狀態(tài)機(jī)來(lái)管理 loading 狀態(tài)也是很有意思的實(shí)現(xiàn)珊蟀。

如果有興趣菊值,可以看下 ComposedDataSource,先不看實(shí)現(xiàn)育灸,如果要自己寫大概會(huì)是什么思路腻窒,比如當(dāng)調(diào)用[UICollectionView cellForItemAtIndexPath:]時(shí),如何找到對(duì)應(yīng)的 DataSource磅崭,找到之后如何渲染對(duì)應(yīng)的 Cell 等儿子。

所以看源碼真的是一件很有意思的事情,像一場(chǎng)冒險(xiǎn)砸喻,總是會(huì)有意外收獲柔逼,可能在不知不覺(jué)中,能力就得到了提升割岛。

版權(quán)歸原作者所有
原文地址:http://limboy.me/ios/2013/08/05/internal-implementation-of-kvo.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末愉适,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子癣漆,更是在濱河造成了極大的恐慌维咸,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惠爽,死亡現(xiàn)場(chǎng)離奇詭異癌蓖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)婚肆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門租副,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人较性,你說(shuō)我怎么就攤上這事附井√衷剑” “怎么了两残?”我有些...
    開(kāi)封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵永毅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我人弓,道長(zhǎng)沼死,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任崔赌,我火速辦了婚禮意蛀,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘健芭。我一直安慰自己县钥,他們只是感情好畔咧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開(kāi)白布脯厨。 她就那樣靜靜地躺著,像睡著了一般浩销。 火紅的嫁衣襯著肌膚如雪痒留。 梳的紋絲不亂的頭發(fā)上谴麦,一...
    開(kāi)封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天,我揣著相機(jī)與錄音伸头,去河邊找鬼匾效。 笑死,一個(gè)胖子當(dāng)著我的面吹牛恤磷,可吹牛的內(nèi)容都是我干的面哼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼扫步,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼魔策!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起锌妻,我...
    開(kāi)封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤代乃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后仿粹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體搁吓,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年吭历,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了堕仔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡晌区,死狀恐怖摩骨,靈堂內(nèi)的尸體忽然破棺而出通贞,到底是詐尸還是另有隱情,我是刑警寧澤恼五,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布昌罩,位于F島的核電站,受9級(jí)特大地震影響灾馒,放射性物質(zhì)發(fā)生泄漏茎用。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一睬罗、第九天 我趴在偏房一處隱蔽的房頂上張望轨功。 院中可真熱鬧,春花似錦容达、人聲如沸古涧。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)羡滑。三九已至,卻和暖如春卒暂,著一層夾襖步出監(jiān)牢的瞬間啄栓,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工也祠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昙楚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓诈嘿,卻偏偏與公主長(zhǎng)得像堪旧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奖亚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,091評(píng)論 25 707
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)淳梦、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,098評(píng)論 4 62
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,806評(píng)論 6 342
  • 我微笑 它便微笑 我哭泣 它陪我哭泣 哪怕啊 最后還要我 去擦干它的...
    孫滸胡閱讀 100評(píng)論 0 0
  • 第三十三章評(píng)估結(jié)果的逆轉(zhuǎn)這一章節(jié)引出了單一評(píng)估與聯(lián)合評(píng)估昔字,涉及到的匹配強(qiáng)度的問(wèn)題爆袍,又一次的回到前面講的內(nèi)容,奚愷元...
    我叫黃小賤閱讀 594評(píng)論 0 0