iOS GCD技術

簡介

GCD(Grand Central Dispatch)是在macOS10.6提出來的,后來在iOS4.0被引入。GCD的引入主要是它的使用比傳統的多線程方案如NSThread、NSOperationQueue、NSInvocationOperation使用起來更加方便,并且GCD的運作是在系統級實現的。由于是作為系統的一部分來實現的,因此比以前的線程更加有效。

同時GCD使用了block語法,在書寫上變得更加簡潔。

至于什么是多線程,多線程編程的優缺點這里就不探討了,主要討論一下GCD的使用。

Dispatch Queue介紹

關于GCD,蘋果所給出最直接的描述是:將想要執行的任務添加到Dispatch Queue中。因此Dispatch Queue將是接下來討論的關鍵。

先來看下面這段代碼:

dispatch_async(queue, ^{

// 要執行的任務

});

dispatch_async()是向隊列中添加任務的函數。這段代碼是將要執行的任務以block代碼塊的形式作為參數,添加到queue的隊列中,而queue則會按照順序處理隊列中的任務

另外,Dispatch Queue以處理方式的不同,分為兩種:

Serial Dispatch Queue,順序依次執行,只有隊列中前一個任務執行完成,后一個才可以開始。也就是我們常說的串行隊列

Concurrent Dispatch Queue,并發執行,將隊列中的任務依次添加到并行的線程中,同時執行。也就是我們常說的并行隊列。??注意:能夠同時執行任務的個數取決于系統當前的處理能力

??注意:Dispatch Queue隊列并不是指我們印象中的線程,它是任務隊列,它只負責任務的管理調度,并不進行任務的執行操作,任務的執行是由Dispatch Queue分配的線程來完成的

Dispatch Queue創建

在了解了什么是Dispatch Queue后,來看一下Dispatch Queue是如何得到的,先來看一段代碼:

dispatch_queue_t aSerialDispatchQueue =

dispatch_queue_create("MySerialDispatchQueue", NULL);

這段代碼就是通過dispatch_queue_create()函數得到一個Dispatch Queue。

其中,第一個參數是指Dispatch Queue的名稱,可以設置為NULL但是不建議這樣做,因為在Xcode和Instruments調試的時候都會以設置的這個參數作為展示名稱,所以建議創建的每一個Dispatch Queue都設置一個合適的名稱;

函數的第二個參數設置成了NULL,此時得到的是Serial Dispatch Queue類型的隊列,也可以直接設置第二個參數為DISPATCH_QUEUE_SERIAL,就像這樣:

dispatch_queue_t aSerialDispatchQueue =

dispatch_queue_create("MySerialDispatchQueue", DISPATCH_QUEUE_SERIAL);

如果我們想得到一個Concurrent Dispatch Queue類型的隊列,第二個參數要設置為DISPATCH_QUEUE_CONCURRENT,就像這樣:

dispatch_queue_t aConcurrentDispatchQueue =

dispatch_queue_create("MyConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

得到的返回值的類型都為dispatch_queue_t。

驗證

下面用代碼來驗證一下這兩種隊列是否像上邊說的那樣執行的。

先來驗證一下Serial Dispatch Queue:

dispatch_queue_t serialQueue

= dispatch_queue_create("queue_1", DISPATCH_QUEUE_SERIAL);

dispatch_async(serialQueue, ^{

NSLog(@"任務1 begin");

[NSThread sleepForTimeInterval:3.f];

NSLog(@"任務1 stop");

});

dispatch_async(serialQueue, ^{

NSLog(@"任務2 begin");

[NSThread sleepForTimeInterval:2.f];

NSLog(@"任務2 stop");

});

dispatch_async(serialQueue, ^{

NSLog(@"任務3 begin");

[NSThread sleepForTimeInterval:1.f];

NSLog(@"任務3 stop");

});

看一下打印結果:

再來驗證一下Concurrent Dispatch Queue:

dispatch_queue_t concurrentQueue

= dispatch_queue_create("queue_2", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(concurrentQueue, ^{

NSLog(@"任務1 begin");

[NSThread sleepForTimeInterval:3.f];

NSLog(@"任務1 stop");

});

dispatch_async(concurrentQueue, ^{

NSLog(@"任務2 begin");

[NSThread sleepForTimeInterval:2.f];

NSLog(@"任務2 stop");

});

dispatch_async(concurrentQueue, ^{

NSLog(@"任務3 begin");

[NSThread sleepForTimeInterval:1.f];

NSLog(@"任務3 stop");

});

看一下打印結果:

多個Dispatch Queue之間的關系

通過上面的驗證確實可以看出Serial Dispatch Queue是串行執行、Concurrent Dispatch Queue是并行執行的。那如果我們創建多個Serial Dispatch Queue會怎樣呢,這些Serial Dispatch Queue也會按照順序依次執行么?不是的,它們之間是并發執行的,也就是說多個Dispatch Queue之間是并發執行

那如果想讓多個Serial Dispatch Queue依然保持串行執行怎么辦呢?后邊會繼續說。

Dispatch Queue持有與釋放

在macOS10.8和iOS6.0以后,GCD已經支持ARC模式了,所以無需手動管理Dispatch Queue的持有與釋放。

這里提一下MRC模式下管理Dispatch Queue的兩個函數:

dispatch_retain(aSerialDispatchQueue);

dispatch_release(aSerialDispatchQueue);

系統提供的Dispatch Queue

除了我們手動創建的Dispatch Queue以外,系統還給我們提供了幾個現成的隊列,Main Dispatch Queue和Global Dispatch Queue:

Main Dispatch Queue是在主線程中執行的Dispatch Queue。因為主線程只有一條,并且主線程中的任務是依次執行的,所以Main Dispatch Queue自然是Serial Dispatch Queue類型的隊列,追加到Main Dispatch Queue的任務都是在主線程RunLoop中執行的,像界面更新等一些任務也都是在這個線程中執行。

獲得方法:

dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();

Global Dispatch Queue是所有應用程序都能使用的Concurrent Dispatch Queue類型隊列。Global Dispatch Queue有四個優先級分別是:高優先級(high priority)、默認優先級(default priority)、低優先級(low priority)、后臺優先級(background priority)

// 高優先級

dispatch_queue_t globalDispatchQueueHigh

= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

// 默認優先級

dispatch_queue_t globalDispatchQueueDefault

= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 低優先級

dispatch_queue_t globalDispatchQueueLow

= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

// 后臺優先級

dispatch_queue_t globalDispatchQueueBackground

= dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

對于Main Dispatch Queue和Global Dispatch Queue,即使在MRC模式下,也無需考慮持有與釋放問題。即使執行dispatch_retain()、dispatch_release()函數也是不會發生任何變化的。

Dispatch Queue目標隊列

GCD中的dispatch_set_target_queue()函數可以將一個dispatch_object_t對象設置到目標隊列來處理,上邊說到的dispatch_queue_t都屬于dispatch_object_t對象。

上邊曾說過多個Serial Dispatch Queue之間是并行執行的,先來驗證一下:

dispatch_queue_t queue1

= dispatch_queue_create("queue_1", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue2

= dispatch_queue_create("queue_2", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue3

= dispatch_queue_create("queue_3", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue1, ^{

NSLog(@"任務1 begin");

[NSThread sleepForTimeInterval:3.f];

NSLog(@"任務1 stop");

});

dispatch_async(queue2, ^{

NSLog(@"任務2 begin");

[NSThread sleepForTimeInterval:2.f];

NSLog(@"任務2 stop");

});

dispatch_async(queue3, ^{

NSLog(@"任務3 begin");

[NSThread sleepForTimeInterval:1.f];

NSLog(@"任務3 stop");

});

打印結果:

通過打印結果來看,雖然創建的是3個串行Dispatch Queue,但是串行的Dispatch Queue間卻是并行執行關系。

如果我們將創建好的這3個Serial Dispatch Queue隊列添加到一個目標隊列中,它們的執行順序又會怎樣呢:

dispatch_queue_t targetQueue

= dispatch_queue_create("target_queue", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue1 = dispatch_queue_create("queue_1", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue2 = dispatch_queue_create("queue_2", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t queue3 = dispatch_queue_create("queue_3", DISPATCH_QUEUE_SERIAL);

dispatch_set_target_queue(queue1, targetQueue);

dispatch_set_target_queue(queue2, targetQueue);

dispatch_set_target_queue(queue3, targetQueue);

dispatch_async(queue1, ^{

NSLog(@"任務1 begin");

[NSThread sleepForTimeInterval:3.f];

NSLog(@"任務1 stop");

});

dispatch_async(queue2, ^{

NSLog(@"任務2 begin");

[NSThread sleepForTimeInterval:2.f];

NSLog(@"任務2 stop");

});

dispatch_async(queue3, ^{

NSLog(@"任務3 begin");

[NSThread sleepForTimeInterval:1.f];

NSLog(@"任務3 stop");

});

打印結果:

通過打印可以看出,被添加到目標隊列里的3個隊列,按照串行順序執行。其實是串行執行還是并行執行跟目標隊列的性質有關。

如果將targetQueue換成一個并行隊列,相信被執行的3個隊列必然是并行執行關系,我已經做了驗證:

繼續,現在我們將目標隊列targetQueue換回成Serial Dispatch Queue串行隊列,而將3個被添加的隊列換成Concurrent Dispatch Queue并行隊列,并分別向其中額外再添加2個任務,此時3個被添加隊列中分別包含的任務是:1-1、1-2、1-3;2-1、2-2、2-3;3-1、3-2、3-3,再來看一下打印結果:

通過打印結果發現,所有任務都是按照串行順序執行下來的,被添加的三個并行隊列本身的并行特性被失效了。

還沒有完,如果把目標隊列換成并行的Concurrent Dispatch Queue又會怎樣呢?

通過打印結果可以看出,所有的任務都被并行執行。

通過上面的測試可以看出:無論被添加的是什么、什么隊列,它們所包含的任務(當然這些任務都是沒有被原所在隊列執行的)最終都會按照目標隊列的自身性質來執行,它們的優先級也遵循目標隊列的優先級。

利用dispatch_queue_create()函數生成的Dispatch Queue不管是Serial Dispatch Queue還是Concurrent Dispatch Queue所使用的都是與Global Dispatch Queue的默認優先級相同優先級的線程,利用dispatch_set_target_queue()函數我們可以改變它們的優先級。

貼一下官方文檔(翻譯不好,只能靠你的英文功力了):

延遲追加任務

dispatch_queue_t mainDispatchQueue =? dispatch_get_main_queue();

dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3* NSEC_PER_SEC);

dispatch_after(time,mainDispatchQueue,^{

// 任務...

});

以上代碼段是將任務延遲3秒添加到隊列中,注意的是:是添加到隊列中而不是執行

第一個參數time是dispatch_time_t類型,該類型值可通過dispatch_time()函數或dispatch_walltime()函數得到。

dispatch_time()函數的含義是獲得從第一個參數指定的時間開始,經過第二個參數指定的時間長度后的時間。DISPATCH_TIME_NOW表示現在的時間,類型為dispatch_time_t。NSEC_PER_SEC為秒的單位,NSEC_PER_MSEC為毫秒單位。dispatch_walltime()函數用于計算絕對時間。

Dispatch Group

在實際應用中,經常需要在執行完一些任務后,再執行某一個特定任務。如果使用的是Serial Dispatch Queue只需將任務全部添加到隊列中,然后再在最后追加上想要執行的任務就可以了。但是在使用Concurrent Dispatch Queue類型的隊列或者同時使用多個Dispatch Queue的時候,想實現這樣的需求就比較困難了。

這時,就要用到Dispatch Group了。下面通過代碼來看一下Dispatch Group是如何使用的:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group,queue, ^{NSLog(@"任務1");});

dispatch_group_async(group,queue, ^{NSLog(@"任務2");});

dispatch_group_async(group,queue, ^{NSLog(@"任務3");});

dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"最后要執行的任務");})

group是通過dispatch_group_create()函數創建的,類型為dispatch_group_t;

dispatch_group_async()函數與dispatch_async()函數相同,都是向隊列中追加任務,不同的是dispatch_group_async()函數中第一個參數是指定當前任務屬于哪個Dispatch Group;

dispatch_group_notify()函數中第一個參數是指定要監視的Dispatch Group,在屬于該group的所有任務都執行完成后會將函數的第三個參數任務追加到第二個參數隊列中執行。

除了添加對任務的監控以外,還可使用等待函數,來看下面一段代碼:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, queue,^{NSLog(@"任務1");});

dispatch_group_async(group, queue,^{NSLog(@"任務2");});

dispatch_group_async(group, queue,^{NSLog(@"任務3");});

dispatch_group_wait(group, 10*NSEC_PER_SEC);

上面代碼中的dispatch_group_wait()函數是對所屬group的任務的處理過程進行等待,函數中第二個參數代表等待時間,為dispatch_time_t類型。如果在設置時間內所有任務執行完成函數返回long類型的值0,如果返回值不為0說明還有任務在執行中。如果第二個參數設置為DISPATCH_TIME_FOREVER,函數必將返回0,因為該函數將無限期掛起等待,直到所有任務執行完成函數才會返回。

那么等待到底意味著什么?這意味著一旦調用dispatch_group_wait()函數,該函數就處于調用狀態而不返回,即執行dispatch_group_wait()函數的所在線程停止。當該函數返回值后,當前線程繼續。

如果將函數中第二個參數設置為DISPATCH_TIME_NOW,則不需要等待即可判定所屬group的任務是否全部執行完成。

dispatch_barrier_async()函數

通常在進行數據讀、寫操作的時候,多個任務同時執行讀操作是可以的,但是多個任務同時執行寫操作可能就會發生數據競爭的問題。尤其在一系列復雜的讀寫操作中,使用Serial Dispatch Queue會導致讀操作效率變低,使用Concurrent Dispatch Queue不但會引起多個寫任務發生數據競爭,還可能因為并發執行導致讀寫順序錯亂。

因此要使用dispatch_barrier_async()函數配合Concurrent Dispatch Queue并行隊列來解決這個問題。

來看下面一段代碼:

dispatch_queue_t? queue = dispatch_create_queue("OneConcurrentDispatchQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, block_mission1_reading);

dispatch_async(queue, block_mission2_reading);

dispatch_async(queue, block_mission3_reading);

dispatch_async(queue, block_mission4_reading);

dispatch_barrier_async(queue, block_mission5_writing);

dispatch_async(queue, block_mission6_reading);

dispatch_async(queue, block_mission7_reading);

dispatch_async(queue, block_mission8_reading);

dispatch_barrier_async()函數會等到

block_mission1_reading、

block_mission2_reading、

block_mission3_reading、

block_mission4_reading這些任務并行執行完畢后再將block_mission5_writing任務追加到隊列中,當dispatch_barrier_async()函數追加的任務執行完成,隊列會恢復為一般動作,繼續并行處理后續追加到隊列中的任務。

Dispatch Queue掛起與恢復

當我們想掛起某一個Dispatch Queue時

dispatch_suspend(queue);

恢復

dispatch_resume(queue);

當Dispatch Queue掛起后,追加到隊列中但還沒有執行的任務在這之后停止執行,恢復后這些任務繼續執行。

指定任務只執行一次

通過dispatch_once()函數指定的任務只執行一次,像單例的初始化就可以用該函數來實現。

正常我們書寫單例的方法是:

static NSObject obj = nil;

@synchronized (self) {

if (obj == nil) {

obj = ...

}

}

使用dispatch_once()函數的實現方式是:

static NSObject obj = nil;

static dispatch_once_t pred;

dispatch_once( &pred, ^{

obj = ...

});

使用dispatch_once()函數可以保證在多線程環境下百分之百安全。

dispatch_sync()與dispatch_async()的區別

前面使用頻率特別高的添加任務函數dispatch_async(),該函數是非同步的,它只負責將任務添加到隊列中,并不在乎添加到隊列中的任務是否處理完成,立刻返回。

而相對于dispatch_async()函數的dispatch_sync()函數是同步的,dispatch_sync()函數不但負責將任務添加到隊列中,還要等待添加的任務執行完成再返回,在此過程中調用dispatch_sync()函數所在的線程被掛起,直到dispatch_sync()函數返回,線程恢復,注意是調用dispatch_sync()函數的線程被掛起

關于dispatch_sync()函數比較重要的一個問題就是死鎖,為什么會出現死鎖的情況呢?比如說有一個串行隊列,并且dispatch_sync()函數的調用也是在該隊列中,這樣串行隊列的線程在調用dispatch_sync()函數的時候被掛起,而線程被掛起之后dispatch_sync()函數添加的任務一直得不到線程的處理,一直不能返回,所以線程將一直處于被掛起的狀態。

出現這種狀況的核心問題就是(可能有點繞):調用dispatch_sync()函數的線程(注意是線程,而不是隊列,并行隊列有多個線程可能并不會發生這種狀況,除非調用函數的任務和函數追加的任務被分配到并行隊列中同一線程中去)和處理函數追加的任務的線程是同一個線程。此時就會發生死鎖。

舉兩個例子體會一下:

dispatch_queue_t mainQueue = dispatch_get_main_queue();

dispatch_sync(mainQueue, ^{NSLog(@"任務");});

// 死鎖

dispatch_queue_t queue = dispatch_queue_create("OneSerialDispatchQueue", NULL);

dispatch_async(queue, ^{

dispatch_sync(queue, ^{NSLog(@"任務");});

});

// 死鎖

dispatch_apply()函數

該函數作用是一次性向隊列中添加多個任務,并且跟dispatch_sync()函數的使用方式一致,是同步的,只有向隊列中添加的所有任務都執行完成才返回。并且dispatch_apply()函數向隊列中追加的block任務都是帶有參數的,這是為了函數將添加序號作為參數傳遞給block任務。

看下面一段代碼:

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_apply(8, queue, ^(size_t index){

NSLog(@"這是第%zu任務", index);

});

NSLog(@"所有任務處理完成");

執行結果:

雖然我這里的執行結果是順序的,但也有可能執行的結果是無序的,因為這里使用的是并行隊列。但無論前8個任務的順序是怎樣所有任務處理完成這個任務一定是最后一個執行。

相信理解了同步概念就一定會明白其中原因了。

< 轉載 >

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容

  • 我們知道在iOS開發中,一共有四種多線程技術:pthread,NSThread,GCD,NSOperation: ...
    請叫我周小帥閱讀 1,499評論 0 1
  • 本篇博客共分以下幾個模塊來介紹GCD的相關內容: 多線程相關概念 多線程編程技術的優缺點比較? GCD中的三種隊列...
    dullgrass閱讀 37,868評論 30 236
  • iOS中GCD的使用小結 作者dullgrass 2015.11.20 09:41*字數 4996閱讀 20199...
    DanDanC閱讀 851評論 0 0
  • 本篇博客共分以下幾個模塊來介紹GCD的相關內容: 多線程相關概念 多線程編程技術的優缺點比較? GCD中的三種隊列...
    有夢想的老伯伯閱讀 1,027評論 0 4
  • 背景 擔心了兩周的我終于輪到去醫院做胃鏡檢查了!去的時候我都想好了最壞的可能(胃癌),之前在網上查的癥狀都很相似。...
    Dely閱讀 9,256評論 21 42