iOS源碼解析—YYCache(YYMemoryCache)

概述

上一篇主要講解了YYCache的文件結(jié)構(gòu)永脓,分析了YYCache類的相關(guān)方法迟赃,本章主要分析內(nèi)存緩存類YYMemoryCache秒旋。該對(duì)象內(nèi)部維護(hù)一個(gè)字典對(duì)象來存取緩存數(shù)據(jù)痴柔,同時(shí)支持緩存容量的限制,當(dāng)緩存數(shù)據(jù)超過指定的內(nèi)存容量大小的時(shí)候咐吼,會(huì)刪除部分緩存數(shù)據(jù)吹缔。這主要是通過LRU算法實(shí)現(xiàn)的。

LRU

LRU全稱是Least recently used锯茄,基于最近最少使用的原則厢塘,屬于一種緩存淘汰算法。實(shí)現(xiàn)思路是維護(hù)一個(gè)雙向鏈表數(shù)據(jù)結(jié)構(gòu)肌幽,每次有新數(shù)據(jù)要緩存時(shí)晚碾,將緩存數(shù)據(jù)包裝成一個(gè)節(jié)點(diǎn),插入雙向鏈表的頭部牍颈,如果訪問鏈表中的緩存數(shù)據(jù),同樣將該數(shù)據(jù)對(duì)應(yīng)的節(jié)點(diǎn)移動(dòng)至鏈表的頭部琅关。這樣的做法保證了被使用的數(shù)據(jù)(存儲(chǔ)/訪問)始終位于鏈表的前部煮岁。當(dāng)緩存的數(shù)據(jù)總量超出容量時(shí),先刪除末尾的緩存數(shù)據(jù)節(jié)點(diǎn)涣易,因?yàn)槟┪驳臄?shù)據(jù)最少被使用過画机。如下圖:

2-1.png

YYMemoryCache內(nèi)部維護(hù)了一個(gè)_YYLinkMap對(duì)象,_YYLinkMap對(duì)象負(fù)責(zé)實(shí)現(xiàn)緩存和LRU的功能新症。下面是代碼注釋:

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; //哈希字典步氏,存放緩存數(shù)據(jù)
    NSUInteger _totalCost; //緩存總大小
    NSUInteger _totalCount; //緩存節(jié)點(diǎn)總個(gè)數(shù)
    _YYLinkedMapNode *_head; //頭結(jié)點(diǎn)
    _YYLinkedMapNode *_tail; //尾結(jié)點(diǎn)
    BOOL _releaseOnMainThread; //在主線程釋放
    BOOL _releaseAsynchronously;//在異步線程釋放
}
@end

_dic是哈希字典,負(fù)責(zé)存放緩存數(shù)據(jù)徒爹,_head和_tail分別是雙鏈表中指向頭節(jié)點(diǎn)和尾節(jié)點(diǎn)的指針荚醒,鏈表中的節(jié)點(diǎn)單元是_YYLinkedMapNode對(duì)象芋类,該對(duì)象封裝了緩存數(shù)據(jù)的信息。

@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; //前向前一個(gè)節(jié)點(diǎn)的指針
    __unsafe_unretained _YYLinkedMapNode *_next; //指向下一個(gè)節(jié)點(diǎn)的指針
    id _key; //緩存數(shù)據(jù)key
    id _value; //緩存數(shù)據(jù)value
    NSUInteger _cost; //節(jié)點(diǎn)占用大小
    NSTimeInterval _time; //節(jié)點(diǎn)操作時(shí)間戳
}
@end

下面分析一下_YYLinkedMap對(duì)象的主要方法:

  1. insertNodeAtHead:方法

    該方法首先將需要新插入的數(shù)據(jù)節(jié)點(diǎn)存入字典中界阁,以節(jié)點(diǎn)中的key作為字典的key侯繁。然后更新總大小_totalCost和節(jié)點(diǎn)總個(gè)數(shù)_totalCount,將節(jié)點(diǎn)置于鏈表頭部泡躯。

    - (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
        CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); //存入字典中
        _totalCost += node->_cost; //更新總大小
        _totalCount++; //更新總數(shù)
        if (_head) { //節(jié)點(diǎn)置于鏈表頭部
            node->_next = _head;
            _head->_prev = node;
            _head = node;
        } else {
            _head = _tail = node;
        }
    }
    
  2. bringNodeToHead:方法

    該方法將節(jié)點(diǎn)移動(dòng)至鏈表的頭部贮竟,因?yàn)檎{(diào)用該方法的場(chǎng)景是節(jié)點(diǎn)已經(jīng)存在于字典中,所以不需要新加入字典中较剃。

    - (void)bringNodeToHead:(_YYLinkedMapNode *)node {
        if (_head == node) return;
        if (_tail == node) {
            _tail = node->_prev;
            _tail->_next = nil;
        } else {
            node->_next->_prev = node->_prev;
            node->_prev->_next = node->_next;
        }
        node->_next = _head;
        node->_prev = nil;
        _head->_prev = node;
        _head = node;
    }
    
  3. removeNode方法和removeTailNode方法

    removeNode方法將數(shù)據(jù)節(jié)點(diǎn)從字典和鏈表中刪除咕别,同時(shí)更新總大小_totalCost和節(jié)點(diǎn)總個(gè)數(shù)_totalCount。removeTailNode方法將鏈表中的尾部節(jié)點(diǎn)刪除写穴,同時(shí)從字典中刪除節(jié)點(diǎn)惰拱。

  4. removeAll方法

    該方法刪除鏈表中所有節(jié)點(diǎn),同時(shí)從字典中刪除所有節(jié)點(diǎn)确垫。

YYMemoryCache

YYMemoryCache實(shí)現(xiàn)了內(nèi)存緩存的功能弓颈,下面是其維護(hù)的成員變量:

pthread_mutex_t _lock;
_YYLinkedMap *_lru;
dispatch_queue_t _queue;

_lock是互斥所,當(dāng)涉及多線程執(zhí)行代碼的情況下删掀,通過pthread_mutex_lock(&_lock)方法給下面的代碼塊加互斥鎖翔冀,這樣其它線程會(huì)被阻塞,直到pthread_mutex_unlock(&_lock)被調(diào)用披泪。如下:

pthread_mutex_lock(&_lock);
//代碼塊1
pthread_mutex_unlock(&_lock);

pthread_mutex_lock(&_lock);
//代碼塊2
pthread_mutex_unlock(&_lock);

線程A執(zhí)行代碼塊1纤子,線程B執(zhí)行代碼塊2,如果線程A先執(zhí)行代碼塊1款票,_lock被鎖住控硼,這樣線程B被阻塞,直到線程A執(zhí)行完代碼塊1后艾少,調(diào)用pthread_mutex_unlock(_lock)卡乾,線程B開始執(zhí)行代碼塊2。由于執(zhí)行緩存的操作很容易涉及到多線程調(diào)用缚够,所以需要通過pthread_mutex_lock來控制幔妨,關(guān)于各種鎖性能的測(cè)試,YYCache的作者ibireme大神在他的博客中進(jìn)行了闡述谍椅。

_lru用來做數(shù)據(jù)緩存误堡,實(shí)現(xiàn)了lru算法。下面分析一下YYMemoryCache主要的方法:

  1. 初始化

    調(diào)用init方法進(jìn)行初始化雏吭,創(chuàng)建了lru對(duì)象锁施,進(jìn)行了一些參數(shù)設(shè)置,包括緩存節(jié)點(diǎn)個(gè)數(shù)限制,總cost限制悉抵,時(shí)間戳界限肩狂,進(jìn)行邊界檢測(cè)的間隔時(shí)長(zhǎng)等等。如下:

    - (instancetype)init {
        self = super.init;
        pthread_mutex_init(&_lock, NULL);
        _lru = [_YYLinkedMap new];
        _queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
        _countLimit = NSUIntegerMax; 
        _costLimit = NSUIntegerMax;
        _ageLimit = DBL_MAX;
        _autoTrimInterval = 5.0;
        ...
        [self _trimRecursively];
        return self;
    }
    

    這些limit參數(shù)如果不設(shè)置基跑,默認(rèn)值都是最大婚温,且這些參數(shù)以及_trimRecursively方法是用來做緩存空間的邊界檢測(cè),在下文中提到媳否。

  2. 存儲(chǔ)數(shù)據(jù)

    調(diào)用setObject: forKey:方法存儲(chǔ)緩存數(shù)據(jù)栅螟,代碼如下:

    - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
        if (!key) return;
        if (!object) {
            [self removeObjectForKey:key];
            return;
        }
        pthread_mutex_lock(&_lock); //上鎖
        _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //從字典中取節(jié)點(diǎn)
        NSTimeInterval now = CACurrentMediaTime();
        if (node) { //如果能取到,說明鏈表中之前存在key對(duì)應(yīng)的緩存數(shù)據(jù)
             //更新totalCost 
             _lru->_totalCost -= node->_cost;
            _lru->_totalCost += cost;
            node->_cost = cost;
            node->_time = now; //更新節(jié)點(diǎn)的訪問時(shí)間
            node->_value = object; //更新節(jié)點(diǎn)中存放的緩存數(shù)據(jù)
            [_lru bringNodeToHead:node]; //將節(jié)點(diǎn)移至鏈表頭部
        } else { //如果不能取到篱竭,說明鏈表中之前不存在key對(duì)應(yīng)的緩存數(shù)據(jù)
            node = [_YYLinkedMapNode new]; //創(chuàng)建新的節(jié)點(diǎn)
            node->_cost = cost;
            node->_time = now; //設(shè)置節(jié)點(diǎn)的時(shí)間
            node->_key = key; //設(shè)置節(jié)點(diǎn)的key
            node->_value = object; //設(shè)置節(jié)點(diǎn)中存放的緩存數(shù)據(jù)
            [_lru insertNodeAtHead:node]; //將新的節(jié)點(diǎn)加入鏈表頭部
        }
        if (_lru->_totalCost > _costLimit) {
            dispatch_async(_queue, ^{
                [self trimToCost:_costLimit];
            });
        }
        if (_lru->_totalCount > _countLimit) {
            _YYLinkedMapNode *node = [_lru removeTailNode];
            ...
        }
        pthread_mutex_unlock(&_lock); //解鎖
    }
    

    首先判斷key和object是否為空力图,object如果為空,刪除緩存中key對(duì)應(yīng)的數(shù)據(jù)掺逼。然后從字典中查找key對(duì)應(yīng)的緩存數(shù)據(jù)吃媒,分為兩種情況,如果訪問到節(jié)點(diǎn)吕喘,說明緩存數(shù)據(jù)存在赘那,則根據(jù)最近最少使用原則,將本次操作的節(jié)點(diǎn)移動(dòng)至鏈表的頭部氯质,同時(shí)更新節(jié)點(diǎn)的訪問時(shí)間募舟。如果訪問不到節(jié)點(diǎn),說明是第一次添加key和數(shù)據(jù)闻察,需要?jiǎng)?chuàng)建一個(gè)新的節(jié)點(diǎn)拱礁,把節(jié)點(diǎn)存入字典中,并且加入鏈表頭部辕漂。cost是指定的呢灶,默認(rèn)是0。

  3. 訪問數(shù)據(jù)

    調(diào)用objectForKey:方法訪問緩存數(shù)據(jù)钉嘹,代碼注釋如下:

    - (id)objectForKey:(id)key {
        if (!key) return nil;
        pthread_mutex_lock(&_lock);
        _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //從字典中讀取key相應(yīng)的節(jié)點(diǎn)
        if (node) {
            node->_time = CACurrentMediaTime(); //更新節(jié)點(diǎn)訪問時(shí)間
            [_lru bringNodeToHead:node]; //將節(jié)點(diǎn)移動(dòng)至鏈表頭部
        }
        pthread_mutex_unlock(&_lock);
        return node ? node->_value : nil;
    }
    

    該方法從字典中獲取緩存數(shù)據(jù)鸯乃,如果key對(duì)應(yīng)的數(shù)據(jù)存在,則更新訪問時(shí)間跋涣,根據(jù)最近最少使用原則缨睡,將本次操作的節(jié)點(diǎn)移動(dòng)至鏈表的頭部。如果不存在仆潮,則直接返回nil宏蛉。

  4. 邊界檢測(cè)

    YYCache通過LRU算法處理緩存數(shù)據(jù)是否超出容量的情況遣臼。首先在初始化時(shí)性置,調(diào)用_trimRecursively方法,通過dispatch_after方法默認(rèn)每隔5秒重新調(diào)用揍堰。下面是代碼注釋:

    - (void)_trimRecursively {
        __weak typeof(self) _self = self;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
            __strong typeof(_self) self = _self;
            if (!self) return;
            [self _trimInBackground]; //在異步隊(duì)列中執(zhí)行邊界檢測(cè)
            [self _trimRecursively]; //遞歸調(diào)用本方法
        });
    }
    

    _trimInBackground分別調(diào)用_trimToCost鹏浅、_trimToCount和_trimToAge方法檢測(cè)嗅义。

    _trimToCost方法判斷鏈表中所有節(jié)點(diǎn)占用大小之和totalCost是否大于costLimit,如果超過隐砸,則從鏈表末尾開始刪除節(jié)點(diǎn)之碗,直到totalCost小于等于costLimit為止。代碼注釋如下:

    - (void)_trimToCost:(NSUInteger)costLimit {
        BOOL finish = NO;
        ...
        NSMutableArray *holder = [NSMutableArray new];
        while (!finish) {
            if (pthread_mutex_trylock(&_lock) == 0) {
                if (_lru->_totalCost > costLimit) {
                    _YYLinkedMapNode *node = [_lru removeTailNode]; //刪除末尾節(jié)點(diǎn)
                    if (node) [holder addObject:node];
                } else {
                    finish = YES; //totalCost<=costLimit,檢測(cè)完成
                }
                pthread_mutex_unlock(&_lock);
            } else {
                usleep(10 * 1000); //10 ms
            }
        }
        ...
    }
    

    其中每個(gè)節(jié)點(diǎn)的cost是人為指定的季希,默認(rèn)是0褪那,且costLimit默認(rèn)是NSUIntegerMax,所以在默認(rèn)情況下式塌,_trimToCost方法不會(huì)刪除末尾的節(jié)點(diǎn)博敬。

    _trimToCount方法判斷鏈表中的所有節(jié)點(diǎn)個(gè)數(shù)之和是否大于countLimit,如果超過峰尝,則從鏈表末尾開始刪除節(jié)點(diǎn)偏窝,直到個(gè)數(shù)之和小于等于countLimit為止。代碼注釋如下:

    - (void)_trimToCount:(NSUInteger)countLimit {
        BOOL finish = NO;
        ...
        NSMutableArray *holder = [NSMutableArray new];
        while (!finish) {
            if (pthread_mutex_trylock(&_lock) == 0) {
                if (_lru->_totalCount > countLimit) {
                    _YYLinkedMapNode *node = [_lru removeTailNode]; //刪除末尾節(jié)點(diǎn)
                    if (node) [holder addObject:node];
                } else {
                    finish = YES; //totalCount<=countLimit,檢測(cè)完成
                }
                pthread_mutex_unlock(&_lock);
            } else {
                usleep(10 * 1000); //10 ms
            }
        }
        ...
    }
    

    初始化時(shí)countLimit默認(rèn)是NSUIntegerMax武学,如果不指定countLimit祭往,節(jié)點(diǎn)的總個(gè)數(shù)永遠(yuǎn)不會(huì)超過這個(gè)限制,所以_trimToCount方法不會(huì)刪除末尾節(jié)點(diǎn)火窒。

    _trimToAge方法遍歷鏈表中的節(jié)點(diǎn)硼补,刪除那些和now時(shí)刻的時(shí)間間隔大于ageLimit的節(jié)點(diǎn),代碼如下:

    - (void)_trimToAge:(NSTimeInterval)ageLimit {
        BOOL finish = NO;
        ...
        NSMutableArray *holder = [NSMutableArray new];
        while (!finish) {
            if (pthread_mutex_trylock(&_lock) == 0) {
                if (_lru->_tail && (now - _lru->_tail->_time) > ageLimit) { //間隔大于ageLimit
                    _YYLinkedMapNode *node = [_lru removeTailNode]; //刪除末尾節(jié)點(diǎn)
                    if (node) [holder addObject:node];
                } else {
                    finish = YES;
                }
                pthread_mutex_unlock(&_lock);
            } else {
                usleep(10 * 1000); //10 ms
            }
        }
        ...
    }
    

    由于鏈表中從頭部至尾部的節(jié)點(diǎn)沛鸵,訪問時(shí)間由晚至早括勺,所以尾部節(jié)點(diǎn)和now時(shí)刻的時(shí)間間隔較大,從尾節(jié)點(diǎn)開始刪除曲掰。ageLimit的默認(rèn)值是DBL_MAX疾捍,如果不人為指定ageLimit,則鏈表中節(jié)點(diǎn)不會(huì)被刪除栏妖。

  5. 線程同步

    YYCache通過在方法中添加互斥所的邏輯乱豆,來保證多線程操作緩存時(shí)數(shù)據(jù)的同步。例如在setObject:forKey:withCost:方法和objectForKey:中添加代碼:

    - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
         pthread_mutex_lock(&_lock);
         //操作鏈表吊趾,寫緩存數(shù)據(jù)
         pthread_mutex_unlock(&_lock);
    }
    - (id)objectForKey:(id)key {
     pthread_mutex_lock(&_lock);
         //訪問緩存數(shù)據(jù)
         pthread_mutex_unlock(&_lock);
    }
    

    如果存在線程A和B宛裕,線程A在寫緩存的時(shí)候,上鎖论泛,線程B讀取緩存數(shù)據(jù)時(shí)揩尸,被阻塞,需要等到線程A執(zhí)行完寫緩存的操作屁奏,調(diào)用pthread_mutex_unlock后岩榆,線程B才能讀緩存數(shù)據(jù),這個(gè)時(shí)候新的緩存數(shù)據(jù)已經(jīng)寫完,保證了操作的數(shù)據(jù)的同步勇边。

    YYCache在每一個(gè)操作的緩存的方法中都是用了互斥所來保證多線程訪問數(shù)據(jù)的同步性犹撒,保證代碼執(zhí)行過程中的安全性。

總結(jié)

YYMemoryCache操作了內(nèi)存緩存粒褒,相較于硬盤緩存需要進(jìn)行I/O操作识颊,在性能上快很多,因此YYCache訪問緩存時(shí)奕坟,優(yōu)先用的是YYMemoryCache祥款。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市月杉,隨后出現(xiàn)的幾起案子镰踏,更是在濱河造成了極大的恐慌,老刑警劉巖沙合,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奠伪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡首懈,警方通過查閱死者的電腦和手機(jī)绊率,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來究履,“玉大人滤否,你說我怎么就攤上這事∽盥兀” “怎么了藐俺?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)泥彤。 經(jīng)常有香客問我欲芹,道長(zhǎng),這世上最難降的妖魔是什么吟吝? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任菱父,我火速辦了婚禮,結(jié)果婚禮上剑逃,老公的妹妹穿的比我還像新娘浙宜。我一直安慰自己,他們只是感情好蛹磺,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布粟瞬。 她就那樣靜靜地躺著,像睡著了一般萤捆。 火紅的嫁衣襯著肌膚如雪裙品。 梳的紋絲不亂的頭發(fā)上乓梨,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音清酥,去河邊找鬼。 笑死蕴侣,一個(gè)胖子當(dāng)著我的面吹牛焰轻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播昆雀,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼辱志,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了狞膘?” 一聲冷哼從身側(cè)響起揩懒,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎挽封,沒想到半個(gè)月后已球,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辅愿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年智亮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片点待。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阔蛉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出癞埠,到底是詐尸還是另有隱情状原,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布苗踪,位于F島的核電站颠区,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏通铲。R本人自食惡果不足惜瓦呼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望测暗。 院中可真熱鬧央串,春花似錦、人聲如沸碗啄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)稚字。三九已至饲宿,卻和暖如春厦酬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瘫想。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工仗阅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人国夜。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓减噪,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親车吹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子筹裕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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