复制大神的runtime文章

iOS开发-- Runtime的几个小例子

字数2756阅读1867评论22喜欢88

一、什么是runtime(也就是所谓的“运行时”,因为是在运行时实现的。)

1.runtime是一套底层的c语言API(包括很多强大实用的c语言类型,c语言函数);  [runtime运行系统]

2.实际上,平时我们编写的oc代码,底层都是基于runtime实现的;                            [OC语言的动态性]

运行时系统 (runtime system),对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。runtime就是OC辛苦的幕后工作人员。(编译器会自动帮助我们编译成runtime代码。

动态特性:使得它在语言层面上支持程序的可扩展性。只有在程序运行时,才会去确定对象的类型,并调用类与对象相应的方法。利用runtime机制让我们可以在程序运行时动态修改类的具体实现、包括类中的所有私有属性、方法。这也是本文runtime例子的出发点。

我们所敲入的代码转化为运行时的runtime函数代码,最终在程序运行时转成了底层的runtime的c语言代码;

举例

当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:

idobjc_msgSend (idself, SEL op, ... );

也就是说,我们平时编写的oc代码,方法调用的本质,就是在编译阶段,编译器转化为向对象发送消息。

【本次开发环境: Xcode:7.2    iOS Simulator:iphone6    By: 啊左

本文Demo下载链接:runtime-Demo

二、runtime的几种使用方法

我们通过继承于NSObject的person类,来对runtime进行学习。

本文共有6个关于runtime机制方法的小例子,分别是:

1.获取person类的所有变量;

2.获取person类的所有方法;

3.改变person类的私有变量name的值;

4.为person的category类增加一个新属性;

5.为person类添加一个方法;

6.交换person类的2个方法的功能;

(个人习惯,喜欢为6个例子添加按钮各自的行为方法,并分别执行相应的行为,以此看清各个runtime函数的具体功能所带来的效果。)

首先,创建新的项目,并在项目中新建一个普通的OC类:person类(继承于NSObject),为了避免后面与其他方法函数搞混,我们把完整的person类编写齐全,用于后面使用runtime的几种方法:

person.h如下:

#import@interfaceperson:NSObject@property(nonatomic,assign)intage;//属性变量-(void)func1;-(void)func2;@end

person.m如下:

#import"person.h"@implementationperson{NSString*name;//实例变量}//初始化person属性-(instancetype)init{self= [superinit];if(self) {  name =@"Tom";self.age=12; }returnself;}//person的2个普通方法-(void)func1{NSLog(@"执行func1方法。");}-(void)func2{NSLog(@"执行func2方法。");}//输出person对象时的方法:-(NSString*)description{return[NSStringstringWithFormat:@"name:%@ age:%d",name,self.age];}@end

从person类的描述中,我们可以看到person类含有一个可供外类使用的共有属性age,以及一个外界不可以访问私有属性name,但是,有木有想过,其实在外类,name也是可以访问的。OC里面,通过runtime系统,苹果允许不受这些私有属性的限制,对私有属性私有方法等进行访问、添加、修改、甚至替换系统的方法。

那么,为项目的故事板添加6个按钮;

在使用runtime的地方,我们都需要包含头文件:

#import//(在需要使用runtime的实现文件.m中包含即可.)

1.获取person类的所有变量

将第一个按钮关联到ViewController.h,添加行为并命名其方法为:“getAllVariable”:

- (IBAction)getAllVariable:(UIButton*)sender;//获取所有变量

在ViewController.m中的实现如下:

/*1.获取person所有的成员变量*/- (IBAction)getAllVariable:(UIButton*)sender {unsignedintcount =0;//获取类的一个包含所有变量的列表,IVar是runtime声明的一个宏,是实例变量的意思.Ivar *allVariables = class_copyIvarList([person class], &count);for(inti =0;i

点击按钮后,得到的输出如下:(i表示类型为int)

2016-05-1817:17:10.502runtime运行时[10164:452725] (Name: name) ----- (Type:@"NSString")2016-05-1817:17:10.503runtime运行时[10164:452725] (Name: _age) ----- (Type:i)

分析Ivar,一个指向objc_ivar结构体指针,包含了变量名、变量类型等信息。

可以看到,私有属性name能够访问到了。 在有些项目中,为了对某些私有属性进行隐藏,某些.h文件中没有出现相应的显式创建,而是如上面的person类中,在.m中进行私有创建,但是我们可以通过runtime这个有效的方法,访问到所有包括这些隐藏的私有变量。

拓展

class_copyIvarList能够获取一个含有类中所有成员变量的列表,列表中包括属性变量和实例变量。需要注意的是,如果如本例中,age返回的是"_age",但是如果在person.m中加入:@synthesize age;

那么控制台第二行返回的是"(Name: age) ----- (Type:i) ;"

(因为@property是生成了"_age",而@synthesize是执行了"@synthesize age = _age;",关于OC属性变量与实例变量的区别、@property、@synthesize的作用等具体的知识,有兴趣的童鞋可以自行了解。)

如果单单需要获取属性列表的话,可以使用函数:class_copyPropertyList();只是返回的属性变量仅仅是“age”,做为实例变量的name是不被获取的。

class_copyIvarList()函数则能够返回实例变量和属性变量的所有成员变量。

2.获取person类的所有方法

将第二个按钮关联到ViewController.h,添加行为并命名其方法为:“getAllMethod”:

- (IBAction)getAllMethod:(UIButton*)sender;//获取所有方法

在ViewController.m中的实现如下:

/*2.获取person所有方法*/- (IBAction)getAllMethod:(UIButton*)sender {unsignedintcount;//获取方法列表,所有在.m文件显式实现的方法都会被找到,包括setter+getter方法;Method *allMethods = class_copyMethodList([person class], &count);for(inti =0;i

点击按钮后,控制台输出:

2016-05-1917:05:19.880runtime运行时[14054:678124] (Method:func1)2016-05-1917:05:19.881runtime运行时[14054:678124] (Method:func2)2016-05-1917:05:19.881runtime运行时[14054:678124] (Method:setAge:)2016-05-1917:05:19.881runtime运行时[14054:678124] (Method:age)2016-05-1917:05:19.881runtime运行时[14054:678124] (Method:.cxx_destruct)2016-05-1917:05:19.882runtime运行时[14054:678124] (Method:description)2016-05-1917:05:19.882runtime运行时[14054:678124] (Method:init)

控制台输出了包括setget等方法名称。【备注:.cxx_destruct方法是关于系统自动内存释放工作的一个隐藏的函数,当ARC下,且本类拥有实例变量时,才会出现;】

分析Method是一个指向objc_method结构体指针,表示对类中的某个方法的描述。在API中的定义:typedef struct objc_method Method;

而objc_method结构体如下:

truct objc_method { SEL method_name OBJC2_UNAVAILABLE;char*method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE;}

method_name :方法选择器@selector(),类型为SEL。 相同名字的方法下,即使在不同类中定义,它们的方法选择器也相同。

method_types:方法类型,是个char指针,存储着方法的参数类型和返回值类型。

method_imp:指向方法的具体实现的指针,数据类型为IMP,本质上是一个函数指针。 在第五个按钮行为“增加一个方法”部分会提到。

SEL:数据类型,表示方法选择器,可以理解为对方法的一种包装。在每个方法都有一个与之对应的SEL类型的数据,根据一个SEL数据“@selector(方法名)”就可以找到对应的方法地址,进而调用方法。

因此可以通过:获取Method结构体->得到SEL选择器名称->得到对应的方法名,这样的方式,便于认识OC中关于方法的定义。

3.改变person类的私有变量name的值.

将第三个按钮关联到ViewController.h,添加行为并命名其方法为:“changeVariable”:

- (IBAction)changeVariable:(UIButton*)sender;//改变其中name变量

在ViewController.m中创建一个person对象,记得初始化

@implementationViewController{  person *per;//创建一个person实例}- (void)viewDidLoad {  [superviewDidLoad];  per = [[person alloc]init];//记得要初始化...不然后果自己尝试下}

在ViewController.m中的实现如下:

/*3.改变person的name变量属性*/- (IBAction)changeVariable:(UIButton*)sender {NSLog(@"改变前的person:%@",per);unsignedintcount =0;Ivar *allList = class_copyIvarList([person class], &count); Ivar ivv = allList[0];//从第一个方法getAllVariable中输出的控制台信息,我们可以看到name为第一个实例属性。object_setIvar(per, ivv,@"Mike");//name属性Tom被强制改为Mike。NSLog(@"改变之后的person:%@",per);}

点击按钮后,控制台输出:

2016-05-1922:45:05.125runtime运行时[1957:34730] 改变前的person:name:Tom age:122016-05-1922:45:05.126runtime运行时[1957:34730] 改变之后的person:name:Mike age:12

4.为person的category类增加一个新属性:

如何在不改动某个类的前提下,添加一个新的属性呢?

答:可以利用runtime为分类添加新属性

在iOS中,category,也就是分类,是不可以为本类添加新的属性的,但是在runtime中我们可以使用对象关联,为person类进行分类的新属性创建:

①新建一个新的OC类:

命名为:PersonCategory,点击next

在出现的新类“person+PersonCategory.h”中,添加“height”:

#import"person.h"@interfaceperson(PersonCategory)@property(nonatomic,assign)floatheight;//新属性@end

person+PersonCategory.m”类的代码如下:

#import"person+PersonCategory.h"#import//runtime API的使用需要包含此头文件constchar* str ="myKey";//做为key,字符常量 必须是C语言字符串;@implementationperson(PersonCategory)-(void)setHeight:(float)height{NSNumber*num = [NSNumbernumberWithFloat:height];/*

第一个参数是需要添加属性的对象;

第二个参数是属性的key;

第三个参数是属性的值,类型必须为id,所以此处height先转为NSNumber类型;

第四个参数是使用策略,是一个枚举值,类似@property属性创建时设置的关键字,可从命名看出各枚举的意义;

*/objc_setAssociatedObject(idobject,constvoid*key,idvalue, objc_AssociationPolicy policy);}//提取属性的值:-(float)height{NSNumber*number = objc_getAssociatedObject(self, str);return[number floatValue];}@end

接下来,我们可以在ViewController.m中对person的一个对象进行height的访问了,

将第四个按钮关联到ViewController.h添加行为并命名其方法为:“addVariable:”(记得:#import "person+PersonCategory.h"

-(IBAction)addVariable:(UIButton *)sender;

在ViewController.m中的实现如下:

/* 4.添加新的属性*/- (IBAction)addVariable:(UIButton*)sender { per.height=12;//给新属性height赋值NSLog(@"%f",[per height]);//访问新属性值}

点击按钮、再点击按钮获取类的属性、方法。

2016-05-2015:39:54.432runtime运行时[4605:178974]12.0000002016-05-2015:39:56.295runtime运行时[4605:178974] (Name: name) ----- (Type:@"NSString")2016-05-2015:39:56.296runtime运行时[4605:178974] (Name: _age) ----- (Type:i)2016-05-2015:39:57.195runtime运行时[4605:178974] (Method:func1)2016-05-2015:39:57.196runtime运行时[4605:178974] (Method:func2)2016-05-2015:39:57.196runtime运行时[4605:178974] (Method:setAge:)2016-05-2015:39:57.196runtime运行时[4605:178974] (Method:age)2016-05-2015:39:57.196runtime运行时[4605:178974] (Method:.cxx_destruct)2016-05-2015:39:57.197runtime运行时[4605:178974] (Method:description)2016-05-2015:39:57.197runtime运行时[4605:178974] (Method:init)2016-05-2015:39:57.197runtime运行时[4605:178974] (Method:height)2016-05-2015:39:57.197runtime运行时[4605:178974] (Method:setHeight:)

分析:可以看到分类的新属性可以在per对象中对新属性height进行访问赋值。

获取到person类属性时,依然没有height的存在,但是却有height和setHeight这两个方法;因为在分类中,即使使用@property定义了,也只是生成set+get方法,而不会生成_变量名,分类中是不允许定义变量的。

使用runtime中objc_setAssociatedObject()objc_getAssociatedObject()方法,本质上只是为对象per添加了对height的属性关联,但是达到了新属性的作用;

使用场景:假设imageCategory是UIImage类的分类,在实际开发中,我们使用UIImage下载图片或者操作过程需要增加一个URL保存一段地址,以备后期使用。这时可以尝试在分类中动态添加新属性MyURL进行存储。

5.为person类添加一个新方法;

将第五个按钮关联到ViewController.h,添加行为并命名其方法为:“addMethod”:

-(IBAction)addMethod:(UIButton *)sender;

在ViewController.m中的实现如下:

/*5.添加新的方法试试(这种方法等价于对Father类添加Category对方法进行扩展):*/- (IBAction)addMethod:(UIButton*)sender {/* 动态添加方法:

第一个参数表示Class cls 类型;

第二个参数表示待调用的方法名称;

第三个参数(IMP)myAddingFunction,IMP一个函数指针,这里表示指定具体实现方法myAddingFunction;

第四个参数表方法的参数,0代表没有参数;

*/class_addMethod([per class],@selector(NewMethod), (IMP)myAddingFunction,0);//调用方法 【如果使用[per NewMethod]调用方法,在ARC下会报“no visible @interface"错误】[per performSelector:@selector(NewMethod)];}//具体的实现(方法的内部都默认包含两个参数Class类和SEL方法,被称为隐式参数。)intmyAddingFunction(idself, SEL _cmd){NSLog(@"已新增方法:NewMethod");return1;}

点击按钮后,控制台输出:

2016-05-2014:08:55.822runtime运行时[1957:34730] 已新增方法:NewMethod

6.交换person类的2个方法的功能:

将第六个按钮关联到ViewController.h,添加行为并命名其方法为:“replaceMethod”:

-(IBAction)replaceMethod:(UIButton *)sender;

在ViewController.m中的实现如下:

/* 6.交换两种方法之后(功能对调),可以试试让苹果乱套... */- (IBAction)replaceMethod:(UIButton*)sender { Method method1 = class_getInstanceMethod([person class],@selector(func1)); Method method2 = class_getInstanceMethod([person class],@selector(func2));//交换方法method_exchangeImplementations(method1, method2); [per func1];//输出交换后的效果,需要对比的可以尝试下交换前运行func1;}

点击按钮后,控制台输出:

2016-05-2014:11:57.381runtime运行时[1957:34730] 执行func2方法。

交换方法的使用场景:项目中的某个功能,在项目中需要多次被引用,当项目的需求发生改变时,要使用另一种功能代替这个功能,且要求不改变旧的项目(也就是不改变原来方法实现的前提下)。那么,我们可以在分类中,再写一个新的方法(符合新的需求的方法),然后交换两个方法的实现。这样,在不改变项目的代码,而只是增加了新的代码的情况下,就完成了项目的改进,很好地体现了该项目的封装性与利用率。

:交换两个方法的实现一般写在类的load方法里面,因为load方法会在程序运行前加载一次。

(转载请标明原文出处,谢谢支持 ~ ^-^ ~)

 by:啊左~

推荐拓展阅读

举报文章

著作权归作者所有

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持

喜欢

88

分享到微博分享到微信

更多分享

喜欢的用户

鑫胖2016.11.03 10:42

congdufs2016.11.02 17:53

是刺猬2016.11.01 05:34

CarlXu2016.10.29 00:21

竹内溪风2016.10.27 09:40

sq_882016.10.13 18:04

康龙归海2016.10.08 10:37

上发条的树2016.09.20 16:54

花花你个花花呦2016.08.21 22:40

Peter_时2016.08.17 23:25

没骆驼de祥子2016.08.16 17:59

常义2016.08.14 22:23

SunshineAU2016.08.12 19:50

iOS_Job2016.08.09 18:47

于_先笙2016.08.08 19:33

22条评论(按时间正序·按时间倒序·按喜欢排序添加新评论

Prospero

2 楼 ·2016.05.24 09:52

不错,好好学习一下

喜欢(0)回复

华之曦

3 楼 ·2016.05.24 18:51

mark

喜欢(0)回复

简单也好

4 楼 ·2016.05.26 22:47

说的明白,还有例子,👍

喜欢(0)回复

简单也好

5 楼 ·2016.05.28 21:47

写了一下,在添加新方法那里,

class_addMethod([_per class], @selector(sayHi), (IMP)myAddingFunction, 0);

selector()里面的方法名好像没什么用?,只会执行IMP类型的C语言函数?

喜欢(0)回复

啊左@简单也好第二个参数是让对象调用的方法名称。

[per performSelector:@selector(NewMethod)];

回复2016.05.30 14:22

简单也好@啊左[per performSelector:@selector(NewMethod)]; 却没有执行NewMethod方法,执行的是(IMP)myAddingFunction里面的内容

回复2016.05.30 16:20

啊左@简单也好NewMetho只是一个方法名,可随意命名,主要是给对象用来调用的。但是实际上具体方法实现是在(IMP)函数指针指向的内容里面,那是“实现这个方法的函数”。

虽然一般都说class_addMethod是添加方法,但个人觉得说是添加函数更贴切些。

回复2016.05.30 17:02

还有2条评论,展开查看添加新回复

夜3033

6 楼 ·2016.06.28 15:58

写的很好,重点是有代码能自己试一下,学习了。

喜欢(0)回复

0ef0376dc7d1

7 楼 ·2016.07.04 16:51

楼主,额能请教你些问题吗?

喜欢(0)回复

啊左@0ef0376dc7d1你可以直接问呀。

回复2016.07.04 20:16

添加新回复

butterflyer

8 楼 ·2016.08.08 16:54

给楼主点个赞。

喜欢(1)回复

啊左@butterflyer木有看到你的赞哦~

回复2016.08.10 22:04

添加新回复

同样的错误不能错两次

9 楼 ·2016.08.16 14:41

🐂

喜欢(0)回复

酷爱西西的伪球迷

10 楼 ·2016.08.24 10:04

这里获取所有对象的所有方法得到的是其所有的实例方法

喜欢(0)回复

鼻毛长长

11 楼 ·2016.10.10 11:12

什么是编译,什么是运行?

喜欢(0)回复

啊左@鼻毛长长

编译:编译器帮你把源代码翻译成机器能识别的代码,或者说识别语法等代码错误并产生能够识别的指令以便让运行时能够进行相应的内存分配。

运行时:代码跑(run)起来了,进行内存的分配与操作。

以上可能不太准确,纯属个人见解。具体的可以网上查找这两个的意义与区别。

回复2016.10.10 11:41

添加新回复

阿呆的乐乐园

12 楼 ·2016.10.28 08:06

获取的实例变量方法中,name没有下划线是因为楼主写的不规范,实例变量name没有加下划线,所以打印出来就没有,而系统自动生成的age实例变量就有下划线

喜欢(0)回复

啊左@阿呆的乐乐园

其实这里一直重点强调的是,age会因为属性变量机制中默认的@synthesize使得它自动补全为_age,所以很明显把name命名为没有下划线是为了区分出来:属性变量age在@property后会补全下划线,在这里没有强调name的下划线,是为了避免混淆;

你可以直接说这种命名不规范,但不是“因为不规范”所以才有下划线😂,那是因为:在语法正确情况下不管怎么命名实例变量,它都就只输出原来的名字,即使我加了2个下划线,它也照样输出2个下划线。

另外,实例变量这种语法属于比较老派的用法,以前为了防止内存管理泄露,与属性变量调用访问方法区分开来,”_变量名”往往意味着实例变量,包括我公司旧的项目也是实例变量加_、属性变量加“@synthesize name = _name;”但是在现在ARC后,不存在上述问题,一年多前我身边的挺多同行也是常常加下划线,包括后来看《精通iOS开发》这些书介绍的时候,也是发现里面的实例变量也没有加下划线,所以我想现在更多的已经是习惯问题而不是规范,当然也会有相关资料建议下划线做为编码规范。这就看开发者怎么去处理了。

回复2016.10.28 22:23

阿呆的乐乐园@啊左写的好多,好详细

回复2016.10.28 22:43

啊左@阿呆的乐乐园😅,翻了资料共享一下。

回复2016.10.28 22:46

添加新回复

⌘+Return 发表

被以下专题收入,发现更多相似内容:

程序员

正在关注

如果你是程序员,或者有一颗喜欢写程序的心,喜欢分享技术干货、项目经验、程序员日常囧事等等,欢迎投稿《程序员》专题。

专题主编:小...

26612篇文章· 209657人关注

首页投稿

添加关注

玩转简书的第一步,从这个专题开始。

想上首页热门榜么?好内容想被更多人看到么?来投稿吧!如果被拒也不要灰心哦~入选文章会进一个队...

113720篇文章· 138152人关注

iOS Developer

正在关注

分享 iOS 开发的知识,解决大家遇到的问题,讨论iOS开发的前沿,欢迎大家投稿~

14867篇文章· 27901人关注

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

推荐阅读更多精彩内容