函數(shù)響應(yīng)式編程與RxSwift

函數(shù)式編程

本文介紹了函數(shù)響應(yīng)式編程(FRP)以及 RxSwift 的一些內(nèi)容, 源自公司內(nèi)部的一次分享.

不變狀態(tài)(immutable state)與沒有副作用(lack of side effects)

通常冲茸,一個函數(shù)盡量不要修改外部的一些變量。
函數(shù)的返回值有唯一性。

行動式思維 VS 采用聲明式思維

- (MTFilterInfoModel *)filterInfoByFilterID:(NSInteger)filterID
                                    ofTheme:(NSString *)themeNumber {
    if (themeNumber) {
        NSArray <MTFilterInfoModel *> *filterModels = [self filterInfosByThemeNumber:themeNumber];
        for (MTFilterInfoModel *filter in filterModels) {
            if (filter.filterID == filterID) {
                return filter;
            }
        }
    }
    return nil;
}

vs

let filters: [MTFilterInfoModel] = filterModels.filter { filter in
    return filter.filterID == filterID
}

Array的filter函數(shù)可以接收一個閉包Closure類型的參數(shù)。

對數(shù)組中的每個元素都執(zhí)行一遍該Closure育灸,根據(jù)Closure的返回值決定是否將該元素作為符合條件的元素放入查找結(jié)果(也是一個Array)中涌萤。

Objective-C中可以使用enumerateObjectsUsingBlock。

*** 注重Action VS 注重Result ***

first class function, closure

func myFilter(filter: MTFilterInfoModel) -> Bool {
    return filter.filterID == "5008"
}
let filters: [MTFilterInfoModel] = filterModels.filter(myFilter)

OC中的 blocks 或 enumeratexxx 也可以做到酝碳。

在Swift中使用高階函數(shù)(map捍歪,reduce户辱,filter等)。避免使用loop或enumeratexxx

Swift的高階函數(shù)使得其比Objective-C更適于函數(shù)式編程糙臼。

柯里化

就是把一個函數(shù)的多個參數(shù)分解成多個函數(shù)庐镐,然后把函數(shù)多層封裝起來,每層函數(shù)都返回一個函數(shù)去接收下一個參數(shù)变逃。

即:用函數(shù)生成另一個函數(shù)

“Swift 里可以將方法進行柯里化 (Currying)必逆,也就是把接受多個參數(shù)的方法變換成接受第一個參數(shù)的方法,并且返回接受余下的參數(shù)并且返回結(jié)果的新方法韧献∧┗迹”

// currying
func greaterThan(_ comparer: Int) -> (Int) -> Bool {
    return { $0 > comparer }
}

let isGreaterThan10 = greaterThan(10);

print(isGreaterThan10(2))
print(isGreaterThan10(20))

參考資料

FRP iOS Learning resources.md

函數(shù)式Swift - 王巍

異步編程

OC中的鏈式代碼

Masonry的寫法:

如B+中的StillCameraViewController:

[circleLoadingView mas_makeConstraints:^ (MASConstraintMaker *maker) {
            maker.leading.equalTo(thumbBottom).with.offset(30);
            maker.top.equalTo(thumbBottom);
            maker.width.equalTo(thumbBottom);
            maker.height.equalTo(thumbBottom);
        }];

原理:

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}

自己實現(xiàn)一個:

@implementation Person

- (Person * (^)(NSString *))named {
    return ^id(NSString *name) {
        self.name = name;
        return self;
    };
}

- (Person * (^)(NSInteger))withAge {
    return ^id(NSInteger age) {
        self.age = age;
        return self;
    };
}

- (Person * (^)(NSString *))liveIn {
    return ^id(NSString *city) {
        self.city = city;
        return self;
    };
}

@end

使用如下:

Person *p = [[Person alloc] init];
[p.named(@"MyName").withAge(18).liveIn(@"Xiamen") doSomething];

請求指定網(wǎng)絡(luò)圖片

登錄API -> 判斷token值 -> 請求API獲取真實的JSON數(shù)據(jù) -> 解析得到圖片URL -> 請求圖片 -> 填充UIImageView

[self request:api_login success:^{
    if (isTokenCorrect) {
        [self request:api_json success:^(NSData *data) {
            NSDictionary *json = [self parse:data];
            NSString *imgURL = json[@"thumbnail"];
            [SDWebImageHelper requestImg:imgURL success:^(UIImage *image, NSError *error) {
                runInMainQueue {
                    self.imageView.image = image;
                }
            }];
        }];
    }
}];

異步代碼,線程切換锤窑。

可以使用類似 Promise 的方式解決異步代碼問題。

狀態(tài)更新

Target-Action

Delegate

KVO

Notification

Blocks

以上是Objective-C中的幾種狀態(tài)更新方式嚷炉。

響應(yīng)式編程

Reactive programming is programming with asynchronous data streams.

響應(yīng)式編程與以上的幾種狀態(tài)更新方式不同渊啰,關(guān)鍵在于 *** 將異步可觀察序列對象模型化 *** 。

命令式編碼-Pull申屹,響應(yīng)式編程-Push绘证。

Push的內(nèi)容即為異步數(shù)據(jù)流。

而函數(shù)式編程可以非常方便地對數(shù)據(jù)流進行合并哗讥、創(chuàng)建嚷那、過濾、加工等操作杆煞,因此與響應(yīng)式編程結(jié)合比較合適魏宽。

RxSwift

*** Function programming + Reactive programming + Swift -> RxSwift ***

Why

是時候?qū)W習(xí) RxSwift 了

rx

btnClose.rx.tap

自己構(gòu)造一個類似的

struct MT<Base> {
    let base: Base

    init(_ base: Base) {
        self.base = base
    }
}

protocol MTProtocol {
    associatedtype CompatibleType

    var mt: MT<CompatibleType> { get set }
}

extension MTProtocol {
    var mt: MT<Self> {
        get {
            return MT(self)
        }
        set {

        }
    }
}

extension NSObject: MTProtocol {}

extension MT where Base: UIViewController {
    var size: CGSize {
        get {
            return base.view.frame.size
        }
    }
}

使用如下:

print(viewController.mt.size)

使用樣例 1

RxSwift Workflow

這里引用limboy博客中的一張圖:

RxSwift Workflow

簡單的計算界面

number1與number2為兩個UITextField

// 將兩個Observable綁定在一起腐泻,構(gòu)成一個Observable
Observable.combineLatest(number1.rx.text, number2.rx.text) { (num1, num2) -> Int in
    if let num1 = num1, num1 != "", let num2 = num2, num2 != "" {
        return Int(num1)! + Int(num2)!
    } else {
        return 0
    }
}
// Observable發(fā)送的消息為Int,不能與result.rx.text綁定队询,所以需使用map進行映射
.map { $0.description }
// Obsever為result.rx.text
.bindTo(result.rx.text)
.addDisposableTo(CS_DisposeBag)

注冊登錄界面

// 聲明Observable派桩,可觀察對象
// username的text沒有太多參考意義,因此使用map來加工蚌斩,得到是否可用的消息
let userValidation = textFieldUsername.rx.text.orEmpty
    // map的參數(shù)是一個closure铆惑,接收element
    .map { (user) -> Bool in
        let length = user.characters.count
        return length >= minUsernameLength && length <= maxUsernameLength
    }
    .shareReplay(1)

let passwdValidataion = textFieldPasswd.rx.text.orEmpty
    .map{ (passwd) -> Bool in
        let length = passwd.characters.count
        return length >= minUsernameLength && length <= maxUsernameLength
    }
    .shareReplay(1)

// 聲明Observable
// 組合兩個Observable
let loginValidation = Observable.combineLatest(userValidation, passwdValidataion) {
        $0 && $1
    }
    .shareReplay(1)


// bind,即將Observable與Observer綁定送膳,最終也會調(diào)用subscribe
// 此處是將isEnabled視為一個Observer员魏,接收userValidation的消息,做出響應(yīng)
// 所以O(shè)bservable發(fā)送的消息與Observer能接收的消息要對應(yīng)起來(此處是Bool)
userValidation
    .bindTo(textFieldPasswd.rx.isEnabled)
    .addDisposableTo(CS_DisposeBag)
userValidation
    .bindTo(lbUsernameInfo.rx.isHidden)
    .addDisposableTo(CS_DisposeBag)

passwdValidataion
    .bindTo(lbPasswdInfo.rx.isHidden)
    .addDisposableTo(CS_DisposeBag)

loginValidation
    .bindTo(btnLogin.rx.isEnabled)
    .addDisposableTo(CS_DisposeBag)

使用樣例 2

監(jiān)控UIScrollView的scroll操作叠聋。

通常:UIScrollViewDelegate

public func scrollViewDidScroll(_ scrollView: UIScrollView) {
    // scrollView 1:
        //
    // scrollView 2:
        //
    // scrollView 3:
        //
}

通過RxSwift:

根本:scrollView的contentOffset在變化

tableView.rx.contentOffset
            .map { $0.y }
            .subscribe(onNext: { (contentOffset) in
                if contentOffset >= -UIApplication.shared.statusBarFrame.height / 2 {
                    UIApplication.shared.statusBarStyle = .lightContent
                } else {
                    UIApplication.shared.statusBarStyle = .default
                }
            })
            .addDisposableTo(CS_DisposeBag)

使用樣例 3

對于UITextField, UISearchController逆趋,UIButton等等,常見的使用步驟如下:

init

setup Delegate晒奕,or addTargetxxx

Delegate callback

而使用RxSwift闻书,則可以做到 *** 高聚合,低耦合 ***

btnClose.rx.tap
    .subscribe(onNext: { [weak self] in
        guard let strongSelf = self else { return }
        strongSelf.dismiss(animated: true, completion: nil)
    })
    .addDisposableTo(CS_DisposeBag)

Rx基本概念

reactivex.io

發(fā)布-訂閱

Observable:

可觀察對象脑慧,可組合魄眉。(發(fā)射數(shù)據(jù))

next新的事件數(shù)據(jù),complete事件序列的結(jié)束闷袒,error異常導(dǎo)致結(jié)束

所以next可以多次調(diào)用坑律,而complete只有最后一次。

/// Type that can be converted to observable sequence (`Observer<E>`).
public protocol ObservableConvertibleType {
    /// Type of elements in sequence.
    associatedtype E

    /// Converts `self` to `Observable` sequence.
    ///
    /// - returns: Observable sequence that represents `self`.
    func asObservable() -> Observable<E>
}

此外囊骤,還有 *** create晃择,just,of也物,from *** 等一系列函數(shù)

from: Converts an array to an observable sequence.

Observer

對Observable發(fā)射的數(shù)據(jù)或數(shù)據(jù)序列做出響應(yīng)宫屠,做出特定的操作。

/// Supports push-style iteration over an observable sequence.
public protocol ObserverType {
    /// The type of elements in sequence that observer can observe.
    associatedtype E

    /// Notify observer about sequence event.
    ///
    /// - parameter event: Event that occured.
    func on(_ event: Event<E>)
}

subscribe

訂閱事件滑蚯。

對事件序列中的事件浪蹂,如next,complete告材,error進行響應(yīng)坤次,

extension ObservableType {
    /**
    Subscribes an element handler, an error handler, a completion handler and disposed handler to an observable sequence.

    - parameter onNext: Action to invoke for each element in the observable sequence.
    - parameter onError: Action to invoke upon errored termination of the observable sequence.
    - parameter onCompleted: Action to invoke upon graceful termination of the observable sequence.
    - parameter onDisposed: Action to invoke upon any type of termination of sequence (if the sequence has
        gracefully completed, errored, or if the generation is cancelled by disposing subscription).
    - returns: Subscription object used to unsubscribe from the observable sequence.
    */
    public func subscribe(onNext: ((E) -> Void)? = nil, onError: ((Swift.Error) -> Void)? = nil, onCompleted: (() -> Void)? = nil, onDisposed: (() -> Void)? = nil)
        -> Disposable {
            xxx
        }
}

所以:

*** Rx的關(guān)鍵在于Observer訂閱Observable,Observable將數(shù)據(jù)push給Observer斥赋,Observer自己做出對應(yīng)的響應(yīng)缰猴。 ***

map

進行數(shù)據(jù)映射

extension ObservableType {

    /**
        Projects each element of an observable sequence into a new form.

        - seealso: [map operator on reactivex.io](http://reactivex.io/documentation/operators/map.html)

        - parameter transform: A transform function to apply to each source element.
        - returns: An observable sequence whose elements are the result of invoking the transform function on each element of source.

        */
    public func map<R>(_ transform: @escaping (Self.E) throws -> R) -> RxSwift.Observable<R>

bindTo

extension ObservableType {
    /**
    Creates new subscription and sends elements to variable.

    In case error occurs in debug mode, `fatalError` will be raised.
    In case error occurs in release mode, `error` will be logged.

    - parameter variable: Target variable for sequence elements.
    - returns: Disposable object that can be used to unsubscribe the observer.
    */
    public func bindTo(_ variable: RxSwift.Variable<Self.E>) -> Disposable
}

Disposable

定義了釋放資源的統(tǒng)一行為。

DisposeBag: 訂閱會有Disposable疤剑,自動銷毀相關(guān)的訂閱滑绒∶票ぃ可簡單類似autorelease機制

BahaviorSubject 與 PublishSubject

創(chuàng)建一個可添加新元素的Observable,讓訂閱對象能夠接收包含初始值與新值的事件蹬挤。

BahaviorSubject代表了一個隨時間推移而更新的值缚窿,包含初始值。

let s = BehaviorSubject(value: "hello")
// s.onNext("hello again") // 會替換到hello消息
s.subscribe { // 不區(qū)分訂閱事件焰扳,所以打印 next(hello)
    print($0)
}
// s.subscribe(onNext: { // 僅區(qū)分訂閱事件倦零,所以打印next事件接收的數(shù)據(jù) hello
//     print($0)
// })
.addDisposableTo(CS_DisposeBag)
s.onNext("world") // 發(fā)送下一個事件
s.onNext("!")
s.onCompleted()
s.onNext("??") // completed之后即不能響應(yīng)了

PublishSubject與BehaviorSubject類似,

但PublishSubject不需要初始值吨悍,且不會將最后一個值發(fā)送給Observer扫茅。

struct Person {
    let name = PublishSubject<String>()
    let age  = PublishSubject<Int>()
}
let person = Person()
person.name.onNext("none")
person.age.onNext(0)
Observable.combineLatest(person.name, person.age) {
        "\($0) \($1)"
    }
    .debug()
    .subscribe {
        print($0)
    }
    .addDisposableTo(CS_DisposeBag)
person.name.onNext("none again") // 該none again數(shù)據(jù)不會發(fā)送
person.name.onNext("chris")
person.age.onNext(18)
person.name.onNext("ada")

使用了combineLatest,則會等待需要combine的數(shù)據(jù)都準備好了才會發(fā)送育瓜。

可以通過combineLatest來直觀感受葫隙。

使用PublishSubject的log如下:

2017-06-22 16:46:51.160: AppDelegate.swift:186 (basicRx()) -> subscribed
2017-06-22 16:46:51.161: AppDelegate.swift:186 (basicRx()) -> Event next(chris 18)
next(chris 18)
2017-06-22 16:46:51.162: AppDelegate.swift:186 (basicRx()) -> Event next(ada 18)
next(ada 18)
2017-06-22 16:46:51.162: AppDelegate.swift:165 (basicRx()) -> Event completed
completed
2017-06-22 16:46:51.162: AppDelegate.swift:165 (basicRx()) -> isDisposed

operations

可以通過combineLatest來直觀感受。

除了combine躏仇,還可以使用concat恋脚,merge,zip等到操作焰手。

zip需要兩個元素都有新值才會發(fā)送數(shù)據(jù)糟描。

let personZip = Person()
// zip需要兩個元素都有新值才會發(fā)送
Observable.zip(personZip.name, personZip.age) {
        "\($0) \($1)"
    }
    .subscribe {
        print($0)
    }
    .addDisposableTo(CS_DisposeBag)
personZip.name.onNext("zip none") // 不會單獨發(fā)送
personZip.name.onNext("zip chris")// 放入序列中,等待age
personZip.age.onNext(18)          // 結(jié)合zip none一起發(fā)送
personZip.name.onNext("zip ada")  // 永遠不會發(fā)送书妻,在其之前已經(jīng)有zip chris
personZip.age.onNext(20)          // 結(jié)合zip chris一起發(fā)送
personZip.name.onCompleted()
personZip.age.onCompleted()

打印的log如下:

next(zip none 18)
next(zip chris 20)
completed
2017-06-23 13:50:48.234: AppDelegate.swift:172 (basicRx()) -> Event completed
completed
2017-06-23 13:50:48.235: AppDelegate.swift:172 (basicRx()) -> isDisposed

zip的場景要好好體會下船响,為何會是這兩個輸出。

可以通過zip來直觀感受躲履。

Variable

Variable基于BahaviorSubject封裝的類见间,通過asObservable()保留出其內(nèi)部的BahaviorSubject的可觀察序列。

表示一個可監(jiān)聽的數(shù)據(jù)結(jié)構(gòu)工猜,可以監(jiān)聽數(shù)據(jù)變化米诉,或者將其他值綁定到變量。

Variable不會發(fā)生任何錯誤事件域慷,即將被銷毀處理的時候荒辕,會自動發(fā)送一個completed事件。因此有些使用Variable

let v = Variable<String>("hello")
v.asObservable()
    .debug()
    .distinctUntilChanged() // 消除連續(xù)重復(fù)的數(shù)據(jù)
    .subscribe {
        print($0)
    }
    .addDisposableTo(CS_DisposeBag)
v.value = "world"
v.value = "world" // 不會對重復(fù)的"world"做出響應(yīng)
v.value = "!"

打印log如下犹褒,可以看出其發(fā)送的可觀察序列:

2017-06-22 16:22:57.208: AppDelegate.swift:162 (basicRx()) -> subscribed
2017-06-22 16:22:57.211: AppDelegate.swift:162 (basicRx()) -> Event next(hello)
next(hello)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(world)
next(world)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(world)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(!)
next(!)
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event completed
completed
2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> isDisposed

其他

如 *** Subject, BehaviorSubject, Driver 等等 ***

RxSwiftStudy

RxSwift學(xué)習(xí)博客

RxSwift學(xué)習(xí)之旅 - Observable 和 Driver

Demos

Login

*** RxSwift 與 RxCocoa ***

兩個Observable進行combine操作:

let loginValidation = Observable.combineLatest(userValidation, passwdValidataion) {
                $0 && $1
            }
            .shareReplay(1)

構(gòu)建UITableView

RxDataSources

對UITableView, UICollectionView的dataSource進行Rx的封裝。

設(shè)置數(shù)據(jù)源
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, User>>()
聲明configureCell
dataSource.configureCell = xxx
bind即可

準備一個Observable<[SectionModel<String, User>]>弛针,然后與tableView進行相關(guān)bind即可

userViewModel.getUsers()
            .bindTo(tableView.rx.items(dataSource: dataSource))
            .addDisposableTo(CS_DisposeBag)
點擊操作
tableView.rx
    .modelSelected(User.self)
    .subscribe(onNext: { user in
        print(user)

        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let loginVC = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
        self.present(loginVC, animated: true, completion: nil)
    })
    .addDisposableTo(CS_DisposeBag)

使用RxSwift來構(gòu)建UICollectionView的步驟類似叠骑。

SearchBar

ViewModel如下:

struct Repo {
    let name: String
    let url: String
}

class SearchBarViewModel {

    let searchText = Variable<String>("")

    let CS_DisposeBag = DisposeBag()

    lazy var repos: Driver<[Repo]> = {
        return self.searchText.asObservable()
            .throttle(0.3, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .flatMapLatest { (user) -> Observable<[Repo]> in
                if user.isEmpty {
                    return Observable.just([])
                }

                return self.searchRepos(user: user)
            }
            .asDriver(onErrorJustReturn: [])
    }()

    func searchRepos(user: String) -> Observable<[Repo]> {
        guard let url = URL(string: "https://api.github.com/users/\(user)/repos") else {
            return Observable.just([])
        }

        return URLSession.shared.rx.json(url: url)
            .retry(3)
            .debug()
            .map {
                var repos = [Repo]()

                if let items = $0 as? [[String: Any]] {
                    items.forEach {
                        guard let name = $0["name"] as? String,
                              let url  = $0["url"]  as? String
                            else { return }
                        repos.append(Repo(name: name, url: url))
                    }
                }

                return repos
            }
    }

}

ViewController中的代碼如下:

var searchBarViewModel = SearchBarViewModel()

tableView.tableHeaderView = searchVC.searchBar

searchBar.rx.text.orEmpty
            .bindTo(searchBarViewModel.searchText)
            .addDisposableTo(CS_DisposeBag)

        searchBarViewModel.repos
            .drive(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, repo, cell) in
                cell.textLabel?.text = repo.name
                cell.detailTextLabel?.text = repo.url
            }
            .addDisposableTo(CS_DisposeBag)

UISearchBar中的text與ViewModel中的searchText進行綁定,

而ViewModel中的searchText是Variable類型削茁,作為Observable會在MainScheduler中輸入間隔達到0.3s只后會觸發(fā)調(diào)用searchRepos函數(shù)進行搜索宙枷。

repos作為Driver掉房,其中元素是包含Repo的數(shù)組。Driver同樣封裝了可觀察序列慰丛,但Driver只在主線程執(zhí)行卓囚。

所以,做數(shù)據(jù)綁定可以使用bindTo和Driver诅病,涉及到UI的綁定可以盡量使用Driver哪亿。

在本例中,跟repos綁定的即是tableView.rx.items贤笆,即repos直接決定了tableView中的items展示內(nèi)容蝇棉。

對應(yīng)使用URLSession進行網(wǎng)絡(luò)請求的場景,RxSwift也提供了非常方便的使用方式芥永。注意各個地方Observable的類型保持一致即可篡殷。

另外,注意 *** throttle *** 埋涧, *** flatMapLatest *** 及 *** distinctUntilChanged *** 的用法板辽。

使用MVVM

ViewModel

優(yōu)點

數(shù)據(jù)綁定,精簡Controller棘催,便于單元測試劲弦。

缺點

數(shù)據(jù)綁定額外消耗,調(diào)試困難巧鸭,

*** 注意input與output即可 ***

*** 難點在于如何合理的處理ViewModel與View的數(shù)據(jù)綁定問題瓶您。***

如何寫好一個ViewModel?

此處關(guān)于寫好ViewModel的建議纲仍,出自:

【漫談】從項目實踐走向RxSwift響應(yīng)式函數(shù)編程

View不應(yīng)該存在邏輯控制呀袱,只綁定展示數(shù)據(jù)而不對其做操作

struct UserViewModel {
    let userName: String
    let userAge:  Int
    let userCity: String
}

textFieldUserName.rx.text.orEmpty
    .bindTo(userViewModel.userName)
// 不推薦    
textFiledUserAge.rx.text.orEmpty
    .map { Int($0) }
    .bindTo(userViewModel.userAge)

View只能通過ViewModel知道View要做什么

userViewModel.register()

self.btnRegister.rx.tap
    .bindTo(userViewModel.register)

ViewModel只暴露View顯示所需要的最少信息

struct UserViewModel {
    let user: UserModel
}

struct UserViewModel {
    let userName: String
    let userAge:  String
    let userCity: String
}

參考資料

LearnRxSwift

rx-sample-code

RxSwift Reactive Programming with Swift by raywenderlich.com

100-days-of-RxSwift

RxSwift-CN

iOS 架構(gòu)模式 - 簡述 MVC, MVP, MVVM 和 VIPER (譯)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市郑叠,隨后出現(xiàn)的幾起案子夜赵,更是在濱河造成了極大的恐慌,老刑警劉巖乡革,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件寇僧,死亡現(xiàn)場離奇詭異,居然都是意外死亡沸版,警方通過查閱死者的電腦和手機嘁傀,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來视粮,“玉大人细办,你說我怎么就攤上這事±倥梗” “怎么了笑撞?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵岛啸,是天一觀的道長。 經(jīng)常有香客問我茴肥,道長坚踩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任瓤狐,我火速辦了婚禮瞬铸,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘芬首。我一直安慰自己赴捞,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布郁稍。 她就那樣靜靜地躺著赦政,像睡著了一般。 火紅的嫁衣襯著肌膚如雪耀怜。 梳的紋絲不亂的頭發(fā)上恢着,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天,我揣著相機與錄音财破,去河邊找鬼掰派。 笑死,一個胖子當(dāng)著我的面吹牛左痢,可吹牛的內(nèi)容都是我干的靡羡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼俊性,長吁一口氣:“原來是場噩夢啊……” “哼略步!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起定页,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤趟薄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后典徊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杭煎,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年卒落,在試婚紗的時候發(fā)現(xiàn)自己被綠了羡铲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡儡毕,死狀恐怖犀勒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情妥曲,我是刑警寧澤贾费,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站檐盟,受9級特大地震影響褂萧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜葵萎,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一导犹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧羡忘,春花似錦谎痢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至漫雕,卻和暖如春滨嘱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背浸间。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工太雨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人魁蒜。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓囊扳,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兜看。 傳聞我的和親對象是個殘疾皇子锥咸,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,976評論 2 355

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