基于 CocoaPods 的組件二進(jìn)制化實(shí)踐

火掌柜 iOS 客戶端經(jīng)過近兩年的組件化推進(jìn)涩盾,組件數(shù)量已經(jīng)頗具規(guī)模防泵,達(dá)到了近 100 個(gè)脱盲。隨著組件數(shù)量和代碼量越來越多,主工程的打包時(shí)間從最初的十幾分鐘每界,增加到了現(xiàn)在的四十分鐘左右捅僵。依賴組件較多,改動(dòng)相對(duì)頻繁的上層業(yè)務(wù)組件眨层,其發(fā)布時(shí)間也較為漫長(zhǎng)命咐。編譯時(shí)長(zhǎng)的困擾,已經(jīng)明顯影響了日常開發(fā)體驗(yàn)谐岁,同時(shí)也造成 CI pipeline 執(zhí)行時(shí)間過長(zhǎng)醋奠,在 runner 資源匱乏的情況下榛臼,不利于內(nèi)部 CI 的推廣。當(dāng)前時(shí)間節(jié)點(diǎn)下窜司,如何減少編譯時(shí)長(zhǎng)沛善,已經(jīng)成為開發(fā)團(tuán)隊(duì)較為迫切的需求。

前言

組件化除了讓模塊復(fù)用更加便捷塞祈,業(yè)務(wù)開發(fā)更加輕量金刁,還有一個(gè)不可忽視的優(yōu)勢(shì)———組件二進(jìn)制化,即可通過將非開發(fā)中的組件預(yù)先編譯打包成靜態(tài) / 動(dòng)態(tài)庫(kù)并存放至某處议薪,待集成此組件時(shí)尤蛮,直接使用二進(jìn)制包,從而提升集成此組件的 App 或者上層組件的編譯速度斯议。

對(duì)比源碼依賴产捞,二進(jìn)制依賴的組件只需要進(jìn)行鏈接而無需編譯,可以極大地提升集成效率哼御。掌柜主工程在大部分組件都二進(jìn)制化的情況下坯临,打包時(shí)長(zhǎng)從四十分鐘左右,下降到最快十二分鐘恋昼,整整減少了三倍多看靠, CI pipeline 涉及到編譯環(huán)節(jié)的 lint、打包液肌、發(fā)布挟炬,其耗時(shí)也成數(shù)倍減少,二進(jìn)制化所帶來的好處不言而喻嗦哆。

在實(shí)踐二進(jìn)制化過程中谤祖,由于沒有找到較為成熟的依賴切換工具,我們編寫了 cocoapods-bin 通用插件吝秕,有需要的開發(fā)者可以嘗試下。

需要說明的是有些二進(jìn)制方案是在首次編譯后空幻,保留組件生成的二進(jìn)制包烁峭,后續(xù)編譯直接使用此二進(jìn)制包。在大多數(shù)情況下秕铛,比如 App 打包约郁,組件 lint 與發(fā)布,這類只進(jìn)行一次編譯的操作但两,首次編譯才是主要關(guān)注點(diǎn)鬓梅。本文所說的二進(jìn)制化和此類方案的最大區(qū)別,就是將組件二進(jìn)制包制作放到首次編譯前谨湘,更多的是在組件發(fā)布時(shí)绽快,同時(shí)生成二進(jìn)制包芥丧。

另外,鑒于 CocoaPods 在 1.3.0 后的版本坊罢,增加了類似增量編譯的功能续担,在首次 install / update 編譯之后,后續(xù)再進(jìn)行 install / update 操作活孩,會(huì)根據(jù)更改結(jié)果進(jìn)行增量編譯物遇,個(gè)人感覺針對(duì) “非首次 install / update 后的編譯“ 優(yōu)化,并不是必須的憾儒,因?yàn)?CocoaPods 已經(jīng)幫我們做好了询兴。

二進(jìn)制化需求

以下是根據(jù)掌柜團(tuán)隊(duì)日常開發(fā)情況,提出的二進(jìn)制化需求點(diǎn):

  1. 不影響未接入二進(jìn)制化方案的業(yè)務(wù)團(tuán)隊(duì)
  2. 組件級(jí)別的源碼 / 二進(jìn)制依賴切換功能
  3. 無二進(jìn)制版本時(shí)起趾,自動(dòng)采用源碼版本
  4. 接近原生 CocoaPods 的使用體驗(yàn) (為了滿足此需求诗舰,我們決定開發(fā)自定義的 CocoaPods 插件。)
  5. 不增加過多額外的工作量

下面我會(huì)參照這幾個(gè)需求點(diǎn)阳掐,逐步說明掌柜 iOS 團(tuán)隊(duì)的二進(jìn)制化過程始衅。

宏定義處理

預(yù)編譯階段處理的宏定義,在組件進(jìn)行二進(jìn)制化后會(huì)失效缭保,特別是某些依賴 DEBUG 宏的調(diào)試工具汛闸,在二進(jìn)制化之后就不可見了。為了方便處理艺骂,我把使用宏的地方分為兩種:

  • 方法內(nèi)部
  • 方法外部

針對(duì)方法內(nèi)部诸老,我們創(chuàng)建了 TDFMacro 類來替換宏,將邏輯挪到運(yùn)行時(shí)處理:

// TDFMacro.h
@interface TDFMacro : NSObject
+ (BOOL)enterprise;
+ (BOOL)debug;

+ (void)debugExecute:(void(^)(void))debugExecute elseExecute:(void(^)(void))elseExecute;
+ (void)enterpriseExecute:(void(^)(void))enterpriseExecute elseExecute:(void(^)(void))elseExecute;
@end

// TDFMacro.m
@implementation TDFMacro
+ (BOOL)enterprise {
#if ENTERPRISE
    return YES;
#else
    return NO;
#endif
}

+ (BOOL)debug {
#if DEBUG
    return YES;
#else
    return NO;
#endif
}

+ (void)debugExecute:(void (^)(void))debugExecute elseExecute:(void (^)(void))elseExecute {
    if ([self debug]) {
        !debugExecute ?: debugExecute();
    } else {
        !elseExecute ?: elseExecute();
    }
}

+ (void)enterpriseExecute:(void (^)(void))enterpriseExecute elseExecute:(void (^)(void))elseExecute {
    if ([self enterprise]) {
        !enterpriseExecute ?: enterpriseExecute();
    } else {
        !elseExecute ?: elseExecute();
    }
}
@end

這樣一來钳恕,只需要確保 TDFMacro 組件中的宏有效就可以了————不對(duì)其進(jìn)行二進(jìn)制化别伏。

針對(duì)方法外部,我們盡量將能改寫到方法內(nèi)部的代碼改寫后按第一種情況處理忧额,不能處理的對(duì)代碼進(jìn)行重構(gòu)以消除宏定義厘肮,比如我們網(wǎng)絡(luò)層的常量,重寫前后:

// 前
#if DEBUG 
NSString * kTDFRootAPI = @"xxx";
#else
NSString * const kTDFRootAPI = @"xxx";
#end
// 后
NSString * kTDFRootAPI = @"xxx";

個(gè)人建議盡量不要跨模塊使用宏定義睦番,特別是可以用常量或函數(shù)代替的宏类茂。比如有組件 A、B 托嚣,B 依賴 A巩检,它們包含如下代碼:

// A
#define TDF_THEME_BACKGROUNDCOLOR [[UIColor whiteColor] colorWithAlphaComponent:0.7]

// B
// .m 使用了 TDF_THEME_BACKGROUNDCOLOR

假設(shè) A 和 B 都已二進(jìn)制化,假設(shè)后續(xù)我們修改了 A :

// A
#define TDF_THEME_BACKGROUNDCOLOR [[UIColor whiteColor] colorWithAlphaComponent:0.4]

由于 B 中的 TDF_THEME_BACKGROUNDCOLOR 宏已經(jīng)在二進(jìn)制化打包預(yù)編譯時(shí)被替換為 [[UIColor whiteColor] colorWithAlphaComponent:0.7] 示启,所以 B 并不會(huì)感知到此次 A 的變更兢哭,這時(shí)我們就不得不重新打包組件 B 以同步 A 的變更,即使 B 并未做任何更改夫嗓,當(dāng)存在較多使用 TDF_THEME_BACKGROUNDCOLOR 宏的組件時(shí)迟螺,就容易遺漏同步某些組件冲秽。

制作二進(jìn)制包

二進(jìn)制化第一步,先要把組件的二進(jìn)制包打出來煮仇。這里說下比較通用的打包工具 cocoapods-packagerCarthage 劳跃,目前我們使用 cocoapods-packager 將組件構(gòu)建 static-framework 。

cocoapods-packager 的工作原理和 pod spec/lib lint 差不多浙垫,都是通過 podspec 動(dòng)態(tài)生成 Podfile 刨仑,然后 install 出目標(biāo)工程,最后通過 xcodebuild 命令構(gòu)建出二進(jìn)制包夹姥。這種方式有一個(gè)好處杉武,只要保證組件 lint 通過了,就可以打出二進(jìn)制包辙售,不需要和 Example 工程掛鉤轻抱,很方便。但是這個(gè)插件作者幾乎不維護(hù)了旦部,很多較久之前的 issue 和 pull request 都是未處理狀態(tài)祈搜。

以下是我們用來構(gòu)建 static-framework 的命令:

pod package TDFNavigationBarKit.podspec --exclude-deps --force --no-mangle --spec-sources=http://git.xxxxx.net/ios/cocoapods-spec.git

在使用過程中,我遇到了兩個(gè)關(guān)于組件資源的問題 :

  • 使用了 --exclude-deps option 后士八,雖然沒有把 dependency 的符號(hào)信息打進(jìn)可執(zhí)行文件容燕,但是它把 dependency 的 bundle 給拷貝過來了 (見 builder.rb 229 copy_resources 方法)
  • subspec 聲明的 resource 不會(huì)被拷貝進(jìn) framework 中

鑒于 cocoapods-packager 近期沒有發(fā)布新版本的計(jì)劃,我只能 fork 并更新代碼之后婚度,重新發(fā)布 cocoapods-packager-pro 來修復(fù)這兩個(gè)問題蘸秘。使用 cocoapods-packager-pro 之后,構(gòu)建 static-framework 的命令變?yōu)椋?/p>

pod package-pro TDFNavigationBarKit.podspec --exclude-deps --force --no-mangle --spec-sources=http://git.xxxxx.net/ios/cocoapods-spec.git

二級(jí)命令 package 改成 package-pro 即可蝗茁。

cocoapods-packager 創(chuàng)建二進(jìn)制包中的 modulemap 時(shí)醋虏,會(huì)先查看目標(biāo)組件的 podspec 是否設(shè)置了 module_map 字段,如有直接拷貝哮翘,否則會(huì)查看是否有和組件同名的頭文件颈嚼,如有則創(chuàng)建 modulemap ,并設(shè)置 umbrella header 為此文件饭寺,如無則不創(chuàng)建 modulemap 阻课。所以 cocoapods-packager 給沒有和組件同名的頭文件,又沒有指定 module_map 的組件打二進(jìn)制包時(shí)佩研,是不會(huì)創(chuàng)建 modulemap 的柑肴,比如 SDWebImage 霞揉,這時(shí)候需要我們自行添加 modulemap旬薯,否則使用 swift 的 import 就會(huì)找不到對(duì)應(yīng)的 module,這點(diǎn)需要注意下适秩。

CocoaPods 目前發(fā)布了 1.6.0 beta 版本绊序,試用之后硕舆,發(fā)現(xiàn)由于某些類的構(gòu)造函數(shù)參數(shù)發(fā)生了變更, 導(dǎo)致 cocoapods-packager 現(xiàn)有代碼已經(jīng)無法正常工作了骤公,所以 cocoapods-packager 只適用低于 1.6.0 版本的 CocoaPods抚官,后期如果官方 cocoapods-packager 還是沒有更新的話,我們應(yīng)該會(huì)在 cocoapods-packager-pro 中適配新版本 CocoaPods阶捆。

cocoapods-packager 作者最近還創(chuàng)建了插件 cocoapods-generate 凌节,此插件可以直接根據(jù) podspec 生成目標(biāo)工程,相當(dāng)于 cocoapods-packager 前半部分功能的增強(qiáng)版洒试。目前這個(gè)插件支持 CocoaPods 1.6.0 beta 版本倍奢,不想用 cocoapods-packager 的開發(fā)者,可以先利用 cocoapods-generate 創(chuàng)建目標(biāo)工程垒棋,然后接管構(gòu)建二進(jìn)制包的后續(xù)操作卒煞,可以選擇自己實(shí)現(xiàn)打包腳本,也可以選擇使用 Carthage叼架。

關(guān)于 Carthage 如何打 static-framework 畔裕,可以參照 Build static frameworks to speed up your app’s launch times 。其中有一步是將需要打包的 scheme 設(shè)置為 shared 乖订,這個(gè) scheme 對(duì)應(yīng) CocoaPods 組件的 develpement pod 扮饶,一般來說通過 CocoaPods 模版工程或者 cocoapods-generate 插件生成目標(biāo)工程的 scheme 都是 shared 的,如果沒有 shared 垢粮,可參照讓 CocoaPods 組件支持 Carthage 打包一文進(jìn)行設(shè)置贴届。

構(gòu)建出 .framework 文件后,需要對(duì)其進(jìn)行壓縮蜡吧,我們使用以下命令將文件壓縮成 zip 格式:

zip --symlinks -r TDFNavigationBarKit.framework.zip TDFNavigationBarKit.framework

通過上述兩個(gè)步驟毫蚓,我們就得到了組件的二進(jìn)制 zip 包。

需要注意的是昔善,如果使用 cocoapods-packager 打包元潘,其 .framework 中的目錄結(jié)構(gòu)如下 :

TDFNavigationBarKit.framework/
├── Headers -> Versions/Current/Headers
├── Modules
│   └── module.modulemap
├── Resources -> Versions/Current/Resources
├── TDFNavigationBarKit -> Versions/Current/TDFNavigationBarKit
└── Versions
    ├── A
    │   ├── Headers
    │   │   ├── TDFNavigationBarKit.h
    │   │   ├── UIViewController+BackgroundConfigure.h
    │   │   └── UIViewController+NavigationBarConfigure.h
    │   ├── Resources
    │   │   └── Media.xcassets
    │   │       ├── Contents.json
    │   │       ├── common_nbc_back.imageset
    │   │       │   ├── Contents.json
    │   │       │   └── common_nbc_back.png
    │   │       ├── common_nbc_cancel.imageset
    │   │       │   ├── Contents.json
    │   │       │   └── common_nbc_cancel.png
    │   │       ├── common_nbc_ok.imageset
    │   │       │   ├── Contents.json
    │   │       │   └── common_nbc_ok.png
    │   └── TDFNavigationBarKit
    └── Current -> A

可以看到,其中的 Headers君仆、Resources翩概、Versions/Current 都是軟鏈接。podspec 中涉及到文件匹配的字段返咱,如 source_files钥庇、public_header_filesresources 等咖摹,對(duì)軟鏈接是無效的评姨,所以需要設(shè)置為文件實(shí)際存放的路徑:

s.source_files = TDFNavigationBarKit.framework/Versions/A/Headers/*.h
s.public_header_files = TDFNavigationBarKit.framework/Versions/A/Headers/*.h

# 或者更全面一點(diǎn)
s.source_files = TDFNavigationBarKit.framework/Versions/A/Headers/*.h, TDFNavigationBarKit.framework/Headers/*.h
s.public_header_files = TDFNavigationBarKit.framework/Versions/A/Headers/*.h, TDFNavigationBarKit.framework/Headers/*.h

針對(duì)二進(jìn)制包的制作,我們創(chuàng)建了以下命令供團(tuán)隊(duì)內(nèi)部使用:

# 將源碼打包成二進(jìn)制萤晴,并壓縮成 zip 包
pod binary package

存儲(chǔ)二進(jìn)制包

通常二進(jìn)制包存放的地址有兩種吐句,目前我們使用的是第二種 ( 服務(wù)器代碼可參照 binary-server ):

  • 組件所在 git 倉(cāng)庫(kù)
  • 靜態(tài)文件服務(wù)器

相較于 git 倉(cāng)庫(kù)胁后,我認(rèn)為存放至靜態(tài)文件服務(wù)器的優(yōu)勢(shì)如下:

  • 接口訪問,易于擴(kuò)展與自動(dòng)化處理
  • 源碼和二進(jìn)制分離嗦枢,依賴二進(jìn)制時(shí)攀芯,只下載二進(jìn)制包比 clone 倉(cāng)庫(kù)快
  • 不會(huì)增大 git 倉(cāng)庫(kù)大小,這點(diǎn)也涉及到源碼依賴的下載速度

這里說下為什么我們對(duì)組件的下載速度這么敏感文虏。

首先侣诺,CocoaPods 針對(duì)下載的組件是有緩存的,在第一次下載后氧秘,CocoaPods 會(huì)將組件存放在 Caches 文件夾中紧武,后續(xù) install 操作會(huì)先從 Caches 中查找是否有此組件的緩存,如果沒有的話敏储,再執(zhí)行下載流程(是不是感覺和 SDWebImage 有點(diǎn)像)阻星。但是目前 CocoaPods 在同一臺(tái)機(jī)器上,只能有一個(gè)版本的緩存 ( ~/Library/Caches/CocoaPods/Pods 下的 VERSION 記錄著當(dāng)前緩存對(duì)應(yīng)的 CocoaPods 版本 )已添,也就是說我第一次使用 pod _1.5.3_ install 下載了所有組件妥箕,再執(zhí)行 pod _1.4.1_ install , CocoaPods 會(huì)把 1.5.3 版本的所有組件緩存清空更舞,然后重新下載 畦幢。

由于團(tuán)隊(duì)內(nèi)部只有 5 臺(tái) Mac mini 機(jī)器,我們只能在機(jī)器上同時(shí)部署 GitLab CI Runner 和 Jenkins Slaver 缆蝉,CI 腳本中使用的 CocoaPods 版本可以統(tǒng)一控制成 1.4.0 ( 這里不使用最新的 1.5.3 是由于這個(gè) bug 會(huì)導(dǎo)致 lint 失敗)宇葱,但是其他業(yè)務(wù)線打包時(shí)使用的 CocoaPods 版本就沒法統(tǒng)一了,有 1.5.3 的刊头,有 1.6.0.beta 的黍瞧,加上各業(yè)務(wù)線的打包頻率還比較高,導(dǎo)致機(jī)器頻繁地在不同 CocoaPods 版本中切換 原杂。

結(jié)合上訴兩個(gè)原因印颤,我們趨向采用下載速度更快的方案。

針對(duì)二進(jìn)制包的增刪查穿肄,我們創(chuàng)建了以下命令供團(tuán)隊(duì)內(nèi)部使用:

# 查看所有二進(jìn)制版本信息
pod binary list 
# 查找組件二進(jìn)制版本信息
pod binary search NAME
# 下載二進(jìn)制 zip 包
pod binary pull NAME VERSION
# 推送二進(jìn)制 zip 包 
pod binary push [PATH] [-name=組件名] [--version=版本號(hào)] [--commit=版本日志]     

切換依賴方式

二進(jìn)制化后年局,整體構(gòu)建速度變快了,但是不利于開發(fā)人員跟蹤調(diào)試咸产,所以就需要有依賴切換功能矢否。這里所說的依賴切換功能包括整個(gè)工程、單個(gè)組件的切換脑溢,以及二進(jìn)制版本的使用封裝僵朗,這也是組件二進(jìn)制化耗費(fèi)時(shí)間和精力最多的地方。

在整個(gè)過程中,我總共嘗試了三種方案衣迷,分別是單私有源單版本、單私有源雙版本以及最終采用的雙私有源單版本酱酬。下面我會(huì)簡(jiǎn)單地說下各方案以及實(shí)踐中遇到的問題壶谒。

單私有源單版本

在不更改私有源和組件版本的前提下,通過動(dòng)態(tài)變更源碼 podspec膳沽,達(dá)到切換依賴的目的

單私有源單版本是我第一次實(shí)踐采用的方案汗菜,也創(chuàng)建了對(duì)應(yīng)的插件 cocoapods-tdfire-binary ,這里結(jié)合插件的實(shí)現(xiàn)過程挑社,聊聊實(shí)現(xiàn)這類方案時(shí)遇到的坑陨界。

前期調(diào)研二進(jìn)制化方案時(shí),我主要參考了 iOS CocoaPods組件平滑二進(jìn)制化解決方案 一文痛阻,所以整體思路和這篇文章差不多菌瘪,也是通過環(huán)境變量加判斷語(yǔ)句實(shí)現(xiàn) podspec 的內(nèi)容變更(雖說 podspec 支持使用 ruby 語(yǔ)法定制,我還是建議最終以 json 格式發(fā)布到私有源上阱当,因?yàn)?CocoaPods 內(nèi)部會(huì)將 podspec json 化后再執(zhí)行一些操作俏扩,比如緩存,如果這一動(dòng)作不冪等弊添,操作結(jié)果便是不可預(yù)知的录淡,從而破壞 CocoaPods 自身的運(yùn)行機(jī)制)。

這種方案最大的困擾在于切換依賴時(shí)油坝,如何規(guī)避組件緩存帶來的負(fù)面影響嫉戚,處理不當(dāng)容易出現(xiàn)工程組件目錄為空的情況,以下是我實(shí)踐過的兩種方案:

  1. 確保緩存中同時(shí)存在源碼和二進(jìn)制的資源及文件(設(shè)置 preserve_paths)

  2. 切換依賴前澈圈,刪除目標(biāo)組件緩存以及本地 Pods 下的組件目錄

在使用二進(jìn)制服務(wù)器的前提下彬檀,方案一的常見實(shí)現(xiàn)方式為,在 pre_command 中設(shè)置下載二進(jìn)制包腳本瞬女,并設(shè)置 preserve_paths 凤覆,讓 CocoaPods 同時(shí)保留兩種依賴方式所需要的文件即可〔鹞海考慮到組件本身有二進(jìn)制版本盯桦,組件 Cache 還沒有下載的情況,這種方案通常輔以方案二渤刃。由于需要同時(shí)下載兩種依賴的資源拥峦,個(gè)人并不是很喜歡這種方案,這也是我們棄用 cocoapods-tdfire-binary 的主要原因卖子。

方案二需要 hook Pod::Installer 類的 resolve_dependencies 方法略号,在這個(gè)方法中清除緩存及本地資源,并且設(shè)置組件的沙盒變動(dòng)標(biāo)記,這樣 CocoaPods 就會(huì)重新下載對(duì)應(yīng)的組件了:

def cache_descriptors
  @cache_descriptors ||= begin
    cache = Downloader::Cache.new(Config.instance.cache_root + 'Pods')
    cache_descriptors = cache.cache_descriptors_per_pod
  end
end

def clean_local_cache(spec)
  pod_dir = Config.instance.sandbox.pod_dir(spec.root.name)
  framework_file = pod_dir + "#{spec.root.name}.framework"
  if pod_dir.exist? && !framework_file.exist?
    # 設(shè)置沙盒變動(dòng)標(biāo)記玄柠,去 cache 中拿
    # 只有 :changed 突梦、:added 兩種狀態(tài)才會(huì)重新去 cache 中拿
    @analysis_result.sandbox_state.add_name(spec.name, :changed)
    begin
      FileUtils.rm_rf(pod_dir)
    rescue => err
      puts err
    end
  end
end

def clean_pod_cache(spec)
  descriptors = cache_descriptors[spec.root.name]
  return if descriptors.nil?
  descriptors = descriptors.select { |d| d[:version] == spec.version}
  descriptors.each do |d|
    # pod cache 文件名由文件內(nèi)容的 sha1 組成,由于生成時(shí)使用的是 podspec羽利,獲取時(shí)使用的是 podspec.json 導(dǎo)致生成的目錄名不一致
    # Downloader::Request slug
    # cache_descriptors_per_pod 表明宫患,specs_dir 中都是以 .json 形式保存 spec
    slug = d[:slug].dirname + "#{spec.version}-#{spec.checksum[0, 5]}"
    framework_file = slug + "#{spec.root.name}.framework"
    unless (framework_file.exist?)
      begin
        FileUtils.rm(d[:spec_file])
        FileUtils.rm_rf(slug)
      rescue => err
        puts err
      end
    end
  end
end

需要注意的是,CocoaPods 在 podspec 不是 json 格式時(shí)这弧,緩存目錄是有問題的娃闲,所以需要我們自己去拼裝緩存路徑后再執(zhí)行刪除動(dòng)作。

使用 cocoapods-tdfire-binary 時(shí)匾浪,我們需要在 podspec 文件中添加以下代碼:

....
tdfire_source_configurator = lambda do |s|
  # 源碼依賴配置
  s.source_files = '${POD_NAME}/Classes/**/*'
  s.public_header_files = '${POD_NAME}/Classes/**/*.{h}'
end
unless %w[tdfire_set_binary_download_configurations tdfire_source tdfire_binary].reduce(true) { |r, m| s.respond_to?(m) & r }
  tdfire_source_configurator.call s
else
  # 內(nèi)部生成源碼依賴配置
  s.tdfire_source tdfire_source_configurator
  # 內(nèi)部生成二進(jìn)制依賴配置
  s.tdfire_binary tdfire_source_configurator
  # 設(shè)置下載腳本皇帮,preseve_paths
  s.tdfire_set_binary_download_configurations
end

然后在 Podfile 使用以下語(yǔ)句切換依賴:

...
plugin 'cocoapods-tdfire-binary'

tdfire_use_binary!

# tdfire_third_party_use_binary!
tdfire_use_source_pods ['AFNetworking']
...

由于編寫此插件時(shí),我對(duì) CocoaPods 源碼以及 ruby 并不熟悉蛋辈,導(dǎo)致我沒有把 podspec 的配置放到插件內(nèi)部属拾,現(xiàn)在回過頭看,更加合理的做法應(yīng)該是在 podspec 中設(shè)置依賴標(biāo)志冷溶,然后在 hook 的 resolve_dependencies 方法中捌年,變更 podspec 的 source 及依賴相關(guān)的字段,這樣的話挂洛,只需要采用上訴的方案二即可礼预。

可以看到,單私有源單版本對(duì) CocoaPods 緩存策略的侵入還是比較大的虏劲。

這里順便說下 cocoapods-tdfire-binary 是如何處理 subspec 的托酸,首先要說明的是,對(duì)于存在 subspec 的組件柒巫,我們將其整體打?yàn)橐粋€(gè)二進(jìn)制包励堡,并沒有分 subspec 構(gòu)建。假設(shè)有組件 A 堡掏,B应结,他們對(duì)應(yīng)的部分 podspec 如下:

# A
Pod::Spec.new do |s|
  s.name             = 'A'
  ...
  s.subspec 'Core' do |ss|
    ss.source_files = 'A/Classes/A.{h,m}'
  end
  s.subspec 'Model' do |ss|
    ss.dependency 'A/Core'
    ss.dependency 'YYModel'
    ss.source_files = 'A/Classes/Next.{h,m}'
  end
  s.subspec 'Image' do |ss|
    ss.dependency 'A/Core'
    ss.dependency 'SDWebImage'
    ss.source_files = 'A/Classes/Prev.{h,m}'
  end
  ...
end

# B
Pod::Spec.new do |s|
  s.name             = 'B'
  ...
  s.dependency 'A/Model'
  ...
end

當(dāng) B 為源碼版本,A 為二進(jìn)制版本時(shí)泉唁,A 的 subspec 必須要包含 Model 鹅龄,也就是說 A 的二進(jìn)制 podspec 必須保證源碼 podspec 中的 subspec 都存在,這樣切換依賴時(shí)才不會(huì)出錯(cuò)亭畜。 cocoapods-tdfire-binary 在組件 A 為二進(jìn)制版本時(shí)扮休,會(huì)動(dòng)態(tài)創(chuàng)建一個(gè)名為 TdfireBinary 的 default subspec ,然后將源碼 subspec 的依賴上移至 TdfireBinary :

# A
Pod::Spec.new do |s|
  s.name             = 'A'
  ...
  s.subspec 'TdfireBinary' do |ss|
    ss.vendored_frameworks = "A.framework"
    ss.source_files = "A.framework/Headers/*", "A.framework/Versions/A/Headers/*"
    ss.public_header_files = "A.framework/Headers/*", "A.framework/Versions/A/Headers/*"
    ss.dependency 'YYModel'    
    ss.dependency 'SDWebImage'
  end
  s.subspec 'Core' do |ss|
    ss.dependency 'A/TdfireBinary'
  end
  s.subspec 'Model' do |ss|
    ss.dependency 'A/TdfireBinary'
  end
  s.subspec 'Image' do |ss|
    ss.dependency 'A/TdfireBinary'
  end
  ...
end

以下是我們實(shí)現(xiàn)過程中遇到的部分問題:

  • 二進(jìn)制版本時(shí)拴鸵,依賴 subspec 會(huì)引入整個(gè)組件
  • 需要拷貝 subspec 的屬性至 TdfireBinary 玷坠,實(shí)現(xiàn)起來比較繁瑣
  • 由于是在插件內(nèi)部對(duì) podspec 進(jìn)行轉(zhuǎn)化蜗搔,擴(kuò)展性比較差

基于以上問題,我們后續(xù)創(chuàng)建 cocoapods-bin 插件時(shí)八堡,就把這部分工作交給使用者處理了樟凄,如果組件擁有 subspec,那么就需要使用者提供一個(gè)模版二進(jìn)制 podspec 兄渺,插件只負(fù)責(zé)同步 source 和 version缝龄。

另外,在大部分情況下溶耘,我更建議對(duì)功能不純粹的組件進(jìn)行物理剝離,而不是組件內(nèi)部再劃分 subspec 服鹅,subspec 這種結(jié)構(gòu)不僅會(huì)增加組件二進(jìn)制化的難度凳兵,而且會(huì)造成 lint 耗時(shí)成倍增加,大大降低 lint 執(zhí)行效率企软。

單私有源雙版本

在不更改私有源的前提下庐扫,通過變更組件版本(版本號(hào)加 -binary),達(dá)到切換依賴的目的

由于單私有源單版本要么需要同時(shí)下載兩種版本的資源仗哨,要么切換依賴時(shí)需要重新下載目標(biāo)版本的資源形庭,我們決定以組件緩存為切入點(diǎn),按照 CocoaPods 的設(shè)計(jì)規(guī)則厌漂,將二進(jìn)制版本和源碼版本從物理上區(qū)分開來萨醒。

最初我想到的就是使用雙版本,在源碼版本號(hào)后添加 -binary苇倡,即預(yù)發(fā)布版本富纸,作為二進(jìn)制版本的版本號(hào)。接下來只要在 CocoaPods 使用源碼 podspec 下載資源前旨椒,將其替換為二進(jìn)制 podspec 就可以實(shí)現(xiàn)二進(jìn)制版本的切換了晓褪。

首先,我們來看下 Pod::Resolver 類综慎,這個(gè)類會(huì)給 target 創(chuàng)建最終可用的 specifications 涣仿,只不過依賴分析工作并不在 Pod::Resolver 中進(jìn)行,它扮演了類似 DataSource 的角色示惊,將需要分析的數(shù)據(jù)提供給 Molinillo::Resolver 類處理好港。

這里說下我嘗試從依賴分析切入時(shí)遇到的問題。要成為 Molinillo::Resolver 的數(shù)據(jù)源米罚,需要實(shí)現(xiàn)/覆蓋 Molinillo::SpecificationProvider 模塊中的方法媚狰,以下是 Pod::Resolver 實(shí)現(xiàn)的 search_for :

def search_for(dependency)
  @search ||= {}
  @search[dependency] ||= begin
    locked_requirement = requirement_for_locked_pod_named(dependency.name)
    additional_requirements = Array(locked_requirement)
    specifications_for_dependency(dependency, additional_requirements)
  end
  @search[dependency].dup
end
def specifications_for_dependency(dependency, additional_requirements = [])
  requirement = Requirement.new(dependency.requirement.as_list + additional_requirements.flat_map(&:as_list))
  find_cached_set(dependency).
    all_specifications(installation_options.warn_for_multiple_pod_sources).
    select { |s| requirement.satisfied_by? s.version }.
    map { |s| s.subspec_by_name(dependency.name, false, true) }.
    compact
end

當(dāng)時(shí)我通過 hook specifications_for_dependency 方法,更改了 requirement 阔拳,以使方法返回我想要的 specification崭孤,最終也實(shí)現(xiàn)了替換 specification 的目的类嗤。但是在執(zhí)行 lint, push 等操作時(shí)辨宠,由于 Podfile 為內(nèi)部自動(dòng)生成遗锣,很多組件都是間接依賴的,在目標(biāo)組件的 podspec 中并沒有聲明版本嗤形,比如間接依賴了 YYModel 精偿,requirement 為 ~> 1.0 ,如果替換 requirement 為 = 1.0.1-binary 就會(huì)出現(xiàn)以下錯(cuò)誤:

Due to the previous na?ve CocoaPods resolver, you were using a pre-release version of `YYModel`, 
without explicitly asking for a pre-release version, which now leads to a conflict. 
Please decide to either use that pre-release version by
adding the version requirement to your Podfile (e.g. `pod 'YYModel', '= 1.0.1-binary, ~> 1.0'`) or
revert to a stable version by running `pod update YYModel`

要解決這個(gè)問題赋兵,可以顯式依賴一個(gè)預(yù)發(fā)布版本笔咽,也可以更改 requirement_satisfied_by? 方法的處理邏輯。
當(dāng)然霹期,我們也不可能會(huì)在 podspec 中顯式依賴一個(gè)預(yù)發(fā)布版本叶组,也不想過多干涉 CocoaPods 的依賴分析邏輯,所以這條路最終失敗了历造。實(shí)際上我們并不需要關(guān)心依賴是如何分析的甩十,只需要等依賴分析完,將最終生成的 specification 替換掉即可吭产,讓我們看下 Pod::Resolver 的 resolve 方法:

def resolve
  dependencies = @podfile_dependency_cache.target_definition_list.flat_map do |target|
    @podfile_dependency_cache.target_definition_dependencies(target).each do |dep|
      next unless target.platform
      @platforms_by_dependency[dep].push(target.platform)
    end
  end
  @platforms_by_dependency.each_value(&:uniq!)
  @activated = Molinillo::Resolver.new(self, self).resolve(dependencies, locked_dependencies)
  resolver_specs_by_target
rescue Molinillo::ResolverError => e
  handle_resolver_error(e)
end

def resolver_specs_by_target
  @resolver_specs_by_target ||= {}.tap do |resolver_specs_by_target|
    dependencies = {}
    @podfile_dependency_cache.target_definition_list.each do |target|
      specs = @podfile_dependency_cache.target_definition_dependencies(target).flat_map do |dep|
        name = dep.name
        node = @activated.vertex_named(name)
        (valid_dependencies_for_target_from_node(target, dependencies, node) << node).map { |s| [s, node.payload.test_specification?] }
      end

      resolver_specs_by_target[target] = specs.
        group_by(&:first).
        map do |vertex, spec_test_only_tuples|
          test_only = spec_test_only_tuples.all? { |tuple| tuple[1] }
          payload = vertex.payload
          spec_source = payload.respond_to?(:spec_source) && payload.spec_source
          ResolverSpecification.new(payload, test_only, spec_source)
        end.
        sort_by(&:name)
    end
  end
end

上面的 resolver_specs_by_target 方法返回就是最終結(jié)果侣监,我們只需要變更其返回值就可以了。為了不污染源碼私有源以及能更好地維護(hù)源碼和二進(jìn)制 podspec 臣淤,我們最終沒有采用單私有源雙版本橄霉,而是采用了雙私有源單版本,不過兩者的實(shí)現(xiàn)思路和入口差不多是一致的邑蒋,這次嘗試也給后續(xù)的實(shí)踐鋪了路酪劫。

雙私有源單版本

在不更改組件版本的前提下,通過變更組件的私有源寺董,達(dá)到切換依賴的目的

雙私有源分別為源碼私有源和二進(jìn)制私有源覆糟,這兩個(gè)私有源中有相同版本組件,只是 podspec 中的 source 和依賴等字段不一樣遮咖,所以切換了組件對(duì)應(yīng)的私有源即切換了組件的依賴方式滩字。

以 YYModel 為例,現(xiàn)有源碼私有源 cocoapods-spec 及 二進(jìn)制私有源 cocoapods-spec-binary 御吞,它們都有 YYModel 組件 1.0.4.2 版本的 podspec 如下:

# cocoapods-spec 
{
  "name": "YYModel",
  "summary": "High performance model framework for iOS/OSX.",
  "version": "1.0.4.2",
  "license": {
    "type": "MIT",
    "file": "LICENSE"
  },
  "authors": {
    "ibireme": "ibireme@gmail.com"
  },
  "social_media_url": "http://blog.ibireme.com",
  "homepage": "https://github.com/ibireme/YYModel",
  "platforms": {
    "ios": "6.0",
    "osx": "10.7",
    "watchos": "2.0",
    "tvos": "9.0"
  },
  "source": {
    "git": "git@git.xxxxx.net:cocoapods-repos/YYModel.git",
    "tag": "1.0.4.2"
  },
  "frameworks": [
    "Foundation",
    "CoreFoundation"
  ],
  "requires_arc": true,
  "source_files": "YYModel/*.{h,m}",
  "public_header_files": "YYModel/*.{h}"
}

# cocoapods-spec-binary 
{
  "name": "YYModel",
  "summary": "High performance model framework for iOS/OSX.",
  "version": "1.0.4.2",
  "authors": {
    "ibireme": "ibireme@gmail.com"
  },
  "social_media_url": "http://blog.ibireme.com",
  "homepage": "https://github.com/ibireme/YYModel",
  "platforms": {
    "ios": "6.0"
  },
  "source": {
    "http": "http://iosframeworkserver-shopkeeperclient.app.2dfire.com/download/YYModel/1.0.4.2.zip"
  },
  "frameworks": [
    "Foundation",
    "CoreFoundation"
  ],
  "requires_arc": true,
  "source_files": [
    "YYModel.framework/Headers/*",
    "YYModel.framework/Versions/A/Headers/*"
  ],
  "public_header_files": [
    "YYModel.framework/Headers/*",
    "YYModel.framework/Versions/A/Headers/*"
  ],
  "vendored_frameworks": "YYModel.framework"
}

當(dāng)采用 YYModel 的源碼版本時(shí)麦箍,我們從 cocoapods-spec 私有源獲取組件的 podspec,那么下載地址為 git@git.xxxxx.net:cocoapods-repos/YYModel.git1.0.4.2 tag 陶珠;當(dāng)采用 YYModel 的二進(jìn)制版本時(shí)挟裂,我們從 cocoapods-spec-binary 私有源獲取組件的 podspec,那么下載地址為http://iosframeworkserver-shopkeeperclient.app.2dfire.com/download/YYModel/1.0.4.2.zip揍诽。

通過上個(gè)方案诀蓉,我們可以知道 resolver_specs_by_target 方法創(chuàng)建了最終使用的 specifications 栗竖,接下來我們結(jié)合 cocoapods-bin 插件代碼,看下如何切換組件的私有源:

module Pod
  class Resolver
    # >= 1.4.0 才有 resolver_specs_by_target 以及 ResolverSpecification
    # >= 1.5.0 ResolverSpecification 才有 source渠啤,供 install 或者其他操作時(shí)狐肢,輸入 source 變更
    # 
    if Pod.match_version?('~> 1.4') 
      old_resolver_specs_by_target = instance_method(:resolver_specs_by_target)
      define_method(:resolver_specs_by_target) do 
        specs_by_target = old_resolver_specs_by_target.bind(self).call()

        sources_manager = Config.instance.sources_manager
        use_source_pods = podfile.use_source_pods

        missing_binary_specs = []
        specs_by_target.each do |target, rspecs|
          # use_binaries 并且 use_source_pods 不包含
          use_binary_rspecs = if podfile.use_binaries? || podfile.use_binaries_selector
                                rspecs.select do |rspec| 
                                  ([rspec.name, rspec.root.name] & use_source_pods).empty? &&
                                  (podfile.use_binaries_selector.nil? || podfile.use_binaries_selector.call(rspec.spec))
                                end
                              else
                                []
                              end
          specs_by_target[target] = rspecs.map do |rspec|
            # developments 組件采用默認(rèn)輸入的 spec (development pods 的 source 為 nil)
            next rspec unless rspec.spec.respond_to?(:spec_source) && rspec.spec.spec_source

            # 采用二進(jìn)制依賴并且不為開發(fā)組件 
            use_binary = use_binary_rspecs.include?(rspec)
            source = use_binary ? sources_manager.binary_source : sources_manager.code_source 

            spec_version = rspec.spec.version
            begin
              # 從新 source 中獲取 spec
              specification = source.specification(rspec.root.name, spec_version)   

              # 組件是 subspec
              specification = specification.subspec_by_name(rspec.name, false, true) if rspec.spec.subspec?
              # 這里可能出現(xiàn)分析依賴的 source 和切換后的 source 對(duì)應(yīng) specification 的 subspec 對(duì)應(yīng)不上
              # 造成 subspec_by_name 返回 nil,這個(gè)是正沉げ埽現(xiàn)象
              next unless specification

              # 組裝新的 rspec 份名,替換原 rspec
              rspec = if Pod.match_version?('~> 1.4.0')
                        ResolverSpecification.new(specification, rspec.used_by_tests_only)
                      else
                        ResolverSpecification.new(specification, rspec.used_by_tests_only, source)
                      end
              rspec
            rescue Pod::StandardError => error  
              # 沒有從新的 source 找到對(duì)應(yīng)版本組件,直接返回原 rspec
              missing_binary_specs << rspec.spec if use_binary
              rspec
            end

            rspec 
          end.compact
        end

        missing_binary_specs.uniq.each do |spec| 
          UI.message "【#{spec.name} | #{spec.version}】組件無對(duì)應(yīng)二進(jìn)制版本 , 將采用源碼依賴." 
        end if missing_binary_specs.any?

        specs_by_target
      end
    end
  end

  if Pod.match_version?('~> 1.4.0')
    # 1.4.0 沒有 spec_source
    class Specification
      class Set
        class LazySpecification < BasicObject
          attr_reader :spec_source

          old_initialize = instance_method(:initialize)
          define_method(:initialize) do |name, version, source|
            old_initialize.bind(self).call(name, version, source)

            @spec_source = source 
          end

          def respond_to?(method, include_all = false) 
            return super unless method == :spec_source
            true
          end
        end
      end
    end
  end
end

上面就是切換私有源的代碼邏輯妓美,可以看到還是比較簡(jiǎn)短的僵腺,這里只單獨(dú)說三點(diǎn):

  • 我們默認(rèn) Development Pods 中的組件為未發(fā)布組件,沒有二進(jìn)制版本壶栋,所以始終采用原版本
  • 因?yàn)闊o法直接從 source 中獲取組件的 subspec 辰如,所以這里統(tǒng)一獲取 root spec ,如果目標(biāo) spec 是 subspec 再?gòu)?root spec 中獲取 subspec
  • 其他業(yè)務(wù)線的組件可能沒有二進(jìn)制化版本委刘,這里我們?nèi)绻麤]有找到組件目標(biāo)版本的 spec 丧没,會(huì)讓組件采用原版本鹰椒,這樣就不會(huì)因?yàn)槟硞€(gè)組件版本的缺失而導(dǎo)致 install 失敗锡移。

存在兩個(gè)私有源意味著會(huì)有兩個(gè)不同的 podspec ,分別為源碼 podspec 和二進(jìn)制 podspec 漆际,手動(dòng)同步這兩個(gè) podspec 將會(huì)是一個(gè)很耗費(fèi)精力的事情淆珊,這時(shí)候就需要 cocoapods-bin 插件的輔助命令了。針對(duì)沒有 subspec 的組件奸汇,cocoapods-bin 會(huì)根據(jù)源碼 podspec 自動(dòng)生成對(duì)應(yīng)的二進(jìn)制 podspec 施符;針對(duì)有 subspec 的組件,cocoapods-bin 會(huì)根據(jù)使用者提供的 template podspec 和源碼 podspec 自動(dòng)生成對(duì)應(yīng)的二進(jìn)制 podspec 擂找。由于源碼 podspec 和二進(jìn)制 podspec 的 diff 是可預(yù)見的戳吝,我們就可以通過這種半自動(dòng)的方式避免同時(shí)維護(hù)兩套 podspec 。

更多使用信息可以查看 cocoapods-bin 的 README 贯涎,這里就不贅述了听哭。

整合 CI

從上文可以看出,二進(jìn)制化還是增加了重復(fù)性工作塘雳,包括制作二進(jìn)制包陆盘、發(fā)布二進(jìn)制版本等,如果不輔以自動(dòng)化工具败明,無疑會(huì)增加組件維護(hù)者的工作隘马。

火掌柜 iOS 團(tuán)隊(duì) GitLab CI 集成實(shí)踐的基礎(chǔ)上,我們對(duì) CI 配置文件做了些調(diào)整:

variables:
  # 二進(jìn)制優(yōu)先
  BINARY_FIRST: 1 
  # 不允許通知 
  DISABLE_NOTIFY: 0

before_script:
  # https://gitlab.com/gitlab-org/gitlab-ce/issues/14983
  # shared runner 會(huì)出現(xiàn)妻顶,special runner只會(huì)報(bào)warning
  - export LANG=en_US.UTF-8
  - export LANGUAGE=en_US:en
  - export LC_ALL=en_US.UTF-8
  
  - pwd
  - git clone git@git.xxxxx.net:ios/ci-yaml-shell.git 
  - ci-yaml-shell/before_shell_executor.sh

after_script:
  - rm -fr ci-yaml-shell

stages:
  - check
  - lint
  - test
  - package
  - publish
  - report
  - cleanup

component_check:
  stage: check
  script: 
    - ci-yaml-shell/component_check_executor.rb
  only:
    - master
    - /^release.*$/
    - /^hotfix.*$/
    - tags
    - CI
  tags:
    - iOSCI
  environment:
    name: qa
...

package_framework:
  stage: package 
  only:
    - tags
  script:
    - ci-yaml-shell/framework_pack_executor.sh
  tags:
    - iOSCD
  environment:
    name: production

publish_code_pod:
  stage: publish
  only:
    - tags
  retry: 0
  script:
    - ci-yaml-shell/publish_code_pod.sh
  tags:
    - iOSCD
  environment:
    name: production

publish_binary_pod:
  stage: publish
  only:
    - tags
  retry: 0
  script:
    - ci-yaml-shell/publish_binary_pod.sh
  tags:
    - iOSCD
  environment:
    name: production

report_to_director:
  stage: report
  script:
    - ci-yaml-shell/report_executor.sh
  only:
    - master
    - tags
  when: on_failure
  tags:
    - iOSCD

推送 tag 后酸员,如果一切順利蜒车,可以看到 pipeline 執(zhí)行結(jié)果如下:

[圖片上傳失敗...(image-20773b-1552478286453)]

其中的 package 、 publish 這兩個(gè) stage 囊括了二進(jìn)制化資源制作的主要工作沸呐,組件維護(hù)者依然可以像二進(jìn)制化前一樣醇王,關(guān)注源碼版本的發(fā)布流程即可。

這里需要注意的是崭添,由于 CocoaPods push 的 Validator 和 lint 基本一致寓娩,上文提到的這個(gè) bug ,對(duì) publish stage 也會(huì)有影響呼渣,需要暫時(shí)指定 CocoaPods 為 1.4.0 版本(pod _1.4.0_ bin repo push)棘伴。

總結(jié)

整個(gè)組件二進(jìn)制化的嘗試與實(shí)踐,耗費(fèi)了我大半年的主要精力屁置,并且我們還需要多維護(hù)一個(gè)二進(jìn)制文件服務(wù)器焊夸,以及對(duì)應(yīng)的二進(jìn)制版本,在組件 / 代碼不多時(shí)蓝角,做這件事情費(fèi)時(shí)費(fèi)力阱穗,還收效甚微,因此我并不建議還未進(jìn)行業(yè)務(wù)組件化并且沒有上 CI 的團(tuán)隊(duì)去做這件事情使鹅。

結(jié)合我們團(tuán)隊(duì)目前的業(yè)務(wù)性質(zhì)以及業(yè)務(wù)組件化進(jìn)程揪阶,在團(tuán)隊(duì)實(shí)施了組件二進(jìn)制化之后,團(tuán)隊(duì)內(nèi)部工程編譯速度的提升還是顯而易見的患朱,并且受益于編譯時(shí)間的減少鲁僚,組件自動(dòng)發(fā)布平臺(tái)的發(fā)布時(shí)間也大大減少,所以對(duì)于我們來說裁厅,花時(shí)間去做這件事情還是值得的冰沙。

參考

iOS CocoaPods組件平滑二進(jìn)制化解決方案

iOS CocoaPods組件平滑二進(jìn)制化解決方案及詳細(xì)教程二之subspecs篇

組件化-二進(jìn)制方案

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌撑瞧,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侥啤,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡插龄,警方通過查閱死者的電腦和手機(jī)愿棋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來均牢,“玉大人糠雨,你說我怎么就攤上這事∨枪颍” “怎么了甘邀?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵琅攘,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我松邪,道長(zhǎng)坞琴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任逗抑,我火速辦了婚禮剧辐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘邮府。我一直安慰自己荧关,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布褂傀。 她就那樣靜靜地躺著忍啤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仙辟。 梳的紋絲不亂的頭發(fā)上同波,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音叠国,去河邊找鬼未檩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛煎饼,可吹牛的內(nèi)容都是我干的讹挎。 我是一名探鬼主播校赤,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼吆玖,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了马篮?” 一聲冷哼從身側(cè)響起沾乘,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浑测,沒想到半個(gè)月后翅阵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡迁央,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年掷匠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岖圈。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡讹语,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜂科,到底是詐尸還是另有隱情顽决,我是刑警寧澤短条,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站才菠,受9級(jí)特大地震影響茸时,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赋访,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一可都、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚓耽,春花似錦汹粤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至贤徒,卻和暖如春芹壕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背接奈。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工踢涌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人序宦。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓睁壁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親互捌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子潘明,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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

  • 大家都說他氣場(chǎng)很強(qiáng),但我卻覺得那只是他仗著身高而裝出來的樣子秕噪;大家都說他是名很好的隊(duì)長(zhǎng)钳降,但我卻覺得他的官腔官調(diào)實(shí)在...
    scmsuki閱讀 476評(píng)論 0 0
  • 小河流冬立 北風(fēng)漸吹 節(jié)氣更 人換冷暖 依依融 不需問何年 夢(mèng)菲始 已是面露春花時(shí) 放下癡癡 當(dāng)下即是故事
    深深是藍(lán)閱讀 616評(píng)論 5 19
  • 由于我負(fù)責(zé)公司的市場(chǎng)工作遂填,平時(shí)很大一部分工作需要負(fù)責(zé)和客戶溝通,和同事交流澈蝙∠偶幔客戶之間合作的項(xiàng)目多了,自己在心里也會(huì)...
    callme小公舉閱讀 1,600評(píng)論 4 49
  • 哈哈灯荧,又到了喵小姐給大家講故事的時(shí)間了礁击,今天就和大家一起聊聊溥儀的第四個(gè)女人——福貴人李玉琴。 1928年7月15...
    甄嬛傳十級(jí)學(xué)者閱讀 3,030評(píng)論 0 6
  • 其實(shí),每個(gè)人都有很多無奈客税,而我選擇一個(gè)較我而言付出許多况褪,去求很少的路。 不是因?yàn)槲埽蚁氩舛猓艺娴男枰獡Q個(gè)環(huán)境,在...
    蜜呢閱讀 266評(píng)論 0 0