Swift 中属性介绍

一. 存储属性

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由 var 关键字引入),要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特别要强调的,因为随处可⻅。

class CXTeacher { 
    var age: Int
    var name: String 
}

从定义上区分 var 与 let

比如这里的 agename 就是我们所说的存储属性,这里我们需要加以区分的是 letvar 两者的区别,从定义上 let 用来声明常量,常量的值一旦设置好便不能再被更改;var 用来声明变量,变量的值可以在将来设置为不同的值。

这里我们来看几个案例:

从汇编的角度分析 var 与 let 的区别

 var age = 20

let x = 10

print(age,x)

这里通过汇编调试可以看到,14 行与 18 行分布代表将 0x14 存入 w8 跟 将 0xa 存入 w8,可以看出在汇编上 varlet 并没有区别。

通过内存的读取也可以看到 agex 就相差 8 个字节,并没有别的区别。

从 SIL 的角度分析 var 与 let 的区别

  • swift 代码
var age = 20

let x = 10
  • sil 代码
// _hasStorage 代表是存储属性,_hasInitialValue 代表都有初始值,{ get set } : 编译器默认会给存储属性生成 get 跟 set 方法,当我们访问存储属性的时候就是访问 get 方法,当我们改变属性的值的时候就是访问 set 方法
@_hasStorage @_hasInitialValue var age: Int { get set }
// 当用 let 修饰的时候,编译器没有生成 set 方法,所以修改属性值的时候编译器会报错
@_hasStorage @_hasInitialValue let x: Int { get }

通过 sil 代码可以看到,varlet 其实也是一种语法糖,let 修饰的属性不会生成 set 方法。

二. 计算属性

存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供 gettersetter 来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。

  • swift 代码
struct square {
    // 在实例当中需要占用内存空间
    var width: Double
    var hegith: Double
    
    // 代表将 set 方法私有,只能在结构体内部通过 self.area 修改 area 的值
//    private(set) var area: Double
    
    var area: Double {
        get {
            return width * hegith
        }
        // newValue 是编译器默认生成的,自己也可以通过 set(自定义名称) {} 修改
        set {
            self.width = newValue
        }
    }
}
  • sil 代码
struct square {
  @_hasStorage var width: Double { get set }
  @_hasStorage var hegith: Double { get set }
  var area: Double { get set }
  init(width: Double, hegith: Double)
}

通过 sil 代码可以看到,计算属性的本质就是 setget 方法。

三. 属性观察者

属性观察者会观察用来观察属性值的变化,一个 willSet 当属性将被改变调用,即使这个值与原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 gettersetter

  • swift 代码
class SubjectName {
    var subjectName: String = "" {
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet {
            print("subjectName has been changed \(oldValue)") }
        }
}
  • sil 代码
// 在赋值之前会调用 willset 方法
   // function_ref SubjectName.subjectName.willset
  %10 = function_ref @$s4main11SubjectNameC07subjectC0SSvw : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %11
  %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()

// 给 subjectName 属性设置新的值
  retain_value %0 : $String                       // id: %12
  %13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %14
  %14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
  %15 = load %14 : $*String                       // user: %17
  store %0 to %14 : $*String                      // id: %16
  release_value %15 : $String                     // id: %17
  end_access %14 : $*String                       // id: %18

// 在赋值之后会调用 didset 方法
  // function_ref SubjectName.subjectName.didset
  %19 = function_ref @$s4main11SubjectNameC07subjectC0SSvW : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %20
  %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()

这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSetdidSet 观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这
段代码,你会发现当前并不会有任何的输出。

  • swift 代码
class SubjectName{
    var subjectName: String = "[unnamed]"{
        willSet{
            print("subjectName will set value \(newValue)")
        }
        didSet{
            print("subjectName has been changed \(oldValue)")
        }
    }
    init(subjectName: String) {
        self.subjectName = subjectName
    }
}
let s = SubjectName(subjectName: "Swift")
  • sil 代码
// SubjectName.subjectName.getter
sil hidden [transparent] @$s4main11SubjectNameC07subjectC0SSvg : $@convention(method) (@guaranteed SubjectName) -> @owned String {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $SubjectName):
  debug_value %0 : $SubjectName, let, name "self", argno 1 // id: %1
// 拿到 subjectName 这个属性的内存地址
  %2 = ref_element_addr %0 : $SubjectName, #SubjectName.subjectName // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*String // users: %4, %6
// 将要赋值的字符串的值直接拷贝到内存地址,并没有调用 setter 方法
  %4 = load %3 : $*String                         // users: %7, %5
  retain_value %4 : $String                       // id: %5
  end_access %3 : $*String                        // id: %6
  return %4 : $String                             // id: %7
} // end sil function '$s4main11SubjectNameC07subjectC0SSvg'

通过 sil 代码可以看到,这个时候是直接将字符串的值拷贝到 subjectName 属性的内存地址,并没有调用 setter 方法。编译器这样做的原因可能是这个时候有些属性并没有初始化完成,通过 setter 方法赋值可能会造成内存错误。

上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需将相关代码添加到属性的 setter。我们先来看这段代码:

这里可以看到在计算属性中添加 willSetdidSet 会报错,因为这里已经实现 set 方法了,可以直接在 set 中属性赋值前跟属性赋值后进行添加监听。

如果子类继承于父类的情况下,willSetdidSet 方法调用是什么样的呢?

这里可以看到当子类继承父类的时候,修改子类实例对象 age 属性的值会先调用子类的 willSet 方法,然后调用父类的 willSet,赋值完成后会先调用父类的 didSet 方法,然后再调用子类的 didSet 方法。

四. 延迟存储属性

  • 延迟存储属性的初始值在其第一次使用时才进行计算。
  • 用关键字 lazy 来标识一个延迟存储属性。

通过案例可以看到,在访问 age 属性之前内存中的值是 0,当访问之后才会对内存空间进行初始化。

  • sil 代码
class CXPerson {
  lazy var age: Int { get set }
// 这里延时属性本质上是可选类型
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

 // variable initialization expression of CXPerson.$__lazy_storage_$_age
sil hidden [transparent] @$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
// Optional.none 相当于 nil,初始化的时候 age 为 0
bb0:
  %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
  return %0 : $Optional<Int>                      // id: %1
} // end sil function '$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi'
// 访问 age 的时候会调用 getter 方法
// CXPerson.age.getter
sil hidden [lazy_getter] [noinline] @$s4main8CXPersonC3ageSivg : $@convention(method) (@guaranteed CXPerson) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $CXPerson):
  debug_value %0 : $CXPerson, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  %4 = load %3 : $*Optional<Int>                  // user: %6
  end_access %3 : $*Optional<Int>                 // id: %5
// 这里会进行枚举判断,当age有值的时候走 bb1,没值的时候走 bb2
  switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

// 返回原因的值
// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  br bb3(%7 : $Int)                               // id: %9

// 构建一个 Int 类型的值并存储到 age 属性的内存地址
bb2:                                              // Preds: bb0
  %10 = integer_literal $Builtin.Int64, 6         // user: %11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  %14 = ref_element_addr %0 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %15
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  store %13 to %15 : $*Optional<Int>              // id: %16
  end_access %15 : $*Optional<Int>                // id: %17
  br bb3(%11 : $Int)                              // id: %18

这里延迟属性也可以理解为懒加载,我们使用 lazy 修饰属性的时候可以帮我们节省内存空间。但是延迟属性不能保证变量只能被访问一次,因为会涉及到多个线程同时访问的情况,所以并不是线程安全的。

内存独占

  • swift 代码
class CXPerson {
    lazy var age: Int = 6
//    lazy var age: Int {
//        return 6
//    }()
}

var p = CXPerson()
//
let t = p.age
p.age = 30
  • sil 代码
// CXPerson.age.setter
sil hidden @$s4main8CXPersonC3ageSivs : $@convention(method) (Int, @guaranteed CXPerson) -> () {
// %0 "value"                                     // users: %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Int, %1 : $CXPerson):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $CXPerson, let, name "self", argno 2 // id: %3
  %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
  %5 = ref_element_addr %1 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %6
  %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
  store %4 to %6 : $*Optional<Int>                // id: %7
  end_access %6 : $*Optional<Int>                 // id: %8
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} 

在我们修改 age 属性的前后会调用 begin_accessend_access,保证在赋值的过程中独占内存,也是为了内存访问安全。

五. 类型属性

  • 类型属性其实就是一个全局变量
  • 类型属性只会被初始化一次

下面我们通过将以下 swift 代码转成 sil 代码来看一下。

  • swift 代码
class CXPerson {
    static var age: Int = 18
}

CXPerson.age = 30
  • sil 代码、IR 代码、源码

下面我们通过阅读 sil 代码、IR 代码及源码,看一下类型属性的底层实现步骤。

  1. CXPerson 声明
class CXPerson {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}
// one-time initialization token for age
sil_global private @$s4main8CXPersonC3age_Wz : $Builtin.Word
// static CXPerson.age
sil_global hidden @$s4main8CXPersonC3ageSivpZ : $Int

CXPerson 类的声明中 age 还是一个存储属性,只是多了 static 修饰。而且 age 会被声明成一个全局变量。

  1. age 属性的访问
  // function_ref CXPerson.age.unsafeMutableAddressor
  %3 = function_ref @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
  %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5

这里注释说明可以看出这里是通过 CXPerson.age 的内存地址进行访问,所以我们搜索 s4main8CXPersonC3ageSivau

// CXPerson.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
// 通过步骤 1 中声明的 token,拿到 age 属性的内存地址,并把地址赋值给 %1
  %0 = global_addr @$s4main8CXPersonC3age_Wz : $*Builtin.Word // user: %1
// 指针类型转换 
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref one-time initialization function for age 调用 initialization 函数,并把函数地址赋值给 %2
  %2 = function_ref @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () // user: %3
 // 在这里传入 %1 %2 作为参数
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
 //拿到全局变量的内存地址
  %4 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %5
 //将全局变量的内存地址转为原生指针
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
 // 这里返回上面转换得到的原生指针
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function '$s4main8CXPersonC3ageSivau'

通过 sil 代码可以看到,在 CXPerson 类的声明中 age 还是一个存储属性,只是多了 static 修饰。而且 age 会被声明成一个全局变量。

  1. 找到 initialization 函数
    xcrun swift-demangle 命令执行结果:
xcrun swift-demangle s4main8CXPersonC3ageSivpZ
$s4main8CXPersonC3ageSivpZ ---> static main.CXPerson.age : Swift.Int
// one-time initialization function for age
sil private [global_init_once_fn] @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () {
bb0:
// 创建全局变量 age 
  alloc_global @$s4main8CXPersonC3ageSivpZ        // id: %0
// 获取到全局变量内存地址
  %1 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %4
// 构建 Int 类型的结构体
  %2 = integer_literal $Builtin.Int64, 18         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
// 初始化 age 变量 
  store %3 to %1 : $*Int                          // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} // end sil function '$s4main8CXPersonC3age_WZ'
  1. swift 代码转为 IR 代码并找到 once
define hidden swiftcc i8* @"$s4main8CXPersonC3ageSivau"() #0 {
entry:
  %0 = load i64, i64* @"$s4main8CXPersonC3age_Wz", align 8
  %1 = icmp eq i64 %0, -1
  %2 = call i1 @llvm.expect.i1(i1 %1, i1 true)
  br i1 %2, label %once_done, label %once_not_done

once_done:                                        ; preds = %once_not_done, %entry
  %3 = load i64, i64* @"$s4main8CXPersonC3age_Wz", align 8
  %4 = icmp eq i64 %3, -1
  call void @llvm.assume(i1 %4)
  ret i8* bitcast (%TSi* @"$s4main8CXPersonC3ageSivpZ" to i8*)

once_not_done:                                    ; preds = %entry
//这里可以看到会调用 swift_once 函数
  call void @swift_once(i64* @"$s4main8CXPersonC3age_Wz", i8* bitcast (void ()* @"$s4main8CXPersonC3age_WZ" to i8*), i8* undef)
  br label %once_done
}

通过 xcrun swift-demangle 命令可以看到 s4main8CXPersonC3ageSivau 就是 sil 代码中的 unsafeMutableAddressor

xcrun swift-demangle s4main8CXPersonC3ageSivau
$s4main8CXPersonC3ageSivau ---> main.CXPerson.age.unsafeMutableAddressor : Swift.Int
  1. 在源码中搜索 swift_once 函数。

通过源码可以看到,全局变量本质上还是使用了 GCDdispatch_once_f,确保类型属性只会被初始化一次,但是可以在外部修改类型属性的值。

swift 中单例写法

final class CXPerson {
    static let sharedInstance = CXPerson()
    
    private init() {} 
}

类型方法

class CXPerson {
    static func staticFunc() {
        print("staticFunc")
    }

    class func classFunc() {
        print("staticFunc")
    }
}

CXPerson.staticFunc()
CXPerson.classFunc()

以上在方法前用 static 修饰是类型方法的写法,通过汇编可以看到 static 修饰的方法跟 class 修饰的方法调度的时候都是通过静态派发的方式。

sil_vtable CXPerson {
  #CXPerson.classFunc: (CXPerson.Type) -> () -> () : @$s4main8CXPersonC9classFuncyyFZ   // static CXPerson.classFunc()
  #CXPerson.init!allocator: (CXPerson.Type) -> () -> CXPerson : @$s4main8CXPersonCACycfC    // CXPerson.__allocating_init()
  #CXPerson.deinit!deallocator: @$s4main8CXPersonCfD    // CXPerson.__deallocating_deinit
}

将以上代码转成 sil 代码可以看到,class 修饰的方法会被注册到 vtable 中,这也是 class 修饰的方法能被子类重写的原因。

如图可以看到 static 修饰的方法在子类中重写会报错。

这里可以看到 class 不能用来修饰 struct(值)类型。

六. 属性在 MachO 文件中的位置信息

Swift 中类与结构体(一)中我们讲到了 Metadata 的元数据结构,我们回顾一下

struct Metadata{
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer
    var iVarDestroyer: UnsafeRawPointer
}

Swift 中类与结构体(二)中讲到方法调度的过程中我们认识了 typeDescriptor,这里面记录了 V-Table 的相关信息,接下来我们需要认识一下 typeDescriptor 中的 fieldDescripto

struct TargetClassDescriptor{
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32
    var metadataPositiveSizeInWords: UInt32
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32
    var Offset: UInt32
    var size: UInt32
    //V-Table
}

fieldDescriptor 记录了当前的属性信息,其中 fieldDescriptor 在源码中的结构如下:

struct FieldDescriptor {
    var MangledTypeName: Int32
    var Superclass: Int32
    var Kind: uint16
    var FieldRecordSize: uint16
    var NumFields: UInt32
    var FieldRecords: [FieldRecord]
}

其中 NumFields 代表当前有多少个属性,FieldRecords 记录了每个属性的信息,FieldRecord 的结构体如下:

struct FieldRecord{
    var Flags: uint32
    var MangledTypeName: Int32
    var FieldName: Int32
}

基于以上认知,下面我们来看一下相关属性在 MachO 中对应的信息。

class CXPerson {
    var age = 10
    var age1 = 20
}

将以上代码编译后的可执行文件用 MachOView 工具打开。

这里 0xFFFFFE78 + 0x00003EFC - 0x100000000 = 3D74 就是 CXPersontypeDescriptorMachO 文件中的位置。

_TEXT, _const 文件中找到 3D74,向后偏移 4 个字节之后就是 fieldDescriptorMachO 中的信息,所以 0x00003D84 + 0x00000150 = 0x3ED4 就是 typeDescriptorfieldDescriptor 属性在 MachO 文件中的位置。

_TEXT,__swift5_fieldmd 文件中可以看到 FieldDescriptor 的结构体的首地址就是 0x3ED4,所以 0x3ED4 向后偏移 4 个字节之后的连续存储空间存储的就是 FieldRecords 属性的信息。所以 0x3EEC + FFFFFFDD - 0x100000000 = 0x3EC9 就是 FieldNameMachO 中的信息。

FieldName 代表属性的名称,如上图所示,0x3EC9 位置确实存储的就是 CXPerson 类的属性名称,0x00656761 就是 age 的 16 进制,0x31656761 就是 age1 的 16 进制。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容