OC底層知識(shí)點(diǎn)之-多線程(二)GCD上篇

GCD簡介

  • GCD全稱:Grand Central Dispatch
  • GCD是純C語言,提供了非常多的強(qiáng)大函數(shù)
  • GCD是非常高效的多線程開發(fā)方式,它并不是Cocoa框架的一部分

GCD優(yōu)勢(shì)

  • 1.GCD 是蘋果公司為多核的并?運(yùn)算提出的解決?案
  • 2.GCD 會(huì)?動(dòng)利?更多的CPU內(nèi)核(?如雙核、四核)
  • 3.GCD 會(huì)?動(dòng)管理線程的?命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷毀線程)
  • 4.開發(fā)者只需要告訴 GCD 想要執(zhí)?什么任務(wù),不需要編寫任何線程管理代碼

【總結(jié)】:GCD就是將任務(wù)添加到隊(duì)列,并且指定執(zhí)行任務(wù)的函數(shù)。

GCD使用

在GCD使用中我們只需要做兩件事:1.定義任務(wù)。2.將任務(wù)添加到隊(duì)列中。所以GCD的核心就是dispatch隊(duì)列和任務(wù)。

GCD隊(duì)列

下面是GCD獲取隊(duì)列的集中方式:

  • 1.主線程隊(duì)列:提交的任務(wù)將會(huì)在主線程完成
    • 可以通過dispatch_get_main_queue()來獲得。
    • 主隊(duì)列就是主線程,它是一個(gè)串行隊(duì)列,在iOS中只有主線程才能擁有權(quán)限向渲染服務(wù)提交圖層信息,完成圖形顯示工作。所以和UI相關(guān)操作,必須在主線程執(zhí)行。
  • 2.全局并發(fā)隊(duì)列(Clobal Queue):全局并發(fā)隊(duì)列由整個(gè)進(jìn)程共享,有高、中(默認(rèn))、低、后臺(tái)四個(gè)優(yōu)先級(jí)
  • 3.自定義隊(duì)列
    • 并發(fā)隊(duì)列:
      • 全局隊(duì)列是并發(fā)隊(duì)列
      • 通過dispatch_queue_create創(chuàng)建,第二個(gè)參數(shù)賦值為DISPATCH_QUEUE_CONCURRENT等
      • 不用等待上個(gè)任務(wù)是否完成,直接啟用新的線程執(zhí)行新的任務(wù)。
    • 串行隊(duì)列:
      • 通過dispatch_queue_create創(chuàng)建,第二個(gè)參數(shù)賦值為DISPATCH_QUEUE_SERIAL或者NULL。
      • 串行隊(duì)列在同一時(shí)間只能執(zhí)行一個(gè)任務(wù)

整體如下圖所示:

GCD任務(wù)

GCD任務(wù)就是操作意思,就是你在block塊中的代碼通過什么方式執(zhí)行。執(zhí)行任務(wù)有兩種方式:同步和異步,兩者主要區(qū)別是:是否等待隊(duì)列的任務(wù)執(zhí)行結(jié)束,以及是否具備開辟線程的能力。

同步執(zhí)行(sync)

  • 1.同步添加任務(wù)到指定的隊(duì)列中,在添加的任務(wù)執(zhí)行結(jié)束之前,會(huì)一直等待,直到隊(duì)列里面的任務(wù)完成之后再繼續(xù)執(zhí)行。
  • 2.只能在當(dāng)前線程中執(zhí)行任務(wù),不具備開啟新線程的能力。

異步執(zhí)行(async)

  • 1.異步添加任務(wù)到指定的隊(duì)列中,它不會(huì)做任何等待,可以繼續(xù)執(zhí)行任務(wù)。
  • 2.可以在新的線程中執(zhí)行任務(wù),具備開啟新線程的能力。

我們看下GCD的最基本的寫法:

下面我們?cè)賹㈥?duì)列和任務(wù)搭配執(zhí)行看看打印結(jié)果,準(zhǔn)備代碼

/**
 同步并發(fā)
 */
- (void)concurrentSyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"同步并發(fā)-%d-%@",i,[NSThread currentThread]);
        });
    }
}

/**
 異步并發(fā)
 */
- (void)concurrentAsyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"異步并發(fā)-%d-%@",i,[NSThread currentThread]);
        });
    }
}

/**
 串行異步
 */
- (void)serialAsyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i<10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"串行異步-%d-%@",i,[NSThread currentThread]);
        });
    }
}

/**
 串行同步
 */
- (void)serialSyncTest{
    dispatch_queue_t queue = dispatch_queue_create("LJ", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i<10; i++) {
        dispatch_sync(queue, ^{
            NSLog(@"串行同步-%d-%@",i,[NSThread currentThread]);
        });
    }
}

通過任務(wù)執(zhí)行方式和不同隊(duì)列組合,我們通過打印信息可以得出如下結(jié)論:

  • 1.任務(wù)執(zhí)行方式是異步或者同步只能決定是否開辟新的線程。同步(不開辟線程),異步(開辟新的線程)
  • 2.隊(duì)列是并行還是串行只能決定是否開辟多條線程。串行(只開辟一條線程),并行(開辟多條線程,開辟多條線程的能力只有在異步執(zhí)行中發(fā)揮作用
  • 3.異步并行執(zhí)行任務(wù)是亂序的。

死鎖

造成死鎖的主要原因就是任務(wù)相互等待,看下面代碼:

運(yùn)行代碼:

發(fā)現(xiàn)報(bào)錯(cuò)了,報(bào)錯(cuò)原因就是死鎖。下面我們分析下為什么會(huì)死鎖: 這個(gè)方法有3步操作:

  • 任務(wù)一:132行打印1任務(wù),此部分在主線程。
  • 任務(wù)二:137行打印3任務(wù)
  • 任務(wù)三:134-136行通過同步任務(wù)向主線程插入打印2任務(wù)

我們知道主線程是同步任務(wù),任務(wù)一和任務(wù)二是先加入主線程,任務(wù)三會(huì)排在任務(wù)一,二后面。但是任務(wù)三是通過同步任務(wù)加入的。這就會(huì)出現(xiàn)下面的情況,任務(wù)三需要等待主線程執(zhí)行完任務(wù)一,二后才會(huì)執(zhí)行。而同步任務(wù)的出現(xiàn)會(huì)讓任務(wù)二等待任務(wù)三執(zhí)行完成后才執(zhí)行,這就造成了在主線程中任務(wù)三等待任務(wù)二完成執(zhí)行,在同步任務(wù)里出現(xiàn)任務(wù)二等待任務(wù)三完成執(zhí)行,這就造成了相互等待。出現(xiàn)死鎖崩潰 如下圖所示更容易解釋:

GCD原理初探

上面我們說了GCD的任務(wù)和隊(duì)列,并通過代碼打印來說明了任務(wù)和隊(duì)列的關(guān)系,線面我們就來看看GCD的底層實(shí)現(xiàn)

確定GCD研究源碼位置

我們想要研究GCD,卻發(fā)現(xiàn)不知從哪入手,代碼點(diǎn)擊進(jìn)去之后就走不下去了。那么我們?cè)趺粗谰€程這部分的源碼在哪呢?我們要確定源碼,我們知道dispatch_queue_create方法可以創(chuàng)建線程,那么我們打斷點(diǎn)試試

運(yùn)行代碼

這時(shí)就可以確定線程的源碼在libdispatch.dylib中。我們?cè)谔O果的官方文檔上下載libdispatch.dylib源碼。

dispatch_get_main_queue()初探

我們先看dispatch_get_main_queue()主線程

下圖是對(duì)主線程的解釋(撿主要的說一下):

  • 569-570行:主隊(duì)列是用來在應(yīng)用程序上下文中進(jìn)行交互的主線程和主runloop。
  • 579-580行:主隊(duì)列會(huì)被自動(dòng)創(chuàng)建,而且會(huì)在main()函數(shù)之前創(chuàng)建

在main()函數(shù)前被調(diào)用,就是在dyld過程中進(jìn)行的

dispatch_get_main_queue()再探

我們打印下主線程,來看看主線程是什么樣的

    dispatch_queue_t serial = dispatch_queue_create("Lj", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t conque = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0);
    NSLog(@"%@-%@-%@-%@",serial,conque,mainQueue,globQueue);

運(yùn)行打印

我們通過打印結(jié)果看到主線程變?yōu)榱薕S_dispatch_queue_main: com.apple.main-thread,說明在底層系統(tǒng)進(jìn)行命名為com.apple.main-thread。我們?cè)趌ibdispatch源碼里搜索com.apple.main-thread看看

我們發(fā)現(xiàn)這個(gè)main函數(shù)是個(gè)結(jié)構(gòu)體對(duì)象,我們看到很重要的DQF_WIDTH(行為寬度,作為非常重要的標(biāo)記)為1(1就是串行,單線程),dq_serialnum也為1。我們看到這個(gè)結(jié)構(gòu)體是_dispatch_main_q,上面說了主線程創(chuàng)建時(shí)間很早,那么我們看看_dispatch_main_q什么時(shí)候被調(diào)用的。我們搜索_dispatch_main_q后發(fā)現(xiàn)有很多

共有8個(gè)文件,42個(gè)地方出現(xiàn)

下面那么我們應(yīng)該怎么辦?

libdispatch_init

多線程的調(diào)用最早是創(chuàng)建,我們?cè)?code>講dyld的加載是提到過線程的加載:libdispatch_init OC底層原理之-dyld加載流程傳從門,搜索libdispatch_init

我們看到libdispatch_init方法很多,我們說主要方法,看下7759行代碼,我們上面說的靜態(tài)結(jié)構(gòu)體_dispatch_main_q的do_targetq等于_dispatch_get_default_queue(true)。后面就是對(duì)_dispatch_main_q進(jìn)行一系列的操作(7762行:設(shè)置當(dāng)前的主隊(duì)列,7763行:綁定到相應(yīng)的線程)。下面我們查看下綁定過程:_dispatch_queue_set_bound_thread。

通過上圖源碼我們可以看到,綁定的底層實(shí)現(xiàn)是通過os_atomic_rmw_loop2o方法處理的,這部分實(shí)不在libdispatch源碼中,后續(xù)有機(jī)會(huì)我們?cè)傺芯俊?/p>

總結(jié)

主線程下層是_dispatch_main_q的結(jié)構(gòu)體,它是在dyly加載中通過libdispatch_init方法進(jìn)行創(chuàng)建,它是一個(gè)相當(dāng)于串行隊(duì)列的隊(duì)列

dispatch_get_global_queue

我們點(diǎn)擊去看下:

dispatch_get_global_queue需要傳入兩個(gè)參數(shù):identifier和flags,注釋對(duì)這兩個(gè)參數(shù)進(jìn)行了說明:

  • identifier:服務(wù)質(zhì)量(優(yōu)先級(jí))
  • flags:預(yù)留使用

因?yàn)?code>存在優(yōu)先級(jí),就說明整個(gè)項(xiàng)目中可以有多個(gè)dispatch_get_global_queue,那么如何去設(shè)計(jì)它呢?我們可以想到通過集合去收集dispatch_get_global_queue,下面我們通過com.apple.root.default-qos來查找一下dispatch_get_global_queue全局隊(duì)列。

上圖發(fā)現(xiàn)都是通過_DISPATCH_ROOT_QUEUE_ENTRY方法去創(chuàng)建的。這里面有各種各樣不同優(yōu)先級(jí)的全局并發(fā)隊(duì)列。

我們?cè)俨榭串?dāng)前的結(jié)構(gòu)體為_dispatch_root_queues,它也是一個(gè)靜態(tài)結(jié)構(gòu)體。

隊(duì)列如何創(chuàng)建,DISPATCH_QUEUE_SERIAL和DISPATCH_QUEUE_CONCURRENT區(qū)別

上面我們簡單的講了下dispatch_get_main_queue()和dispatch_get_global_queue,知道他們底層是靜態(tài)結(jié)構(gòu)體。下面我們主要講下隊(duì)列的創(chuàng)建,以及串行和并行的實(shí)現(xiàn)原理

dispatch_queue_create

看下底層實(shí)現(xiàn)

發(fā)現(xiàn)dispatch_queue_create是通過_dispatch_lane_create_with_target創(chuàng)建的,參數(shù)分別為label以及attr,后面的DISPATCH_TARGET_QUEUE_DEFAULT、true是默認(rèn)值

我們搜索_dispatch_lane_create_with_target看下其內(nèi)部實(shí)現(xiàn)

方法很長,我們?cè)趺囱芯??我們只需要關(guān)注返回值第2809行就可以了。它返回的就是我們的線程。下面我們看下_dispatch_trace_queue_create方法

我們看到_dispatch_introspection_queue_create方法傳入的是dq,先創(chuàng)建dqic(653行創(chuàng)建,654行將dqic的dqic_queue._dq賦值為dq)。659行又將dq的do_finalizer賦值為dqic。之后就返回了upcast(dq)的_dqu。

上面并沒有我們想要的東西。我們回到_dispatch_lane_create_with_target方法,再看返回值return _dispatch_trace_queue_create(dq)._dq;這個(gè)方法返回的是_dq,上面我們知道_dispatch_introspection_queue_create返回的dq._dq中的dq是進(jìn)行賦值,和傳入的dq其實(shí)是同一個(gè)。我們只需要研究_dispatch_lane_create_with_target傳入的dq就可以了。

此時(shí)dq被創(chuàng)建

我們看到init方法里我們看到dqai.dqai_concurrent的屬性,這個(gè)屬性對(duì)線程的影響

我們看到dqai.dqai_concurrent確定的值就是width,1172行DOF_WIDTH()就是該隊(duì)列支持的線程數(shù),1就是串行(單線程),>1就是并行(多線程),也就是如果dqai.dqai_concurrent為true就是多線程,否則為單線程

下面我們看下dqai的創(chuàng)建

傳入的dqa就是我們外界傳入的值,我們看下_dispatch_queue_attr_to_info實(shí)現(xiàn)

我們看到dqai.dqai_concurrent跟idx相關(guān),而idx跟dqa相關(guān)。而dqa就是我們?cè)趧?chuàng)建線程是傳入的值(DISPATCH_QUEUE_CONCURRENT或者DISPATCH_QUEUE_SERIAL)

這個(gè)截圖是如果dqa==&_dispatch_queue_attr_concurrent就為true就是多線程。

我們?cè)倩氐絖dispatch_lane_create_with_target方法

如果是串行,vtable賦值傳值為queue_concurrent,如果是并行vtable賦值傳值為queue_serial,這樣寫是為了賦值,vtable也是個(gè)對(duì)象

通過上圖我們可以知道:并行隊(duì)列vtable為:OS_dispatch_queue_concurrent_class,而串行隊(duì)列vtable為:OS_dispatch_queue_serial_class。而vtable對(duì)象應(yīng)該為并行:OS_dispatch_queue_concurrent,串行:OS_dispatch_queue_serial 下面我們?nèi)ゴ蛴〔l(fā)和串行隊(duì)列:

我們發(fā)現(xiàn)串行和并行打印的結(jié)果和上面推測(cè)的一致。

我們?cè)倩氐絖dispatch_lane_create_with_target方法,繼續(xù)看_dispatch_object_alloc方法。

我們是onjc2所以會(huì)走_(dá)os_object_alloc_realized方法,上面我們已經(jīng)知道vtable在串行和并行賦值不同,在_os_object_alloc_realized中vtable值就是cls,1509行:創(chuàng)建是將isa指針指向了cls也就是指向了vtable。

總結(jié)

隊(duì)列創(chuàng)建底層是_dispatch_lane_create_with_target創(chuàng)建,通過傳入的值來確定是串行還是并行隊(duì)列,dispatch_queue_t也是個(gè)對(duì)象,也會(huì)通過alloc,init進(jìn)行創(chuàng)建。在alloc中將isa指針指向并發(fā)還是串行,通過init來確定DOF_WIDTH()等屬性。

dispatch_async

上面講了隊(duì)列的創(chuàng)建,下面我們看下異步任務(wù)的實(shí)現(xiàn)
  • dq:就是穿過進(jìn)來的隊(duì)列
  • work:就是傳進(jìn)來的任務(wù)

我們看下代碼怎么操作的

  • 890行:創(chuàng)建dc
  • 896行:任務(wù)包裝器,用來接收,保存block

2633-2638行:將work保存在dc的dc_ctxt中,其實(shí)這個(gè)判斷是不走的,會(huì)走下面,我們看下_dispatch_continuation_init_f,重點(diǎn)關(guān)注ctxt(將work進(jìn)行copy)func(將work進(jìn)行調(diào)用)。注意:func在2642行執(zhí)行了方法,也就是func執(zhí)行完后會(huì)進(jìn)行析構(gòu)或者釋放。

此時(shí)我們看到上面參數(shù)對(duì)應(yīng)的就是ctxt和f。方法將ctxt和f分別保存到dc的dc_ctxt和dc_func屬性中。

探究dispatch_async中work的執(zhí)行

從上面知道work就是任務(wù),我們探究下

此時(shí)的work就是打印123456,我們打斷點(diǎn),運(yùn)行,然后bt一下

我們看到start_wqthread,_pthread_wqthread是在libsystem_pthread源碼中,而libdispatch源碼中走的第一個(gè)方法就是_dispatch_worker_thread2。我們搜索一下

_dispatch_worker_thread2

紅框就是下面執(zhí)行的代碼

在6581-6588行的循環(huán)中執(zhí)行了_dispatch_continuation_pop_inline方法

最后調(diào)用的是f(ctxt)方法,我們?cè)谏厦?code>講dispatch_async的_dispatch_continuation_init_f方法說了,最后會(huì)將調(diào)用任務(wù)方法放在f中,將任務(wù)放在ctxt中,此處得到驗(yàn)證。

最后會(huì)調(diào)用_dispatch_continuation_init方法中的_dispatch_call_block_and_release

這就是block任務(wù)執(zhí)行的整個(gè)流程。

拓展

相關(guān)面試題

【面試題 - 1】異步函數(shù)+并行隊(duì)列 下面打印結(jié)果是什么?

- (void)textDemo2{
    dispatch_queue_t queue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    // 異步函數(shù)
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
         NSLog(@"4");
    });
    NSLog(@"5");
}

答案:1,5,2,4,3

解題:上面講了,queue為并發(fā)隊(duì)列,不會(huì)阻塞線程,所以1,5先執(zhí)行。而并行隊(duì)列里包含并行隊(duì)列,所以他們?nèi)蝿?wù)互不影響。所以2,4先打印,最后為3。

代碼修改

【修改1】:將并行隊(duì)列 改成 串行隊(duì)列,對(duì)結(jié)果沒有任何影響,順序仍然是 1 5 2 4 3

【修改2】:在任務(wù)5之前,休眠2s,即sleep(2),執(zhí)行的順序?yàn)椋? 2 4 3 5,原因是因?yàn)镮/O的打印,相比于休眠2s,復(fù)雜度更簡單,所以異步block1 會(huì)先于任務(wù)5執(zhí)行。當(dāng)然如果主隊(duì)列堵塞,會(huì)出現(xiàn)其他的執(zhí)行順序。

【修改3】:將打印 NSLog(@"3");的異步dispatch_async,改為同步dispatch_sync。執(zhí)行順序是:1,5,2,3,4,原因:將之前的異步改為同步夠,會(huì)阻塞打印2的線程,導(dǎo)致只有打印3執(zhí)行完后才能執(zhí)行打印4。

【面試題 - 2】異步函數(shù)嵌套同步函數(shù) + 串行隊(duì)列(即同步隊(duì)列)

- (void)textDemo2{
    // 同步隊(duì)列
    dispatch_queue_t queue = dispatch_queue_create("Lj", NULL);
    NSLog(@"1");
    // 異步函數(shù)
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
         NSLog(@"4");
    });
    NSLog(@"5");
}

答案:1,5,2崩潰 [圖片上傳失敗...(image-338b41-1628941505662)]

原因:queue是串行隊(duì)列,1,5,2正常打印不再解釋,執(zhí)行完2后(2當(dāng)前線程為串行),打印3任務(wù)通過同步任務(wù)插入到串行隊(duì)列,放在打印4的后面(在執(zhí)行2串行任務(wù)里,4的打印在3的前面),但是同步任務(wù)有需要先執(zhí)行3在執(zhí)行4,就造成相互等待,造成死鎖。

【修改】:將打印4去掉呢?

  • 還是會(huì)死鎖,因?yàn)槿蝿?wù)3等待的是異步block執(zhí)行完畢,而異步block等待任務(wù)3執(zhí)行完成,還是會(huì)相互等待,造成死鎖

【面試題 - 3】 異步函數(shù) + 同步函數(shù) + 并發(fā)隊(duì)列

下面代碼的執(zhí)行順序是什么?(答案是 AC)
- (void)interview04{
    //并發(fā)隊(duì)列
    dispatch_queue_t queue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{ // 耗時(shí)
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });

    // 同步
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });

    NSLog(@"0");

    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
}
A: 1230789
B: 1237890
C: 3120798
D: 2137890

答案:AC

  • 1.任務(wù)1 和 任務(wù)2由于是異步函數(shù)+并發(fā)隊(duì)列,會(huì)開啟線程,所以沒有固定順序
  • 2.任務(wù)7、任務(wù)8、任務(wù)9同理,會(huì)開啟線程,所以沒有固定順序
  • 3.任務(wù)3是同步函數(shù)+并發(fā)隊(duì)列,同步函數(shù)會(huì)阻塞主線程,但是也只會(huì)阻塞0,所以,可以確定的是 0一定在3之后,在789之前

【面試題 - 4】下面代碼中,隊(duì)列的類型有幾種?

//串行隊(duì)列 - Serial Dispatch Queue
dispatch_queue_t serialQueue = dispatch_queue_create("Lj", NULL);

//并發(fā)隊(duì)列 - Concurrent Dispatch Queue
dispatch_queue_t concurrentQueue = dispatch_queue_create("Lj", DISPATCH_QUEUE_CONCURRENT);

//主隊(duì)列 - Main Dispatch Queue
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//全局并發(fā)隊(duì)列 - Global Dispatch Queue
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

答案:1.串行隊(duì)列:serialQueue,mainQueue 2.并發(fā)隊(duì)列:concurrentQueue,globalQueue

收錄

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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