iOS組件化及架構(gòu)設(shè)計(jì)

關(guān)于組件化

網(wǎng)上組件化的文章很多。很多文章一提到組件化,就會(huì)說(shuō)解耦,一說(shuō)到解耦就會(huì)說(shuō)路由或者runtime。好像組件化 == 解耦 == 路由/Runtime,然而這是一個(gè)非常錯(cuò)誤的觀念。持有這一觀點(diǎn)的人,沒(méi)有搞清楚在組件化中什么是想要結(jié)果,什么是過(guò)程。

組件化和解耦

大家不妨先思考兩個(gè)問(wèn)題:

1、為何要進(jìn)行組件化開發(fā)?

2、各個(gè)組件之間是否一定需要解耦?

采用組件化,是為了組件能單獨(dú)開發(fā),單獨(dú)開發(fā)是結(jié)果。要讓組件能單獨(dú)開發(fā),組件必須職責(zé)單一,職責(zé)單一需要用到重構(gòu)和解耦的技術(shù),所以重構(gòu)和解耦是過(guò)程。那解耦是否是必須的過(guò)程?不一定。比如UIKit,我們用這個(gè)系統(tǒng)組件并沒(méi)有使用任何解耦手段。問(wèn)題來(lái)了,UIKit蘋果可以獨(dú)立開發(fā),我們使用它為什么沒(méi)用解耦手段?答案很簡(jiǎn)單,UIKit沒(méi)有依賴我們的代碼所以不用解耦。

PS:我這里不糾結(jié)組件、服務(wù)、模塊、框架的概念,網(wǎng)上對(duì)這些概念的定義五花八門,實(shí)際上把簡(jiǎn)單的事說(shuō)復(fù)雜了。我這里只關(guān)心一件事,這一部分代碼能否獨(dú)立開發(fā),能就叫組件,不能我管你叫什么

我們之所以要解耦才能獨(dú)立開發(fā),通常是出現(xiàn)了循環(huán)依賴。這時(shí)候當(dāng)然可以無(wú)腦的用路由把兩個(gè)組件的耦合解開,也可以獨(dú)立開發(fā)。然而,這樣做只是把強(qiáng)引用改成了弱引用,代碼還是爛代碼。站在重構(gòu)的角度來(lái)說(shuō),A、B組件循環(huán)依賴就是設(shè)計(jì)有問(wèn)題,要么應(yīng)該重構(gòu)A、B讓依賴單向;要么應(yīng)該抽離一個(gè)共用組件C,讓A、B組件都只依賴于C。

如果我們每個(gè)組件都只是單向依賴其他組件,各個(gè)組件之間也就沒(méi)有必要解耦。再換個(gè)角度說(shuō),如果一個(gè)組件職責(zé)不單一,即使跟其他組件解耦了,組件依然不能很好的工作。如何解耦只是重構(gòu)過(guò)程中可選手段,代碼設(shè)計(jì)的原則如依賴倒置、接口隔離、里氏替換,都可以指導(dǎo)我們寫出好的組件。

所以在組件化中重要的是讓組件職責(zé)單一,職責(zé)單一的重要標(biāo)志之一就是沒(méi)有組件間的循環(huán)依賴。

架構(gòu)圖

一般來(lái)講,App的組件可以分為三層,上層業(yè)務(wù)組件、中層UI組件、底層SDK組件

同一層之間的組件互相獨(dú)立,上層的組件耦合下層的組件。一般來(lái)講,底層SDK組件和中層UI組件都是獨(dú)立的功能,不會(huì)出現(xiàn)同層耦合。

架構(gòu)圖

業(yè)務(wù)組件解耦

上層業(yè)務(wù)組件之間的解耦,采用依賴注入的方式實(shí)現(xiàn)。每個(gè)模塊都聲明一個(gè)自己依賴的協(xié)議,在App集成方里去實(shí)現(xiàn)這些協(xié)議。

我之前的做法是每個(gè)模塊用協(xié)議提供自己對(duì)外的能力,其他模塊通過(guò)協(xié)議來(lái)訪問(wèn)它。這樣做雖然也可以解耦,但是維護(hù)成本很高,每個(gè)模塊都要去理解其他模塊。同時(shí)也引入了其他模塊自己用不到的功能,不符合最小依賴的原則。

使用依賴注入,APP集成方統(tǒng)一去管理各個(gè)模塊的依賴,每個(gè)模塊也能單獨(dú)編譯,是業(yè)務(wù)層解耦的最佳實(shí)踐。

包管理

要解除循環(huán)依賴,引入包管理技術(shù)cocoapods會(huì)讓我們更有效率。pod不允許組件間有循環(huán)依賴,若有pod install時(shí)就會(huì)報(bào)錯(cuò)。

cocoapods,提供私有pod repo,使用時(shí)把自己的組件放在私有pod repo里,然后在Podfile里直接通過(guò)pod命令集成。一個(gè)組件對(duì)應(yīng)一個(gè)私有pod,每個(gè)組件依賴自己所需要的三方庫(kù)。多個(gè)組件聯(lián)合開發(fā)的時(shí)候,可以再一個(gè)podspec里配置子模塊,這樣在每個(gè)組件自己的podspec里,只需要把子模塊里的pod依賴關(guān)系拷貝過(guò)去就行了。

在多個(gè)組件集成時(shí)會(huì)有版本沖突的問(wèn)題。比如登錄組件(L)、廣告組件(A)都依賴了埋點(diǎn)組件(O),L依賴O的1.1版本,A依賴O的1.2版本,這時(shí)候集成就會(huì)報(bào)錯(cuò)。為了解決這個(gè)錯(cuò)誤,在組件間依賴時(shí),不寫版本號(hào),版本號(hào)只在APP集成方寫。即podfile里引用所有組件,并寫上版本號(hào),.podspec里不寫版本號(hào)。

這樣做既可以保證APP集成方的穩(wěn)定性,也可以解決組件依賴的版本沖突問(wèn)題。這樣做的壞處是,所有組件包括App集成方,在使用其他組件時(shí),都必須使用其他組件最新的API,這會(huì)造成額外的升級(jí)工作量。如果不想接受組件升級(jí)最新api的成本,可以私有化一個(gè)三方庫(kù)自己維護(hù)。

組件開發(fā)完畢后告訴集成方,目前的組件穩(wěn)定版本是多少,引用的三方庫(kù)穩(wěn)定版本集成方自己去決定

推薦的組件版本號(hào)管理方式

另一種版本管理的方式,是在podspec里寫依賴組件的版本號(hào),podfile里不寫組件依賴的版本,然后通過(guò)內(nèi)部溝通來(lái)解決版本沖突的問(wèn)題。我認(rèn)為雖然也能做,但有很多弊端。

1.作為App集成方,沒(méi)辦法單獨(dú)控制依賴的三方庫(kù)版本。三方庫(kù)升級(jí)會(huì)更復(fù)雜

2.每個(gè)依賴的三方庫(kù),都應(yīng)該做了完整的單元測(cè)試,才能被集成到App中。所以正確的邏輯不是組件內(nèi)測(cè)試過(guò)三方庫(kù)沒(méi)問(wèn)題就在組件內(nèi)寫死版本號(hào),而是這個(gè)三方庫(kù)經(jīng)過(guò)我們測(cè)試后,可以在我們系統(tǒng)中使用XX版本。

3.在工程中就沒(méi)有一個(gè)地方能完整知道所有的pod組件,而App集成方有權(quán)利知道這一點(diǎn)

4.溝通成本高

不推薦的方式

順便說(shuō)一句,基礎(chǔ)組件庫(kù)可以通過(guò)pod子模塊單獨(dú)暴露獨(dú)立功能,較常用。

以上,就是組件化的所有東西。你可能會(huì)奇怪,解耦在組件化過(guò)程中有什么用。答案是解耦是為了更好的實(shí)現(xiàn)組件的單一職責(zé),解耦的作用在架構(gòu)設(shè)計(jì)中談。需要再次強(qiáng)調(diào),組件化 ≠ 解耦。

如果非要給組件化下一個(gè)定義,我的理解是:

組件化意味著重構(gòu),目的是讓每個(gè)組件職責(zé)單一。在結(jié)構(gòu)上,每個(gè)組件都最小依賴它所需要的東西。


關(guān)于架構(gòu)設(shè)計(jì)

在我看來(lái),iOS客戶端架構(gòu)主要為了解決兩個(gè)問(wèn)題,一是解決大型項(xiàng)目分組件開發(fā)的效率的問(wèn)題,二是解決單進(jìn)程App的穩(wěn)定性的問(wèn)題。

設(shè)計(jì)到架構(gòu)設(shè)計(jì)的都是大型App,小型App主要是業(yè)務(wù)的堆疊。很多公司在業(yè)務(wù)初期都不會(huì)考慮架構(gòu),在業(yè)務(wù)發(fā)展到一定規(guī)模的時(shí)候,才會(huì)重新審視架構(gòu)混亂帶來(lái)的開發(fā)效率和業(yè)務(wù)穩(wěn)定性瓶頸。這時(shí)候就會(huì)引入組件化的概念,我們常常面臨的是對(duì)已有項(xiàng)目的組件化,這一過(guò)程會(huì)異常困難。

組件拆分原則

對(duì)老工程的組件拆分,我的辦法是,從底層開始拆。SDK>??模塊 > 業(yè)務(wù)?。如果App沒(méi)有SDK可以抽離,就從模塊開始拆,不要為了抽離SDK而抽離。常見(jiàn)的誤區(qū)是,大家一拿到代碼就把公共函數(shù)提出來(lái)作為共用框架,起的名字還特別接地氣,如XXCommon。

事實(shí)上,這種框架型SDK,是最雞肋的組件,原因是它實(shí)用性很小,無(wú)非就是減少了點(diǎn)冗余代碼。而且在架構(gòu)能力不強(qiáng)的情況下,它很容易變成“垃圾堆”,什么東西都想往里面放,后面越來(lái)越龐大。所以,開始拆分架構(gòu)的時(shí)候,盡量以業(yè)務(wù)優(yōu)先,比如先拆分享模塊。

如果兩個(gè)組件中有共同的函數(shù),前期不要想著提出來(lái),改個(gè)名字讓它冗余是更好的辦法。如果共同耦合的是一個(gè)靜態(tài)庫(kù),可以利用動(dòng)態(tài)庫(kù)的隔離性封裝靜態(tài)庫(kù),具體方法可以網(wǎng)上找。

響應(yīng)式

基礎(chǔ)組件常常要在系統(tǒng)啟動(dòng)時(shí)初始化,或者接受App生命周期時(shí)間。這就引出了個(gè)問(wèn)題,如何給appDelegate瘦身?比如我們現(xiàn)在有兩個(gè)基礎(chǔ)組件A、B,他們都需要監(jiān)聽App生命周期事件,傳統(tǒng)的做法是,A、B兩個(gè)組件都提供一些函數(shù)在appDelegate中調(diào)用。但這樣做的壞處是,如果某一天我不想引入B組件了,還得去改appDelegate代碼。理想的方式是,基礎(chǔ)組件的使用不需要在appDelegate里寫代碼

為了實(shí)現(xiàn)基礎(chǔ)組件與appDelegate分離,得對(duì)appDelegate改造。首先得提出一個(gè)觀點(diǎn),蘋果的appDelegate設(shè)計(jì)的有問(wèn)題,它在用代理模式解決觀察者模式的問(wèn)題。在《設(shè)計(jì)模式》中,代理模式的設(shè)計(jì)意圖定義是:為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問(wèn)。反過(guò)來(lái)看appDelegate你會(huì)發(fā)現(xiàn),它大部分代理函數(shù)都沒(méi)有辦法控制application,如applicationDidBecomeActive。applicationDidBecomeActive這種事件常常需要多個(gè)處理者,這種場(chǎng)景用觀察者模式更適合。而openURL需要返回BOOL值,才需要使用代理模式。App生命周期事件雖然可以用監(jiān)聽通知獲取,但用起來(lái)不如響應(yīng)式監(jiān)聽信號(hào)方便。

基于響應(yīng)式編程的思想,我寫了一個(gè)TLAppEventBus,提供屬性來(lái)監(jiān)聽生命周期事件。我并不喜歡龐大的ReactiveObjectC,所以我通過(guò)category實(shí)現(xiàn)了簡(jiǎn)單的響應(yīng)式,用戶只需要監(jiān)聽需要的信號(hào)即可。在TLAppEventBus里,我默認(rèn)提供了8個(gè)系統(tǒng)事件用來(lái)監(jiān)聽,如果有其他的系統(tǒng)事件需要監(jiān)聽,可以使用擴(kuò)展的方法,給TLAppEventBus添加屬性(見(jiàn)文末Demo)。

路由

對(duì)于Appdelegate中的openURL的事件,蘋果使用代理模式并沒(méi)有問(wèn)題,但我們常常需要在openURL里面寫if-else區(qū)分事件的處理者,這也會(huì)造成多個(gè)URL處理模塊耦合在Appdelegate中。我認(rèn)為appdelegate中的openURL應(yīng)該用路由轉(zhuǎn)發(fā)的方式來(lái)解耦。

openURL代理需要同步返回處理結(jié)果,但網(wǎng)上開源的路由框架能同步返回結(jié)果的。所以我這邊實(shí)現(xiàn)了一個(gè)能同步返回結(jié)果的路由TLRouter,同時(shí)支持了注冊(cè)scheme。注冊(cè)scheme這一特性,在第三方分享的場(chǎng)景下會(huì)比較有用(見(jiàn)文末Demo)。

另外,網(wǎng)上大部分方案都搞錯(cuò)了場(chǎng)景。以蘑菇街的路由方案為例(好像iOS的路由就是他們提出來(lái)的?),蘑菇街認(rèn)為路由主要有兩個(gè)作用,一是發(fā)送數(shù)據(jù)讓路由接收者處理,二是返回對(duì)象讓路由發(fā)送者繼續(xù)處理。我不禁想問(wèn),這是路由嗎?不妨先回到URL的定義

URL: 統(tǒng)一資源標(biāo)識(shí)符(Uniform Resource Locator,統(tǒng)一資源定位符)是一個(gè)用于標(biāo)識(shí)某一互聯(lián)網(wǎng)資源名稱的字符串

openURL就是在訪問(wèn)資源,在瀏覽器中,openURL意味著打開一個(gè)網(wǎng)頁(yè),openURL的發(fā)起者并不關(guān)心打開的內(nèi)容是什么,只關(guān)心打開的結(jié)果。所以蘋果的openURL Api 就只返回了除了結(jié)果YES/NO,沒(méi)有返回一個(gè)對(duì)象。所以,我對(duì)openURL這一行為定義如下

openURL:訪問(wèn)資源,返回是否訪問(wèn)成功

那把蘑菇街的路由,返回的對(duì)象改成BOOL值就可以了么?我認(rèn)為還不夠。對(duì)于客戶端的路由,使用的實(shí)際上是通知的形式在解耦,帶來(lái)的問(wèn)題是路由的注冊(cè)代碼散落在各地,所以路由方案必須要配路由文檔,要不然開發(fā)者會(huì)不知道路由在干嘛。

有沒(méi)有比文檔更好的方式呢?我的思路是:用schema區(qū)分路由職責(zé)

系統(tǒng)的openURL只干了兩件事:打開App和打開網(wǎng)頁(yè)

[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"weixin://"]]; // 打開App

[[UIApplicationsharedApplication] openURL:[NSURLURLWithString:@"https://www.baidu.com"]];//打開網(wǎng)頁(yè)

兩者的共性是頁(yè)面切換。所以我這邊設(shè)計(jì)的路由openURL,只擴(kuò)充了controller跳轉(zhuǎn)的功能,比如打開登錄頁(yè)

[TLRouter openURL:@"innerJump://account/login"];

只擴(kuò)充了controller跳轉(zhuǎn)的功能好處是讓路由的職責(zé)更單一,同時(shí)也更符合蘋果對(duì)openURL的定義。工程師在看到url schema的時(shí)候就知道他的作用,避免反復(fù)查看文檔。

對(duì)于數(shù)據(jù)的傳遞,我認(rèn)為不應(yīng)該用路由的方式。相比路由,通過(guò)依賴注入傳入信號(hào)是更好的選擇。

App配置

有時(shí)候我們需要組件的跨App復(fù)用,在App集成組件時(shí),能夠不改代碼只改配置是最理想的方式。使用組件+plist配置是一個(gè)方案,具體做法是把A組件的配置放在A.plist中,在A組件內(nèi)寫死要讀取A.plist。

以配置代替硬編碼,防止對(duì)代碼的侵入,是一個(gè)很好的思路。設(shè)想一下,如果我們可以通過(guò)配置在決定App是否使用組件、也可通過(guò)配置來(lái)改變組件和app所需的參數(shù),那運(yùn)維可以代替app開發(fā)來(lái)出包,這對(duì)效率和穩(wěn)定性都會(huì)有提升。為了實(shí)現(xiàn)這一效果,我使用了OC的runtime來(lái)動(dòng)態(tài)注冊(cè)組件。需要在didfinishLaunch初始化的組件,可以實(shí)現(xiàn)代理?- (void)initializeWhenLaunch; 這樣,自動(dòng)初始化函數(shù),就可以通過(guò)runtime+plist里配置的class name自動(dòng)初始化。組件需要初始化的代碼,可以在自己的initializeWhenLaunch里做。

由于路由只擴(kuò)充了controller跳轉(zhuǎn)的功能,所以路由注冊(cè)這一行為也可進(jìn)行一次抽象,把不同的部分放在plist配置文件,相同的放到runtime里做。這樣做還有個(gè)好處是,程序內(nèi)的路由跳轉(zhuǎn)在一個(gè)plist里可以都可以看到

appdelegate改造后示例

iOS解耦工具Tourelle

Tourelle,是根據(jù)上面的思路寫的一個(gè)開源項(xiàng)目?https://github.com/zhudaye12138/Tourelle,可以通過(guò)pod集成 ?pod 'Tourelle'。下面介紹一下他的使用方式

TLAppEventBus

TLAppEventBus通過(guò)接收系統(tǒng)通知來(lái)獲取app生命周期事件,收到生命周期事件后改變對(duì)應(yīng)屬性的值。默認(rèn)提供了didEnterBackground等八個(gè)屬性,可以使用響應(yīng)式函數(shù)來(lái)監(jiān)聽?

- (void)observeWithBlock:(TLObservingBlock)block;?

? ? [TLAppEventBus.shared.didBecomeActive observeWithBlock:^(idnewValue) {

????????//do some thing

? ? }];

需要注意,如果在其它地方使用observeWithBlock,需要設(shè)置屬性的owner,否則沒(méi)有辦法監(jiān)聽到。這里不用單獨(dú)設(shè)置是因?yàn)樵赥LAppEventBus里已設(shè)置好

TLAppEventBus使用前需要調(diào)用 - (void)start; 如果需要監(jiān)聽更多的事件,可以調(diào)用

- (void)startWithNotificationMap:(NSDictionary *)map;?

? NSMutableDictionary *defaultMap = [NSMutableDictionary dictionaryWithDictionary:[TLAppEventBus defaultNotificationMap]]; //獲取默認(rèn)map

? ? [defaultMapsetObject:KDidChangeStatusBarOrientation forKey:UIApplicationWillChangeStatusBarOrientationNotification]; //添加新的事件

? ? [TLAppEventBus.shared startWithNotificationMap:defaultMap];//開啟EventBus

添加新事件需要用分類添加TLAppEventBus的屬性,添加后就可正常使用了

-(void)setDidChangeStatusBarOrientation:(NSNotification*)didChangeStatusBarOrientation {

? ? objc_setAssociatedObject(self, (__bridge const void *)KDidChangeStatusBarOrientation , didChangeStatusBarOrientation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

}

-(NSNotification*)didChangeStatusBarOrientation {

? ? returnobjc_getAssociatedObject(self, (__bridge const void *)KDidBecomeActive);

}

TLRouter

路由支持兩種注冊(cè)方式,一種只寫schema,一種寫url路徑

?[TLRouter registerURL:@"wx1234567://" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {? ? ? ?

? ? ? ? //do something? ? ?

}]//注冊(cè)schema

[TLRouter registerURL:@"InnerJump://account/login" hander:^(TLRouterURL *routeURL, void (^callback)(BOOL result)) {

? ? ? ? ? ? ? ? //do something

?}]//注冊(cè)u(píng)rl路徑

支持同步 & 異步獲取返回值,其中異步轉(zhuǎn)同步內(nèi)部通過(guò)semaphore實(shí)現(xiàn)

+(void)openURL:(NSString*)url callback:(void(^)(BOOLresult))callback;

+(BOOL)openURL:(NSString*)url;

另外openURL除了支持url中帶參數(shù),也支持參數(shù)放在字典中

+(BOOL)openURL:(NSString*)url param:(NSDictionary *)param;

TLAppLaunchHelper

TLAppLaunchHelper有兩個(gè)函數(shù),一個(gè)用來(lái)初始化組件。該函數(shù)會(huì)讀取AutoInitialize.plist中的classes,通過(guò)runtime + 自動(dòng)初始化協(xié)議完成初始化

-(void)autoInitialize;

AutoInitialize.plist

另一個(gè)函數(shù)用來(lái)自動(dòng)注冊(cè)路由,該函數(shù)會(huì)讀取AutoRegistURL.plist完成路由注冊(cè)。其中controller代表類名,params代表默認(rèn)參數(shù),如果openURL傳的參數(shù)與默認(rèn)參數(shù)不符合,路由會(huì)報(bào)錯(cuò)

-(void)autoRegistURL;


?AutoRegistURL.plist


路由注冊(cè)時(shí),并不決定controller跳轉(zhuǎn)的方式。注冊(cè)者只是調(diào)用presentingSelf方法,跳轉(zhuǎn)方式由controller中presentingSelf方法決定。

-(BOOL)presentingSelf {

? ? UINavigationController *rootVC = (UINavigationController *) APPWINDOW.rootViewController;

? ? if(rootVC) {

? ? ? ? [rootVCpushViewController:self animated:YES];

? ? ? ? returnYES;

? ?}

? ? return NO;

}


耦合檢測(cè)工具

針對(duì)既有代碼的組件化重構(gòu),我這邊開發(fā)了一個(gè)耦合檢測(cè)工具,目前只支持OC。

耦合檢測(cè)工具的原理是這樣:工具認(rèn)為工程中一級(jí)文件夾由組件構(gòu)成,比如A工程下面有aa、bb、cc三個(gè)文件夾,aa、bb、cc就是三個(gè)待檢測(cè)的組件。耦合檢測(cè)分三步,第一步通過(guò)正則找到組件內(nèi).h文件中所有關(guān)鍵字(包括函數(shù)、宏定義和類)。第二步通過(guò)找到的組件內(nèi)關(guān)鍵字,再通過(guò)正則去其它組件的.m中找是否使用了該組件的關(guān)鍵字,如果使用了,兩個(gè)組件就有耦合關(guān)系。第三步,輸出耦合檢測(cè)報(bào)告

代碼:開源中....

總結(jié)

本文給出了組件化的定義:組件化意味著重構(gòu),目的是讓每個(gè)組件職責(zé)單一以提升集成效率。包管理技術(shù)Pod是組件化常用的工具,iOS組件依賴及組件版本號(hào)確定,都可以用pod實(shí)現(xiàn)。整個(gè)iOS工程的組件通常分為3層,業(yè)務(wù)組件、模塊組件和SDK組件。在老工程重構(gòu)時(shí),優(yōu)先抽離SDK組件,切記不要寫XXCommon讓它變成垃圾堆。

關(guān)于解耦的技術(shù),appldegate適合用觀察者模式替換代理模式,路由只用來(lái)做controller之間的跳轉(zhuǎn),上層業(yè)務(wù)組件的解耦靠依賴注入而不是全用路由。工程的組件和路由都可通過(guò)runtime + 配置的形式自動(dòng)注冊(cè),這樣做維護(hù)和集成都會(huì)很方便。

Demo地址:https://github.com/zhudaye12138/Tourelle

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容