Swift5.x入门10 -- 闭包

闭包表达式

  • 在Swift中可以通过func定义一个函数,也可以通过闭包表达式来定义函数;
import Foundation

func test1() -> Void {
    //闭包表达式 用一个常量来接收
    let fn = {
        (v1: Int,v2: Int) -> Int in
        return v1 + v2
    }
    print(fn(10, 20)) //30
}

func test2() -> Void {
   let sum = {
      (v1: Int,v2: Int) -> Int in
       return v1 + v2
    }(20,30) //匿名的闭包表达式 直接调用
    print(sum) //50
}

test1()
test2()
  • 闭包表达式的定义格式:
{
    (参数列表) -> 返回值类型 in
     函数体代码
}

闭包表达式的简写

  • 定义一个函数exec,最后一个参数为函数,也就是闭包表达式,如下所示:
import Foundation

func sum(v1: Int,v2: Int) -> Int {
    v1 + v2
}

func exec(v1: Int,v2: Int,fn: (Int,Int) -> Int) -> Void {
    print(fn(v1,v2))
}
  • 调用exec函数时,最后一个实参的传递方式有如下几种写法:
  • 第一种方式:函数实参传入 函数名
exec(v1: 100, v2: 200, fn: sum)
  • 第二种方式:函数实参 传入闭包表达式
exec(v1: 100, v2: 200, fn: { (v1: Int,v2: Int) -> Int in
  return v1 + v2
})
  • 第三种方式:函数实参 传入闭包表达式 尾随闭包
exec(v1: 100, v2: 200) { (v1: Int, v2: Int) -> Int in
  return v1 + v2
}
  • 第四种方式:尾随闭包 省略 参数类型
stu.exec(v1: 100, v2: 200) { (v1, v2) -> Int in
  return v1 + v2
}
  • 第五种方式:尾随闭包 省略 参数类型返回值类型 return关键字箭头
exec(v1: 100, v2: 200) { (v1, v2) in
   v1 + v2
}
  • 第六种方式:尾随闭包 采用美元符号,最简便的写法
stu.exec(v1: 100, v2: 200) { 
  $0 + $1
}

尾随闭包

  • 如果将一个很长的闭包表达式作为函数的 最后一个实参,使用尾随闭包可以增强函数的可读性;
  • 尾随闭包是一个被写在函数调用括号外面的闭包表达式;
func sum(v1: Int,v2: Int) -> Int {
    v1 + v2
}

func exec(v1: Int,v2: Int,fn: (Int,Int) -> Int) -> Void {
    print(fn(v1,v2))
}

exec(v1: 100, v2: 200, fn: sum)

//尾随闭包
exec(v1: 100, v2: 200) { (v1, v2) -> Int in
    v1 + v2
}

//尾随闭包
exec(v1: 100, v2: 200) {
    $0 + $1
}
  • 如果闭包表达式是函数唯一的实参,而且使用了尾随闭包的语法,那就不需要在函数名后面写圆括号了
func exme(fn: (Int,Int) -> Int) -> Void {
    print(fn(1,2))
}

exme(fn: {$0 + $1})
exme(){$0 + $1}
exme{$0 + $1}

数组的排序

func ArrSort() -> Void {
    var arr = [23,12,45,22,100,67]
    arr.sort(by: cmp)
    //完整写法
    arr.sort(by: { (v1: Int,v2: Int) -> Bool in return v1 > v2})
    //省略参数括号与返回值
    arr.sort(by: {v1,v2 in return v1 > v2})
   //省略参数括号与返回值,return
    arr.sort(by: {v1,v2 in v1 > v2})
    arr.sort(by: {$0 > $1})
    arr.sort(by: >)
    arr.sort(){$0 > $1}
    arr.sort{$0 > $1}
    
    print(arr)
}

//排序规则函数
func cmp(v1: Int,v2: Int) -> Bool {
    //从大到小
    return v1 > v2
}

ArrSort()

闭包

  • 一个函数和它所捕获的常量/变量 环境组合起来,称为闭包;
  • 一般指定义在函数内部的函数;
  • 一般它捕获的是外层函数的常量/变量;
import Foundation

typealias Fn = (Int) -> Int

func getFn() -> Fn {
    func plus(_ v1: Int) -> Int{
        return v1
    }
    return plus
}

var fn = getFn()
  • 当断点停在return plus所在代码行时,其汇编代码如下:
Swift11_闭包`getFn():
    0x100003f70 <+0>:  pushq  %rbp
    0x100003f71 <+1>:  movq   %rsp, %rbp
->  0x100003f74 <+4>:  leaq   0x15(%rip), %rax          ; plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:96
    0x100003f7b <+11>: xorl   %ecx, %ecx
    0x100003f7d <+13>: movl   %ecx, %edx
    0x100003f7f <+15>: popq   %rbp
    0x100003f80 <+16>: retq   
  • 其中rax寄存器中存储着plus函数的内存地址,因为rax寄存器通常
    用来存放函数的返回值;
  • LLDB输入指令register read rax,可以看到rax寄存器中存储内容为0x0000000100003f90
  • leaq 0x15(%rip), %rax是将(rip + 0x15)得到的地址值赋值给rax寄存器,其中rip = 0x100003f7b,那么(0x100003f7b+ 0x15) = 0x0000000100003f90,就是上面rax寄存器中的值;
typealias Fn = (Int) -> Int

func getFn() -> Fn {
    //局部变量
    var num = 0
    
    func plus(_ v1: Int) -> Int{
        num += v1
        return num
    }
    return plus
}

var fn = getFn()
print(fn(1)) //1
print(fn(2)) //3
print(fn(3)) //6
print(fn(4)) //10
  • 当断点停在return plus所在代码行时,其汇编代码如下:
Swift11_闭包`getFn():
    0x100003d70 <+0>:  pushq  %rbp
    0x100003d71 <+1>:  movq   %rsp, %rbp
    0x100003d74 <+4>:  subq   $0x20, %rsp
    0x100003d78 <+8>:  movq   $0x0, -0x8(%rbp)
    0x100003d80 <+16>: leaq   0x291(%rip), %rdi
    0x100003d87 <+23>: movl   $0x18, %esi
    0x100003d8c <+28>: movl   $0x7, %edx
    0x100003d91 <+33>: callq  0x100003f14               ; symbol stub for: swift_allocObject
    0x100003d96 <+38>: movq   %rax, %rcx
    0x100003d99 <+41>: addq   $0x10, %rcx
    0x100003d9d <+45>: movq   %rcx, -0x8(%rbp)
    0x100003da1 <+49>: movq   $0x0, 0x10(%rax)
->  0x100003da9 <+57>: movq   %rax, %rdi
    0x100003dac <+60>: movq   %rax, -0x10(%rbp)
    0x100003db0 <+64>: callq  0x100003f32               ; symbol stub for: swift_retain
    0x100003db5 <+69>: movq   -0x10(%rbp), %rdi
    0x100003db9 <+73>: movq   %rax, -0x18(%rbp)
    0x100003dbd <+77>: callq  0x100003f2c               ; symbol stub for: swift_release
    0x100003dc2 <+82>: leaq   0x137(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100003dc9 <+89>: movq   -0x10(%rbp), %rdx
    0x100003dcd <+93>: addq   $0x20, %rsp
    0x100003dd1 <+97>: popq   %rbp
    0x100003dd2 <+98>: retq   
  • symbol stub for: swift_allocObject 看到这,就知道堆区分配了内存空间,创建了实例对象,我们可以大胆猜测此堆区的实例对象主要是用来存储局部变量num的;
  • 0x100003d96 <+38>: movq %rax, %rcx,rax寄存器存储了堆区分配的实例对象的内存地址;
  • LLDB输入register read rax,得到堆区的内存地址0x0000000100705570
  • x/4gx 0x0000000100705570读取它的内容:
    Snip20210731_63.png
  • 前面16个字节存储的是其他内容,往后的8个字节存储的是num的数值;
  • 后面调用了四次fn函数,那么每次调用传参,都会相加然后存到堆区内存中,所以num值会递增;
  • 返回的plus函数与在堆区分配内存空间存储局部变量,两者组成了闭包这种结构;
总结:
  • 可以把闭包想象成一个类的实例对象;
  • 内存在堆空间;
  • 捕获的常量/变量就是对象的成员;
  • 组成闭包的函数就是类内部定义的方法;

汇编分析闭包

import Foundation
typealias Fn = (Int) -> Int

func getFn() -> Fn {
    func plus(_ i: Int) -> Int{
        return i
    }
    return plus
}

var fn = getFn()
print(MemoryLayout.size(ofValue: fn)) //16个字节
fn(1)
fn(2)
  • 根据MemoryLayout.size(ofValue: fn)可知闭包fn变量占用16个字节;
  • 当断点停在var fn = getFn()所在的代码行,汇编代码如下:
Swift11_闭包`main:
    0x100003ad0 <+0>:   pushq  %rbp
    0x100003ad1 <+1>:   movq   %rsp, %rbp
    0x100003ad4 <+4>:   pushq  %r13
    0x100003ad6 <+6>:   subq   $0xe8, %rsp
    0x100003add <+13>:  movl   %edi, -0x54(%rbp)
    0x100003ae0 <+16>:  movq   %rsi, -0x60(%rbp)
    0x100003ae4 <+20>:  callq  0x100003d30               ; Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:95
->  0x100003ae9 <+25>:  movq   0x518(%rip), %rcx         ; (void *)0x00007fff80cc5020: type metadata for Any
    0x100003af0 <+32>:  addq   $0x8, %rcx
    0x100003af7 <+39>:  movq   %rax, 0x456a(%rip)        ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int
    0x100003afe <+46>:  movq   %rdx, 0x456b(%rip)        ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int + 8
    0x100003b05 <+53>:  movl   $0x1, %edi
    0x100003b0a <+58>:  movq   %rcx, %rsi
  • callq 0x100003d30:根据后面注释,可以知道是在调用getFn函数;
  • movq %rax, 0x456a(%rip):rax寄存器存放的是getFn函数的返回也就是plus函数的内存地址,然后将plus函数的内存地址写入到(rip+ 0x456a)内存地址中,由右侧的注释可知,(rip+ 0x456a)=(0x100003afe+ 0x456a) = 0x100008068是全局变量fn的内存地址;由上面知道fn占16个字节,现在只写入了8字节plus函数的内存地址,还有8个字节存放什么呢?
  • LLDB执行si指令,进入到getFn函数内部,汇编代码如下:
Swift11_闭包`getFn():
->  0x100003d30 <+0>:  pushq  %rbp
    0x100003d31 <+1>:  movq   %rsp, %rbp
    0x100003d34 <+4>:  leaq   0x15(%rip), %rax          ; plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:96
    0x100003d3b <+11>: xorl   %ecx, %ecx
    0x100003d3d <+13>: movl   %ecx, %edx
    0x100003d3f <+15>: popq   %rbp
    0x100003d40 <+16>: retq
  • leaq 0x15(%rip), %rax:可以看出plus函数的地址写入rax寄存器;

  • xorl %ecx, %ecx:按位异或,相同的位置为0,不同的位置为1,ecx和ecx的每一位都相同,所以相当于清零,即ecx中写入0;

  • movl %ecx, %edx:edx写入0,等价于rdx写入0;

  • getFn函数调用完成之后,回到main函数;

  • movq %rdx, 0x456b(%rip):rdx寄存器中为0,将0写入(rip+ 0x456b)=(0x100003b05+ 0x456b) = 0x100008070,由右侧的注释可知0x100008070是全局变量fn的后8个字节,即fn+8;

  • 所以全局变量fn占16个字节空间,前8个字节存储plus函数地址,后8个字节存储0;

  • 将上述代码做如下修改:

import Foundation
typealias Fn = (Int) -> Int

func getFn() -> Fn {
    
    var num = 0
    
    func plus(_ i: Int) -> Int{
        num += i
        return num
    }
    return plus
}

var fn = getFn()
print(MemoryLayout.stride(ofValue: fn)) //16个字节

fn(1)
fn(2)
Snip20210801_65.png
  • 当断点停在var fn = getFn()所在的代码行,汇编代码如下:
Swift11_闭包`main:
    0x100003940 <+0>:   pushq  %rbp
    0x100003941 <+1>:   movq   %rsp, %rbp
    0x100003944 <+4>:   pushq  %r13
    0x100003946 <+6>:   subq   $0xe8, %rsp
    0x10000394d <+13>:  movl   %edi, -0x54(%rbp)
    0x100003950 <+16>:  movq   %rsi, -0x60(%rbp)
->  0x100003954 <+20>:  callq  0x100003ba0               ; Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at main.swift:95
    0x100003959 <+25>:  movq   0x6a8(%rip), %rcx         ; (void *)0x00007fff80cc5020: type metadata for Any
    0x100003960 <+32>:  addq   $0x8, %rcx
    0x100003967 <+39>:  movq   %rax, 0x4702(%rip)        ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int
    0x10000396e <+46>:  movq   %rdx, 0x4703(%rip)        ; Swift11_闭包.fn : (Swift.Int) -> Swift.Int + 8
    0x100003975 <+53>:  movl   $0x1, %edi
    0x10000397a <+58>:  movq   %rcx, %rsi
  • LLDB输入指令si,进入getFn函数的汇编代码实现:
Swift11_闭包`getFn():
->  0x100003ba0 <+0>:  pushq  %rbp
    0x100003ba1 <+1>:  movq   %rsp, %rbp
    0x100003ba4 <+4>:  subq   $0x20, %rsp
    0x100003ba8 <+8>:  movq   $0x0, -0x8(%rbp)
    0x100003bb0 <+16>: leaq   0x499(%rip), %rdi
    0x100003bb7 <+23>: movl   $0x18, %esi
    0x100003bbc <+28>: movl   $0x7, %edx
    0x100003bc1 <+33>: callq  0x100003e96               ; symbol stub for: swift_allocObject
    0x100003bc6 <+38>: movq   %rax, %rcx
    0x100003bc9 <+41>: addq   $0x10, %rcx
    0x100003bcd <+45>: movq   %rcx, -0x8(%rbp)
    0x100003bd1 <+49>: movq   $0x0, 0x10(%rax)
    0x100003bd9 <+57>: movq   %rax, %rdi
    0x100003bdc <+60>: movq   %rax, -0x10(%rbp)
    0x100003be0 <+64>: callq  0x100003ec0               ; symbol stub for: swift_retain
    0x100003be5 <+69>: movq   -0x10(%rbp), %rdi
    0x100003be9 <+73>: movq   %rax, -0x18(%rbp)
    0x100003bed <+77>: callq  0x100003eba               ; symbol stub for: swift_release
    0x100003bf2 <+82>: leaq   0x277(%rip), %rax         ; partial apply forwarder for plus #1 (Swift.Int) -> Swift.Int in Swift11_闭包.getFn() -> (Swift.Int) -> Swift.Int at <compiler-generated>
    0x100003bf9 <+89>: movq   -0x10(%rbp), %rdx
    0x100003bfd <+93>: addq   $0x20, %rsp
    0x100003c01 <+97>: popq   %rbp
    0x100003c02 <+98>: retq   
  • callq 0x100003e96:调用函数,从右侧注释可知,是在堆区分配空间,即创建了实例对象,然后将实例对象的内存地址返回,存放到rax寄存器中;
  • LLDB中输入register read rax,得到rax = 0x0000000103023160,即堆空间的地址值;
  • movq %rax, -0x10(%rbp)movq -0x10(%rbp), %rdx,先将(rbp-0x10)=rax,然后(rbp-0x10)=rdx,即rdx=rax,也就是rdx寄存器中也存放着堆空间的地址值;
  • leaq 0x277(%rip), %rax:将(rip+ 0x277)地址写入rax寄存器,由右侧的注释可知(rip+ 0x277)地址是与plus函数相关的内存地址;
  • 当汇编代码停在下面的代码行,LLDB调试如下:
Snip20210801_64.png
  • 最终 rax = plus函数相关的内存地址,rdx = 堆空间的内存地址
  • 过掉断点,进入到plus函数内部,如下所示:
Snip20210801_66.png
  • 然后回到main函数,如下所示:
Snip20210801_67.png
  • 将plus函数地址与堆内存地址分别赋值给全局变量fn的16个字节;
  • 上面是创建闭包变量的汇编逻辑,下面来阐述闭包调用函数的逻辑;
Snip20210801_68.png
  • fn(1)是调用plus函数,而plus函数是存储在变量fn的前8个字节,也就是存储在rax寄存器中的,所以fn(1)对应的汇编代码如下:
Snip20210801_71.png
  • fn(1) --> call *%rax
  • fn(1),参数1,是怎么传的呢,如下:
  • movl $0x1, %edi,将参数1,写入edi寄存器,也就是写入rdi寄存器;
  • fn(1)本质是调用plus函数,其内部会访问堆空间中的num,所以plus函数必定会访问堆空间,那么其是怎么访问的呢?如下:
  • 首先num在堆空间的内存布局如下:
Snip20210801_72.png
  • movq 0x438c(%rip), %rcx,由右侧注释可知rcx=fn+8,也就是堆内存地址;
  • movq %rcx, -0x60(%rbp)
  • movq -0x60(%rbp), %r13:r13 = 堆内存地址;
  • 最终:rdi = 1,r13 = 堆内存地址,由于以下报错,不能进入plus函数内部的执行逻辑,中断推理;
Snip20210801_73.png
  • 将上述代码再做修改,如下:
Snip20210801_74.png
  • 将局部变量改成全局变量,LLDB输入指令si,进入getFn函数的汇编如下所示:
Snip20210801_75.png
  • 看到没有创建堆空间对象,也就是说没有捕获全局变量,这是因为全局变量作用域广,不会被释放,而局部变量出了作用域就会被释放,为了保住局部变量不被释放,所以将其捕获至堆空间;
  • 看下面的代码,如下所示:
Snip20210801_76.png
  • 汇编代码如下:
Snip20210801_77.png
  • 看到两个swift_allocObject函数,说明创建了两个堆内存空间对象,分别捕获num1,num2,是两块独立的堆内存;
  • 将断点停在m(5)所在行,且汇编代码中的断点,加在第一个swift_allocObject函数下面,如下所示:
    Snip20210801_78.png
  • rax = 0x0000000100709870是num1的堆内存空间;
  • 然后过掉断点,来到m(5)所在行的断点,然后查看堆内存空间0x0000000100709870,如下所示:
Snip20210801_79.png
  • 然后将汇编代码中的断点,加在第二个swift_allocObject函数下面,重新运行,如下所示:
Snip20210801_81.png
  • rax = 0x000000010073adf0是num2的堆内存空间;
  • 然后过掉断点,来到m(5)所在行的断点,然后查看堆内存空间0x000000010073adf0,如下所示:
Snip20210801_82.png

自动闭包

//若第一个数>0,就返回第一个数,否则返回第二个数
func getFirstPositive1(_ v1: Int, _ v2: Int) -> Int {
    return v1 > 0 ? v1 : v2
}

getFirstPositive1(10, 20) //10
getFirstPositive1(-2, 20) //20
getFirstPositive1(0, -4)  //-4
  • 将上述代码进行改写,如下:
//将第二个参数改成 无参返回Int的函数类型 可以让v2延迟执行
func getFirstPositive2(_ v1: Int,_ v2: () -> Int) -> Int? {
    return v1 > 0 ? v1 : v2()
}

getFirstPositive2(10, {20})
getFirstPositive2(10) {20}
  • 将第二个参数改成 无参返回Int的函数类型,那么传参时,传入的是函数闭包;
  • getFirstPositive2(10, {20}) { 20 }是闭包函数;
  • getFirstPositive2(10) {20}尾随闭包的写法;
  • v2这个闭包函数,只有当v1<0的时候才会调用,也就是所谓的v2闭包函数可能会延迟执行;
  • 使用自动闭包的关键字autoclosure,改写如下:
func getFirstPositive3(_ v1: Int,_ v2: @autoclosure () -> Int) -> Int? {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive3(10, 20)
  • 与上面的区别在于,由于加了自动闭包的关键字autoclosure,所以在函数调用,传参时会自动加上闭包的大括号,所以可简写成getFirstPositive3(10, 20),虽然闭包参数传入的是20,其本质还是传入{ 20 }
  • @autoclosure 会自动将20 封装成闭包{ 20 }
  • @autoclosure 只支持 () -> T 类型的参数;
  • 空合并运算符 ?? 使用了@autoclosure技术;
  • 有@autoclosure与无@autoclosure 的函数,构成了函数重载;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容