一文了解Flutter架构

前言

本文总结Flutter架构概览,包含其设计层面的核心原则以及概念。

Flutter是一个跨平台的UI工具集,它允许在各种操作系统上复用相同的代码,同时应用程序直接与底层平台交互,避免了不同平台视图的差异,同时也让开发者能够在不同平台上都能交付拥有原生体验的高性能应用。

开发阶段,FLutter应用会在一个VM(程序虚拟机)中运行,从而可以保留状态且无需重新编译的情况下,热重载相关的更新。对于发行版(release),Flutter程序会直接编译错机器码,或者针对Web平台的JavaScript。

概览分为以下几个部分:

  1. 分层模型:Flutter的构成要素
  2. 响应式用户界面:Flutter用户界面开发的核心概念
  3. widgets介绍:构建Flutter用户界面的基石
  4. 渲染过程:Flutter如何将界面布局转化为像素
  5. 平台嵌入层的概念:让Flutter应用可以再移动端以及桌面端操作系统执行的代码

架构层

Flutter被设计为一个可扩展的分层系统。它可以被看做是各个独立的组件系列合集,上层的组件各自依赖下层的组件。组件无法越权访问底层的内容,并且框架层的各个部分都是可选且可替代。

image

对于底层操作系统而言,Flutter应用程序的包装方式与其他原生应用相同。在每一个平台上,都回去包含一个特定的嵌入层,从而提供一个程序入口,程序由此可以与底层操作系统进行协调,访问诸如Surface渲染,辅助功能和输入等等服务,并且管理时间循环队列。该嵌入层采用了适合当前平台语言编写,例如Android使用的是Java/C++,IOS和MacOSSierra使用的是OC和OC++,Windows和Linux使用的是C++,Flutter代码可以通过嵌入层,以模块方式集成到现有的应用中,也可以作为应用的主体。Flutter本身包含了各个常见平台的嵌入层,同时也存在一些其他的嵌入层。

Flutter引擎毫无疑问是Flutter的核心,它主要是C++编写,并提供了Flutter应用所需要的原语。当需要绘制新的一帧的内容时,引擎将负责对需要合成的场景进行栅格化。它提供了Flutter核心API的底层实现,包括图形(通过Skia)、文本布局、文件以及网络IO、辅助功能支持、插件架构和Dart运行环境以及编译环境的工具链。

引擎将C++ 代码包装成Dart代码,通过dart:ui暴露给Flutter框架层。该库暴露了最底层的原语,包括用于驱动图形输入、图形、和文本渲染的子系统的类。

通常,开发者可以通过Flutter Framework与Flutter进行交互,该Framework提供了以Dart语音编写的现代响应式框架。它包括由一系列层组成的一组丰富的平台,布局和基础库。从下层到上层,依次有:

  • 基础的 foundational 类及一些基层之上的构建块服务,如 animationpaintinggestures,它们可以提供上层常用的抽象。
  • 渲染层 用于提供操作布局的抽象。有了渲染层,你可以构建一棵可渲染对象的树。在你动态更新这些对象时,渲染树也会自动根据你的变更来更新布局。
  • widget 层 是一种组合的抽象。每一个渲染层中的渲染对象,都在 widgets 层中有一个对应的类。此外,widgets 层让你可以自由组合你需要复用的各种类。响应式编程模型就在该层级中被引入。
  • MaterialCupertino 库提供了全面的 widgets 层的原语组合,这套组合分别实现了 Material 和 iOS 设计规范。

Flutter 框架相对较小,因为一些开发者可能会使用到的更高层级的功能已经被拆分到不同的软件包中,使用 Dart 和 Flutter 的核心库实现,其中包括平台插件,例如 camerawebview;与平台无关的功能,例如 charactershttpanimations。还有一些软件包来自于更为宽泛的生态系统中,例如 应用内支付Apple 认证Lottie 动画

该概览的其余部分将从 UI 开发的响应式范例开始,浏览各个构建层。而后,我们会讲述 widgets 如何被组织,并转换成应用程序的渲染对象。同时我们也会讲述 Flutter 如何在平台层面与其他代码进行交互,最终,我们会对目前 Flutter 对于 Web 平台的支持与其他平台的异同做一个总结。

响应式用户界面

Flutter 是一个响应式的且伪声明式的 UI 框架,开发者负责提供应用状态与界面状态之间的映射,框架则在运行时将应用状态的更改更新到界面上。在大部分传统的 UI 框架中,界面的初始状态通常会被一次性定义,然后,在运行时根据用户代码分别响应事件进行更新。

Flutter 与其他响应式框架类似,采用了显式剥离基础状态和用户界面的方式,来解决这一问题。你可以通过 React 风格的 API,创建 UI 的描述,让框架负责通过配置优雅地创建和更新用户界面。

在 Flutter 里,widgets(类似于 React 中的组件)是用来配置对象树的不可变类。这些 widgets 会管理单独的布局对象树,接着参与管理合成的布局对象树。 Flutter 的核心就是一套高效的遍历树的变动的机制,它会将对象树转换为更底层的对象树,并在树与树之间传递更改。

build() 是将状态转化为 UI 的方法,widget 通过重写该方法来声明 UI 的构造。build() 方法在框架需要时都可以被调用(每个渲染帧可能会调用一次),从设计角度来看,它应当能够快速执行且没有额外影响的。这样的实现设计依赖于语言的运行时特征(特别是对象的快速实例化和清除)。幸运的是,Dart 非常适合这份工作。

Widgets

应用额如前所述,FLutter强调以widgets作为组成单位。Widgets是构建Flutter应用界面的基础块,每个widget都是一部分不可变的UI声明。

Widgets通过布局组合形成一种层次结构关系。每个Widget都是嵌套在其父级的内部,并可以通过父级接收上下文。从根布局(托管Flutter应用的容器,通常是MaterialApp或者CupertinoApp)开始,自上而下就是这样的结构,如下面实例;

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My Home Page'),
        ),
        body: Center(
          child: Builder(
            builder: (BuildContext context) {
              return Column(
                children: [
                  const Text('Hello World'),
                  const SizedBox(height: 20),
                  ElevatedButton(
                    onPressed: () {
                      print('Click!');
                    },
                    child: const Text('A button'),
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}

在上面的代码中,所有的实例化的类都是widgets。

应用会根据事件交互,通知框架替换层级中的旧的widget为新的widget,最后框架会比较新旧widgets,高效的更新用户界面。

Flutter拥有其自己的UI控制实现,而不是由系统自带的方法进行托管:例如,IOS的Switch控件和Android的选择控件都有一个Dart实现。

这样的实现有几个优势:

  • 提供了诬陷的扩展性。
  • Flutter可以直接合成所有的场景,而无需在Flutter与原生平台之间来回的切换,从而避免了明显的性能瓶颈。
  • 将应用的行为与操作系统的依赖解耦。

组成

Widget通常由更小的且用途单一的widgets组合而成,提供更强大的功能。

在设计的时候,相关的概念设计已尽可能地少量存在,而通过大量的内容进行填充。eg,Flutter在widgets层中使用了相同的概念(一个Widget)来表示屏幕上的绘制、布局(位置和大小)、用户交互、状态管理、主题、动画以及导航。在动画层,Animation和Tween这对概念组合,涵盖了大部分的设计空间。在渲染层,RenderObject用来描述布局、绘制、触摸判断以及可访问性。在这些场景中,最终对于包含的内容都很多:有数百个widgets和Render objects,以及数十种的动画和补间类型。

类的层次结构是有意的浅而广,以最大限度的增加可能的组合数量,重点放在小的,可组合的widget上,确保每个widget都能横好的完成一件事情。核心功能均被抽象,甚至像编剧和对齐这样的基础功能,都被实现为单独的组件,而不是内置于核心中。(这样的实现也与传统的API形成了对比,类似于边距这样的功能通常都内置在了每个组件的公共核心内,Flutter中的widget则不同。)因此,如果你需要讲一个widget居中,预期调整Align这样的属性,不如将他包裹在一个Center widget内。

Flutter中包含了边距,对齐,行,列和网格系列的widgets。这些布局类型的widgets自身没有视觉内容,而只用于控制其他的widgets的部分布局条件。Flutter也包含了以这种组合方法组成的实用性widgets。

例如,一个常用的widget Container,是由几个widget组合而成,包含了布局、绘制、定位和大小的功能。更具体地说,Container是由LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox和Transform组合而成的,你也可以通过查看源码看到这些组合。Flutter 有一个典型的特征,即你可以深入到任意一个 widget,查看其源码。因此,你可以通过同样的方式组合其他的 widgets,也可以参考 Container 来创建其他的 widget,而不需要继承 Container 来实现自定义的效果。

构建widgets

先前提到,可以通过重写build()方法,返回一个新的元素树,来定义视觉展示。这棵树用更为具体的术语表示了widget在UI中的部分。例如,工具栏widget的build方法可能会返回水平布局,其中可能包含了一些文字,各种各样的按钮。根据需要,框架会递归请求每个widget进行构建,直到整棵树都被具体的可渲染的对象描述为止。然后框架会将可渲染的对象缝合在一起,组成可渲染的对象树。

Widget的build方法应该是没有副作用的。每当一个方法要求构建市,widget都应当能返回一个widget的元素树,与先签返回的widget也没有关联。框架会根据渲染对象树来确定哪些构建方法需要被调用,这是一响略显繁重的工作。

每个渲染帧,Flutter都可以根据变换的状态,调用build()方法重建部分UI。因此,保证build方法轻量且能够快速返回widget是非常关键的,繁重的计算工作应该通过一些异步的方法来完成,然后作为构建方法build的一部分存储。

尽管这样的实现看起来不够成熟,但是这样的自动对比方法非常有效,可以实现高性能的交互应用。同时,以这种方式设计的build方法,将重点放在widget组合的声明上,从而简化了代码,而不是以一种状态去更新另一种状态这样的复杂过程。

状态管理

那么,在众多的widget都持有状态的情况下,系统中的状态是如何被传递和管理的呢?

与其他类相同,你可以通过widget的构造函数来初始化数据,如此一来build()方法可以确保子widget使用其所需要的数据进行实例化:

@override
Widget build(BuildContext context) {
   return ContentWidget(importantState);
}

然而,随着widget树层级的逐渐增加加深,依赖树结构上下传递状态信息会变得十分麻烦。这时,第三张类型的widget——InheritedWidget,提供了一种从共享的祖先节点获取数据的简易办法。你可以使用InheritedWidget创建包含状态的widget,该widget会将一个共同的祖先节点包裹在widget树中,如下:

image

现在,当ExamWidget或者GradeWIdget对象需要获取StudentState的数据时,可以直接使用以下方式:

final studentState = StudentState.of(context);

调用of(context)会根据当前构建的上下文(即当前的widge位置的句柄),并返回类型为StudentState的在树中距离最近的祖先节点。InheritedWidget同时也包含了updateShouldNotify()方法,Flutter会调用它来判断依赖了某个状态的widget是否需要更新重建。

InheritedWidget在Flutter中被大量用于共享状态,例如应用的视觉主题,包含了应用于整个应用的颜色和字体样式等属性。MaterialApp的build()方法会在构建市在树中插入一个主题,更生层次的widget便可以使用.of()方法来查找相关的主题数据,例如:

Container(
  color: Theme.of(context).secondaryHeaderColor,
  child: Text(
    'Text with a background color',
    style: Theme.of(context).textTheme.headline6,
  ),
);

类似的,以该方法实现的还有提供了路由页面的Navigator,提供了屏幕信息指标,包括方向,尺寸和高度的MediaQuery等等。

随着应用程序的不断迭代,更高级的状态管理方法变得更加有吸引力,它们可以减少有状态的widget的创建。许多Flutter应用使用了provider用于状态管理,它对InheritedWidget进行了进一步的包装。FLutter的分层架构也允许使用其他的实现来替换状态管理只UI的方案,例如flutter_hooks

渲染和布局

本节介绍Flutter的渲染机制,包括将widget层级结构转换成屏幕上绘制的实际像素的一系列步骤。

Flutter的渲染模型

你可能思考过:既然Flutter是一个跨平台的框架,那么它又是如何提供与原生平台框架相当的性能的呢?

让我们从Android原生应用的角度开始思考。当你在编写绘制的内容的时候,你需要调用Android框架的Java代码。Android的系统库提供了可以将自身绘制到Canvas对象的组件,接下来Android就可以使用由C/C++编写的Skia图形引擎,调用CPU和GPU完成在设备上的绘制。

跨平台框架都会在Android和IOS的UI底层库上创建一层抽象,该抽象层尝试抹平各个系统之间的差异。这时,应用程序的代码通常使用JavaScript等解释型语言来进行编写,这些代码会与基于Java的Android和基于OC的IOS进行交互,最终展示UI界面。所有流程都增加了显著的开销,在UI和应用逻辑有凡在的交互时更为如此。

相比之下,Flutter通过染过系统UI组件库,使用自己的widget内容集,消减了抽象层的开销。用于绘制Flutter图像内容的Dart代码被编译成机器码,并使用Skia进行渲染。Flutter同时也嵌入了自己的Skia副本作文引擎的一部分,让开发者能再设备未更新到最新系统时,也能跟进升级自己的应用,保证稳定性并提升性能。

从用户操作到GPU

对于Flutter渲染机制而言,首要原则就是简单快捷。Flutter为数据流向系统提供了直通的通道,如以下的流程图所示:

Render pipeline sequencing diagram

接下来让我们更加深入了解其中的一些阶段。

构建:从Widget到Element

首先观察以下的代码片段,它代表了一个简单的widget结构:

Container(
  color: Colors.blue,
  child: Row(
    children: [
      Image.network('https://www.example.com/1.png'),
      const Text('A'),
    ],
  ),
);

当Flutter需要绘制这段代码时,框架会调用build()方法,返回一颗基于当前应用状态来绘制UI的widget子树。在这个过程中,build()方法可能会在必要时,根据状态引入新的widget。在上面的例子中,Container的color和child就是电信的例子。我们可以查看Container的源码,会发现当color属性不为空时,ColoredBox会被加入用于颜色布局。

if (color != null)
  current = ColoredBox(color: color!, child: current);

与之对应的,Image和Text在构建过程中也会引入RawImage和RichText。如此一来,最终生成的widget结构比代码表示的层级更深,在该场景中如下图:

Render pipeline sequencing diagram

这就是为什么你在使用Dart DevTools的Flutter inspector调试widget树结构时,会发下实际的结构比你原本代码中的结构更深。

在构建阶段,Flutter会将代码中描述的widgets转化成对应的Element树,每一个Widget都有一个对应的Element。每一个Element代表了梳妆层次结构中特定位置的widget实例。目前有两种Element的基本类型:

  • ComponentElement,其他Element的宿主。
  • RenderObjectElement,参与布局或绘制阶段的Element。
Render pipeline sequencing diagram

RenderObjectElement是底层RenderObject与对应的widget之间的桥梁,我们晚点会介绍。

任何widget都可以通过其BuildContext引用到Element,它是该widget在树中的位置的句柄。类似于Theme.of(context)方法调用中的context,它作为build()方法的参数被传递。

由于widgets以及它上下节点的关系都是不可变的,因此,对widget树做任何操作(例如将Text('A')修改成Text('B'))都会返回一个新的widget对象集合。但是这并不是意味着底层呈现的内容必须要重新构建。Element树每一帧之间都是持久化的,因此起着至关重要的性能作用,Flutter依靠该优势,实现类一种好似widget树被完全抛弃,而缓存了底层表示的机制。Flutter可以根据发生变化的widget,来重建需要重新配置的Element树的部分。

布局和渲染

很少有应用只绘制单个widget。因此,有效的排布widget的结构以及在渲染完成前决定每个Element的大小和位置,是所有UI框架的重点之一。

在渲染树中,每个节点的基类都是RenderObject,该基类为布局和绘制定义了一个抽象的模型。这是再平凡不过的事情:它并不总是一个固定大小,甚至不尊徐笛卡尔坐标系规律。每一个RenderObjectElement都了解其父节点的信息,对于其子节点,除了如何访问和获得他们的布局约束,并没有更多的信息。这样设计让RenderObject拥有高效的抽象能力,能够处理各种各样的使用场景。

在构建阶段,Flutter会为Element树中的每个RenderObjectElement创建或更新其对于的一个从RenderObject继承的对象。RenderObject实际上是原语:渲染文字的RenderParagraph、渲染图片的RenderImage以及在绘制子节点内容前应用变换的RenderTransform是更为上层的实现。

Differences between the widgets hierarchy and the element and render trees

大部分的Flutter widget是由一个继承了RenderBox的子类对象渲染的,他们呈现出的RenderObject会在二维迪卡空间中拥有固定的大小。RenderBox提供了盒子模型限制,为每个widget关联了渲染的最小和最大的宽度和高度。

在进行布局的时候,Flutter会议DFS(深度优先遍历)方式遍历渲染书,并将限制以自上而下的方式从父节点传递给子节点。子节点若要确定自己的大小,则必须遵循父节点传递的限制。子节点的响应方式是在父节点简历的约束内将大效益自下而上的方式传递给父节点。

Constraints go down, sizes go up

在遍历完成一次树之后,每个对象都通过父级约束而拥有了明确的大小,随时可以通过调用paint()进行渲染。

盒子限制模型十分强大,它的对象布局的时间复杂度是O(n):

  • 父节点可以通过设定最大和最小的尺寸限制,决定其子节点对象的大小。例如:在一个手机应用中,最高层级的渲染对象将会限制其子节点的大小为屏幕的尺寸。(子节点可以选择如何占用空间。例如,它们可能在设定的限制中以居中的方式布局。)
  • 父节点可以决定子节点的宽度,而让子节点灵活地自适应布局高度(或决定高度而自适应宽度)。现实中有一种例子就是流式布局的文本,它们常常会填充横向限制,再根据文字内容的多少决定高度。

这样的盒子约束模型,同样也适用于子节点对象需要知道有多少可用空间渲染其内容的场景,通过使用 LayoutBuilder widget,子节点可以得到从上层传递下来的约束,并合理利用该约束对象,使用方法如下:

Widget build(BuildContext context) {
  return LayoutBuilder(
    builder: (context, constraints) {
      if (constraints.maxWidth < 600) {
        return const OneColumnLayout();
      } else {
        return const TwoColumnLayout();
      }
    },
  );
}

所有的RenderObject的根节点是RenderView,代表了渲染树的总体输出。当平台需要渲染新的一帧内容时(例如一个vsync型号或者一个纹理的更新完成),会调用一次compositeFrame()方法,它是RenderView的一部分。该方法会创建一个SceneBuilder来触发当前画面的更新。当画面更新完毕,RenderView会将合成的画面传递给dart:ui中的Window.render()方法,控制GPU进行渲染。

Platform embedding

我们都知道,Flutter 的界面构建、布局、合成和绘制全都由 Flutter 自己完成,而不是转换为对应平台系统的原生组件。获取纹理和联动应用底层的生命周期的方法,不可避免地会根据平台特性而改变。 Flutter 引擎本身是与平台无关的,它提供了一个稳定的 ABI(应用二进制接口),包含一个 平台嵌入层,可以通过其方法设置并使用 Flutter。

平台嵌入层是用于呈现所有 Flutter 内容的原生系统应用,它充当着宿主操作系统和 Flutter 之间的粘合剂的角色。当你启动一个 Flutter 应用时,嵌入层会提供一个入口,初始化 Flutter 引擎,获取 UI 和栅格化线程,创建 Flutter 可以写入的纹理。嵌入层同时负责管理应用的生命周期,包括输入的操作(例如鼠标、键盘和触控)、窗口大小的变化、线程管理和平台消息的传递。 Flutter 拥有 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层,当然,开发者可以创建自定义的嵌入层,正如这个 可用的例子 以 VNC 风格的帧缓冲区支持了远程 Flutter,还有 [支持树莓派运行的例子]https://github.com/ardera/flutter-pi)。

每一个平台都有各自的一套 API 和限制。以下是一些关于平台简短的说明:

  • 在 iOS 和 macOS 上, Flutter 分别通过 UIViewControllerNSViewController 载入到嵌入层。这些嵌入层会创建一个 FlutterEngine,作为 Dart VM 和您的 Flutter 运行时的宿主,还有一个 FlutterViewController,关联对应的 FlutterEngine,传递 UIKit 或者 Cocoa 的输入事件到 Flutter,并将 FlutterEngine 渲染的帧内容通过 Metal 或 OpenGL 进行展示。

  • 在 Android 上,Flutter 默认作为一个 Activity 加载到嵌入层中。此时视图是通过一个 FlutterView 进行控制的,基于 Flutter 内容的合成和 z 排列 (z-ordering) 的要求,将 Flutter 的内容以视图模式或纹理模式进行呈现。

  • 在 Windows 上,Flutter 的宿主是一个传统的 Win32 应用,内容是通过一个将 OpenGL API 调用转换成 DirectX 11 的等价调用的库 ANGLE 进行渲染的。目前正在尝试将 UWP 应用作为 Windows 的一种嵌入层,并将 ANGLE 替换为通过 DirectX 12 直接调用 GPU 的方式。

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

推荐阅读更多精彩内容