Swift进阶02: 类、对象、属性

Swift编译过程

编译过程(OC、Swift的区别)

  • OC中通过clang编译器,编译成IR,然后再生成可执行文件.o(即机器码)
  • swift中通过swiftc编译器,编译成IR,然后再生成可执行文件

iOS开发语言,不管是OC还是Swift,后端都是通过LLVM进行编译的,如下图所示

编译过程

Swift编译过程

下面是Swift中的编译流程,其中SIL(Swift Intermediate Language),是Swift编译过程中的中间代码,主要用于进一步分析和优化Swift代码。如下图所示,SIL位于在ASTLLVM IR之间

Swift编译过程@2x

我们可以通过swiftc -h终端命令,查看swiftc的所有命令

swiftc-h命令@2x

例如:在main.swift文件定义如下代码

class YCTeacher {
    var age: Int = 18
    var name: String = "teacher"
}

var t = YCTeacher()
  • 查看抽象语法树:swiftc -dump-ast main.swift
    swift抽象语法树@2x
  • 生成SIL文件(Swift Intermediate Language):swiftc -emit-sil main.swift >> ./main.sil,其中main的入口函数如下:
/ main
// `@main`:标识当前main.swift的`入口函数`,SIL中的标识符名称以`@`作为前缀
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
// `%0、%1` 在SIL中叫做寄存器,可以理解为开发中的常量,一旦赋值就不可修改,如果还想继续使用,就需要不断的累加数字(注意:这里的寄存器,与`register read`中的寄存器是有所区别的,这里是指`虚拟寄存器`,而`register read`中是`真寄存器`)
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  // `alloc_global`:创建一个`全局变量`,即代码中的`t`
  alloc_global @$s4main1tAA9YCTeacherCvp          // id: %2
  // `global_addr`:获取全局变量地址,并赋值给寄存器%3
  %3 = global_addr @$s4main1tAA9YCTeacherCvp : $*YCTeacher // user: %7
  // `metatype`获取`YCTeacher`的`MetaData`赋值给%4
  %4 = metatype $@thick YCTeacher.Type            // user: %6
  // 将`__allocating_init`的函数地址赋值给 %5
  // function_ref YCTeacher.__allocating_init()
  %5 = function_ref @$s4main9YCTeacherCACycfC : $@convention(method) (@thick YCTeacher.Type) -> @owned YCTeacher // user: %6
  // `apply`调用 `__allocating_init` 初始化一个变量,赋值给%6
  %6 = apply %5(%4) : $@convention(method) (@thick YCTeacher.Type) -> @owned YCTeacher // user: %7
  // 将%6的值存储到%3,即全局变量的地址(这里与前面的%3形成一个闭环)
  store %6 to %3 : $*YCTeacher                    // id: %7
  // 构建`Int`,并`return`
  %8 = integer_literal $Builtin.Int32, 0          // user: %9
  %9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10
  return %9 : $Int32                              // id: %10
} // end sil function 'main'
  • 从SIL文件中,可以看出,代码是经过混淆的,可以通过xcrun swift-demangle还原
xcrun swift-demangle s4main1tAA9YCTeacherCvp
$s4main1tAA9YCTeacherCvp ---> main.t : main.YCTeacher
  • SIL更多语法信息,可参考github地址
  • SIL文件中搜索s4main9YCTeacherC3age4nameACSi_SStcfC,其内部主要是分配内存+初始化变量
// ************* main入口函数中的代码 ****************
// function_ref YCTeacher.__allocating_init(age:name:)
  %13 = function_ref @$s4main9YCTeacherC3age4nameACSi_SStcfC : $@convention(method) (Int, @owned String, @thick YCTeacher.Type) -> @owned YCTeacher // user: %14

// s4main9YCTeacherC3age4nameACSi_SStcfC 实际上就是__allocating_init(age:name:)
// YCTeacher.__allocating_init(age:name:)
sil hidden [exact_self_class] @$s4main9YCTeacherC3age4nameACSi_SStcfC : $@convention(method) (Int, @owned String, @thick YCTeacher.Type) -> @owned YCTeacher {
// %0 "age"                                       // user: %5
// %1 "name"                                      // user: %5
// %2 "$metatype"
bb0(%0 : $Int, %1 : $String, %2 : $@thick YCTeacher.Type):
// 堆上分配内存空间
  %3 = alloc_ref $YCTeacher                       // user: %5
  // function_ref YCTeacher.init(age:name:) 初始化当前变量
  %4 = function_ref @$s4main9YCTeacherC3age4nameACSi_SStcfc : $@convention(method) (Int, @owned String, @owned YCTeacher) -> @owned YCTeacher // user: %5
  %5 = apply %4(%0, %1, %3) : $@convention(method) (Int, @owned String, @owned YCTeacher) -> @owned YCTeacher // user: %6
  return %5 : $YCTeacher                          // id: %6
} // end sil function '$s4main9YCTeacherC3age4nameACSi_SStcfC'

对象的创建过程

符号断点调试

  • 在工程中添加__allocating_init符号断点
__allocating_init符号断点
  • 发现其内部调用的是swift_allocObject
image

源码调试

下面我们通过swift_allocObject来探索swift中对象的创建过程

  • REPL(Read Eval PrintLoop)swift交互式解释器中编写代码,也可以拷贝,并在HeapObject.cpp文件中搜索swift_allocObject函数加一个断点,然后定义一个实例对象t
image
  • 其中requiredSize是分配的实际内存大小,40
  • requiredAlignmentMask是swift中的字节对齐方式,这个和OC是一样的,必须是8的倍数,不足的会自动补齐,目的是以空间换时间,来提高内存操作效率

swift_allocObject源码分析

swift_allocObject源码如下,主要有以下几个部分

  • 通过swift_slowAlloc分配内存,并进行内存字节对齐
  • 通过new (object) HeapObject(metadata);初始化一个实例对象
  • 函数的返回值是HeapObject类型,所以当前对象的内存结构就是HeapObject的内存结构
static HeapObject *_swift_allocObject_(HeapMetadata const *metadata,
                                       size_t requiredSize,
                                       size_t requiredAlignmentMask) {
  assert(isAlignmentMask(requiredAlignmentMask));
  auto object = reinterpret_cast<HeapObject *>(
      swift_slowAlloc(requiredSize, requiredAlignmentMask)); // 分配内存、字节对齐

  // NOTE: this relies on the C++17 guaranteed semantics of no null-pointer
  // check on the placement new allocator which we have observed on Windows,
  // Linux, and macOS.
  new (object) HeapObject(metadata); // 初始化一个实例对象

  // If leak tracking is enabled, start tracking this object.
  SWIFT_LEAKS_START_TRACKING_OBJECT(object);

  SWIFT_RT_TRACK_INVOCATION(object, swift_allocObject);

  return object;
}
  • Heap.cpp文件中,进入swift_slowAlloc函数,其内部主要是通过malloc中分配size大小的内存空间,并返回内存地址p,主要是用来存储实例变量
void *swift::swift_slowAlloc(size_t size, size_t alignMask) {
  void *p;
  // This check also forces "default" alignment to use AlignedAlloc.
  if (alignMask <= MALLOC_ALIGN_MASK) {
#if defined(__APPLE__)
    p = malloc_zone_malloc(DEFAULT_ZONE(), size);
#else
    p = malloc(size); // 堆中创建size大小的内存空间,用于存储实例变量
#endif
  } else {
    size_t alignment = (alignMask == ~(size_t(0)))
                           ? _swift_MinAllocationAlignment
                           : alignMask + 1;
    p = AlignedAlloc(size, alignment);
  }
  if (!p) swift::crash("Could not allocate memory.");
  return p;
}
  • 进入HeapObject初始化方法,需要两个参数metadatarefCounts
struct HeapObject {
  /// This is always a valid pointer to a metadata object.
  HeapMetadata const *metadata;

  SWIFT_HEAPOBJECT_NON_OBJC_MEMBERS;

#ifndef __swift__
  HeapObject() = default;

  // Initialize a HeapObject header as appropriate for a newly-allocated object.
  constexpr HeapObject(HeapMetadata const *newMetadata) 
    : metadata(newMetadata)
    , refCounts(InlineRefCounts::Initialized)
  { }
  • 其中metadata类型是HeapMetadata,是一个指针类型,占8字节大小
  • refCounts(引用计数,类型是InlineRefCounts,而InlineRefCounts是一个类RefCounts的别名,占8个字节),swift采用arc引用计数
    RefCounts

总结

  • 对于实例对象t来说,其本质是一个HeapObject 结构体,默认16字节内存大小(metadata 8字节 + refCounts 8字节),与OC的对比如下
    • OC中实例对象的本质是结构体,是以objc_object为模板继承的,其中有一个isa指针,占8字节
    • Swift中实例对象,默认的比OC中多了一个refCounted引用计数大小,默认属性占16字节
  • Swift中对象的内存分配流程是:__allocating_init --> swift_allocObject_ --> _swift_allocObject --> swift_slowAlloc --> malloc
  • init在其中的职责就是初始化变量,这点与OC中是一致的

针对上面的分析,我们还遗留了两个问题:metadata是什么,40是怎么计算的?下面来继续探索
在demo中,我们可以通过Runtime方法获取类的内存大小

class_getInstanceSize

这点与在源码调试时左边local的requiredSize值是相等的,从HeapObject的分析中我们知道了,一个类在没有任何属性的情况下,默认占用16字节大小
对于IntString类型,进入其底层定义,两个都是结构体类型,那么是否都是8字节呢?可以通过打印其内存大小来验证

//********* Int底层定义 *********
@frozen public struct Int : FixedWidthInteger, SignedInteger {...}

//********* String底层定义 *********
@frozen public struct String {...}

//********* 验证 *********
print(MemoryLayout<Int>.stride)
print(MemoryLayout<String>.stride)

//********* 打印结果 *********
8
16
  • 从打印的结果中可以看出,Int类型占8字节,String类型占16字节(后面文章会进行详细讲解),这点与OC中是有所区别的
    所以这也解释了为什么YCTeacher的内存大小等于40,即40 = metadata(8字节) +refCount(8字节)+ Int(8字节)+ String(16字节)

探索Swift中类的结构

  • 在OC中类是从objc_class模板继承过来的
  • 在Swift中,类的结构在底层是HeapObject,其中有 metadata + refCounts

HeapMetadata类型分析

  • 进入HeapMetadata定义,是TargetHeapMetaData类型的别名,接收了一个参数Inprocess
using HeapMetadata = TargetHeapMetaData<Inprocess>;
  • 进入TargetHeapMetaData定义,其本质是一个模板类型,其中定义了一些所需的数据结构。这个结构体中没有属性,只有初始化方法,传入了一个MetadataKind类型的参数(该结构体没有,那么只有在父类中了)这里的kind就是传入的Inprocess
//模板类型
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  //初始化方法
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};
  • 进入TargetMetaData定义,有一个kind属性,kind的类型就是之前传入的Inprocess。从这里可以得出,对于kind,其类型就是unsigned long,主要用于区分是哪种类型的元数据
//******** TargetMetaData 定义 ********
struct TargetMetaData{
   using StoredPointer = typename Runtime: StoredPointer;
    ...
    
    StoredPointer kind;
}

//******** Inprocess 定义 ********
struct Inprocess{
    ...
    using StoredPointer = uintptr_t;
    ...
}

//******** uintptr_t 定义 ********
typedef unsigned long uintptr_t;

TargetHeapMetadataTargetMetaData定义中,均可以看出初始化方法中参数kind的类型是MetadataKind

  • 进入MetadataKind定义,里面有一个#include "MetadataKind.def",点击进入,其中记录了所有类型的元数据,所以kind种类总结如下
name value
Class 0x0
Struct 0x200
Enum 0x201
Optional 0x202
ForeignClass 0x203
Opaque 0x300
Tuple 0x301
Function 0x302
Existential 0x303
Metatype 0x304
ObjCClassWrapper 0x305
ExistentialMetatype 0x306
HeapLocalVariable 0x400
HeapGenericLocalVariable 0x500
ErrorObject 0x501
LastEnumerated 0x7FF
  • 回到TargetMetaData结构体定义中,找方法getClassObject,在该方法中去匹配kind返回值是TargetClassMetadata类型, 如果是Class,则直接对this(当前指针,即metadata)强转为ClassMetadata
 const TargetClassMetadata<Runtime> *getClassObject() const;
 
//******** 具体实现 ********
template<> inline const ClassMetadata *
  Metadata::getClassObject() const {
    //匹配kind
    switch (getKind()) {
      //如果kind是class
    case MetadataKind::Class: {
      // Native Swift class metadata is also the class object.
      //将当前指针强转为ClassMetadata类型
      return static_cast<const ClassMetadata *>(this);
    }
    case MetadataKind::ObjCClassWrapper: {
      // Objective-C class objects are referenced by their Swift metadata wrapper.
      auto wrapper = static_cast<const ObjCClassWrapperMetadata *>(this);
      return wrapper->Class;
    }
    // Other kinds of types don't have class objects.
    default:
      return nullptr;
    }
  }

这一点,我们可以通过lldb来验证

  • po metadata->getKind(),得到其kind是Class
  • po metadata->getClassObject()、x/8g 0x0000000110efdc70,这个地址中存储的是元数据信息!
image

所以,TargetMetadataTargetClassMetadata 本质上是一样的,因为在内存结构中,可以直接进行指针的转换,所以可以说,我们认为的结构体,其实就是TargetClassMetadata

  • 进入TargetClassMetadata定义,继承自TargetAnyClassMetadata,有以下这些属性,这也是类结构的部分
template <typename Runtime>
struct TargetClassMetadata : public TargetAnyClassMetadata<Runtime> {
    ...
    //swift特有的标志
    ClassFlags Flags;
    //实例对象内存大小
    uint32_t InstanceSize;
    //实例对象内存对齐方式
    uint16_t InstanceAlignMask;
    //运行时保留字段
    uint16_t Reserved;
    //类的内存大小
    uint32_t ClassSize;
    //类的内存首地址
    uint32_t ClassAddressPoint;
  ...
}
  • 进入TargetAnyClassMetadata定义,继承自TargetHeapMetadata
template <typename Runtime>
struct TargetAnyClassMetadata : public TargetHeapMetadata<Runtime> {
    ...
    ConstTargetMetadataPointer<Runtime, swift::TargetClassMetadata> Superclass;
    TargetPointer<Runtime, void> CacheData[2];
    StoredSize Data;
    ...
}

总结

综上所述,当metadatakind为Class时,有如下继承链:

image

  • 当前类返回的实际类型是 TargetClassMetadata,而TargetMetaData中只有一个属性kindTargetAnyClassMetaData中有4个属性,分别是kindsuperclasscacheDatadata(图中未标出)
  • 当前Class在内存中所存放的属性TargetClassMetadata属性 + TargetAnyClassMetaData属性 + TargetMetaData属性 构成,所以得出的metadata的数据结构体如下所示
struct swift_class_t: NSObject{
    void *kind;//相当于OC中的isa,kind的实际类型是unsigned long
    void *superClass;
    void *cacheData;
    void *data;
    uint32_t flags; //4字节
    uint32_t instanceAddressOffset;//4字节
    uint32_t instanceSize;//4字节
    uint16_t instanceAlignMask;//2字节
    uint16_t reserved;//2字节
    
    uint32_t classSize;//4字节
    uint32_t classAddressOffset;//4字节
    void *description;
    ...
}

与OC的区别

  • 实例对象 & 类

    • OC中的实例对象本质结构体,是通过底层的objc_object模板创建,类是继承自objc_class
    • Swift中的实例对象本质也是结构体,类型是HeapObject,比OC多了一个refCounts
  • 方法列表

    • OC中的方法存储在objc_class结构体class_rw_tmethodList
    • swift中的方法存储在 metadata 元数据中
  • 引用计数

    • OC中的ARC维护的是散列表
    • Swift中的ARC是对象内部有一个refCounts属性

Swift属性

在swift中,属性主要分为以下几种

  • 存储属性
  • 计算属性
  • 延迟存储属性
  • 类型属性

存储属性

存储属性分为两种:

  • 常量存储属性,用let修饰
  • 变量存储属性,用var修饰

存储属性特征:会占用分配实例对象的内存空间

计算属性

计算属性是不占用内存空间的,本质是set/get方法

属性观察者(didSet、willSet)

  • willSet:新值存储之前调用 newValue
  • didSet:新值存储之后调用 oldValue

问题1:init方法中是否会触发属性观察者?
以下代码中,init方法中设置name,是否会触发属性观察者?

class YCTeacher{
    var name: String = "测试"{
        //新值存储之前调用
        willSet{
            print("willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("didSet oldValue \(oldValue)")
        }
    }
    
    init() {
        self.name = "teacher"
    }
}

运行结果发现,并没有走willSet、didSet中的打印方法,所以有以下结论:

  • init方法中,如果调用属性,是不会触发属性观察者的
  • init中主要是初始化当前变量,除了默认的前16个字节,其他属性会调用memset清理内存空间(因为有可能是脏数据,即被别人用过),然后才会赋值

问题2:哪里可以添加属性观察者?
主要有以下三个地方可以添加:

  • 1、类中定义的存储属性
  • 2、通过类继承的存储属性
  • 3、通过类继承的计算属性

问题3:子类和父类的属性同时存在didset、willset时,其调用顺序是什么?

class YCTeacher{
    var age: Int = 18{
        //新值存储之前调用
        willSet{
            print("父类 willSet newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("父类 didSet oldValue \(oldValue)")
        }
    }
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
}


class YCMediumTeacher: YCTeacher{
    override var age: Int{
        //新值存储之前调用
        willSet{
            print("子类 newValue \(newValue)")
        }
        //新值存储之后调用
        didSet{
            print("子类 didSet oldValue \(oldValue)")
        }
    }
    
}

var t = YCMediumTeacher()
t.age = 20

运行结果如下:


image

结论:对于同一个属性,子类和父类都有属性观察者,其顺序是:先子类willset,后父类willset,再父类didset, 子类的didset,即:子父 父子

延迟存储属性

  • 使用lazy修饰的存储属性
  • 延迟属性必须有一个默认的初始值
  • 延迟存储在第一次访问的时候才被赋值
  • 延迟存储属性并不能保证线程安全
  • 延迟存储属性对实例对象大小的影响

类型属性

  • 使用关键字static修饰,且是一个全局变量
  • 类型属性必须有一个默认的初始值
  • 类型属性只会被初始化一次,线程安全

单例的写法

//****** Swift单例 ******
class YCTeacher{
    //1、使用 static + let 创建声明一个实例对象
    static let shareInstance = YCTeacher.init()
    //2、给当前init添加private访问权限
    private init(){ }
}
//使用(只能通过单例,不能通过init)
var t = YCTeacher.shareInstance

//****** OC单例 ******
@implementation YCTeacher
+ (instancetype)shareInstance{
    static YCTeacher *shareInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[YCTeacher alloc] init];
    });
    return shareInstance;
}
@end

总结

  • 存储属性会占用实例变量的内存空间
  • 计算属性不会占用内存空间,其本质是set/get方法
  • 属性观察者
    • willset:新值存储之前调用,先通知子类,再通知父类(因为父类中可能需要做一些额外的操作),即子父
    • didSet:新值存储完成后,先告诉父类,再通知子类(父类的操作优先于子类),即父子
    • 类中的init方法赋值不会触发属性观察
    • 属性可以添加在 类定义的存储属性、继承的存储属性、继承的计算属性
    • 子类调用父类的init方法,会触发观察属性
  • 延迟存储属性
    • 使用lazy修饰存储属性,且必须有一个默认值
    • 只有在第一次被访问时才会被赋值,且是线程不安全
    • 使用lazy和不使用lazy,会对实例对象的内存大小有影响,主要是因为lazy在底层是optional类型,optional的本质是enum,除了存储属性本身的内存大小,还需要一个字节用于存储case
  • 类型属性
    • 使用static 修饰,且必须有一个默认初始值
    • 是一个全局变量,只会被初始化一次,是线程安全
    • 用于创建单例对象:
      • 使用static + let创建实例变量
      • init方法的访问权限为private
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容