Swift — 协议(Protocol)

Swift — 协议(Protocol)

[TOC]

协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体和枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这些遵循协议的类型就能够使用这些功能。

1. 协议的基本用法

1.1 协议语法

协议的定义方式与类、结构体和枚举的定义非常相似

  • 基本语法
protocol SomeProtocol {
    // 这里是协议的定义部分
}
  • 如果让自定义的类型遵循某个协议,在定义类型时,需要在类型名称后面加上协议名称,中间以冒号(:)隔开,如果需要遵循多个协议时,个协议之间用逗号(,)分割:
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 这里是结构体的定义部分
}
  • 如果自定义类型拥有一个父类,应该将父类名放在遵循协议名之前,以逗号分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 这里是类的定义部分
}

1.2 属性要求

我们可以在协议中添加属性,但需要注意以下几点:

  1. 属性可以是实例属性和类型属性
  2. 属性需要使用var修饰,不能属于let
  3. 类型属性只能使用static修饰,不能使用class
  4. 我们需要声明属性必须是可读的或者可读可写的
protocol SomeProtocol {
    var propertyOne: Int { get set }
    var propertyTwo: Int { get }
    static var propertyThree: Int { get set }
}

1.3 方法要求

我们可以在协议中添加方法,但需要注意以下几点:

  1. 可以是实例方法或类方法
  2. 像普通方法一样放在协议定义中,但不需要大括号和方法体
  3. 协议中不支持为协议中的方法提供默认参数
  4. 协议中的类方法也只能使用static关键字作为前缀,不能使用class
  5. 可以使用mutating提供异变方法,以使用该方法时修改实体的属性等。
  6. 可以定义构造方法,但是使用的时候需要使用required关键字
protocol SomeProtocol {
    func someMethod1()
    func someMethod2() ->Int
}

构造方法

protocol SomeProtocol {
    init(param: Int)
}

class SomeClass: SomeProtocol {
    required init(param: Int) { }
}

异变方法

protocol Togglable {
    mutating func toggle()
}

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}

1.4 协议作为类型

尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。协议作为类型使用,有时被称作「存在类型」,这个名词来着存在着一个类型T,该类型遵循协议T。

协议可以像其他普通类型一样使用,使用场景如下:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型
protocol SomeProtocol { }

class SomeClass {
    required init(param: SomeProtocol) {}
}

1.5 其他

  • 协议还可以被继承
  • 可以在扩展里面遵循协议
  • 在扩展里面声明采纳协议
  • 使用合成来采纳协议
  • 可以定义由类专属协议,只需要继承自AnyObject
  • 协议可以合成
  • 协议也可以扩展

更多的关于协议的用法请参考:

Protocols

以及它的译文:

SwiftGG 协议

2. 协议中方法的调用

举个例子,在数学中我们会求某个图形的面积,但是不同形状求面积的公式是不一样的,如果用代码来实现可以怎么来实现呢?

首先我们可以通过继承父类的方法来实现,但是在这里我们就可以使用协议来实现:

protocol Shape {
    var area: Double {get}
}

class Circle: Shape{
    var radius: Double
   
    init(_ radius: Double) {
        self.radius = radius
    }
    
    var area: Double{
        get{
            return radius * radius * 3.14
        }
    }
}
class Rectangle: Shape{
    var width, height: Double
    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }
    
    var area: Double{
        get{
            return width * height
        }
    }
}

var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10.0, 20.0)

print(circle.area)
print(rectangle.area)

<!--打印结果-->
314.0
200.0

此时的打印结果是符合我们的预期的。

我们知道协议可以扩展,此时我们把协议的代码修改成如下:

protocol Shape {
//    var area: Double {get}
}
extension Shape{
    var area: Double {
        get{return 0.0}
    }
}

<!--打印结果-->
0.0
0.0

此时并没有如我们预期的打印,如果我们声明变量的时候写成如下呢:

var circle: Circle = Circle.init(10.0)
var rectangle: Rectangle = Rectangle.init(10.0, 20.0)

<!--打印结果-->
314.0
200.0

此时的打印就符合我们的预期了。

其实我们也能够清楚的了解到为什么会打印0.0,在Swift 方法调度这篇文章中我们介绍了extension中声明的方法是静态调用的,也就是说在编译后当前代码的地址已经确定,我们无法修改,当声明为Shap类型后,默认调用的就是Shape extension中的属性的get方法。下面我们在通过sil代码来验证一下,关于生成sil代码的方法,请参考我以前的文章。

为了方便查看,我们精简并修改代码为如下:

protocol Shape {
//    var area: Double {get}
}
extension Shape{
    var area: Double {
        get{return 0.0}
    }
}

class Circle: Shape{
    var radius: Double
   
    init(_ radius: Double) {
        self.radius = radius
    }
    
    var area: Double{
        get{
            return radius * radius * 3.14
        }
    }
}

var circle: Shape = Circle.init(10.0)

var a = circle.area

生成的sil代码:


image

通过sil代码我们可以清晰的看到,这里直接调用的Shape.area.getter方法。

下面我们换一些简单的代码再次看一下:

protocol PersonProtocol {
    func eat()
}
extension PersonProtocol{
    func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
    func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()

<!--打印结果-->
Person eat
Person eat

可以看到上面这段代码的打印结果都是Person eat,那么为什么会打印相同的结果呢?首先通过代码我们可以知道,在PersonProtocol中声明了eat方法。对于声明的协议方法,如果类中也实现了,就不会调用协议扩展中的方法。上面的属性的例子中并没有在协议中声明属性,只是在协议扩展中添加了一个属性。下面我们看看上面这段代码的sil代码:

image

首先我们可以看到,对于两个eat方法的确实存在不同,首先声明为协议类型的变量调用eat方法是通过witness_method调用,另一个则是通过class_method调用。

  • witness_method是通过PWT(协议目击表)获取对应的函数地址
  • class_method是通过类的函数表来查找函数进行调用
image

在刚刚sil代码中我们可以找到sil_witness_table,在里面有PersonProtocol.eat方法,找到PersonProtocol.eat方法可以发现里面是调用class_method寻找的类中VTablePerson.eat方法。

如果我们不在协议中声明eat方法:

protocol PersonProtocol {
//    func eat()
}
extension PersonProtocol{
    func eat(){ print("PersonProtocol eat") }
}
class Person: PersonProtocol{
    func eat(){ print("Person eat") }
}
let p: PersonProtocol = Person()
p.eat()
let p1: Person = Person()
p1.eat()

<!--打印结果-->
PersonProtocol eat
Person eat

查看sil代码:

image

此时我们可以看到,对于不在协议中声明方法的时候,依然是直接调用(静态调用)。

所以对于协议中方法的调度:

  • 对于不在协议中声明的方法
    • 在协议扩展中有实现就是直接调用
    • 在遵循协议的实体中按照其调度方式决定
    • 两处都实现了,声明的实例是协议类型则直接调用协议扩展中的方法,反之调用遵循协议实体中的方法
  • 对于声明在协议中的方法
    • 如果遵循该协议的实体实现了该方法,则通过PWT协议目击表查找到实现的方法进行调用(与声明变量的类型无关)
    • 如果遵循协议的实体没实现,协议扩展实现了,则会调用协议扩展中的方法

3. 协议原理探索

在上面探索协议中的方法调用的时候,我们提到过PWT也就是Protocol witness table,协议目击表,那么它存储在什么地方呢?我们在Swift 方法调度这篇文章中讲过,V-Table是存储在metadata中的,那么我们就探索一下PWT的存储位置。

3.1 内存占用

首先我们先来看看如下代码的的打印结果:

protocol Shape {
    var area: Double { get }
}
class Circle: Shape {
    var radius: Double

    init(_ radius: Double) {
        self.radius = radius
    }

    var area: Double{
        get{ return radius * radius * 3.14 }
    }
}

var circle: Shape = Circle(10.0)
print(MemoryLayout.size(ofValue: circle))
print(MemoryLayout.stride(ofValue: circle))

var circle1: Circle = Circle(10.0)
print(MemoryLayout.size(ofValue: circle1))
print(MemoryLayout.stride(ofValue: circle1))

<!--打印结果-->
40
40
8
8

3.2 lldb探索内存结构

看到这个打印结果我能第一时间想到的就是生命为协议类型会存储更多的信息。生命为类的时候,存储的是类的实例对象的指针8字节。下面我们通过lldb调试来探索一下这个40字节都存储了什么信息。

image

3.3 sil 探索内存结构

通过lldb我们可以看到其内部应该存储着一些信息,那么具体存了什么呢?我们在看看sil代码:

image

在sil代码中我们可以看到,在初始化circle这个变量的时候使用到了init_existential_addr,查看SIL文档

image

译文:用一个准备好包含类型为$T的存在容器部分初始化%0引用的内存。该指令的结果是一个地址,该地址引用了所包含值的存储空间,该存储空间仍然没有初始化。包含的值必须存储为-d或copy_addr-ed,以便完全初始化存在值。如果存在容器的值未初始化时需要销毁,则必须使用deinit_existential_addr来完成此操作。可以像往常一样使用destroy_addr销毁完全初始化的存在性容器。销毁一个部分初始化存在容器的addr是未定义的行为。

文档中的意思是,使用了包含$Texistential container来初始化%0引用的内存。在这里就是使用包含Circleexistential container来初始化circle引用的内存,简单来说就是将circle包装到了一个existential container初始化的内存。

existential container是编译器生成的一种特殊的数据类型,也用于管理遵守了相同协议的协议类型。因为这些塑化剂类型的内存空间尺寸不同,使用existential container进行管理可以实现存储一致性。

3.4 IR代码探索内存结构

那么这个existential container都包装了什么呢?目前通过sil代码是看不出来什么了,那么我们就看看IR代码:

; 一个结构体,占用24字节内存的数组,wift.type指针, i8*指针
%T4main5ShapeP = type { [24 x i8], %swift.type*, i8** }

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  ; main.Circle 的 metadata
  %3 = call swiftcc %swift.metadata_response @"type metadata accessor for main.Circle"(i64 0) #7
  %4 = extractvalue %swift.metadata_response %3, 0
  ;init放
  %5 = call swiftcc %T4main6CircleC* @"main.Circle.__allocating_init(Swift.Double) -> main.Circle"(double 1.000000e+01, %swift.type* swiftself %4)
  ; 存%4 也就是metadata,存到T4main5ShapeP结构体中,这里存的位置是第二个位置
  store %swift.type* %4, %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 1), align 8
  ; 存pwt 也就是协议目击表,存到第三个位置
  store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Circle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.circle : main.Shape", i32 0, i32 2), align 8
  ; 存放%5到二级指针,%5是init出来的对象,所以这里也就是个HeapObject结构,也就是T4main6CircleC结构体的第一个8字节内存空间处
  store %T4main6CircleC* %5, %T4main6CircleC** bitcast (%T4main5ShapeP* @"main.circle : main.Shape" to %T4main6CircleC**), align 8
}

从IR代码中我们可以知道,这里面的存储是一个结构体,结构体中主要分为三个方面:

  1. 一个连续的24字节空间
  2. 一个存放metadata的指针
  3. 存放pwt指针

3.5 仿写

下面我们就来仿写一下这个结构:

struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}

struct protocolData {
    //24 * i8 :因为是8字节读取,所以写成3个指针
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    //type 存放metadata,目的是为了找到Value Witness Table 值目录表
    var type: UnsafeRawPointer
    // i8* 存放pwt指针
    var pwt: UnsafeRawPointer
}

3.5.1 类遵循协议重绑定

进行内存的重新绑定:

protocol Shape {
    var area: Double { get }
}
class Circle: Shape {
    var radius: Double

    init(_ radius: Double) {
        self.radius = radius
    }

    var area: Double{
        get{ return radius * radius * 3.14 }
    }
}

var circle: Shape = Circle(10.0)

// 将circle强转为protocolData结构体
withUnsafePointer(to: &circle) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

<!--打印结果-->
protocolData(value1: 0x00000001006082b0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x0000000100008180, pwt: 0x0000000100004028)
image

通过lldb查看:

  1. 我们也可以看到对应HeapObject结构
    1. 该结构存储的是Circle的实例变量
    2. 并且在这里面的metadataprotocolData里面的存储的metadata的地址是一致的;
  2. 通过cat address命令查看pwt对应的指针,可以看到这段内存对应的就是SwiftProtocol.Circleprotocol witness table

至此我们就清楚的找到你了PWT的存储位置,PWT存在协议类型实例的内存结构中。

3.5.2 结构体遵循协议重绑定

在上面这个例子中我们使用的是类,我们知道类是引用类型,如果换成结构体呢?

protocol Shape {
    var area: Double {get}
}
struct Rectangle: Shape{
    var width, height: Double
    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var rectangle: Shape = Rectangle(10.0, 20.0)


struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}


struct protocolData {
    //24 * i8 :因为是8字节读取,所以写成3个指针
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    //type 存放metadata,目的是为了找到Value Witness Table 值目录表
    var type: UnsafeRawPointer
    // i8* 存放pwt指针
    var pwt: UnsafeRawPointer
}

// 将circle强转为protocolData结构体
withUnsafePointer(to: &rectangle) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

<!--打印结果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x0000000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)
image

此时我们可以看到,此时并没有存储一个HeapObject结构的指针,而是直接存储Double类型的值,metadatapwt没有变。

在看下IR代码:

define i32 @main(i32 %0, i8** %1) #0 {
entry:
  %2 = bitcast i8** %1 to i8*
  %3 = call swiftcc { double, double } @"main.Rectangle.init(Swift.Double, Swift.Double) -> main.Rectangle"(double 1.000000e+01, double 2.000000e+01)
  ; 10
  %4 = extractvalue { double, double } %3, 0
  ; 20
  %5 = extractvalue { double, double } %3, 1
  ;metadata
  store %swift.type* bitcast (i64* getelementptr inbounds (<{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>, <{ i8**, i64, <{ i32, i32, i32, i32, i32, i32, i32 }>*, i32, i32 }>* @"full type metadata for main.Rectangle", i32 0, i32 1) to %swift.type*), %swift.type** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 1), align 8
  ;pwt
  store i8** getelementptr inbounds ([2 x i8*], [2 x i8*]* @"protocol witness table for main.Rectangle : main.Shape in main", i32 0, i32 0), i8*** getelementptr inbounds (%T4main5ShapeP, %T4main5ShapeP* @"main.rectangle : main.Shape", i32 0, i32 2), align 8
  ;存%4 也就是10
  store double %4, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 0, i32 0), align 8
  ; 存%5 也就是20
  store double %5, double* getelementptr inbounds (%T4main9RectangleV, %T4main9RectangleV* bitcast (%T4main5ShapeP* @"main.rectangle : main.Shape" to %T4main9RectangleV*), i32 0, i32 1, i32 0), align 8
}

通过IR代码我们可以看到:

  • 对于metadatapwt的存储依旧
  • 然后存储了两个Double值,并没有存储HeapObject类型的指针

那么如果有3个属性呢?

struct Rectangle: Shape{
    var width, width1, height: Double
    init(_ width: Double, _ width1: Double, _ height: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

<!--内存绑定后的打印结果-->
protocolData(value1: 0x4024000000000000, value2: 0x4034000000000000, value3: 0x403e000000000000, type: 0x0000000100004098, pwt: 0x0000000100004028)

这个三个Value的值分别是10,20,30

那如果是4个呢?

struct Rectangle: Shape{
    var width, width1, height, height1: Double
    init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
        self.height1 = height1
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)

<!--内存绑定后的打印结果-->
protocolData(value1: 0x0000000100715870, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)

此时并没有直接看到Double值了,查看value1的内存:

image

此时我们可以看到,这个内存中存储了10,20,30,40这四个值。

所以如果我们需要存储的数据超过了24 x i8*,也就是24字节时,就会开辟内存空间进行存储。这里只存储指向新开辟内存空间的指针。

这里的顺序是,如果不够存储就直接开辟内存空间,存储值,记录指针。而不是先存储不够了在开辟内存空间。

我们都知道,结构体是值类型,如果超过这24字节的存储空间就会开辟内存用来存储结构体中的值,如果此时发生拷贝会是神马结构呢?下面我们就来验证一下:

结构体拷贝:

protocol Shape {
    var area: Double {get}
}
struct Rectangle: Shape{
    var width, width1, height, height1: Double
    init(_ width: Double, _ width1: Double, _ height: Double, _ height1: Double) {
        self.width = width
        self.width1 = width1
        self.height = height
        self.height1 = height1
    }

    var area: Double{
        get{
            return width * height
        }
    }
}

var rectangle: Shape = Rectangle(10.0, 20.0, 30.0, 40.0)
var rectangle1 = rectangle

struct HeapObject {
    var type: UnsafeRawPointer
    var refCount1: UInt32
    var refCount2: UInt32
}


struct protocolData {
    //24 * i8 :因为是8字节读取,所以写成3个指针
    var value1: UnsafeRawPointer
    var value2: UnsafeRawPointer
    var value3: UnsafeRawPointer
    //type 存放metadata,目的是为了找到Value Witness Table 值目录表
    var type: UnsafeRawPointer
    // i8* 存放pwt指针
    var pwt: UnsafeRawPointer
}

// 内存重绑定
withUnsafePointer(to: &rectangle) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

withUnsafePointer(to: &rectangle1) { ptr in
    ptr.withMemoryRebound(to: protocolData.self, capacity: 1) { pointer in
        print(pointer.pointee)
    }
}

<!--打印结果-->
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)
protocolData(value1: 0x000000010683bac0, value2: 0x0000000000000000, value3: 0x0000000000000000, type: 0x00000001000040c0, pwt: 0x0000000100004050)

此时我们看到打印结果是一样的。

那么修改呢?

添加如下代码:

protocol Shape {
    // 为了方便修改,在这声明一下
    var width: Double {get set}
    var area: Double {get}
}

rectangle1.width = 50
image

通过lldb重新打印,我们可以看到在修改值后,内存地址已经修改了,此时就是写时复制。当复制时并没有值的修改,所以两个变量指向同一个堆区内存。当修改变量的时候,会原本的堆区内存的值拷贝到一个新的内存区域,并进行值的修改。

如果我们将struct修改成class,这里并不会触发写时复制,因为在Swift中类是引用类型,修改类的值就是修改其引用地址中的值。这里就不验证了,感兴趣的可以自己去试试。

如果我们将Double换成String原理也是一致的,这里也就不一一验证了。

3.5.3 小结

至此我们也就清楚了,为什么协议中通过witness_method调用,最终能找到V-Table中的方法,原因就是存储了metadatapwt。这也是我们都声明为协议类型,最终能打印出不同形状的面积根本原因。

4. 总结

至此我们对Swift中协议的分析就结束了,现总结如下:

  1. Swift中类、结构体、枚举都可以遵守协议
  2. 遵守多个协议使用逗号(,)分隔
  3. 有父类的,父类写在前面,协议在后面用逗号(,)分隔
  4. 协议中可以添加属性
    1. 属性可以是实例属性和类型属性
    2. 属性需要使用var修饰,不能属于let
    3. 类型属性只能使用static修饰,不能使用class
    4. 我们需要声明属性必须是可读的或者可读可写的
  5. 协议中可以添加方法
    1. 可以是实例方法或类方法
    2. 像普通方法一样放在协议定义中,但不需要大括号和方法体
    3. 协议中不支持为协议中的方法提供默认参数
    4. 协议中的类方法也只能使用static关键字作为前缀,不能使用class
    5. 可以使用mutating提供异变方法,以使用该方法时修改实体的属性等。
    6. 可以定义构造方法,但是使用的时候需要使用required关键字
  6. 如果定义由类专属协议,则需要继承自AnyObject
  7. 协议可以作为类型
    1. 作为函数、方法或构造器中的参数类型或返回值类型
    2. 作为常量、变量或属性的类型
    3. 作为数组、字典或其他容器中的元素类型
  8. 协议的底层存储结构是:24字节的ValueBuffer+ metadata(8字节,也就是vwt) + pwt(8字节)
    1. 前24字节,官方说法是ValueBuffer,主要用于存储遵循了协议的实体的属性值
    2. 如果超过ValueBuffer最大容量就会开辟内存进行存储,此24字节拿出8字节存储指向该内存区域的指针
    3. 目前对于类,发现其存储的都是指针
    4. 存储metadata是为了查找遵守协议的实体中实现协议的方法
    5. pwt就是protocol witness table协议目击表,存储协议中的方法
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341