Swift 内存管理 & Runtime

前言

上篇文章Swift 指针重点介绍了指针的类别和对应的应用场景,本篇文章接着介绍Swift中的内存管理,以及Runtime的一些应用场景,尽管Swift是一门静态语言

一、内存管理

和OC一样,Swift中也是通过引用计数的方式来管理对象的内存的,之前的文章Swift编译流程 & Swift类中,也分析过引用计数refCounts,它是类RefCounts类型,好比一个指针,占8字节大小。接下来我们重点看看强引用弱引用循环引用这几个主要场景。

1.1 强引用

首先我们看一个例子👇

class LGTeacher {
    var age: Int = 18
    var name: String = "Luoji"
}

var t = LGTeacher()
var t1 = t
var t2 = t

x/8g查看变量t的内存👇

可以看到,t的引用计数是0x0000000600000003,why?不应该是个单独的数字吗?
接下来还是要回到类RefCounts,查看这个类的定义👇


类RefCounts其实是个模板类,我们来看看传入的模板类型是什么?

回到refCounts的定义👇 它是InlineRefCounts类型

#define SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS       \
  InlineRefCounts refCounts

接着搜索InlineRefCounts👇

typedef RefCounts<InlineRefCountBits> InlineRefCounts;

所以类RefCounts是模板类,InlineRefCountsInlineRefCountBits别名,接着我们看看InlineRefCountBits的定义👇

typedef RefCountBitsT<RefCountIsInline> InlineRefCountBits;

同理,InlineRefCountBits又是RefCountIsInline的别名,通过模板类RefCountBitsT,最终我们定位到RefCountBitsT👇

这个模板类中,只有一个成员bits,它的实质是RefCountBitsInt中的type属性取的一个别名,所以bits的真正类型是uint64_t64位整型数组👇

至此,我们分析得出结论

referCounts的本质是64位整型数组

接下来,我们看看Swift底层创建对象的过程_swift_allocObject_👇

其中调用了new (object) HeapObject(metadata);👇

定位到refCounts对应的构造是InlineRefCounts::Initialized👇

  enum Initialized_t { Initialized };

  // Refcount of a new object is 1.
  constexpr RefCounts(Initialized_t)
    : refCounts(RefCountBits(0, 1)) {}

Initialized是一个枚举Initialized_t,而Initialized_t又是模板类RefCounts的类型T对应的是RefCountBits(0, 1),最终定位到RefCountBits👇

之前我们分析过,referCounts的本质是RefCountBitsInt中的type属性,而RefCountBitsInt又是模板类RefCountBitsT模板类型T,所以RefCountBits(0, 1)实质调用的是模板类RefCountBitsT的构造方法👇

LLVM_ATTRIBUTE_ALWAYS_INLINE
constexpr
RefCountBitsT(uint32_t strongExtraCount, uint32_t unownedCount)
: bits((BitsType(strongExtraCount) << Offsets::StrongExtraRefCountShift) |
       (BitsType(1)                << Offsets::PureSwiftDeallocShift) |
       (BitsType(unownedCount)     << Offsets::UnownedRefCountShift))
{ }

strongExtraCount传值为0,unownedCount传值为1。
完整的bits位域结构体👇

大致分布图👇

其中需要重点关注UnownedRefCountStrongExtraRefCount

那么至此,我们把样例中的引用计数值0x0000000600000003用二进制展示👇

可见,33位置开始的强引用计数StrongExtraRefCount0011,转换成十进制就是3

SIL层验证

我们查看样例的SIL层代码👇

swiftc -emit-sil xx.swift | xcrun swift-demangle >> ./xx.sil && vscode xx.sil

SIL官方文档中关于copy_addr的解释👇

其中的strong_retain对应的就是swift_retain,其内部是一个宏定义,内部是_swift_retain_,其实现是对object的引用计数作+1操作👇

//内部是一个宏定义
HeapObject *swift::swift_retain(HeapObject *object) {
  CALL_IMPL(swift_retain, (object));
}
👇
//本质调用的就是 _swift_retain_
static HeapObject *_swift_retain_(HeapObject *object) {
  SWIFT_RT_TRACK_INVOCATION(object, swift_retain);
  if (isValidPointerForNativeRetain(object))
    object->refCounts.increment(1);
  return object;
}
👇
void increment(uint32_t inc = 1) {
    auto oldbits = refCounts.load(SWIFT_MEMORY_ORDER_CONSUME);
    
    // constant propagation will remove this in swift_retain, it should only
    // be present in swift_retain_n
    if (inc != 1 && oldbits.isImmortal(true)) {
      return;
    }
    //64位bits
    RefCountBits newbits;
    do {
      newbits = oldbits;
      bool fast = newbits.incrementStrongExtraRefCount(inc);
      if (SWIFT_UNLIKELY(!fast)) {
        if (oldbits.isImmortal(false))
          return;
        return incrementSlow(oldbits, inc);
      }
    } while (!refCounts.compare_exchange_weak(oldbits, newbits,
                                              std::memory_order_relaxed));
  }

接着搜索incrementStrongExtraRefCount,定义如下👇

LLVM_NODISCARD LLVM_ATTRIBUTE_ALWAYS_INLINE
bool incrementStrongExtraRefCount(uint32_t inc) {
// This deliberately overflows into the UseSlowRC field.
// 对inc做强制类型转换为 BitsType
// 其中 BitsType(inc) << Offsets::StrongExtraRefCountShift 等价于 1<<33位,16进制为 0x200000000
//这里的 bits += 0x200000000,将对应的33-63转换为10进制,为
bits += BitsType(inc) << Offsets::StrongExtraRefCountShift;
return (SignedBitsType(bits) >= 0);
}

所以,以t的refCounts为例(其中62-33位是strongCount,每次增加强引用计数增加都是在33-62位上增加的,固定的增量为1左移33位,即0x200000000

为何t的引用计数是0x0000000600000003
  1. 当代码运行到var t = LGTeacher()时,t的refCounts是 0x0000000200000003
  2. var t1 = t时,refCounts是 0x0000000400000003 = 0x0000000200000003 + 0x200000000
  3. var t2 = t时,refCounts是0x0000000600000003 = 0x0000000400000003 + 0x200000000
Swift与OC初始化时的引用计数

我们注意到,var t = LGTeacher()此时已经有了引用计数,所以👇

  • OC中创建实例对象时为0
  • Swift中创建实例对象时默认为1
CFGetRetainCOunt

可以通过CFGetRetainCOunt获取引用计数,应用到上面的例子,运行查看👇

如果把上述代码放入方法中运行,则👇

t的引用计数会再次增加。

1.2 弱引用

接下来,我们来看看弱引用,还是先看下面示例👇

class LGTeacher {
    var age: Int = 18
    var name: String = "Luoji"
    var stu: LGStudent?
}

class LGStudent {
    var age = 20
    var teacher: LGTeacher?
}

func test(){
    var t = LGTeacher()
    weak var t1 = t
    
    print("end")
}

test()

运行👇

t的引用计数是0xc0000000200abbca,why?接下来我们来看看原因👇

弱引用声明的变量是一个可选值,因为在程序运行过程中是允许将当前变量设置为nil

首先在t1处打上断点,查看汇编

我们锁定到swift_weakInit,接着在源码中搜索swift_weakInit👇

WeakReference *swift::swift_weakInit(WeakReference *ref, HeapObject *value) {
  ref->nativeInit(value);
  return ref;
}

接着看看nativeInit

  void nativeInit(HeapObject *object) {
    auto side = object ? object->refCounts.formWeakReference() : nullptr;
    nativeValue.store(WeakReferenceBits(side), std::memory_order_relaxed);
  }

接着看formWeakReference

// SideTableRefCountBits specialization intentionally does not exist.
template <>
HeapObjectSideTableEntry* RefCounts<InlineRefCountBits>::formWeakReference()
{
  auto side = allocateSideTable(true);
  if (side)
    return side->incrementWeak();
  else
    return nullptr;
}

看到这里,就比较明朗了,系统会创建一个sideTable,创建成功的话,side->incrementWeak()增加弱引用计数,失败则return nullptr。看来重点就是这个allocateSideTable了👇

通过上图的底层流程分析,我们可以get到关键的2点👇

  1. 通过HeapObjectSideTableEntry初始化散列表👇
class HeapObjectSideTableEntry {
  // FIXME: does object need to be atomic?
  std::atomic<HeapObject*> object;
  SideTableRefCounts refCounts;
...
}

上述源码中可知

弱引用对象对应的引用计数refCountsSideTableRefCounts类型
强引用对象的是InlineRefCounts类型

接下来我们看看SideTableRefCounts👇

typedef RefCounts<SideTableRefCountBits> SideTableRefCounts;

继续搜索SideTableRefCountBits👇

里面包含了成员uint32_t weakBits;,即一个32位域的信息。

  1. 通过InlineRefCountBits初始化散列表的数据👇
  LLVM_ATTRIBUTE_ALWAYS_INLINE
  RefCountBitsT(HeapObjectSideTableEntry* side)
    : bits((reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits)
           | (BitsType(1) << Offsets::UseSlowRCShift)
           | (BitsType(1) << Offsets::SideTableMarkShift))
  {
    assert(refcountIsInline);
  }

这里继承的bits构造方法,而bits定义👇

BitsType bits;

typedef typename RefCountBitsInt<refcountIsInline, sizeof(void*)>::Type
    BitsType;

和强引用一样,来到了RefCountBitsInt,这个之前分析过,就是uint64_t类型,存的是64位域信息

综合1 和 2两点的论述可得出:

  • 64位 用于记录 原有引用计数
  • 32位 用于记录 弱引用计数
为何t的引用计数是0xc0000000200abbca

上述分析中我们知道,在InlineRefCountBits初始化散列表的数据时,执行了(reinterpret_cast<BitsType>(side) >> Offsets::SideTableUnusedLowBits这句代码,而

static const size_t SideTableUnusedLowBits = 3;

对side右移了3位,所以此时,将0xc0000000200abbca左移3位是0x10055DE50,就是散列表的地址。再x/8g查看👇

小结

对于HeapObject来说,其refCounts有两种计算情况:

  1. 无弱引用:strongCount(强引用计数)+ unownedCount(无主引用计数)
  2. 有弱引用:object + xxx + (strongCount + unownedCount) + weakCount

1.3 循环引用

循环引用也是一个很经典的面试题,按照惯例,我们还是先看看案例👇

var age = 10
let clourse = {
    age += 1
}
clourse()
print(age)

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

从输出结果中可以看出:闭包内部对变量的修改改变外部原始变量的值,主要原因是闭包会捕获外部变量,这个与OC中的block一致的

deinit

接着,我们看看deinit的作用

class LGTeacher {
    deinit {
        print("LGTeacher deinit")
    }
}
func test(){
    var t = LGTeacher()
}

test()

<!--打印结果-->
LGTeacher deinit

可见,deinit是在当前实例对象即将被回收时触发。
接下来,我们把age放到类中,闭包中再去修改时会怎样👇

一样,没有问题,如果将闭包那块代码放入函数中呢👇

func test(){
    var t = LGTeacher()
    let clourse = {
        t.age += 1
    }
    clourse()
}

test()

运行结果发现,闭包对 t 并没有强引用,直接被释放了。我们继续修改👇

  1. 在类LGTeacher中添加闭包completionBlock
class LGTeacher {
    var age = 18
    
    var completionBlock: (() ->())?
    
    deinit {
        print("LGTeacher deinit")
    }
}
  1. completionBlock中修改age
func test(){
    var t = CJLTeacher()
    t.completionBlock = {
        t.age += 1
    }
}
test()

运行👇

从运行结果发现,t.age还是18,并且没有执行deinit方法,所以这里存在循环引用

如何解决循环引用

有两种方式:

  1. weak修饰闭包传入的参数
func test(){
    var t = LGTeacher()
    t.completionBlock = { [weak t] in
        t?.age += 1
    }
}

因为weak修饰后的变量是optional类型,所以t?.age += 1

  1. unowned修饰闭包参数
func test(){
    var t = LGTeacher()
    t.completionBlock = { [unowned t] in
        t.age += 1
    }
}
捕获列表

什么是捕获列表?例如上面的代码[weak t][unowned t] ,有以下特点:

  • 定义在参数列表之前
  • [变量]写成用逗号连来的表达式列表,并用方括号括起来
  • 如果使用捕获列表,那么即使省略参数名称、参数类型和返回类型,也必须使用in关键字

捕获的值的变化
看以下示例,输出什么?👇

func test(){
    var age = 0
    var height = 0.0
    let clourse = {[age] in
        print(age)
        print(height)
    }
    age = 10
    height = 1.85
    clourse()
}

age被捕获了,即使后面改变了它的值,但是结果还是0,而未被捕获的height的值却发生了变化。所以,从这个结果中可知:

对于捕获列表中的每个常量,闭包会利用周围范围内具有相同名称的常量/变量,来初始化捕获列表中定义的常量

上述结论大致可以分为以下几点:

  1. 捕获列表中的常量是值拷贝,而不是引用拷贝
  2. 捕获列表中的常量的相当于复制了变量age的值
  3. 捕获列表中的常量是只读的,即不可修改

二、Swift中的Runtime场景

Swift是一门静态语言,本身不具备动态性,不像OC有Runtime运行时的机制(此处指OC提供运行时API供程序员操作)。但由于Swift兼容OC,所以可以转成OC类和函数,利用OC的运行时机制,来实现动态性

2.1 探索

老规矩,先上示例代码,

class LGTeacher {
    var age: Int = 18
    func teach(){
        print("teach")
    }
}

let t = LGTeacher()

func test(){
    var methodCount: UInt32 = 0
    let methodList = class_copyMethodList(LGTeacher.self, &methodCount)
    for i in 0..<numericCast(methodCount) {
        if let method = methodList?[i]{
            let methodName = method_getName(method)
            print("=-=-方法名称:\(methodName)")
        }else{
            print("not found method")
        }
    }
    
    var count: UInt32 = 0
    let proList = class_copyPropertyList(LGTeacher.self, &count)
    for i in 0..<numericCast(count) {
        if let property = proList?[i]{
            let propertyName = String(utf8String: property_getName(property))
            print("=-=-成员属性名称:\(propertyName!)")
        }else{
            print("没有找到你要的属性")
        }
    }
    print("test run")
}
test()

代码很检点,test()方法中通过class_copyMethodListclass_copyPropertyList变量方法名称和属性名称,并打印出来。我们运行来看看👇

并没有打印出来,下面我们来试试修改代码,让其能打印出来。

  • 修改1:给方法和属性添加@objc修饰
class LGTeacher {
    @objc var age: Int = 18
    @objc func teach(){
        print("teach")
    }
}

可以打印。

  • 修改2:类LGTeacher继承NSObject不用@objc修饰
class LGTeacher: NSObject{
    var age: Int = 18
    func teach(){
        print("teach")
    }
}

只打印了初始化方法,是因为在swift.h文件中暴露出来的只有init方法

注意:如果要让OC调用,那么必须 继承NSObject + @objc修饰👇

class LGTeacher: NSObject{
    @objc var age: Int = 18
    @objc func teach(){
        print("teach")
    }
}
  • 修改3:去掉@objc修饰,改成dynamic修饰
class LGTeacher: NSObject{
    dynamic var age: Int = 18
    dynamic func teach(){
        print("teach")
    }
}

和第2种情况一样。

*修改4:同时用@objc 和 dynamic修饰方法

class LGTeacher: NSObject{
    dynamic var age: Int = 18
    @objc dynamic func teach(){
        print("teach")
    }
}

可以输出方法名称。

小结
  1. 对于纯Swift类来说,没有动态特性dynamic(因为Swift是静态语言),方法和属性不加任何修饰符的情况下,不具备runtime特性,此时的方法调度,依旧是函数表调度,即·V_Table调度
  2. 纯swift类的方法和属性添加@objc修饰的情况下,可通过runtime API获取到,但是在OC中无法调度的,原因是swift.h文件中没有该Swift类的声明。
  3. 对于继承NSObject类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加 @objc关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性。

2.2 元类型、AnyClass、Self

元类型

主要是AnyAnyObject这两个关键字。

  • AnyObject :可代表类的Instance实例类的类型类遵守的协议,但struct❌不行
class LGPerson {
   var age = 18
}

// 1. 类实例
var p1: AnyObject =  LGPerson()

// 2. 类的类型
var p2: AnyObject = LGPerson.self

// 3. 类遵守的协议 (继承AnyObjec)
protocol JSONMap: AnyObject { }

// 4. struct不是AnyObject类型
// struct报错: [Non-class type 'HTJSON' cannot conform to class protocol 'JSONMap']
 struct HTJSON: JSONMap { }

// 5. 基础类型强转为Class,就属于AnyObject
var age: AnyObject = 10 as NSNumber  // Int不属于AnyObject,强转NSNumber就属于AnyObject
  • Any:Any比AnyObject代表的范围更广,不仅支持类实例对象类类型类协议,还支持struct函数以及Optioanl可选类型
// 1. 类实例
var p1: Any =  LGPerson()

// 2. 类的类型
var p2: Any = LGPerson.self

// 3. 类遵守的协议 (继承AnyObjec)
protocol JSONMap: Any { }

// 4. struct
struct LGJSON: JSONMap { }

// 5. 函数
func test() {}

// 6. struct对象
let s = LGJSON()

// 7. 可选类型
let option: LGPerson? = nil

// Any类型的数组
var array: [Any] = [1,                // Int
                   "2",              // String
                   LGPerson.self,    // class类型
                   p1,               // 类的实例对象
                   JSONMap.self,     // 协议本身
                   LGJSON.self,      // struct类型
                   s,                // struct实例对象
                   option,           // 可选值
                   test()            // 函数
                   ]
print(array)

通过上述[Any]数组,我们可以看到Any可指代范围是有多广。
option是可选类型,所以会有警告,可以通过option as Any消除该警告。

AnyClass

AnyClass仅代表类的类型

// 1. 类实例
var p1: AnyObject =  LGPerson()

// 2. 类的类型
var p2: AnyObject = LGPerson.self

// 3. 类遵守的协议 (继承AnyObjec)
protocol JSONMap: AnyObject { }

class LGTest: JSONMap { }
var p3: JSONMap = LGTest()

// Any类型的数组
var array: [AnyObject] = [ LGPerson.self,    // class类型
                           p1,               // 类的实例对象
                           p3                // 遵守AnyObject协议的类对象也符合(类对象本身符合)
                         ]

即使array是接受AnyObject所有对象,但实际只存储了类的类型

Self

Self有关的关键字有T.selfT.Type。在讲这两个之前,我们先来看看type(of:)这个方法的作用。

  • type(of:)
    用于获取一个值的动态类型
var age = 10

// 编译器任务value接收Any类型
func test(_ value: Any) {
   
   // type(of:)可获取真实类型
   print(type(of: value))    // 打印Int
   
}

test(age)

编译期时,value的类型是Any类型👇

运行期时,type(of:)获取的是真实类型👇

  • type(of:)三种特殊的应用场景
    1. 继承场景:type(of:)是读取真实调用的对象
class LGPerson { }

class LGStudent: LGPerson { }

func test(_ value: LGPerson) {
   print(type(of: value)) 
}

var person = LGPerson()
var student = LGStudent()

test(person)
test(student)
  1. 遵循协议的场景:type(of:)也是读取真实调用的对象
protocol TestProtocol { }

class LGPerson: TestProtocol { }

func test(_ value: TestProtocol) {
   print(type(of: value))
}

var p = LGPerson()
var p1: TestProtocol = LGPerson()

test(p)
test(p1)

注意:p1是LGPerson类型,并不是TestProtocol协议类型。

  1. 使用泛型T时的场景:type(of:)读取的就是T类型
protocol TestProtocol { }

class LGPerson: TestProtocol { }

func test<T>(_ value: T) {
   print(type(of: value))
}

var p = LGPerson()
var p1: TestProtocol = LGPerson()

test(p)
test(p1) 

这种情况下,p1是TestProtocol协议类型。

如果想让p1取到的是LGPerson类型,需要改动代码👇

弄清楚了type(of:)的作用后,我们再回过头看T.selfT.Type

T.self

如果T实例对象,就返回实例本身。如果T,就返回metadata(首地址:类的类型)。示例代码👇

class LGPerson {
    var age = 18
}

struct LGTest {
    var name = "test"
}

// 1. class实例对象,返回对象本身
var p =  LGPerson().self
print(type(of: p))

// 2. class类型, 返回class类型
var pClass = LGPerson.self
print(type(of: pClass))

// 3. struct实例对象,返回对象本身
var t = LGTest().self
print(type(of: t))

// 4. struct类型,返回struct类型
var tStruct = LGTest.self
print(type(of: tStruct))
T.Type

T.Type就是一种类型,T.self是T.Type类型。(使用type(of:)读取)。上述例子中的👇

总结

本篇文章主要讲了2大知识点:内存管理Runtime相关,内存管理中主要分析了,在底层源码中,强引用对象和弱引用对象的引用计数的计算方式的区别,通过示例证明了引用计数位域的存储。接着讲到了Runtime的场景,OC与Swift混编时会用到,最后讲述了下元类型的几种特殊的应用场景。

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

推荐阅读更多精彩内容