iOS源碼解析:多線程<一>

iOS開發(fā)中經(jīng)常要使用到多線程,在面試的時(shí)候也是經(jīng)常問到夺衍,比較常見的面試題有下面這些:

  • iOS的多線程方案有哪幾種金蜀?你更傾向于哪一種刷后?
  • GCD的隊(duì)列類型。
  • 說一下OperationQueue和GCD的區(qū)別渊抄,以及各自的優(yōu)勢(shì)丧裁。
  • 線程安全的處理手段有哪些?
  • OC你了解的鎖有哪些煎娇?在此基礎(chǔ)上進(jìn)行二次提問“
    1.自旋和互斥對(duì)比
    2.使用以上鎖需要注意哪些二庵?
    3.用C/OC/C++贪染,任選其一催享,實(shí)現(xiàn)自旋或互斥。
    下面就帶著這些問題痰憎,來總結(jié)一下多線程的相關(guān)問題攀涵。
iOS中的常見多線程方案
技術(shù)方案 簡(jiǎn)介 語言 線程聲明周期 使用頻率
pthread 一套通用的多線程API,適用于Unix\Linux\windows等系統(tǒng)以故,跨平臺(tái)怒详,可移植,使用難度大 C 程序員管理 幾乎不用
NSThread 使用更加面向?qū)ο罄ニ福?jiǎn)單易用善玫,可直接操作線程對(duì)象 OC 程序員管理 偶爾使用
GCD 旨在替代NSThread等線程技術(shù),充分利用設(shè)備的多核 C 自動(dòng)管理 經(jīng)常使用
NSOperation 基于GCD茅郎,比GCD多了一些更簡(jiǎn)單實(shí)用的功能系冗,使用更加面向?qū)ο?/td> OC 自動(dòng)管理 經(jīng)常使用

GCD基礎(chǔ)回顧

GCD中有兩個(gè)用來執(zhí)行任務(wù)的函數(shù):

  • 用同步的方式執(zhí)行任務(wù)
    即在當(dāng)前線程中去做事情
dispatch_sync(dispatch_queue_t  _Nonnull queue, ^{})
  • 用異步的方式執(zhí)行任務(wù)
    即另外開線程去做事情
dispatch_async(dispatch_queue_t  _Nonnull queue, ^{})
  • 并發(fā)隊(duì)列:
    可以讓多個(gè)任務(wù)同時(shí)執(zhí)行(自動(dòng)開啟多個(gè)線程同時(shí)執(zhí)行任務(wù))
    并發(fā)功能只有在異步函數(shù)下才有效
  • 串行隊(duì)列:
    讓任務(wù)一個(gè)接著一個(gè)的執(zhí)行(只會(huì)開啟一條線程,一個(gè)任務(wù)執(zhí)行完畢后惯豆,再執(zhí)行下一個(gè)任務(wù))
同步奔害,異步,串行芯杀,并行
  • 同步和異步的主要影響:能不能開啟新的線程
    同步:在當(dāng)前線程中執(zhí)行任務(wù),不具備開啟新線程的能力
    異步:在新的線程中執(zhí)行任務(wù)却特。具備開啟新線程的能力
  • 并發(fā)和和串行的主要影響:任務(wù)的執(zhí)行方法
    并發(fā):多個(gè)任務(wù)同時(shí)執(zhí)行(會(huì)開啟多條線程)
    串行:一個(gè)任務(wù)執(zhí)行完畢后筛圆,再執(zhí)行下一個(gè)任務(wù)(只會(huì)開啟一個(gè)線程)
下面是各種隊(duì)列的執(zhí)行效果:
并發(fā)隊(duì)列 手動(dòng)創(chuàng)建的串行隊(duì)列 主隊(duì)列
同步(sync) 沒有開啟新線程,串行執(zhí)行任務(wù) 沒有開啟新線程闽晦,串行執(zhí)行任務(wù) 沒有開啟新線程粉寞,串行執(zhí)行任務(wù)
異步(async) 有開啟新線程,并發(fā)執(zhí)行任務(wù) 有開啟新線程捅儒,串行執(zhí)行任務(wù) 沒有開啟新線程振亮,串行執(zhí)行任務(wù)
GCD中死鎖的問題

面試題一:以下代碼在主線程中執(zhí)行,是否會(huì)產(chǎn)生死鎖麸祷?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //問題:以下代碼在主線程中執(zhí)行褒搔,會(huì)不會(huì)產(chǎn)生死鎖?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"執(zhí)行任務(wù)2");
    });
    
    NSLog(@"執(zhí)行任務(wù)3");
}

我們運(yùn)行代碼走孽,發(fā)現(xiàn)產(chǎn)生了崩潰琳状,說明產(chǎn)生了死鎖。下面來分析一下為什么會(huì)產(chǎn)生死鎖:
我們知道困食,dispatch_sync()是同步執(zhí)行翎承,不會(huì)開辟新線程,并且要dispatch_sync()的block執(zhí)行完了才會(huì)繼續(xù)往下執(zhí)行叨咖,所以任務(wù)2是加入到了主隊(duì)列中芒澜。主隊(duì)列是串行隊(duì)列,南吮,所以會(huì)串行執(zhí)行加入其中的任務(wù)誊酌,等一個(gè)任務(wù)執(zhí)行完了再執(zhí)行另外一個(gè),在任務(wù)2加入主隊(duì)列之前涂邀,viewDidLoad這個(gè)大任務(wù)已經(jīng)加入了主隊(duì)列箱锐,所以任務(wù)2要等ViewDIdLoad執(zhí)行完,才會(huì)執(zhí)行任務(wù)2浩聋,也就是等到任務(wù)3執(zhí)行完臊恋,才會(huì)執(zhí)行任務(wù)2,但是由于任務(wù)3在任務(wù)2后面坊夫,所以要等到任務(wù)2執(zhí)行完了撤卢,才執(zhí)行任務(wù)3。這樣就造成了任務(wù)2等任務(wù)3拷邢,任務(wù)3等任務(wù)2屎慢,造成死鎖。
面試題二:以下代碼是否會(huì)發(fā)生死鎖:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //問題:以下代碼在主線程中執(zhí)行环肘,會(huì)不會(huì)產(chǎn)生死鎖集灌?
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"執(zhí)行任務(wù)2");
    });
    
    //
    
    NSLog(@"執(zhí)行任務(wù)3");
}

運(yùn)行程序,發(fā)現(xiàn)程序并沒有崩潰腌零,并且產(chǎn)生打右娼А:

2018-09-25 21:02:04.155692+0800 TEST[2610:87716] 執(zhí)行任務(wù)3
2018-09-25 21:02:04.168868+0800 TEST[2610:87716] 執(zhí)行任務(wù)2

為什么把同步改成異步,就不死鎖了呢闲询?我們分析一下扭弧,由于隊(duì)列是主隊(duì)列,一定是把任務(wù)2加到主隊(duì)列中呼巴,并且在此之前viewDidLoad的任務(wù)已經(jīng)加入到了主隊(duì)列中泊愧,所以要viewDidLoad執(zhí)行完了才能執(zhí)行任務(wù)2,由于dispatch_async()是異步執(zhí)行屑埋,所以不用等到任務(wù)2執(zhí)行完了再執(zhí)行任務(wù)3痰滋,可以直接執(zhí)行任務(wù)3,任務(wù)3執(zhí)行完了团搞,viewDidLoad也就執(zhí)行完了多艇,也就可以執(zhí)行任務(wù)2了,所以打印的結(jié)果一定是先執(zhí)行任務(wù)3再執(zhí)行任務(wù)2复隆。
面試題3:以下代碼是否會(huì)發(fā)生死鎖:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //問題:以下代碼在主線程中執(zhí)行姆涩,會(huì)不會(huì)產(chǎn)生死鎖骨饿?
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{//1
        NSLog(@"執(zhí)行任務(wù)2");
        
        dispatch_sync(queue, ^{
            NSLog(@"執(zhí)行任務(wù)3");
        });
        
        NSLog(@"執(zhí)行任務(wù)4");
    });
    
        NSLog(@"執(zhí)行任務(wù)3");
}

運(yùn)行代碼台腥,發(fā)現(xiàn)在dispatch_sync這里產(chǎn)生了崩潰绒北,打印結(jié)果如下:

2018-09-25 21:15:47.637042+0800 TEST[2816:95982] 執(zhí)行任務(wù)3
2018-09-25 21:15:47.637048+0800 TEST[2816:96030] 執(zhí)行任務(wù)2

下面分析一下為什么會(huì)產(chǎn)生死鎖:
dispatch_async是異步執(zhí)行镇饮,所以先打印了執(zhí)行任務(wù)3,然后把1這個(gè)block加入了串行隊(duì)列中,這時(shí)串行隊(duì)列中有1這個(gè)block嘶是,然后又向串行隊(duì)列中加入了任務(wù)3聂喇,任務(wù)3需要同步執(zhí)行,所以任務(wù)3執(zhí)行完了才會(huì)執(zhí)行任務(wù)4克饶,由于串行隊(duì)列中1這個(gè)block排在任務(wù)3前面誊辉,所以要1這個(gè)block完成才會(huì)執(zhí)行任務(wù)3,也就是要任務(wù)4執(zhí)行完了才會(huì)執(zhí)行任務(wù)3邀跃,而任務(wù)4又要等到任務(wù)3執(zhí)行完成蛙紫,這樣互相等待,造成死鎖僵驰。

總結(jié)

造成死鎖的條件1是同步唁毒,2是往當(dāng)前的串行隊(duì)列中添加事件枉证。

直接獲取全局隊(duì)列和手動(dòng)創(chuàng)建隊(duì)列的關(guān)系

看下面一段代碼:

    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue2 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_queue_t queue3 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    dispatch_queue_t queue4 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue5 = dispatch_queue_create("muqueue1", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"%p, %p, %p, %p, %p", queue1, queue2, queue3, queue4, queue5);

打印結(jié)果:

0x1062e2500, 0x1062e2500, 0x1062e2680, 0x600000146bf0, 0x600000146ca0

queue1,queue2毡鉴,queue3是直接獲取的全局隊(duì)列,從打印結(jié)果可以看出憎瘸,如果優(yōu)先級(jí)相同陈瘦,則獲取的是同一個(gè)隊(duì)列,如果優(yōu)先級(jí)不同锅风,則獲取的是不同的隊(duì)列鞍泉。queue4咖驮,queue5是手動(dòng)創(chuàng)建的隊(duì)列,即便它們的identifier相同托修,但是仍然創(chuàng)建了不同的隊(duì)列睦刃。

面試題

面試題一:?jiǎn)栂铝写a的打印結(jié)果:

    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(queue1, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
    });
- (void)test{
    
    NSLog(@"2");
}

我們先看一下打印結(jié)果:

1
3

很奇怪,2壓根就沒有打印枣宫,也就是壓根就沒有執(zhí)行test方法吃环,這是為什么呢郁轻?
我們把

[self performSelector:@selector(test) withObject:nil afterDelay:.0];

改成

[self performSelector:@selector(test) withObject:nil];

看看打印結(jié)果:

1
2
3

說明這樣是能成功執(zhí)行test函數(shù)的,我們?cè)趓untime中找一下- (id)performSelector:(SEL)aSelector withObject:(id)object;的源碼:

- (id)performSelector:(SEL)sel withObject:(id)obj {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}

可以看到竭沫,就是簡(jiǎn)單的調(diào)用objc_msgSend()骑篙。但是在runtime的源碼中卻沒有找到- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;的實(shí)現(xiàn)。

我們?cè)傩薷囊幌麓a谎势,讓其直接在主線程中執(zhí)行:

        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");

打印結(jié)果:

1
2
3

那么就有理由猜測(cè)NSLog(@"1"); [self performSelector:@selector(test) withObject:nil afterDelay:.0]; NSLog(@"3");的執(zhí)行和線程有關(guān)脏榆。
[self performSelector:@selector(test) withObject:nil afterDelay:.0];這句代碼的本質(zhì)是往runloop中去添加一個(gè)NSTimer,由于主線程中有runloop吁断,所以可以正常執(zhí)行坞生,但是在子線程中默認(rèn)是沒有啟動(dòng)runloop的,所以NSTimer也就沒有辦法成功執(zhí)行骂因。

我們可以啟動(dòng)子線程中的runloop試一下:

    dispatch_queue_t queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_async(queue1, ^{
        NSLog(@"1");
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");
        
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    });

打印結(jié)果:

1
3
2
GNUstep

Foundation框架是不開源的,所以我們想看其中的源碼是看不到的乘盼。GNUstep是GNU計(jì)劃的項(xiàng)目之一绸栅,它將Cocoa的OC庫(kù)重新開源實(shí)現(xiàn)了一遍,雖然它不是蘋果官方的完整實(shí)現(xiàn)粹胯,但是和官方的實(shí)現(xiàn)十分接近风纠,區(qū)別不大,在學(xué)習(xí)的時(shí)候我們可以用來作為參考镐捧。
源碼地址:http://www.gnustep.org/resources/downloads.php

我們下載了GNUstep的代碼后臭增,打開找到Foundation文件夾,在這個(gè)文件夾下找到NSRunLoop.m這個(gè)文件列牺,在這個(gè)文件中找到- (void) performSelector: (SEL)aSelector withObject: (id)argument afterDelay: (NSTimeInterval)seconds這個(gè)方法的實(shí)現(xiàn):

- (void) performSelector: (SEL)aSelector
          withObject: (id)argument
          afterDelay: (NSTimeInterval)seconds
{
  NSRunLoop     *loop = [NSRunLoop currentRunLoop];
  GSTimedPerformer  *item;

  item = [[GSTimedPerformer alloc] initWithSelector: aSelector
                         target: self
                       argument: argument
                          delay: seconds];
  [[loop _timedPerformers] addObject: item];
  RELEASE(item);
  [loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}

GCD隊(duì)列組

思考:如何用gcd實(shí)現(xiàn)以下功能:
異步并發(fā)執(zhí)行任務(wù)1瞎领,任務(wù)2
等任務(wù)1,任務(wù)2都執(zhí)行完畢后甥郑,再回到主線程執(zhí)行任務(wù)3
我們可以用dispatch_group_t和dispatch_group_notify來完成這個(gè)功能:

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)2-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
        }
    });

如果需要在任務(wù)1和任務(wù)2完成之后再完成任務(wù)3澜搅,任務(wù)4邪锌,可以這樣:

    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)1-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)2-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)3-%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_notify(group, queue, ^{
        
        for (int i = 0; i < 5; i++) {
            NSLog(@"任務(wù)4-%@", [NSThread currentThread]);
        }
    });
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末觅丰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蜕企,更是在濱河造成了極大的恐慌冠句,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唇牧,死亡現(xiàn)場(chǎng)離奇詭異丐重,居然都是意外死亡杆查,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門径缅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來纳猪,“玉大人桃笙,你說我怎么就攤上這事∈笮猓” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵粗悯,是天一觀的道長(zhǎng)同欠。 經(jīng)常有香客問我铺遂,道長(zhǎng),這世上最難降的妖魔是什么襟锐? 我笑而不...
    開封第一講書人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任粮坞,我火速辦了婚禮,結(jié)果婚禮上妇押,老公的妹妹穿的比我還像新娘姓迅。我一直安慰自己丁存,他們只是感情好柴我,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開白布艘儒。 她就那樣靜靜地躺著,像睡著了一般界睁。 火紅的嫁衣襯著肌膚如雪翻斟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評(píng)論 1 285
  • 那天嘹履,我揣著相機(jī)與錄音,去河邊找鬼幼苛。 笑死焕刮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的暑椰。 我是一名探鬼主播荐绝,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼低滩,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了监憎?” 一聲冷哼從身側(cè)響起婶溯,我...
    開封第一講書人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤迄委,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后渔扎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體信轿,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡财忽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了笤虫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酬凳,死狀恐怖宁仔,靈堂內(nèi)的尸體忽然破棺而出峦睡,到底是詐尸還是另有隱情,我是刑警寧澤煎谍,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布呐粘,位于F島的核電站转捕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏五芝。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一沉删、第九天 我趴在偏房一處隱蔽的房頂上張望醉途。 院中可真熱鬧,春花似錦、人聲如沸嵌屎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尼夺。三九已至,卻和暖如春寝衫,著一層夾襖步出監(jiān)牢的瞬間拐邪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工汹胃, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留着饥,地道東北人惰赋。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓谤逼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親流部。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

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

  • iOS多線程編程 基本知識(shí) 1. 進(jìn)程(process) 進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序,就是一段程序的執(zhí)...
    陵無山閱讀 6,004評(píng)論 1 14
  • 本文首發(fā)于我的個(gè)人博客:「程序員充電站」[https://itcharge.cn]文章鏈接:「?jìng)魉烷T」[https...
    ITCharge閱讀 347,279評(píng)論 308 1,925
  • 本文用來介紹 iOS 多線程中 GCD 的相關(guān)知識(shí)以及使用方法。這大概是史上最詳細(xì)绒障、清晰的關(guān)于 GCD 的詳細(xì)講...
    花花世界的孤獨(dú)行者閱讀 495評(píng)論 0 1
  • 記得兒時(shí)户辱,和父親最親密的接觸,就是趴在他的肩頭咬著手指的安逸恩商,幸福必逆。 漸漸長(zhǎng)大的我揽乱,開始用我的眼光...
    瑩兒醉閱讀 191評(píng)論 0 0
  • 《隱》 大隱于朝凰棉,中隱于市嚷炉,陶淵明“種菊東籬下渊啰,悠然見南山”式的歸隱山野,已然難以尋覓申屹。 村莊曾是隱者的福地绘证,自給...
    合肥張建春閱讀 321評(píng)論 0 1