iOS 多線程相關(guān)

在 iOS 中其實目前有 4 套多線程方案俱饿,他們分別是:

  • Pthreads
  • NSThread
  • GCD
  • NSOperation & NSOperationQueue

使用方法

1.Pthreads (很少用)

POSIX線程(POSIX threads),簡稱Pthreads,是線程的POSIX標(biāo)準(zhǔn)。該標(biāo)準(zhǔn)定義了創(chuàng)建和操縱線程的一整套API来氧。在類Unix操作系統(tǒng)(Unix攒钳、Linux挽荠、Mac OS X等)中,都使用Pthreads作為操作系統(tǒng)的線程德挣。

簡單地說恭垦,這是一套在很多操作系統(tǒng)上都通用的多線程API,所以移植性很強(qiáng)(然并卵),當(dāng)然在 iOS 中也是可以的番挺。不過這是基于 c語言 的框架唠帝,使用起來諸多不便。

使用方法:
1.導(dǎo)入頭文件

#import <pthread.h>

2.然后創(chuàng)建線程玄柏,并執(zhí)行任務(wù)

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    pthread_t thread;
    //創(chuàng)建一個線程并自動執(zhí)行
    pthread_create(&thread, NULL, start, NULL);
}

void *start(void *data) {
    NSLog(@"%@", [NSThread currentThread]);

    return NULL;
}

這種多線程方式需要手動處理線程的各個狀態(tài)的轉(zhuǎn)換即管理生命周期襟衰,比如,這段代碼雖然創(chuàng)建了一個線程粪摘,但并沒有銷毀瀑晒。

2.NSThread

這種方式是完全面向?qū)ο蟮摹K阅憧梢灾苯硬倏鼐€程對象徘意,非常直觀和方便苔悦。但是,它的生命周期還是需要我們手動管理椎咧,所以這套方案也是偶爾用用间坐,比如 [NSThread currentThread],它可以獲取當(dāng)前線程類邑退,你就可以知道當(dāng)前線程的各種屬性竹宋,用于調(diào)試十分方便。下面來看看它的一些用法地技。

使用方法:

  1. 先創(chuàng)建線程類蜈七,再啟動
// 創(chuàng)建
  NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];

  // 啟動
  [thread start];
  1. 創(chuàng)建并自動啟動
  [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];

其實,NSThread 用起來也挺簡單的莫矗,因為它就那幾種方法飒硅。同時,我們也只有在一些非常簡單的場景才會用 NSThread作谚。

3.GCD

Grand Central Dispatch三娩,宏大的中央處理中樞。它是蘋果為多核的并行運算提出的解決方案妹懒,所以會自動合理地利用更多的CPU內(nèi)核(比如雙核雀监、四核),最重要的是它會自動管理線程的生命周期(創(chuàng)建線程眨唬、調(diào)度任務(wù)会前、銷毀線程),完全不需要我們管理匾竿,我們只需要告訴干什么就行瓦宜。同時它使用的也是 c語言,不過由于使用了 Block(Swift里叫做閉包)岭妖,使得使用起來更加方便临庇,而且靈活反璃。所以基本上大家都在使用 GCD 。

首先理解幾個概念

  • 任務(wù):即操作假夺,你想要干什么淮蜈,說白了就是一段代碼,在 GCD 中就是一個 Block侄泽,所以添加任務(wù)十分方便礁芦。任務(wù)有兩種執(zhí)行方式: 同步執(zhí)行 和 異步執(zhí)行,他們之間的區(qū)別是 是否會創(chuàng)建新的線程悼尾。
    同步(sync)異步(async) 的主要區(qū)別在于會不會阻塞當(dāng)前線程柿扣,直到 Block 中的任務(wù)執(zhí)行完畢!
    如果是 同步(sync) 操作闺魏,它會阻塞當(dāng)前線程并等待 Block 中的任務(wù)執(zhí)行完畢未状,然后當(dāng)前線程才會繼續(xù)往下運行。
    如果是 異步(async)操作析桥,當(dāng)前線程會直接往下執(zhí)行司草,它不會阻塞當(dāng)前線程。

  • ** 隊列**:用于存放任務(wù)泡仗。一共有兩種隊列埋虹, 串行隊列 和 并行隊列。

  • 串行隊列 中的任務(wù)會根據(jù)隊列的定義 FIFO 的執(zhí)行娩怎,一個接一個的先進(jìn)先出的進(jìn)行執(zhí)行搔课。放到串行隊列的任務(wù),GCD 會 FIFO(先進(jìn)先出) 地取出來一個截亦,執(zhí)行一個爬泥,然后取下一個,這樣一個一個的執(zhí)行崩瓤。

  • 并行隊列 中的任務(wù) 根據(jù)同步或異步有不同的執(zhí)行方式袍啡。放到并行隊列的任務(wù),GCD 也會 FIFO的取出來却桶,但不同的是境输,它取出來一個就會放到別的線程,然后再取出來一個又放到另一個的線程肾扰。這樣由于取的動作很快畴嘶,忽略不計,看起來集晚,所有的任務(wù)都是一起執(zhí)行的。不過需要注意区匣,GCD 會根據(jù)系統(tǒng)資源控制并行的數(shù)量偷拔,所以如果任務(wù)很多蒋院,它并不會讓所有任務(wù)同時執(zhí)行。

創(chuàng)建隊列

  • 主隊列:這是一個特殊的 串行隊列莲绰。什么是主隊列欺旧,大家都知道吧,它用于刷新 UI蛤签,任何需要刷新 UI 的工作都要在主隊列執(zhí)行辞友,所以一般耗時的任務(wù)都要放到別的線程執(zhí)行。
//OBJECTIVE-C
  dispatch_queue_t queue = dispatch_get_main_queue();
  • 自己創(chuàng)建的隊列:其中第一個參數(shù)是標(biāo)識符震肮,用于 DEBUG 的時候標(biāo)識唯一的隊列称龙,可以為空。大家可以看xcode的文檔查看參數(shù)意義戳晌。
//OBJECTIVE-C
  //串行隊列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", NULL);
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
  //并行隊列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);
  • 全局并行隊列: 只要是并行任務(wù)一般都加入到這個隊列鲫尊。這是系統(tǒng)提供的一個并發(fā)隊列。
//OBJECTIVE-C
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

創(chuàng)建任務(wù)

  • 同步任務(wù):會阻塞當(dāng)前線程 (SYNC)
dispatch_sync(<#queue#>, ^{
      //code here
      NSLog(@"%@", [NSThread currentThread]);
  });
  • 異步任務(wù):不會阻塞當(dāng)前線程 (ASYNC)
dispatch_async(<#queue#>, ^{
      //code here
      NSLog(@"%@", [NSThread currentThread]);
  });

隊列組
隊列組可以將很多隊列添加到一個組里沦偎,這樣做的好處是疫向,當(dāng)這個組里所有的任務(wù)都執(zhí)行完了,隊列組會通過一個方法通知我們豪嚎。下面是使用方法搔驼,這是一個很實用的功能。

//1.創(chuàng)建隊列組
dispatch_group_t group = dispatch_group_create();
//2.創(chuàng)建隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//3.多次使用隊列組的方法執(zhí)行任務(wù), 只有異步方法
//3.1.執(zhí)行3次循環(huán)
dispatch_group_async(group, queue, ^{
    for (NSInteger i = 0; i < 3; i++) {
        NSLog(@"group-01 - %@", [NSThread currentThread]);
    }
});

//3.2.主隊列執(zhí)行8次循環(huán)
dispatch_group_async(group, dispatch_get_main_queue(), ^{
    for (NSInteger i = 0; i < 8; i++) {
        NSLog(@"group-02 - %@", [NSThread currentThread]);
    }
});

//3.3.執(zhí)行5次循環(huán)
dispatch_group_async(group, queue, ^{
    for (NSInteger i = 0; i < 5; i++) {
        NSLog(@"group-03 - %@", [NSThread currentThread]);
    }
});

//4.都完成后會自動通知
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"完成 - %@", [NSThread currentThread]);
});

4.NSOperation & NSOperationQueue

NSOperation 是蘋果公司對 GCD 的封裝,完全面向?qū)ο筇跆颍允褂闷饋砀美斫狻?大家可以看到 NSOperation 和 NSOperationQueue 分別對應(yīng) GCD 的 任務(wù) 和 隊列 魄健。操作步驟也很好理解:

  1. 將要執(zhí)行的任務(wù)封裝到一個 NSOperation 對象中。
  2. 將此任務(wù)添加到一個 NSOperationQueue 對象中泼菌。

然后系統(tǒng)就會自動在執(zhí)行任務(wù)。至于同步還是異步啦租、串行還是并行請繼續(xù)往下看:

添加任務(wù)

值得說明的是哗伯,NSOperation 只是一個抽象類,所以不能封裝任務(wù)篷角。但它有 2 個子類用于封裝任務(wù)焊刹。分別是:NSInvocationOperation 和 NSBlockOperation 。創(chuàng)建一個 Operation 后恳蹲,需要調(diào)用 start 方法來啟動任務(wù)虐块,它會 默認(rèn)在當(dāng)前隊列同步執(zhí)行。當(dāng)然你也可以在中途取消一個任務(wù)嘉蕾,只需要調(diào)用其 cancel 方法即可贺奠。

  • NSInvocationOperation : 需要傳入一個方法名。
//1.創(chuàng)建NSInvocationOperation對象
  NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

  //2.開始執(zhí)行
  [operation start];
  • NSBlockOperation
//1.創(chuàng)建NSBlockOperation對象
  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"%@", [NSThread currentThread]);
  }];

  //2.開始任務(wù)
  [operation start];

之前說過這樣的任務(wù)错忱,默認(rèn)會在當(dāng)前線程執(zhí)行儡率。但是 NSBlockOperation 還有一個方法:addExecutionBlock: 挂据,通過這個方法可以給 Operation 添加多個執(zhí)行 Block。這樣 Operation 中的任務(wù) 會并發(fā)執(zhí)行儿普,它會 在主線程和其它的多個線程 執(zhí)行這些任務(wù)崎逃,注意下面的打印結(jié)果:

//1.創(chuàng)建NSBlockOperation對象
      NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
          NSLog(@"%@", [NSThread currentThread]);
      }];

      //添加多個Block
      for (NSInteger i = 0; i < 5; i++) {
          [operation addExecutionBlock:^{
              NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
          }];
      }

      //2.開始任務(wù)
      [operation start];

打印輸出

2015-07-28 17:50:16.585 test[17527:4095467] 第2次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}

2015-07-28 17:50:16.585 test[17527:4095666] 第1次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}

2015-07-28 17:50:16.585 test[17527:4095665] <NSThread: 0x7ff5c961b610>{number = 3, name = (null)}

2015-07-28 17:50:16.585 test[17527:4095662] 第0次 - <NSThread: 0x7ff5c948d310>{number = 2, name = (null)}

2015-07-28 17:50:16.586 test[17527:4095666] 第3次 - <NSThread: 0x7ff5c972caf0>{number = 4, name = (null)}

2015-07-28 17:50:16.586 test[17527:4095467] 第4次 - <NSThread: 0x7ff5c9701910>{number = 1, name = main}

注意:addExecutionBlock 方法必須在 start() 方法之前執(zhí)行,否則就會報錯:

‘*** -[NSBlockOperation addExecutionBlock:]: blocks cannot be added after the operation has started executing or finished'

創(chuàng)建隊列

看過上面的內(nèi)容就知道眉孩,我們可以調(diào)用一個 NSOperation 對象的 start() 方法來啟動這個任務(wù)个绍,但是這樣做他們默認(rèn)是 同步執(zhí)行 的。就算是 addExecutionBlock 方法浪汪,也會在 當(dāng)前線程和其他線程 中執(zhí)行巴柿,也就是說還是會占用當(dāng)前線程。這是就要用到隊列 NSOperationQueue 了吟宦。而且篮洁,按類型來說的話一共有兩種類型:主隊列、其他隊列殃姓。只要添加到隊列袁波,會自動調(diào)用任務(wù)的 start() 方法

  • 主隊列
//OBJECTIVE-C
NSOperationQueue *queue = [NSOperationQueue mainQueue];
  • 其他隊列
    因為主隊列比較特殊,所以會單獨有一個類方法來獲得主隊列蜗侈。那么通過初始化產(chǎn)生的隊列就是其他隊列了篷牌,因為只有這兩種隊列,除了主隊列踏幻,其他隊列就不需要名字了枷颊。

注意:其他隊列的任務(wù)會在其他線程并行執(zhí)行。

//1.創(chuàng)建一個其他隊列    
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

//2.創(chuàng)建NSBlockOperation對象
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
}];

//3.添加多個Block
for (NSInteger i = 0; i < 5; i++) {
    [operation addExecutionBlock:^{
        NSLog(@"第%ld次:%@", i, [NSThread currentThread]);
    }];
}

//4.隊列添加任務(wù)
[queue addOperation:operation];

NSOperationQueue 有一個參數(shù) maxConcurrentOperationCount 最大并發(fā)數(shù)该面,用來設(shè)置最多可以讓多少個任務(wù)同時執(zhí)行夭苗。當(dāng)你把它設(shè)置為 1 的時候,那就是串行隔缀!

NSOperationQueue 還有一個添加任務(wù)的方法题造,- (void)addOperationWithBlock:(void (^)(void))block; ,這是不是和 GCD 差不多猾瘸?這樣就可以添加一個任務(wù)到隊列中了界赔,十分方便。

NSOperation 有一個非常實用的功能牵触,那就是添加依賴淮悼。比如有 3 個任務(wù):A: 從服務(wù)器上下載一張圖片,B:給這張圖片加個水印揽思,C:把圖片返回給服務(wù)器袜腥。這時就可以用到依賴了:

//1.任務(wù)一:下載圖片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"下載圖片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//2.任務(wù)二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"打水印   - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//3.任務(wù)三:上傳圖片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"上傳圖片 - %@", [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1.0];
}];

//4.設(shè)置依賴
[operation2 addDependency:operation1];      //任務(wù)二依賴任務(wù)一
[operation3 addDependency:operation2];      //任務(wù)三依賴任務(wù)二

//5.創(chuàng)建隊列并加入任務(wù)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

注意:不能添加相互依賴,會死鎖钉汗,比如 A依賴B瞧挤,B依賴A锡宋;可以使用 removeDependency 來解除依賴關(guān)系儡湾;可以在不同的隊列之間依賴特恬,反正就是這個依賴是添加到任務(wù)身上的,和隊列沒關(guān)系徐钠。

其他用法

線程同步

所謂線程同步就是為了防止多個線程搶奪同一個資源造成的數(shù)據(jù)安全問題癌刽,所采取的一種措施。當(dāng)然也有很多實現(xiàn)方法尝丐,請往下看:

  • 互斥鎖 :給需要同步的代碼塊加一個互斥鎖显拜,就可以保證每次只有一個線程訪問此代碼塊。
@synchronized(self) {
    //需要執(zhí)行的代碼塊
}
  • 同步執(zhí)行 :我們可以使用多線程的知識爹袁,把多個線程都要執(zhí)行此段代碼添加到同一個串行隊列远荠,這樣就實現(xiàn)了線程同步的概念。當(dāng)然這里可以使用 GCD 和 NSOperation 兩種方案失息,我都寫出來譬淳。
//GCD
  //需要一個全局變量queue,要讓所有線程的這個操作都加到一個queue中
  dispatch_sync(queue, ^{
      NSInteger ticket = lastTicket;
      [NSThread sleepForTimeInterval:0.1];
      NSLog(@"%ld - %@",ticket, [NSThread currentThread]);
      ticket -= 1;
      lastTicket = ticket;
  });


  //NSOperation & NSOperationQueue
  //重點:1. 全局的 NSOperationQueue, 所有的操作添加到同一個queue中
  //       2. 設(shè)置 queue 的 maxConcurrentOperationCount 為 1
  //       3. 如果后續(xù)操作需要Block中的結(jié)果盹兢,就需要調(diào)用每個操作的waitUntilFinished邻梆,阻塞當(dāng)前線程,一直等到當(dāng)前操作完成绎秒,才允許執(zhí)行后面的浦妄。waitUntilFinished 要在添加到隊列之后!

  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSInteger ticket = lastTicket;
      [NSThread sleepForTimeInterval:1];
      NSLog(@"%ld - %@",ticket, [NSThread currentThread]);
      ticket -= 1;
      lastTicket = ticket;
  }];

  [queue addOperation:operation];

  [operation waitUntilFinished];

  //后續(xù)要做的事
延遲執(zhí)行

所謂延遲執(zhí)行就是延時一段時間再執(zhí)行某段代碼见芹。下面說一些常用方法剂娄。

  • perform
// 3秒后自動調(diào)用self的run:方法,并且傳遞參數(shù):@"abc"
  [self performSelector:@selector(run:) withObject:@"abc" afterDelay:3];
  • NSTimer

NSTimer 是iOS中的一個計時器類玄呛,除了延遲執(zhí)行還有很多用法阅懦,不過這里直說延遲執(zhí)行的用法。同樣只寫 OC 版的把鉴,Swift 也是相同的故黑。

[NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(run:) userInfo:@"abc" repeats:NO];
單例模式
@interface Tool : NSObject <NSCopying>

+ (instancetype)sharedTool;

@end

@implementation Tool

static id _instance;

+ (instancetype)sharedTool {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[Tool alloc] init];
    });

    return _instance;
}

@end

文章摘自:伯恩的遺產(chǎn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市庭砍,隨后出現(xiàn)的幾起案子场晶,更是在濱河造成了極大的恐慌,老刑警劉巖怠缸,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诗轻,死亡現(xiàn)場離奇詭異,居然都是意外死亡揭北,警方通過查閱死者的電腦和手機(jī)扳炬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門吏颖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恨樟,你說我怎么就攤上這事半醉。” “怎么了劝术?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵缩多,是天一觀的道長。 經(jīng)常有香客問我养晋,道長衬吆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任绳泉,我火速辦了婚禮逊抡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘零酪。我一直安慰自己冒嫡,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布蛾娶。 她就那樣靜靜地躺著灯谣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛔琅。 梳的紋絲不亂的頭發(fā)上胎许,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音罗售,去河邊找鬼辜窑。 笑死,一個胖子當(dāng)著我的面吹牛寨躁,可吹牛的內(nèi)容都是我干的穆碎。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼职恳,長吁一口氣:“原來是場噩夢啊……” “哼所禀!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起放钦,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤色徘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后操禀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體褂策,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了斤寂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耿焊。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖遍搞,靈堂內(nèi)的尸體忽然破棺而出罗侯,到底是詐尸還是另有隱情,我是刑警寧澤尾抑,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布歇父,位于F島的核電站,受9級特大地震影響再愈,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜护戳,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一翎冲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧媳荒,春花似錦抗悍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鱼炒,卻和暖如春衔沼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昔瞧。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工指蚁, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人自晰。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓凝化,卻偏偏與公主長得像,于是被迫代替她去往敵國和親酬荞。 傳聞我的和親對象是個殘疾皇子搓劫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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