[貝聊科技]如何將 iOS 項(xiàng)目的編譯速度提高5倍

作者 子豪 貝聊iOS工程師

前言

貝聊目前開(kāi)發(fā)的兩款A(yù)pp分別是貝聊家長(zhǎng)版和貝聊老師版,最近因?yàn)樵诳焖俚_(kāi)發(fā)新功能,項(xiàng)目規(guī)模急速增長(zhǎng),單個(gè)端業(yè)務(wù)代碼約23萬(wàn)行,私有庫(kù)約6萬(wàn)行,第三方庫(kù)代碼約15萬(wàn)行,單個(gè)客戶端的代碼行數(shù)約60萬(wàn)。現(xiàn)在打包一次耗時(shí)需要11~12分鐘。雖然還遠(yuǎn)遠(yuǎn)比不上 Facebook 的40分鐘,但是我們?cè)趦?nèi)測(cè)的時(shí)候,經(jīng)常一天要發(fā)布內(nèi)測(cè)版兩到三次。打包時(shí)CPU占用基本上是百分百的,因?yàn)闆](méi)有專門的 CI 機(jī)器,對(duì)負(fù)責(zé)打包的同事(其實(shí)就是我自己)的工作時(shí)間占用比較多,所以最近一直在尋找加快打包速度的方案。

目前的項(xiàng)目架構(gòu)

我們的項(xiàng)目使用 CocoaPods 來(lái)管理第三方庫(kù)和私有庫(kù)的依賴,對(duì)大部分項(xiàng)目來(lái)說(shuō)應(yīng)該是標(biāo)配了。目前還是純 Objective-C 的項(xiàng)目,沒(méi)有引入 Swift。

調(diào)研過(guò)的方案

下面列出我研究過(guò)的一些主流方案以及我最后沒(méi)有采用的原因,這些方案有各自的局限性,但是也給了我不少啟發(fā),思考過(guò)程跟最終方案一樣有價(jià)值。

cocoapods-packager

cocoapods-packager 可以將任意的 pod 打包成 Static Library,省去重復(fù)編譯的時(shí)間,一定程度上可以加快編譯時(shí)間,但是也有自身的缺點(diǎn):

  1. 優(yōu)化不徹底,只能優(yōu)化第三方和私有 Pod 的編譯速度,對(duì)于其他改動(dòng)頻繁的業(yè)務(wù)代碼無(wú)能為力
  2. 私有庫(kù)和第三方庫(kù)的后續(xù)更新很麻煩,當(dāng)有源碼修改后,需要重新打包上傳到內(nèi)部的 Git 倉(cāng)庫(kù)
  3. 過(guò)多的二進(jìn)制文件會(huì)拖慢 Git 的操作速度(目前還沒(méi)部署 Git 的 LFS
  4. 難以調(diào)試源碼

Carthage

這個(gè)方案跟 cocoapods-packager 比較類似,優(yōu)缺點(diǎn)都差不多,但 Carthage 可以比較方便地調(diào)試源碼。因?yàn)槲覀兡壳耙呀?jīng)大規(guī)模使用 CocoaPods,轉(zhuǎn)用 Carthage 來(lái)做包管理需要做大量的轉(zhuǎn)換工作,所以不考慮這個(gè)方案了。

Buck

Buck 是一套通用的構(gòu)建系統(tǒng),由 Facebook 開(kāi)源。最大的特色是智能的增量編譯可以極大地提高構(gòu)建速度。最早聽(tīng)說(shuō) Buck 的時(shí)候,它還只能用在安卓上,現(xiàn)在已經(jīng)適配了 iOS。

它能增快構(gòu)建速度的主要原因是緩存了編譯結(jié)果,通過(guò)持續(xù)監(jiān)視項(xiàng)目目錄的文件變化,每次編譯時(shí)只編譯有改動(dòng)的文件。另外一個(gè)讓我很受啟發(fā)的功能是 HTTP Cache Server,通過(guò)一臺(tái)緩存文件服務(wù)器來(lái)保存大家的編譯結(jié)果,這樣只要團(tuán)隊(duì)里其中一人編譯過(guò)的文件,其他人就不用再編譯了,直接下載就行。

Buck 是個(gè)相當(dāng)完備的解決方案,很多國(guó)外的大公司例如 Uber 都已經(jīng)用上。我也花了很多時(shí)間來(lái)研究,最終還是認(rèn)為對(duì)我們的項(xiàng)目和團(tuán)隊(duì)來(lái)說(shuō),目前并不是很適合,主要原因是:

  1. Buck 拋棄了 Xcode 的項(xiàng)目文件,需要手工編寫配置文件來(lái)指定編譯規(guī)則,這要對(duì)現(xiàn)有項(xiàng)目作出大幅度的調(diào)整。我們目前還在快速迭代新功能,沒(méi)有余暇和人手來(lái)實(shí)施。
  2. 開(kāi)發(fā)和調(diào)試的流程都得做出很大的改變。因?yàn)?Buck 接管了項(xiàng)目編譯的過(guò)程,想調(diào)試項(xiàng)目不能簡(jiǎn)單地在 Xcode 里面 ?+R 了,得先反過(guò)來(lái)讓 Buck 生成 Xcode 的項(xiàng)目文件。Uber 的工程師甚至推薦使用 Nuclide 來(lái)代替 Xcode 作為開(kāi)發(fā)環(huán)境。雖然原理上是可行的,但是團(tuán)隊(duì)需要花不少時(shí)間來(lái)適應(yīng),短期內(nèi)效率降低無(wú)可避免。
  3. 用 Xcode 調(diào)試代碼享受不到加快編譯速度的好處。雖然可以用 buck 命令啟動(dòng) App,然后在命令行里啟動(dòng) lldb 來(lái)調(diào)試,但那就無(wú)法使用 Xcode 的調(diào)試工具 例如 View Debugging 和 Memory Graph Debugger。

Bazel

Bazel 跟 Buck 很相似,是 Google 開(kāi)源的,優(yōu)缺點(diǎn)跟 Buck 都差不多,不再詳細(xì)說(shuō)了。

distcc 分布式編譯

原理是把一部分需要編譯的文件發(fā)送到服務(wù)器上,服務(wù)器編譯完成后把編譯產(chǎn)物傳回來(lái)。我嘗試了一下比較出名的 distcc,搭建過(guò)程比較簡(jiǎn)單,最后也能成功地把編譯任務(wù)分派到內(nèi)網(wǎng)的多臺(tái)服務(wù)器上。但是其他編譯服務(wù)器的 CPU 占用總是很低,只有 20% 左右;也就是說(shuō)分派任務(wù)的速度甚至還趕不上服務(wù)器編譯的速度,分派任務(wù)然后回傳編譯產(chǎn)物這個(gè)過(guò)程所耗費(fèi)的時(shí)間超過(guò)了本地直接編譯。不停調(diào)整參數(shù)反復(fù)試驗(yàn)了很多次,最后發(fā)現(xiàn)編譯時(shí)間完全沒(méi)有變快,甚至還有點(diǎn)變慢了??赡芤晕覀兡壳绊?xiàng)目的規(guī)模并不適合使用分布式編譯。

最終方案:CCache

先來(lái)看看我對(duì)于解決方案的訴求:

  1. 能大幅度地提升編譯速度,起碼要減少掉 50% 的編譯時(shí)間
  2. 不需要對(duì)項(xiàng)目作出重大調(diào)整
  3. 不需要改變開(kāi)發(fā)工具鏈

CCache 是一個(gè)能夠把編譯的中間產(chǎn)物緩存起來(lái)的工具,在其他領(lǐng)域已經(jīng)有不少應(yīng)用,只是在 iOS 界的實(shí)踐比較少。經(jīng)過(guò)我的實(shí)踐,它能夠滿足我前面的三點(diǎn)要求。我最早認(rèn)識(shí)到它是搜到了這篇文章:https://pspdfkit.com/blog/2015/ccache-for-fun-and-profit/

如果你不使用 CocoaPods,參照上面的文章即可。因?yàn)獒槍?duì) CocoaPods 需要作出一些額外的調(diào)整,所以還是說(shuō)明一下。下面就來(lái)說(shuō)說(shuō)要怎樣把 CCache 應(yīng)用在用 CocoaPods 作為包管理工具的 iOS 項(xiàng)目中。

安裝步驟:

注意:項(xiàng)目路徑不能有中文,否則會(huì)影響 CCache 的正常工作

安裝 CCache

首先你需要在電腦上安裝 Homebrew,對(duì)使用 macOS 的程序員來(lái)說(shuō)應(yīng)該是標(biāo)配,略過(guò)。

通過(guò) Homebrew 安裝 CCache, 在命令行中執(zhí)行
$ brew install ccache

命令跑完后即安裝成功。

創(chuàng)建 CCache 編譯腳本

為了能讓 CCache 介入到整個(gè)編譯的過(guò)程,我們要把 CCache 作為項(xiàng)目的 C 編譯器,當(dāng) CCache 找不到編譯緩存時(shí),它會(huì)再把編譯指令傳遞給真正的編譯器 clang。

新建一個(gè)文件命名為ccache-clang, 內(nèi)容為下面這段腳本,放到你的項(xiàng)目里

ccache-clang

#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
  export CCACHE_MAXSIZE=10G
  export CCACHE_CPP2=true
  export CCACHE_HARDLINK=true
  export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
  
  # 指定日志文件路徑到桌面,等下排查集成問(wèn)題有用,集成成功后刪除,否則很占磁盤空間
  export CCACHE_LOGFILE='~/Desktop/CCache.log'
  exec ccache /usr/bin/clang "$@"
else
  exec clang "$@"
fi

在命令行中,cd 到 ccache-clang 文件的目錄,把它的權(quán)限改成可執(zhí)行文件
$ chmod 777 ccache-clang

如果你的代碼或者是第三方庫(kù)的代碼用到了C++,則把ccache-clang這個(gè)文件復(fù)制一份,重命名成ccache-clang++。相應(yīng)的對(duì)clang的調(diào)用也要改成clang++,否則 CCache 不會(huì)應(yīng)用在 C++ 的代碼上。

ccache-clang++

#!/bin/sh
if type -p ccache >/dev/null 2>&1; then
  export CCACHE_MAXSIZE=10G
  export CCACHE_CPP2=true
  export CCACHE_HARDLINK=true
  export CCACHE_SLOPPINESS=file_macro,time_macros,include_file_mtime,include_file_ctime,file_stat_matches
  
  # 指定日志文件路徑到桌面,等下排查集成問(wèn)題有用,集成成功后刪除,否則很占磁盤空間
  export CCACHE_LOGFILE='~/Desktop/CCache.log'
  exec ccache /usr/bin/clang++ "$@"
else
  exec clang++ "$@"
fi

完成后項(xiàng)目中應(yīng)該有這兩個(gè)文件


scripts

Xcode 項(xiàng)目的調(diào)整

定義CC常量

在你項(xiàng)目的構(gòu)建設(shè)置(Build Settings)中,添加一個(gè)常量CC,這個(gè)值會(huì)讓 Xcode 在編譯時(shí)把執(zhí)行路徑的可執(zhí)行文件當(dāng)做 C 編譯器。

user-defined-build-settings
CC

CC常量的值為 $(SRCROOT)/ccache-clang,如果你的腳本不是放在項(xiàng)目根目錄,則自行調(diào)整路徑。如果一運(yùn)行項(xiàng)目就報(bào)錯(cuò),檢查下路徑是不是填錯(cuò)了。

關(guān)閉 Clang Modules

因?yàn)?CCache 不支持 Clang Modules,所以需要把 Enable Modules 的選項(xiàng)關(guān)掉。這個(gè)問(wèn)題在 CocoaPods 上如何處理,后面會(huì)講。

enable-modules
關(guān)閉了 Enable Modules 后需要作出的調(diào)整

因?yàn)殛P(guān)閉了 Enable Modules,所以必須刪除所有的 @import語(yǔ)句,替換為#import的語(yǔ)法
例如將 @import UIKit 替換為 #import <UIKit/UIKit.h>。之后,如果你用到了其他的系統(tǒng)框架例如 AVFoundation、CoreLocation等,現(xiàn)在 Xcode 不會(huì)再幫你自動(dòng)引入了,你得要在項(xiàng)目 Target 的 Build Phrase -> Link Binary With Libraries 里面自己手動(dòng)引入。

測(cè)試效果

嘗試編譯一遍,然后在命令行里輸入 ccache -s 就能看見(jiàn)類似下面的 ccache 運(yùn)行情況統(tǒng)計(jì):

cache directory                     /Users/mac/.ccache
primary config                      /Users/mac/.ccache/ccache.conf
secondary config      (readonly)    /usr/local/Cellar/ccache/3.3.4_1/etc/ccache.conf
cache hit (direct)                 14378
cache hit (preprocessed)            1029
cache miss                          7875
cache hit rate                     66.18 %
called for link                       61
called for preprocessing              48
compile failed                         2
preprocessor error                     4
can't use precompiled header          70
unsupported compiler option         2332
no input file                         11
cleanups performed                     0
files in cache                     35495
cache size                           1.3 GB
max cache size                       5.0 GB

如果成功接入,就能看見(jiàn) cache miss 不為0。因?yàn)榈谝淮尉幾g沒(méi)有緩存,肯定是全 miss 的。接著編譯第二遍,如果能看見(jiàn) cache hit 的數(shù)字開(kāi)始飆升,恭喜你,接入成功了。

CocoaPods 的 處理

如果你的項(xiàng)目不用 CocoaPods 來(lái)做包管理,那你已經(jīng)完全接入成功了,不用執(zhí)行下面的操作。

因?yàn)?CocoaPods 會(huì)單獨(dú)把第三方庫(kù)打包成一個(gè) Static Library(或者是Dynamic Framework,如果用了 use_frameworks!選項(xiàng)),所以 CocoaPods 生成的 Static Library 也需要把 Enable Modules 選項(xiàng)給關(guān)掉。但是因?yàn)?CocoaPods 每次執(zhí)行 pod update 的時(shí)候都會(huì)把 Pods 項(xiàng)目重新生成一遍,如果直接在 Xcode 里面修改 Pods 項(xiàng)目里面的 Enable Modules 選項(xiàng),下次執(zhí)行pod update的時(shí)候又會(huì)被改回來(lái)。我們需要在 Podfile 里面加入下面的代碼,讓生成的項(xiàng)目關(guān)閉 Enable Modules 選項(xiàng),同時(shí)加入 CC 參數(shù),否則 pod 在編譯的時(shí)候就無(wú)法使用 CCache 加速:

post_install do |installer_representation|
  installer_representation.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      #關(guān)閉 Enable Modules
      config.build_settings['CLANG_ENABLE_MODULES'] = 'NO'
      
      # 在生成的 Pods 項(xiàng)目文件中加入 CC 參數(shù),路徑的值根據(jù)你自己的項(xiàng)目來(lái)修改
      config.build_settings['CC'] = '$(PODS_ROOT)/../ccache-clang' 
    end
  end
end

需要注意的是,如果你使用的某個(gè) Pod 引用了系統(tǒng)框架,例如AFNetworking引用了System Configuration,你需要在你自己項(xiàng)目的Build Phrase -> Link Binary With Libraries里面代為引入,否則你編譯時(shí)可能會(huì)收到 Undefined symbols xxx for architecture yyy一類的錯(cuò)誤。有點(diǎn)回到了原始時(shí)代的感覺(jué),但考慮到編譯速度的極大提升,這一點(diǎn)代價(jià)可以接受。

集成問(wèn)題排查

重點(diǎn)關(guān)注日志文件的輸出和ccache -s 命令的統(tǒng)計(jì),如果在日志中看到了 unsupported compiler option -fmodules 這樣的字眼,就是你的 Enable Modules 沒(méi)有關(guān)掉了,根據(jù)前面的步驟仔細(xì)檢查。其他問(wèn)題,參考官方文檔的 Troubleshooting

進(jìn)一步的優(yōu)化

移除 Precompiled Header File

PCH 的內(nèi)容會(huì)被附加在每個(gè)文件前面,而 CCache 是根據(jù)文件內(nèi)容的 MD4 摘要來(lái)查找緩存的,因此當(dāng)你修改了 PCH 或者 PCH 引用到的頭文件的內(nèi)容時(shí),會(huì)造成全部緩存失效,只能全體重新編譯。CCache 在首次編譯的時(shí)候因?yàn)樾枰戮彺妫瑫?huì)造成編譯時(shí)間變長(zhǎng),對(duì)貝聊的項(xiàng)目來(lái)說(shuō)變長(zhǎng)了差不多一倍。因此如果 PCH 或者 PCH 引入的文件被頻繁修改的話,緩存就會(huì)頻繁地 miss,這種情況下還不如不用 CCache。

為了避免以上這種情況,我建議在 PCH 里面盡量少引入頭文件,只保留比較少更改的系統(tǒng)框架和第三方類庫(kù)的頭文件。最好是把 PCH 徹底刪除,反正蘋果現(xiàn)在也不建議使用 PCH 了,Xcode 新建的項(xiàng)目默認(rèn)都是不帶 PCH 的。

在團(tuán)隊(duì)內(nèi)部共享緩存文件夾

這個(gè)優(yōu)化方式我嘗試過(guò),最終效果不是很好,因此沒(méi)有采用。CCache 的官方文檔中有一段關(guān)于共享緩存文件夾的說(shuō)明,描述了如何修改 CCache 的配置,讓編譯緩存能夠在多臺(tái)電腦之間公用,理論上只要其中一個(gè)人編譯過(guò)的文件其他人就能直接下載到了,節(jié)約了整個(gè)團(tuán)隊(duì)的時(shí)間。因?yàn)?Buck 也有類似的機(jī)制,我覺(jué)得值得嘗試一下,便在公司局域網(wǎng)內(nèi)搭建了一個(gè) OwnCloud 網(wǎng)盤,讓大家把自己電腦上的 CCache 緩存目錄放上去共享。雖然試驗(yàn)是成功了,但是實(shí)際效果并不好。因?yàn)橥皆诙嗯_(tái)電腦上大小達(dá)到幾個(gè)G的緩存目錄,需要在后臺(tái)進(jìn)行很多文件的對(duì)比和傳輸?shù)墓ぷ?,在編譯的同時(shí)進(jìn)行這些操作會(huì)耗費(fèi)不少計(jì)算資源,反而會(huì)拖慢編譯速度。加上移除掉 PCH 后,其實(shí)緩存的命中率已經(jīng)相當(dāng)可觀了,不太需要通過(guò)共享緩存來(lái)進(jìn)一步提高緩存命中率,所以我最后放棄了共享緩存這個(gè)想法。如果你對(duì)緩存命中率還是不滿意的話,可以考慮往這個(gè)方向嘗試一下。

總結(jié)

通過(guò)集成 CCache,我們的項(xiàng)目在 Xcode 里面的打包(在菜單里面選擇 Product -> Archive)時(shí)間從 11~12分鐘減少到了 130 秒,大概有五倍的提升,成果喜人。集成的過(guò)程其實(shí)很簡(jiǎn)單,我從開(kāi)始嘗試到集成成功總共就花了兩個(gè)小時(shí)。如果你也被過(guò)長(zhǎng)的編譯時(shí)間困擾,建議嘗試一下。

文章同步發(fā)布在 https://zhuanlan.zhihu.com/p/27584726

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,577評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 178,486評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 63,852評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,600評(píng)論 6 412
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,944評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 43,108評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,652評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,385評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,616評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,798評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 35,205評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,537評(píng)論 1 295
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,334評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,570評(píng)論 2 379

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