iOS開發之多線程(3)—— GCD

版本

Xcode 11.5
Swift 5.2.2

簡介

Grand Central Dispatch, 強大的中央調度器, 額… 我們還是叫GCD吧.

幾個概念

1. 任務(Task) 和 隊列(Queue)

  • 任務就是將要在線程中執行的代碼(塊), GCD中爲block. 執行任務有兩種方式: 同步執行和異步執行.
    dispatch_async(queue, ^{
        // 任務
    });
  • 隊列不是線程, 把這兩者區分開會比較容易駕馭本文. 隊列是用於裝載線程任務的隊形結構, 遵循FIFO(先進先出)原則. 隊列有兩種: 串行隊列和併發隊列. 另外有兩個系統提供的特殊隊列: 主隊列(串行隊列) 和 全局隊列(併發隊列).
    dispatch_queue_t queue = dispatch_queue_create("我是標籤", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        // 這裏的任務(代碼塊)會被添加到隊列queue中
    });

2. 同步(sync) 和 異步(async)

  • 同步執行會阻塞當前代碼, 不具備開啓線程的能力.
    但不代表同步執行就一定在當前線程執行, 例如在其他線程同步執行到主隊列, 最終是在主線程執行的(因爲主線程始終存在, 所以我們說沒有開啓新線程). 除了調用主隊列, 同步執行的任務都是在當前線程完成.
    // 在其他線程同步執行到主隊列
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"1 %@", [NSThread currentThread]);
        dispatch_sync(dispatch_get_main_queue(), ^{
            NSLog(@"2 %@", [NSThread currentThread]);
        });
    });
  
-----------------------------------------------------------------------------------------------------  
    log:
    1 <NSThread: 0x600000e78140>{number = 4, name = (null)}
    2 <NSThread: 0x600000e30180>{number = 1, name = main}
  • 異步執行不會阻塞當前線程, 具備開啓新線程能力.
    但不是說異步執行就一定會開啓新線程, 例如異步執行到主隊列不會開啓新線程, 又例如多個異步執行到相同隊列也可能不會開啓相應數量的線程.
    // 異步執行+全局隊列(併發隊列)
    for (int i=0; i<8; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"%d %@", i, [NSThread currentThread]);
        });
    }

-----------------------------------------------------------------------------------------------------
    log:
    0 <NSThread: 0x600000b76680>{number = 5, name = (null)}
    2 <NSThread: 0x600000b75700>{number = 6, name = (null)}
    1 <NSThread: 0x600000b75a40>{number = 4, name = (null)}
    3 <NSThread: 0x600000b76a00>{number = 3, name = (null)}
    4 <NSThread: 0x600000b76680>{number = 5, name = (null)}
    5 <NSThread: 0x600000b75700>{number = 6, name = (null)}
    6 <NSThread: 0x600000b76a00>{number = 3, name = (null)}
    7 <NSThread: 0x600000b75a40>{number = 4, name = (null)}

GCD會根據系統資源控制並行的數量, 所以如果任務很多, 它並不會讓所有任務同時執行.

3. 串行(Serial) 和 併發(Concurrent)

隊列有兩種: 串行隊列和併發隊列. 這兩個隊列都是遵循FIFO(先進先出)原則的.

  • 串行隊列裏的任務是一個一個執行的.
    串行隊列.png

  • 併發隊列裏的任務是可以多個同時執行的.
    雖然併發隊列也是一個一個任務取出來(FIFO原則), 但是由於取出來很快(因爲可以開啓多個線程來執行任務), 我們認爲這些已經取出來的任務是同步執行的.
    併發隊列.png

注:

  1. 任務被添加到併發隊列的順序是任意的, 所以最終可能以任意順序完成, 你不會知道何時開始運行下一個任務, 或者任意時刻有多少 Block 在運行. 這些完全取決於 GCD.
  2. GCD 會根據系統資源控制並行的數量, 所以如果任務很多, 它並不會讓所有任務同時執行. 也就是說, 開不開啓新線程(或者說開啓多少條新線程)由GCD決定, 但會保證不會阻塞當前線程.

4. 主隊列(Main Queue) 和 全局隊列(Global Queue)

  • 主隊列也是串行隊列, 但是主隊列中的所有任務都會被系統放到主線程中執行. 主線程用來處理UI相關操作, 所以不要把耗時操作放到主線程中執行(不要把耗時任務放到主隊列中).
  • 全局隊列是併發隊列, 系統提供了四個枚舉優先級常量(background、low、default 以及 high). 注意, 全局隊列並不是後臺線程, 隊列和線程是不同的東西, 全局隊列中的任務放到哪個線程去執行由執行方式(同步或異步)以及GCD根據資源分配來決定.

注: Apple 的 API 也會使用這些全局隊列, 所以你添加的任何任務都不會是這些隊列中唯一的任務.

GCD的基本使用

通過了解前面的概念, 我們知道執行方式(同步和異步)和隊列(串行和併發)組合起來有四種寫法, 但由於主隊列是特殊的隊列(主隊列中的任務都會被系統放入主線程中去執行), 因此我們還得討論主隊列和兩種執行方式的組合. 而全局隊列也是併發隊列, 故不需要單獨拿出來討論.
綜上, 我們將討論以下六種寫法:

  • 同步執行 + 串行隊列
  • 同步執行 + 併發隊列
  • 異步執行 + 串行隊列
  • 異步執行 + 併發隊列
  • 同步執行 + 主隊列
  • 異步執行 + 主隊列

先列表總結一下吧:

串行隊列 併發隊列 主隊列
同步 阻塞 / 不開新線程 / 按順序 阻塞 / 不開新線程 / 按順序 阻塞 / 不開新線程(在主線程) / 按順序
異步 不阻塞 / 不開或只開一條 / 按順序 不阻塞 / 至少開一條 / 亂序 不阻塞 / 不開新線程(在主線程) / 按順序

什麼時候阻塞?
只要是同步執行肯定會阻塞; 只要是異步執行肯定不阻塞.

什麼時候開啓新線程(不包括主線程)?
tag1: 需要同時執行兩個或兩個以上任務(block)時, 纔會開啓新線程 (因爲一個線程同一時間只能執行一個任務).
!!! 前方高能, 請戴好口罩. !!!
首先, 同步執行肯定不會開啓新線程. 因爲同步執行的目的是爲了阻塞當前線程, 等執行完了block, 纔會繼續往下執行. 也就是說, 同步執行在同一時刻只有一個任務, 所以不會開啓新線程.
異步執行可分四種情況討論:

  1. 當前線程對應的隊列是串行隊列(串行1), 異步執行+當前隊列(串行1)不會開啓新線程. 因爲當前只有一個隊列(串行1), 而這個隊列裏的任務是一個一個順序執行的, 沒有必要開啓新線程.
  2. 當前線程對應的隊列是串行隊列(串行1), 異步執行+其他串行隊列(串行2)會開啓一條新線程. 因爲異步執行不阻塞串行1當前的任務, 而串行2裏面的任務需要同時執行, 所以只能開新線程. 又因爲串行2裏任務是一個個順序執行的, 所以只會新開一個線程.
  3. 當前線程對應的隊列是併發隊列(併發1), 異步執行+串行隊列(串行1)會開啓一條新線程. 因爲併發1當前的任務和串行1裏的任務都要同時執行, 所以需要開啓新線程. 又因爲串行1是串行的, 所以只會開一條線程.
  4. 當前線程對應的隊列是併發隊列(併發1), 異步執行+併發隊列(不管是不是當前隊列, 併發2)都會開啓新線程. 因爲併發1當前的任務和併發2裏的任務需要同時執行, 所以只能開啓新線程. 又因爲併發2是併發的(多個任務同時執行), 所以會開啓多條線程. 線程數量由GCD根據當前資源(內存使用狀況, 線程池中線程數等因素)決定.

總結: 請回頭看tag1.

1. 同步執行 + 串行隊列

  • 阻塞當前線程
  • 不開啓新線程
  • 任務按順序依次執行

OC

// 同步執行 + 串行隊列
- (void)syncSerial {
    
    NSLog(@"start, %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.syncSerialQueue", DISPATCH_QUEUE_SERIAL);
    for (int i=0; i<5; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"end, %@", [NSThread currentThread]);
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600003000240>{number = 1, name = main}
0, <NSThread: 0x600003000240>{number = 1, name = main}
1, <NSThread: 0x600003000240>{number = 1, name = main}
2, <NSThread: 0x600003000240>{number = 1, name = main}
3, <NSThread: 0x600003000240>{number = 1, name = main}
4, <NSThread: 0x600003000240>{number = 1, name = main}
end, <NSThread: 0x600003000240>{number = 1, name = main}

Swift

// 同步執行 + 串行隊列
@objc func syncSerial() {
    
    print("start, \(Thread.current)")
    
    let queue = DispatchQueue(label: "com.KKThreadsDemo.syncSerialQueue")
    for i in 0..<5 {
        queue.sync {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

2. 同步執行 + 併發隊列

  • 阻塞當前線程
  • 不開啓新線程
  • 任務按順序執行

OC

// 同步執行 + 併發隊列
- (void)syncConcurrent {
    
    NSLog(@"start, %@", [NSThread currentThread]);
    
    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.syncConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i=0; i<5; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }
    
    NSLog(@"end, %@", [NSThread currentThread]);
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600000354280>{number = 1, name = main}
0, <NSThread: 0x600000354280>{number = 1, name = main}
1, <NSThread: 0x600000354280>{number = 1, name = main}
2, <NSThread: 0x600000354280>{number = 1, name = main}
3, <NSThread: 0x600000354280>{number = 1, name = main}
4, <NSThread: 0x600000354280>{number = 1, name = main}
end, <NSThread: 0x600000354280>{number = 1, name = main}

Swift

// 同步執行 + 併發隊列
@objc func syncConcurrent() {
    
    print("start, \(Thread.current)")
    
    // 二選一
    let queue = DispatchQueue(label: "com.KKThreadsDemo.syncConcurrentQueue", attributes: .concurrent)
    let queue = DispatchQueue(label: "com.KKThreadsDemo.syncConcurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: .none)
    for i in 0..<5 {
        queue.sync {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

3. 異步執行 + 串行隊列

  • 不阻塞當前線程
  • 不開或只開一條新線程
  • 任務按順序執行

OC

// 異步執行 + 串行隊列
- (void)asyncSerial {
    
#if 1
    NSLog(@"start, %@", [NSThread currentThread]);

    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncSerialQueue", DISPATCH_QUEUE_SERIAL);
    for (int i=0; i<5; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }

    NSLog(@"end, %@", [NSThread currentThread]);
    
#else
    
    /* ------ 如果添加到當前線程對應的隊列, 則不開啓新線程 ------ */
    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncSerialQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        
        NSLog(@"start, %@", [NSThread currentThread]);
        
        for (int i=0; i<5; i++) {
            dispatch_async(queue, ^{
                NSLog(@"%d, %@", i, [NSThread currentThread]);
            });
        }
        
        NSLog(@"end, %@", [NSThread currentThread]);
    });
#endif
}

-----------------------------------------------------------------------------------------------------
log if 1:
start, <NSThread: 0x600003ad00c0>{number = 1, name = main}
end, <NSThread: 0x600003ad00c0>{number = 1, name = main}
0, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
1, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
2, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
3, <NSThread: 0x600000a303c0>{number = 5, name = (null)}
4, <NSThread: 0x600000a303c0>{number = 5, name = (null)}

-----------------------------------------------------------------------------------------------------
log if 0:
start, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
end, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
0, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
1, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
2, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
3, <NSThread: 0x6000012a7080>{number = 3, name = (null)}
4, <NSThread: 0x6000012a7080>{number = 3, name = (null)}

Swift

// 異步執行 + 串行隊列
@objc func asyncSerial() {
    
    print("start, \(Thread.current)")
            
    // 二選一
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncSerialQueue")
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncSerialQueue", attributes: .init(rawValue: 0))
    for i in 0..<5 {
        queue.async {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

爲什麼先打印了任務0再打印end ?
因爲新開了一個線程4來執行任務, 且沒有阻塞當前線程, 線程4和當前線程形成並行關係, CPU在線程間來回切換運算, 不確定是先執行那一條線程.

4. 異步執行 + 併發隊列

  • 不阻塞當前線程
  • 至少開啓一條新線程 (數量由GCD根據當前資源決定)
  • 任務亂序執行

OC

// 異步執行 + 併發隊列
- (void)asyncConcurrent {
    
    NSLog(@"start, %@", [NSThread currentThread]);

    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.asyncConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    for (int i=0; i<5; i++) {
        dispatch_async(queue, ^{
            NSLog(@"%d, %@", i, [NSThread currentThread]);
        });
    }

    NSLog(@"end, %@", [NSThread currentThread]);
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600000c1c100>{number = 1, name = main}
end, <NSThread: 0x600000c1c100>{number = 1, name = main}
0, <NSThread: 0x600001c02880>{number = 6, name = (null)}
3, <NSThread: 0x600001c023c0>{number = 3, name = (null)}
4, <NSThread: 0x600001c02880>{number = 6, name = (null)}
1, <NSThread: 0x600001c024c0>{number = 5, name = (null)}
2, <NSThread: 0x600001c75c00>{number = 4, name = (null)}

Swift

// 異步執行 + 併發隊列
@objc func asyncConcurrent() {
    
    print("start, \(Thread.current)")
            
    // 二選一
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncConcurrentQueue", attributes: .concurrent)
    let queue = DispatchQueue(label: "com.KKThreadsDemo.asyncConcurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .inherit, target: .none)
    for i in 0..<5 {
        queue.async {
            print("\(i), \(Thread.current)")
        }
    }
    
    print("end, \(Thread.current)")
}

5. 同步執行 + 主隊列

如在主線程中使用, 將會造成死鎖, 系統報錯. 關於死鎖, 詳見下小節.

在非主線程中使用:

  • 阻塞當前線程
  • 不開新線程, 在主線程中執行
  • 任務按順序執行

OC

// 同步執行 + 主隊列
- (void)syncMain {
    
    // 在非主線程中使用
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"start, %@", [NSThread currentThread]);

        for (int i=0; i<5; i++) {
            dispatch_sync(dispatch_get_main_queue(), ^{
                NSLog(@"%d, %@", i, [NSThread currentThread]);
            });
        }

        NSLog(@"end, %@", [NSThread currentThread]);
    });
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x60000291a880>{number = 5, name = (null)}
0, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
1, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
2, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
3, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
4, <NSThread: 0x6000004e0cc0>{number = 1, name = main}
end, <NSThread: 0x60000291a880>{number = 5, name = (null)}

Swift

// 同步執行 + 主隊列
@objc func syncMain() {
    
    // 在非主線程中使用
    DispatchQueue.global().async {
        
        print("start, \(Thread.current)")

        for i in 0..<5 {
            DispatchQueue.main.sync {
                print("\(i), \(Thread.current)")
            }
        }
        
        print("end, \(Thread.current)")
    }
}

6. 異步執行 + 主隊列

  • 不阻塞當前線程
  • 不開新線程, 在主線程中執行
  • 任務按順序執行 (主線程是串行線程)

OC

// 異步執行 + 主隊列
- (void)asyncMain {
    
    // 可在任意線程中使用
//    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"start, %@", [NSThread currentThread]);

        for (int i=0; i<5; i++) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"%d, %@", i, [NSThread currentThread]);
            });
        }

        NSLog(@"end, %@", [NSThread currentThread]);
//    });
}

-----------------------------------------------------------------------------------------------------
log:
start, <NSThread: 0x600001f94100>{number = 1, name = main}
end, <NSThread: 0x600001f94100>{number = 1, name = main}
0, <NSThread: 0x600001f94100>{number = 1, name = main}
1, <NSThread: 0x600001f94100>{number = 1, name = main}
2, <NSThread: 0x600001f94100>{number = 1, name = main}
3, <NSThread: 0x600001f94100>{number = 1, name = main}
4, <NSThread: 0x600001f94100>{number = 1, name = main}

Swift

// 異執行 + 主隊列
@objc func asyncMain() {
    
    // 可在任意線程中使用
    DispatchQueue.global().async {
        
        print("start, \(Thread.current)")

        for i in 0..<5 {
            DispatchQueue.main.async {
                print("\(i), \(Thread.current)")
            }
        }
        
        print("end, \(Thread.current)")
    }
}

死鎖

什麼是死鎖?

網友
兩個線程卡住了, 彼此等待對方完成或執行其他操作.

蘋果
You attempted to lock a system resource that would have resulted in a deadlock.
您試圖鎖定可能導致死鎖的系統資源。

百度
死鎖是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。

維基
In an operating system, a deadlock occurs when a process or thread enters a waiting state because a requested system resource is held by another waiting process, which in turn is waiting for another resource held by another waiting process. If a process is unable to change its state indefinitely because the resources requested by it are being used by another waiting process, then the system is said to be in a deadlock.
在操作系統中,當進程或線程進入等待狀態時會發生死鎖,因爲所請求的系統資源由另一個等待進程持有,而該等待進程又正在等待另一個等待進程持有的另一個資源。如果某個進程由於另一個進程正在使用該進程所請求的資源而無法無限期更改其狀態,則稱該系統處於死鎖狀態

以上定義, 我都不滿意.


在GCD中, 當線程進入等待狀態時, 該線程中的一個任務和另一個任務形成循環依賴 (即每個任務都要等待另一個任務執行完, 自己才能開始或繼續), 這種陷入僵局的無限等待狀態稱之爲死鎖狀態, 也即死鎖.

🍺🍺🍺

造成死鎖的原因?
兩個任務(block)相互等待.

爲什麼會等待?
舉例說明:

    dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{

        // block1

        dispatch_sync(queue, ^{
            // block2
        });
    });

例子中, 在block1中同步提交了一個任務block2到隊列queue, 這時queue裏面就同時有了兩個任務: block1和block2. 一方面, block1被阻塞, 需要等block2執行完返回後纔算執行結束; 另一方面, 由於這queue是串行的, 只能一個個任務順序執行 (前一個任務執行完了後一個才能開始), 也就是說block1不執行完block2是沒辦法從隊列中取出來開始執行的, 沒開始何談結束? 你以爲談戀愛啊😂😂 就這樣, 死鎖了.

舉個反例吧, 上面例子中DISPATCH_QUEUE_SERIAL改爲DISPATCH_QUEUE_CONCURRENT就不會死鎖了:

dispatch_queue_t queue = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_CONCURRENT);

解析
queue變成了併發隊列, 併發隊列裏的任務是可以多個同時執行的, 或者說, 不用等前面的任務執行完就可以立馬取出下一個任務來執行. 例子中, 雖然此時線程只有一個(因爲同步執行), 猜測是block1被保存了上下文, 中斷去執行block2, block2返回後再根據上下文取出block1繼續執行.

結論: 同步提交一個任務到一個串行隊列, 並且這個隊列與執行當前代碼的隊列相同, 則一定會導致死鎖.

以下蘋果文檔可作爲佐證:

蘋果文檔
If the queue you pass as a parameter to the function is a serial queue and is the same one executing the current code, calling these functions will deadlock the queue.

事實上, 這個結論是我見到過的所有GCD中死鎖的情況了. 如果有人有不同見解, 還望不因賜教. 😁😁

幾種常見的死鎖情形 (都符合上文結論):

// 死鎖 (主線程裏調用)
- (void)deadlock {
    
    // 死鎖1
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"%@", [NSThread currentThread]);
    });
    

    // 死鎖2
    dispatch_queue_t queue2 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue2, ^{

        NSLog(@"start, %@", [NSThread currentThread]);

        dispatch_sync(queue2, ^{
            NSLog(@"1, %@", [NSThread currentThread]);
        });

        NSLog(@"end, %@", [NSThread currentThread]);
    });
    
    
    // 死鎖3
    dispatch_queue_t queue3 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue3, ^{

        NSLog(@"start, %@", [NSThread currentThread]);

        dispatch_sync(dispatch_get_main_queue(), ^{

            NSLog(@"1, %@", [NSThread currentThread]);

            dispatch_sync(queue3, ^{
                NSLog(@"2, %@", [NSThread currentThread]);
            });
        });

        NSLog(@"end, %@", [NSThread currentThread]);
    });
    
    
    // 死鎖4
    dispatch_queue_t queue4 = dispatch_queue_create("com.KKThreadsDemo.deadlockQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue4, ^{

        NSLog(@"start, %@", [NSThread currentThread]);

        dispatch_barrier_sync(queue4, ^{
            NSLog(@"1 %@", [NSThread currentThread]);
        });
        
        NSLog(@"end %@", [NSThread currentThread]);
    });
}

解決辦法有二:

  1. 使用async
  2. 提交任務到另一個串行/併發隊列 (只要不是當前正在執行的串行隊列就行)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章