RxSwift by Examples #4 – Multithreading

RxSwift by Examples 分成 4 部分。以下是 PART 4 的學(xué)習(xí)筆記和翻譯整理。原文在這里触幼。

當(dāng)我們談?wù)?Rx 的時(shí)候,常常歸結(jié)為連接數(shù)據(jù)源與 UI究飞。

在這個(gè)系列教程的此前部分置谦,除了 UI 綁定堂鲤,我們還講到了獲取數(shù)據(jù)。當(dāng)從服務(wù)端獲取到數(shù)據(jù)的時(shí)候媒峡,常常要解析它瘟栖。如果數(shù)據(jù)量很大,map 的任務(wù)就要消耗內(nèi)存和時(shí)間谅阿,尤其是當(dāng)操作在主線程中進(jìn)行時(shí)半哟,會(huì)阻塞 UI,導(dǎo)致給最終產(chǎn)品造成糟糕的用戶體驗(yàn)签餐。

在 part 3 我們 map 了對(duì)象寓涨。在一些操作器中使用 MainScheduler.instance,因?yàn)闉榱舜_保我們的數(shù)據(jù)在主線程中氯檐。事實(shí)上戒良,這是一個(gè) Scheduler,不是一個(gè) Thread男摧,那么為什么我們要討論線程呢蔬墩?而且我們只是認(rèn)識(shí)到不應(yīng)該在主線程 map 對(duì)象译打,但是似乎我們最后的案例這樣做了耗拓。為什么?

你可以在 part 1 中找到答案奏司。

調(diào)度者 Scheduler

開(kāi)始我們先討論一點(diǎn)關(guān)于 scheduler 的理論知識(shí)乔询。當(dāng)我們用 Rx 做操作時(shí),理論上所有操作都在一個(gè)線程上韵洋。只要你沒(méi)有手動(dòng)改變線程竿刁,當(dāng)前線程的入口也就是所執(zhí)行的線程。

scheduler 并不真的是線程搪缨,但如它的名字所表示的一樣食拜,它們調(diào)度所得到的任務(wù)。有兩種 scheduler:串行和并行(serial and concurrent)副编。以下列出的是已經(jīng)內(nèi)置的 scheduler:

  • CurrentThreadScheduler(串行) - 安排在當(dāng)前線程负甸,也是默認(rèn) scheduler
  • MainScheduler(串行) - 安排在主線程
  • SerialDispatchQueueScheduler(串行) - 安排在指定隊(duì)列(dispatch_queue_t)
  • ConcurrentDispatchQueueScheduler(并行) - 安排在指定隊(duì)列(dispatch_queue_t)
  • OperationQueueScheduler(并行) - 安排在指定隊(duì)列(NSOperationQueue)

有意思的是鳄厌,當(dāng)你傳遞一個(gè)并行隊(duì)列到一個(gè) 串行的 scheduler句旱, RxSwift 會(huì)將它轉(zhuǎn)換成串行隊(duì)列。相反地趁怔,傳遞串行隊(duì)列到并行 scheduler 不會(huì)有任何問(wèn)題队腐,不過(guò)如果可以最好避免這樣做蚕捉。

你還可以實(shí)現(xiàn)你自己的 scheduler,這個(gè)文檔會(huì)對(duì)你有幫助如果需要這樣做的話柴淘。

observeOn() & subscribeOn()

這兩個(gè)方法是多線程的核心迫淹。從它們的命名應(yīng)該能看出來(lái)它們是做什么的秘通。事實(shí)上,很少人理解兩者之間的區(qū)別以及使用時(shí)的具體行為敛熬。

暫時(shí)忘掉這兩個(gè)詞充易。假設(shè)我們接到 Emily 的電話,她曾請(qǐng)我們幫她找看她的貓咪 Ethan 在她度假的時(shí)候≥┬停現(xiàn)在她回來(lái)了盹靴,我們需要把 Ethan 還回去。我們要駕車數(shù)小時(shí)才能到她家瑞妇,最好為此做點(diǎn)準(zhǔn)備稿静。

通常我們開(kāi)車去 Emily 家默認(rèn)會(huì)走公路。不過(guò)今天我們想為我們的生活做一點(diǎn)改變辕狰,我們選擇雙車道的高速路改备。天氣很不錯(cuò),開(kāi)了一小時(shí)后我們停下來(lái)呼吸新鮮空氣蔓倍。突然一個(gè)念頭浮起——這樣的天氣里在公路上開(kāi)車會(huì)很棒悬钳。因?yàn)樾迈r空氣對(duì)我們總是有壞的影響。于是我們決定回到老公路上偶翅。伴隨著好聽(tīng)的音樂(lè)默勾,可愛(ài)的貓和美妙的天氣,開(kāi)了一小時(shí)后我們終于見(jiàn)到 Emily聚谁,把貓交給她母剥,每個(gè)人都很高興。我們已經(jīng)學(xué)習(xí)了 observeOn() 和 subscribeOn()形导。

我們是一個(gè) Observable环疼,Ethan 是我們產(chǎn)生的信號(hào) Signal,路線是一個(gè) Scheduler朵耕,Emyly 是一個(gè) Observer炫隶。Emily 訂閱了我們,她相信她會(huì)得到一個(gè)信號(hào)(貓)阎曹。當(dāng)開(kāi)車去 Emily 家時(shí)我們也有一個(gè)默認(rèn)路線伪阶。不過(guò)開(kāi)始的時(shí)候我們走了另一條路線(scheduler)。當(dāng)你開(kāi)始旅程的時(shí)候不用默認(rèn)路線而選擇不同的路芬膝,你使用了 subscribeOn() 方法望门。如果你使用了 subscribeOn(),你不能確定旅程結(jié)束的時(shí)候(Emily 用了 subscribeNext())所在的路與開(kāi)始的路一致锰霜。你只能確保你從哪兒開(kāi)始筹误。

第二個(gè)方法,observeOn() 可以改變路線癣缅。不過(guò)它不限制旅途的起點(diǎn)厨剪,在旅途中任何時(shí)候你可以使用 observeOn() 切換路線哄酝。作為對(duì)比,subscribeOn() 只在起點(diǎn)的時(shí)候切換路線 - 這就是不同之處祷膳。然而大部分時(shí)候你將使用 observeOn()陶衅。

回到貓的傳遞,在 RxSwift 中用偽代碼表達(dá)我們的傳遞是這樣:

catObservable // 1
    .breatheFreshAir() // 2
    .observeOn(MainRouteScheduler.instance) // 3
    .subscribeOn(TwoLaneFreewayScheduler.instance) // 4
    .subscribeNext { cat in // 5
        if cat is Ethan {
            hug(cat)
        }
    }
    .addDisposableTo(disposeBag)

分步講解:

  1. 我們訂閱了 observable 貓直晨,它發(fā)送貓信號(hào)
  2. 在訂閱之前我們?cè)谕粋€(gè) scheduler(Rx 的默認(rèn)行為)搀军。
  3. 切換 sheduler 至 MainRouteScheduler。在這之下的所有操作都將安排到 MainRouteScheduler(如果之后不改變的話)勇皇。
  4. 我們?cè)?TwoLaneFreewayScheduler 啟動(dòng)罩句,所以 breatheFreshAir() 將被安排在 TwoLaneFreewayScheduler。然后用 observeOn() 改變 sheduler敛摘。
  5. subscribeNext() 被安排到 MainRouteScheduler门烂。如果在這行之前我們沒(méi)有添加 observeOn(),它會(huì)被安排到 TwoLaneFreewayScheduler兄淫。

總結(jié):subscribeOn() 指向整個(gè)鏈條的開(kāi)始點(diǎn)屯远,observeOn() 指向下一個(gè)去向。(看原文的圖捕虽,不轉(zhuǎn)了)慨丐。

示例

創(chuàng)建一個(gè) project。用 Cocoapods 安裝依賴庫(kù)薯鳍。

Podfine

platform :ios, '8.0'
use_frameworks!
 
target 'RxAlamofireExample' do
 
pod 'RxAlamofire/RxCocoa'
pod 'ObjectMapper'
 
end

列一下任務(wù)大綱:

  1. 創(chuàng)建 UI咖气。UISearchBar 和 UITableView挨措。
  2. 觀察 search bar挖滤,每一次它的值改變,轉(zhuǎn)換成 repo 的 array浅役。這里需要 model 做網(wǎng)絡(luò)請(qǐng)求斩松。
  3. 用新數(shù)據(jù)去更新 table view。思考關(guān)于 scheduler觉既,考慮不要拖累到 UI惧盹。

第1步 - Controller 和 UI

創(chuàng)建 RepositoriesViewController.swift

import UIKit
import ObjectMapper
import RxAlamofire
import RxCocoa
import RxSwift
 
class RepositoriesViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!
 
    override func viewDidLoad() {
        super.viewDidLoad()
        setupRx()
    }
    
    func setupRx() {
    }
}

創(chuàng)建可觀察的 search bar 的 rx.text 屬性。不過(guò)這一次加個(gè)過(guò)濾器:我們不想要空值瞪讼。

class RepositoriesViewController: UIViewController {
    ...
    var rx_searchBarText: Observable<String> {
        return searchBar
            .rx.text
            .filter { $0.characters.count > 0 } // notice the filter new line
            .throttle(0.5, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
    }
    ...
}

我們剛剛添加了一個(gè)變量到 RepositoryViewController【現(xiàn)在我們需要連接轉(zhuǎn)換過(guò)的 Observable<[Repository]> 并傳給 UITableView。

第2步 - Network model and mapping objects

首先設(shè)置 map 對(duì)象符欠。這一次我們用不一樣的 mapper嫡霞。創(chuàng)建 Repository.swift

import ObjectMapper
 
class Repository: Mappable {
    var identifier: Int!
    var language: String!
    var url: String!
    var name: String!
    
    required init?(_ map: Map) { }
    
    func mapping(map: Map) {
        identifier <- map["id"]
        language <- map["language"]
        url <- map["url"]
        name <- map["name"]
    }
}

我們有一個(gè) controller,還有一個(gè) Repository 對(duì)象的 model∠J粒現(xiàn)在實(shí)現(xiàn) network model诊沪。

我們將初始化這個(gè) model 為 Observable<String>养筒,實(shí)現(xiàn)方法返回 Observable<[Repository]>。然后連接到 RepositoriesViewController 的 view端姚。初步實(shí)現(xiàn) RepositoryNetworkModel.swift:

import ObjectMapper
import RxAlamofire
import RxCocoa
import RxSwift
 
struct RepositoryNetworkModel {
 
    private var repositoryName: Observable<String>
    
    private func fetchRepositories() -> Driver<[Repository]> {
        ...
    }
    
}

為什么返回的不是 Observable<[Repository]> 而是 Driver<[Repository]>晕粪?

今天我們要討論的是 Scheduler。當(dāng)你想綁定數(shù)據(jù)到 UI渐裸,總是想用 MainScheduler 來(lái)做這件事巫湘。基本上這是一個(gè) Driver 的角色昏鹃。Driver 是一個(gè) Variable剩膘,它說(shuō):好的伙計(jì),我就在主線程上所以別猶豫了綁定我吧盆顾。這個(gè)方法使我們的綁定不易出錯(cuò)怠褐,實(shí)現(xiàn)安全的連接。

我們從 flatMapLatest() 開(kāi)始您宪,把 Observable<String> 轉(zhuǎn)換成 Observable<[Repositories]>:

struct RepositoryNetworkModel {
    ...
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .flatMapLatest { text in
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .map { (response, json) -> [Repository] in
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
    }
    ...
}

看起來(lái)不太一樣奈懒,這兒有另一個(gè) map() 添加到 flatMapLatest() 中。但事實(shí)上沒(méi)有你需要擔(dān)心的宪巨。在 flatMapLatest() 中做了常規(guī)的網(wǎng)絡(luò)請(qǐng)求磷杏,如果出現(xiàn) error 則用 Observable.never() 中斷傳輸管道。然后把從 Alamofire 得到的響應(yīng) map 成 Observable<[Repository]>捏卓。我們也可以鏈?zhǔn)?flatMapLatest() 的在 catchError() 之后极祸,不過(guò)我們需要它在 flatMapLatest() 外部,這只是個(gè)偏好問(wèn)題怠晴。

上門這個(gè)代碼無(wú)法編譯遥金,因?yàn)槲覀兎祷亓?Observable,然而我們希望返回 Driver蒜田。所以我們需要更深入稿械。如何轉(zhuǎn)換 Observable<[Repository]> 成 Driver<[Repository]>?非常簡(jiǎn)單冲粤。之需要用 asDriver() 操作器就可以把任何 Observable 可以轉(zhuǎn)換成 Driver美莫。在這個(gè)示例中,我們將使用 .asDriver(onErrorJustReturn: [])梯捕,它的意思是:如果鏈條中有任何錯(cuò)誤(很可能沒(méi)有厢呵,因?yàn)槲覀冊(cè)诖酥稗D(zhuǎn)換它了),返回空數(shù)組傀顾。這是代碼:

struct RepositoryNetworkModel {
    ...
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .flatMapLatest { text in
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .map { (response, json) -> [Repository] in
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
    }
    ...
}

看襟铭,我們甚至沒(méi)有用到 observeOn() 或 subscribeOn(),但已經(jīng)兩次切換了 scheduler。第一次用 throttle()蝌矛,現(xiàn)在用 asDriver()(它確保我們?cè)?MainScheduler) - 這只是個(gè)開(kāi)始〉琅現(xiàn)在代碼可以執(zhí)行了。最后我們要做的事情是連接 RepositoryNetworkModel 中的 repositories 到 view controller入撒。不過(guò)在此之前先用其他東西替換這個(gè)方法隆豹,因?yàn)檫@樣我們每次用它的時(shí)候會(huì)創(chuàng)建新的管道。取而代之茅逮,我更喜歡屬性璃赡。但不是一個(gè)計(jì)算屬性,因?yàn)榻Y(jié)果將和一個(gè)方法一樣献雅。我們將創(chuàng)建一個(gè) lazy var碉考,它將被約束到獲取 repositories 的方法。這種方式避免多次創(chuàng)建序列挺身。我們還需要隱藏不是屬性的氣體東西侯谁,確保任何使用這個(gè) model 的人可以得到正確的 Driver 屬性。這個(gè)方案的唯一麻煩是我們不得不在 init 中明確類型章钾。最終 RepositoryNetworkModel.swift 如下:

struct RepositoryNetworkModel {
    
    lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
    private var repositoryName: Observable<String>
    
    init(withNameObservable nameObservable: Observable<String>) {
        self.repositoryName = nameObservable
    }
    
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .flatMapLatest { text in
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .map { (response, json) -> [Repository] in
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
    }
}

很好∏郊現(xiàn)在我們只要連接數(shù)據(jù)到 view controller。我們要綁定 Driver 到 table view贱傀,不用 bindTo(之前用過(guò))惨撇,而用 drive() 操作器,語(yǔ)法和其他跟 bindTo 一樣府寒。為了綁定數(shù)據(jù)到 table view魁衙,我們還做了另一個(gè)訂閱,每次 repositories 的 count 等于 0 時(shí)株搔,顯示一個(gè) alert剖淀。

RepositoriesViewController.swift:

class RepositoriesViewController: UIViewController {
    
    @IBOutlet weak var tableViewBottomConstraint: NSLayoutConstraint!
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!
    let disposeBag = DisposeBag()
    var repositoryNetworkModel: RepositoryNetworkModel!
    
    var rx_searchBarText: Observable<String> {
        return searchBar
            .rx_text
            .filter { $0.characters.count > 0 }
            .throttle(0.5, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupRx()
    }
    
    func setupRx() {
        repositoryNetworkModel = RepositoryNetworkModel(withNameObservable: rx_searchBarText)
        
        repositoryNetworkModel
            .rx_repositories
            .drive(tableView.rx_itemsWithCellFactory) { (tv, i, repository) in
                let cell = tv.dequeueReusableCellWithIdentifier("repositoryCell", forIndexPath: NSIndexPath(forRow: i, inSection: 0))
                cell.textLabel?.text = repository.name
                
                return cell
            }
            .addDisposableTo(disposeBag)
        
        repositoryNetworkModel
            .rx_repositories
            .driveNext { repositories in
                if repositories.count == 0 {
                    let alert = UIAlertController(title: ":(", message: "No repositories for this user.", preferredStyle: .Alert)
                    alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
                    if self.navigationController?.visibleViewController?.isMemberOfClass(UIAlertController.self) != true {
                        self.presentViewController(alert, animated: true, completion: nil)
                    }
                }
            }
            .addDisposableTo(disposeBag)
    }
}

這段代碼唯一的新事物是 driveNext() 操作器。不過(guò)你可以認(rèn)為它只是一個(gè) Driver 的 subscribeNext邪狞。

第3步 - 多線程優(yōu)化

你可能認(rèn)為事實(shí)上所有事情都在 MainScheduler 中完成祷蝌。為什么?因?yàn)槲覀兊逆湕l從 searchBar.rx.text 中開(kāi)始帆卓,并保證這個(gè)是在 MainScheduler 中。因?yàn)樗衅渌露荚诋?dāng)前 scheduler 我們的 UI 線程可能會(huì)不堪重負(fù)米丘。如何避免這種情況剑令?在 request 和 map 之前切換到背景線程,僅在更新 UI 的時(shí)候回主線程:

RepositoryNetworkModel.swift

struct RepositoryNetworkModel {
    ...
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .flatMapLatest { text in // .Background thread, network request
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .map { (response, json) -> [Repository] in // again back to .Background, map objects
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
    }
    ...
}

為什么兩次一樣的方法使用 observeOn()拄查?因?yàn)槲覀儾⒉淮_切地知道 requestJSON 是否將在與它啟動(dòng)時(shí)的同一個(gè)線程返回?cái)?shù)據(jù)吁津,為了確定它在背景線程 map。

現(xiàn)在我們的 map 是在背景線程了,結(jié)果傳遞給 UI 線程碍脏。還可不可以做得更多一些梭依?我們希望用戶知道網(wǎng)絡(luò)請(qǐng)求正在進(jìn)行。為了達(dá)到這個(gè)目的典尾,我們將使用 UIApplication.sharedApplication().networkActivityIndicatorVisible 屬性役拴,顯示旋轉(zhuǎn)的菊花。不過(guò)現(xiàn)在我們必須小心對(duì)待線程钾埂,因?yàn)槲覀兿朐?request 和 mapping 操作的中間更新 UI河闰。我們將使用一個(gè)優(yōu)雅的方法叫做 doOn(),它可以做任何你置頂?shù)氖录ū热?.Next, .Error 等)褥紫。我們想在 flatMapLatest():之前顯示句話姜性,doOn是可以做到的。我們只需要在動(dòng)作執(zhí)行前切換到 MainScheduler髓考。

完整的獲取 repo 的代碼如下:

RepositoryNetworkModel.swift:

struct RepositoryNetworkModel {
    
    lazy var rx_repositories: Driver<[Repository]> = self.fetchRepositories()
    private var repositoryName: Observable<String>
    
    init(withNameObservable nameObservable: Observable<String>) {
        self.repositoryName = nameObservable
    }
    
    private func fetchRepositories() -> Driver<[Repository]> {
        return repositoryName
            .subscribeOn(MainScheduler.instance) // Make sure we are on MainScheduler
            .doOn(onNext: { response in
                UIApplication.sharedApplication().networkActivityIndicatorVisible = true
            })
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .flatMapLatest { text in // .Background thread, network request
                return RxAlamofire
                    .requestJSON(.GET, "https://api.github.com/users/\(text)/repos")
                    .debug()
                    .catchError { error in
                        return Observable.never()
                    }
            }
            .observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
            .map { (response, json) -> [Repository] in // again back to .Background, map objects
                if let repos = Mapper<Repository>().mapArray(json) {
                    return repos
                } else {
                    return []
                }
            }
            .observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
            .doOn(onNext: { response in
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
            })
            .asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
    }
}

現(xiàn)在你知道為什么當(dāng)解析的時(shí)候不需要關(guān)心線程問(wèn)題:Moya-ModelMapper 的 extension 為我們切換了 scheduler部念。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市氨菇,隨后出現(xiàn)的幾起案子印机,更是在濱河造成了極大的恐慌,老刑警劉巖门驾,帶你破解...
    沈念sama閱讀 221,430評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件射赛,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡奶是,警方通過(guò)查閱死者的電腦和手機(jī)楣责,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)聂沙,“玉大人秆麸,你說(shuō)我怎么就攤上這事〖昂海” “怎么了沮趣?”我有些...
    開(kāi)封第一講書人閱讀 167,834評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)坷随。 經(jīng)常有香客問(wèn)我房铭,道長(zhǎng),這世上最難降的妖魔是什么温眉? 我笑而不...
    開(kāi)封第一講書人閱讀 59,543評(píng)論 1 296
  • 正文 為了忘掉前任缸匪,我火速辦了婚禮,結(jié)果婚禮上类溢,老公的妹妹穿的比我還像新娘凌蔬。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,547評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布砂心。 她就那樣靜靜地躺著懈词,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辩诞。 梳的紋絲不亂的頭發(fā)上坎弯,一...
    開(kāi)封第一講書人閱讀 52,196評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音躁倒,去河邊找鬼荞怒。 笑死,一個(gè)胖子當(dāng)著我的面吹牛秧秉,可吹牛的內(nèi)容都是我干的褐桌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,776評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼象迎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼荧嵌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起砾淌,我...
    開(kāi)封第一講書人閱讀 39,671評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤啦撮,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后汪厨,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體赃春,經(jīng)...
    沈念sama閱讀 46,221評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,303評(píng)論 3 340
  • 正文 我和宋清朗相戀三年劫乱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了织中。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,444評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡衷戈,死狀恐怖狭吼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情殖妇,我是刑警寧澤刁笙,帶...
    沈念sama閱讀 36,134評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站谦趣,受9級(jí)特大地震影響疲吸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蔚润,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,810評(píng)論 3 333
  • 文/蒙蒙 一磅氨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嫡纠,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,285評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至者蠕,卻和暖如春窃祝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背踱侣。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,399評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工粪小, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抡句。 一個(gè)月前我還...
    沈念sama閱讀 48,837評(píng)論 3 376
  • 正文 我出身青樓探膊,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親待榔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子逞壁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,455評(píng)論 2 359

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