JavaScriptCore 详解

写在前面

WebViewJavascriptBridgeReactNativeJSPatch 这些 JavaScriptObjective-C 交互框架都有 JavaScriptCore 的影子,所以有必要好好了解一下 JavaScriptCore

  • JavaScript:下文简称 JS
  • Objective-C:下文简称 OC
  • JavaScriptCore:下文简称 JSCore

JSCore 简介

JSCore 是 JS 引擎,通常会被叫做虚拟机,专门设计来解释和执行 JS 代码。在 WebKit 中的结构如下。

JavaScriptCore_img01.png
  • WebKit Embedding API 是 Browser UI 与 WebPage 进行交互的 API 接口
  • Platform API 提供与底层驱动的交互,如网络,字体渲染,影音文件解码,渲染引擎等
  • WebCore 它实现了对文档的模型化,包括了 CSS, DOM, Render 等的实现
  • JSCore 是专门处理 JS 脚本的引擎

另外 Google 的 Chromium(Chorme的开源项目)也是使用 WebKit 。WebKit 起源于 KDE 的开源项目 Konqueror 的分支,由苹果公司用于 Safari 浏览器。其一条分支发展成为 Chorme 的内核,2013 年 Google 在此基础上开发了新的 Blink 内核。

Snip20200609_2.png

其他 JS 解析引擎

JavaScriptCore_img03.png

JSCore 提供给 OC 的接口

JSCore 提供给 OC 的接口如下

OC 接口类 作用
JSVirtualMachine 为 JS 的运行提供了底层资源,虚拟机是线程安全的
JSContext 为 JS 提供运行环境,所有的 JS 在这个上下文中执行,这里可以用来管理对象,添加方法
JSValue 是 JS 和 OC 之间交互的桥梁,负责两端对象的互相转换
JSManagedValue 可以处理内存管理中的一些特殊情形,它能帮助引用技术和垃圾回收这两种内存管理机制之间进行正确的转换
JSExport 实现 JSExport 协议可以开放 OC 类和它们的实例方法,类方法,以及属性给 JS 调用
C 接口类 作用
JSBase 定义了 JavaScriptCore 接口文件
JSContextRef 主要提供 JS 执行所需所有资源和环境
JSObjectRef 是一个 JavaScript 对象,主要提供了两部分API,一部分是创建 JS 对象,还有一部分是给创建的 JS 对象添加对应的 Callback。
JSValueRef 一个 JS 值,提供用 OC 的基础数据类型来创建 JS 的值,或者将 JS 的值转变为 OC 的基础数据类型
JSStringRef JavaScript 对象中字符串对象
JSStringRefCF CFString 与 JavaScript String 相互转化

我们在主要使用的是 OC 接口类,接下来依次分析 OC 接口类

JSVirtualMachine

一个 JSVirtualMachine 的实例就是一个完整独立的 JS 的执行环境,为 JS 的执行提供底层资源。

这个类主要用来做两件事情:

  1. 实现并发的 JavaScript 执行
  2. JavaScript 和 Objective-C 桥接对象的内存管理

提供的接口也非常简单

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject

(instancetype)init;

// 进行内存管理
- (void)addManagedReference:(id)object withOwner:(id)owner;
- (void)removeManagedReference:(id)object withOwner:(id)owner;

@end

每一个 JSVirtualMachine 可以包含多个 JS 上下文(JSContext 对象)。同一个虚拟机下不同的上下文之间可以相互传值(JSValue对象)。

然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(Garbage Collector ),GC 无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。

JSContext

一个 JSContext 表示了一次 JS 的执行环境。我们可以通过创建一个 JSContext 去调用 JS 脚本,访问一些 JS 定义的值和函数,同时也提供了让 JS 访问 Native 对象,方法的接口。

一个 JSContext 对象对应了一个全局对象。例如 Web 浏览器中的 JSContext ,其全局对象就是 Window 对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的 JavaScript Context 的作用域。

同样我们看一下接口,也是非常精简的

JS_EXPORT API_AVAILABLE(macos(10.9), ios(7.0))
@interface JSContext : NSObject

// 初始化,可以指定一个虚拟机,如果没有指定底层默认创建一个
- (instancetype)init;
- (instancetype)initWithVirtualMachine:(JSVirtualMachine *)virtualMachine;

// 执行 JS 脚本,返回值是 JS 中最后生成的一个值,sourceURL 认作其源码 URL,用作标记
- (JSValue *)evaluateScript:(NSString *)script;
- (JSValue *)evaluateScript:(NSString *)script
              withSourceURL:(NSURL *)sourceURL API_AVAILABLE(macos(10.10), ios(8.0));

// 获取当前执行的 JavaScript 代码的 context
+ (JSContext *)currentContext;

// 获取当前执行的 JavaScript function
+ (JSValue *)currentCallee API_AVAILABLE(macos(10.10), ios(8.0));

// 获取当前执行的 JavaScript 代码的 this
+ (JSValue *)currentThis;

// 获取当前 context 回调函数的参数
+ (NSArray *)currentArguments;

// 获取当前 context 的全局对象
@property (readonly, strong) JSValue *globalObject;

// 用于 JavaScript 执行异常
@property (strong) JSValue *exception;
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);

// 获取当前虚拟机
@property (readonly, strong) JSVirtualMachine *virtualMachine;

// 标记当前 context 
@property (copy) NSString *name API_AVAILABLE(macos(10.10), ios(8.0));

@end

JS 访问 OC

{
    JSContext *context = [[JSContext alloc] init];
    context[@"add"] = ^(NSInteger a, NSInteger b) {
        NSLog(@"a + b = %ld", a + b);
    };
    [context evaluateScript:@"add(1, 2)"];
}

OC 访问 JS

{
    JSContext *context = [[JSContext alloc] init];
    [context evaluateScript:@"function add(a, b) {return a + b}"];
    JSValue *addFun = context[@"add"];
    JSValue *resValue = [addFun callWithArguments:@[@1, @2]];
    NSLog(@"a + b = %@", resValue);
}

JSValue

JSValue 实例是一个指向 JS 值的引用指针。我们可以使用 JSValue 类,在 OC 和 JS 的基础数据类型之间相互转换。你也可以使用这个类去创建包装了自定义类的 Native 对象的 JS 对象,或者是那些由 Native 方法或者 Block 实现的 JS 函数。

在 JSCore 中,JSValue 自动做了 OC 和 JS 的类型转换

Objective-C type JavaScript type
nil undefined
NSNull null
NSString string
NSNumber number, boolean
NSDictionary Object object
NSArray Array object
NSDate Date object
NSBlock Function object
id Wrapper object
Class Constructor object

我们继续看一下接口,非常简洁

NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSValue : NSObject

@property (readonly, strong) JSContext *context;

+ (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context;
+ (JSValue *)valueWithBool:(BOOL)value inContext:(JSContext *)context;
+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;
+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;
+ (JSValue *)valueWithUInt32:(uint32_t)value inContext:(JSContext *)context;
+ (JSValue *)valueWithNewObjectInContext:(JSContext *)context;
+ (JSValue *)valueWithNewArrayInContext:(JSContext *)context;
+ (JSValue *)valueWithNewRegularExpressionFromPattern:(NSString *)pattern flags:(NSString *)flags inContext:(JSContext *)context;
+ (JSValue *)valueWithNewErrorFromMessage:(NSString *)message inContext:(JSContext *)context;
+ (JSValue *)valueWithNewPromiseInContext:(JSContext *)context fromExecutor:(void (^)(JSValue *resolve, JSValue *reject))callback API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNewPromiseResolvedWithResult:(id)result inContext:(JSContext *)context API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNewPromiseRejectedWithReason:(id)reason inContext:(JSContext *)context API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNewSymbolFromDescription:(NSString *)description inContext:(JSContext *)context API_AVAILABLE(macos(10.15), ios(13.0));
+ (JSValue *)valueWithNullInContext:(JSContext *)context;
+ (JSValue *)valueWithUndefinedInContext:(JSContext *)context;

- (id)toObject;
- (id)toObjectOfClass:(Class)expectedClass;
- (BOOL)toBool;
- (double)toDouble;
- (int32_t)toInt32;
- (uint32_t)toUInt32;
- (NSNumber *)toNumber;
- (NSString *)toString;
- (NSDate *)toDate;
- (NSArray *)toArray;
- (NSDictionary *)toDictionary;

@property (readonly) BOOL isUndefined;
@property (readonly) BOOL isNull;
@property (readonly) BOOL isBoolean;
@property (readonly) BOOL isNumber;
@property (readonly) BOOL isString;
@property (readonly) BOOL isObject;
@property (readonly) BOOL isArray API_AVAILABLE(macos(10.11), ios(9.0));
@property (readonly) BOOL isDate API_AVAILABLE(macos(10.11), ios(9.0));
@property (readonly) BOOL isSymbol API_AVAILABLE(macos(10.15), ios(13.0));

- (BOOL)isEqualToObject:(id)value;
- (BOOL)isEqualWithTypeCoercionToObject:(id)value;
- (BOOL)isInstanceOf:(id)value;

// 当前 JSValue 为一个函数的时候,可以通过这个方法调用
- (JSValue *)callWithArguments:(NSArray *)arguments;
// 调用 JS 中的构造函数,arguments 数组内容必须是 JSValue 对象,以供 JS 能顺利转化
- (JSValue *)constructWithArguments:(NSArray *)arguments;
// 当前 JSValue 对象为 JS 中的全局对象名称,method 为全局对象的方法名称,arguments 为参数
- (JSValue *)invokeMethod:(NSString *)method withArguments:(NSArray *)arguments;

@end

OC 的 Block 转换

OC 层面的 Block 是可以自动转换为 JS 层面的函数,JS 可以直接访问;但是 JS 的函数 OC 确不能直接访问,而要通过 callWithArguments: 方法来调用。

OC 的 id 类型转换

OC 的 id 类型传给 JS,只是一个指针,是没法访问其属性和方法的,但是 JS 回传到 OC 的时候 OC 还是可以正常访问的。如果需要在 JS 中,访问 OC 对象的属性和方法可以通过 JSExport 协议来实现,下面会详细介绍。

JSExport

实现 JSExport 协议可以开放 OC 类和它们的实例方法,类方法,以及属性给 JS 调用

我们先看一个常规例子

@protocol OC2JSObjectExport <JSExport>

@property (nonatomic, strong) NSString *name;

- (void)callName;

+ (void)helloAtOC;

@end

@interface OC2JSObject : NSObject<OC2JSObjectExport>

@end

@implementation OC2JSObject

@synthesize name = _name;

- (void)callName
{
    NSLog(@"callName:%@", self.name);
}

+ (void)helloAtOC
{
    NSLog(@"helloAtOC");
}

@end

// 调用如下
{
    OC2JSObject *ocObj = [OC2JSObject new];
    ocObj.name = @"bob";
    
    JSContext *context = [[JSContext alloc] init];
    context[@"log"] = ^(NSString *msg){
        NSLog(@"%@", msg);
    };
    context[@"ocObj"] = ocObj;
    context[@"OC2JSObject"] = OC2JSObject.class;
    
    // 访问属性
    [context evaluateScript:@"log(ocObj.name)"];
    // 访问实例方法
    [context evaluateScript:@"ocObj.callName()"];
    // 访问类方法
    [context evaluateScript:@"OC2JSObject.helloAtOC()"];
}

如果 OC 方法有多个参数的时候

@protocol OC2JSObjectExport <JSExport>

+ (void)callVal1:(NSString *)val1 val2:(NSString *)val2;

@end

@interface OC2JSObject : NSObject<OC2JSObjectExport>

@end

@implementation OC2JSObject

+ (void)callVal1:(NSString *)val1 val2:(NSString *)val2
{
    NSLog(@"val1:%@ val2:%@", val1, val2);
}

@end

// 调用如下
{
    JSContext *context = [[JSContext alloc] init];
    context[@"OC2JSObject"] = OC2JSObject.class;
    [context evaluateScript:@"OC2JSObject.callVal1Val2('a', 'b')"];
}

多个参数的时候,转换规则成驼峰形式

  • 去掉所有的冒号
  • 所有冒号后的第一个小写字母都会被转为大写

如果不喜欢默认的转换规则,也可以使用 JSExportAs 来自定义转换,比如

@protocol OC2JSObjectExport <JSExport>

JSExportAs(callVal, + (void)callVal1:(NSString *)val1 val2:(NSString *)val2);

@end

@interface OC2JSObject : NSObject<OC2JSObjectExport>

@end

@implementation OC2JSObject

+ (void)callVal1:(NSString *)val1 val2:(NSString *)val2
{
    NSLog(@"val1:%@ val2:%@", val1, val2);
}

@end

// 调用如下
{
    JSContext *context = [[JSContext alloc] init];
    context[@"OC2JSObject"] = OC2JSObject.class;
    [context evaluateScript:@"OC2JSObject.callVal('a', 'b')"];
}

小结

  • 实现 JSExport 协议可以开放 OC 类和它们的实例方法,类方法,以及属性给 JS 调用
  • 多个参数的时候,自动按照如下规则转换,如果不喜欢可以通过 JSExportAs 自定义
    • 去掉所有的冒号
    • 所有冒号后的第一个小写字母都会被转为大写

JSCore 多线程

我们先看看一段 OC 的多线程代码

{
    __block NSInteger cnt = 0;
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger i = 0; i < 1000; i++)
    {
        dispatch_group_async(group, queue, ^{
            cnt = cnt + 1;
        });
    }
    dispatch_group_notify(group, queue, ^{
        NSLog(@"cnt:%ld", cnt);
    });
}

因为访问 cnt 是线程不安全的,所以最后 cnt 的值,不一定是 1000

我们再看一段 JS 的代码

{
    JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
    JSContext *context = [[JSContext alloc] initWithVirtualMachine:vm];
    context[@"log"] = ^(NSString *msg) {
        NSLog(@"log:%@", msg);
    };
    
    [context evaluateScript:@"var cnt = 0"];
    [context evaluateScript:@"function addCnt(){cnt = cnt + 1}"];
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (NSInteger i = 0; i < 1000; i++)
    {
        dispatch_group_async(group, queue, ^{
            [context evaluateScript:@"addCnt()"];
        });
    }
    dispatch_group_notify(group, queue, ^{
        [context evaluateScript:@"log(cnt)"];
    });
}

这段代码发现,cnt 都是 1000,说明线程安全。

JavaScriptCore 提供的 API 是线程安全的。

你可以在不同的线程中,创建 JSValue,用 JSContext 执行 JS 语句,但是当一个线程正在执行 JS 语句时,其他线程想要使用这个正在执行 JS 语句的 JSContext 所属的 JSVirtualMachine 就必须得等待,等待前前一个线程执行完,才能使用这个 JSVirtualMachine。

JSCore 内存管理

目前 OC 使用的是 ARC,不能自动解决循环引用的问题,需要我们程序员手动去解除循环,但是 JS 使用的是 GC(垃圾回收机制),所有的引用都是强引用,同时垃圾回收器可以帮我们解决循环引用的问题,JSCore 也是一样的,一般来说,大多数情况下不需要我们去手动的管理内存。

注意1:OC 或 JS 对象,只要有一端存在强引用,对象就不会释放

比如:

@interface TCURLRequest : NSURLRequest

@end

@implementation TCURLRequest

- (void)dealloc
{
    NSLog(@"dealloc");
}

@end

{
    JSContext *context = [[JSContext alloc] init];
    {
        TCURLRequest *request = [TCURLRequest requestWithURL:[NSURL URLWithString:@"https://www.google.com"]];
        JSValue *value = [JSValue valueWithObject:request inContext:context];
        [context evaluateScript:@"var ocValue"];
        [context evaluateScript:@"function setValue(value){ocValue = value}"];
        JSValue *setValueFun = context[@"setValue"];
        [setValueFun callWithArguments:@[value]];
        // 在 OC 中,request 这之后就要释放了
    }
    JSValue *ocValue = context[@"ocValue"];
    TCURLRequest *request = ocValue.toObject;
    NSLog(@"%@", request);
    // 在这之后才释放的
}

注意2:循环引用,由于 JSValueJSContextJSVirtualMachine 都是强引用

比如,下面的代码就会出现循环引用

{
    JSContext *context = [[JSContext alloc] init];
    JSValue *value = [JSValue valueWithInt32:10086 inContext:context];
    context[@"log"] = ^{
        NSLog(@"%@", value);
    };
    [context evaluateScript:@"log()"];
}

我们可以使用 JSManagedValue 来解决,JSManagedValueJSValue 采用的是弱引用

{
    JSContext *context = [[JSContext alloc] init];
    JSValue *value = [JSValue valueWithInt32:10086 inContext:context];
    JSManagedValue *mValue = [JSManagedValue managedValueWithValue:value];
    context[@"log"] = ^{
        NSLog(@"%@", mValue);
    };
    [context evaluateScript:@"log()"];
}

当然,这里我们也可以使用 weak 来处理。有一种情况是 OC 引用了 JS 的属性,然后 JS 中也引用了 OC 的属性,这个时候由于 JS 中没有弱引用之说,所以必须要用 JSManagedValue。

比如:

@protocol QSExport <JSExport>

@property (nonatomic, strong) JSValue *jsValue;

@end

@interface QSObject : NSObject <QSExport>

@end

@implementation QSObject

@synthesize jsValue = _jsValue;

- (void)dealloc 
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

@interface QSContext : JSContext

@end

@implementation QSContext

- (void)dealloc
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

// 调用
{
    NSString *script = @"var arr = [1,2,3];\
                         function setObj(obj) {\
                            this.obj = obj;\
                            obj.jsValue = arr;\
                        }";
    QSContext *context = [[QSContext alloc] init];
    [context evaluateScript:script];
    
    QSObject *obj = [[QSObject alloc] init];
    [context[@"setObj"] callWithArguments:@[obj]];
}

QSObject 中的 jsValue 引用了 JS 中的 arr,而且 QSObject 对象同时也被 JS 引用。当然我们可以让 QSObject 中的 jsValue 为 weak 指针,但是由于 JS 最新在 OC 中没有强引用对象,所以 weak 指针是行不通的,

比如:

@protocol QSExport <JSExport>

@property (nonatomic, weak) JSValue *jsValue;

@end

// 调用
{
    NSString *script = @"var arr = [1,2,3];\
                         function setObj(obj) {\
                            this.obj = obj;\
                            obj.jsValue = arr;\
                        }";
    QSContext *context = [[QSContext alloc] init];
    [context evaluateScript:script];
    
    QSObject *obj = [[QSObject alloc] init];
    [context[@"setObj"] callWithArguments:@[obj]];
    NSLog(@"%@", obj.jsValue); // 这里是没有值的
}

在这种情况下,我们只能使用 JSManagedValue

@protocol QSExport <JSExport>

@property (nonatomic, strong) JSValue *jsValue;

@end

@interface QSObject : NSObject <QSExport>

@property (nonatomic, strong) JSManagedValue *jsManagedValue;

@end

@implementation QSObject

@synthesize jsValue = _jsValue;

- (void)setJsValue:(JSValue *)jsValue
{
    _jsManagedValue = [JSManagedValue managedValueWithValue:jsValue];
}

- (JSValue *)jsValue
{
    return self.jsManagedValue.value;
}

- (void)dealloc
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

@interface QSContext : JSContext

@end

@implementation QSContext

- (void)dealloc
{
    NSLog(@"dealloc:%@", NSStringFromClass(self.class));
}

@end

// 调用
{
    NSString *script = @"var arr = [1,2,3];\
                         function setObj(obj) {\
                            this.obj = obj;\
                            obj.jsValue = arr;\
                        }";
    QSContext *context = [[QSContext alloc] init];
    [context evaluateScript:script];
    
    QSObject *obj = [[QSObject alloc] init];
    [context[@"setObj"] callWithArguments:@[obj]];
    NSLog(@"%@", obj.jsValue); 
}

这样就能正常调用和释放了。

参考资料

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