Flutter & Native 混合开发

Flutter & Native 混合开发

项目集成方案

问题

  1. 项目中仍有大量业务使用 Native 开发。项目引入 Flutter 后,要求所有 Native 开发者都配置 Flutter 开发环境并改动项目工程结构,会对开发效率造成影响。
  2. 引入 Flutter 后,混合项目的构建流程会发生变动。打包环境需要配置 Flutter 开发环境,且 Native 项目无法单独构建。

需求

  1. 独立开发环境
  2. 独立构建

Flutter 项目模式

Flutter 项目模式
  1. Flutter 模式就是纯 Flutter 开发模式,原生项目被包在 Flutter 项目中,通过 Flutter 命令启动项目,命令会先编译 Flutter 工程,然后将 Flutter 编译产物复制到 Native 项目中,接着编译 Native ,最后启动 Native ,Native 调用 Flutter 。

  2. module 模式,Flutter 工程只负责产出 Flutter 编译产物,提供给 Native 工程使用。Flutter 和 Native 工程没有代码和环境依赖,Native 工程只依赖 Flutter 的编译产物。Flutter 工程和 Native 工程可以独立编译。

我们通过 flutter create 命令创建新的 Flutter 工程。这个命令有一个 --type 参数,通过传入 module 这个参数 , 命令会为我们创建一个 module 类型的 Flutter 项目。它和普通的 Flutter 项目只有些许不同,我们可以通过项目结构窥探一二。

module 模式

Flutter 模式

从上面的图片可知,Flutter 项目模式和 module 模式在项目结构和文件上差不多,但在 iOS 和 Android 这两个和 Native 相关的文件夹上,Flutter 项目是直接将两个文件夹暴露出来,而 module 模式则是选择隐藏这两个 Native 文件夹。很显然,对于 Flutter 项目而言,Native 项目直接包在了 Flutter 项目中,必然需要在其中直接进行 Native 页面和逻辑的开发。而 module 模式中,整个 Flutter 工程是和 Native 工程隔离开的,原则上不应该有 Native 的逻辑代码,因而隐藏了 Native 相关文件,不让用户有机会触碰或者添加 Native 代码。

既然 module 模式和 Native 项目完全隔离, 为什么还要保留一个隐藏的 Native 的项目呢?因为在 Native 平台上,Flutter 无法独立运行, 始终需要依附于一个 Native 项目才能够启动。因此为了在 module 模式下运行 App 进行调试,必定需要一个 Native App 的空壳来调起 Flutter 页面。

Native 调用 Flutter

要理解 module 模式如何工作,我们首先需要了解,一个纯 Flutter 工程是如何被 Native App 引用并调用的。

Flutter for iOS 的产物:

  1. App.framework:Dart 业务源码相关文件,以及项目依赖的静态资源,如字体,图片等
  2. Flutter.framework:Flutter 引擎库文件
  3. pubs 插件目录及用于索引的文件:Flutter 下的插件,包括各种系统的和自定义的channels

iOS Native 项目通过引入 Flutter.framework ,便可以调用 Flutter 项目中的代码,将 Flutter 嵌入到项目中。

Flutter for Android 的产物则是一个 flutter.aar,其中同样包含了 Flutter 引擎库以及项目中用到的静态资源等。Android 工程通过引入 flutter.aar ,便可以调用 Flutter 项目中的代码。

从上面的实践可知,只要我们取得了 Flutter for Native 的编译产物,便可以轻松的将 Flutter 工程嵌入到现有的 Native 项目中。其中,iOS 相对简单,直接将 Flutter 编译出的几个产物(Framwork、配置文件) 拖入 Native 项目中即可调用。安卓则需要修改 Native 工程中的 gradle,添加 aar 中的 Flutter 依赖。

Native 接入 Flutter 实践

目录结构如下

some/path/ flutter_host_android/ flutter_host_ios/ flutter_module/

保持 Native 和 Flutter 工程入口在同一目录下,三个工程各自由自己的 Git 进行版本管理。

flutter_module 目录下包含了 Flutter 开发人员编写的 Flutter 代码和 Flutter 的编译产物。对于 Native 工程而言,他们不关心其中的 Flutter 代码,他们只需要将资源定位到其中的编译产物,然后引入项目即可。

Android 引入 Flutter

在工程 settings.gradle 文件中添加

//增加内容

setBinding(new Binding([gradle: this]))

evaluate(new File( settingsDir.parentFile,'flutter_module/.android/include_flutter.groovy' ))

在工程 build.gradle 文件中添加

dependencies {
.
.
implementation project(':flutter')
}

当然配置完 gradle 后不要忘记同步。

需要注意的是,Flutter 开发人员在编写完代码提交到 Git 之前,需要执行一次完整的 Flutter 项目,以便生成最新的编译产物,否则 Native 端在调用 Flutter 时不会执行最新的代码。

当然,结偶性更好的方案是,只给 Native 开发人员提供 Flutter 产物,甚至使用 Cocoapods 这类包管理工具进行自动导入和版本控制。大家可以根据自己的需求来实现具体的工程配置。

Flutter & Native 交互

Flutter 和 Native 之间的交互以及数据交换传输,依赖于一种称为 Platform Channel 的工具。

Platform Channel

PlatformChannels.png

Platform Channel 主要做了三件事:

  1. 发送消息
  2. 监听发送过来的消息
  3. 对不同平台的数据类型进行自动转换

Flutter 定义了三种不同类型的 Channel :

  • BasicMessageChannel:用于传递字符串和半结构化的信息。
  • MethodChannel:用于传递方法调用(method invocation)。
  • EventChannel: 用于数据流(event streams)的通信。

三种 Channel 看似复杂,而且他们都有各自的使用场景,但他们在设计上却非常相近。

每种 Channel 均有三个重要成员变量:

  1. name: String 类型,代表 Channel 的名字,也是其唯一标识符。
  2. messager:BinaryMessenger 类型,代表消息信使,是消息的发送与接收的工具。
  3. codec: MessageCodec 类型或 MethodCodec 类型,代表消息的编解码器。

Channel name

一个 Flutter 应用中可能存在多个 Channel ,每个 Channel 在创建时必须指定一个独一无二的 name ,Channel 之间使用 name 来区分彼此。当有消息从 Flutter 端发送到 Platform 端时,会根据其传递过来的 channel name 找到该 Channel 对应的 Handler(消息处理器)。

BinaryMessenger 消息信使

1*BIknuDE2gMHmbYg7S8_F0Q.png

虽然三种 Channel 用途不同,但是他们与 Flutter 通信的工具却是相同的,均为 BinaryMessager 。

BinaryMessenger 是 Platform 端与 Flutter 端通信的工具,其通信使用的消息格式为二进制格式数据。当我们初始化一个 Channel ,并向该 Channel 注册处理消息的 Handler 时,实际上会生成一个与之对应的 BinaryMessageHandler ,并以 channel name 为 key ,注册到 BinaryMessenger 中。当Flutter端发送消息到 BinaryMessenger 时,BinaryMessenger 会根据其入参 channel 找到对应的 BinaryMessageHandler ,并交由其处理。

Binarymessenger 在 Android 端是一个接口,其具体实现为 FlutterNativeView 。而其在 iOS 端是一个协议,名称为 FlutterBinaryMessenger ,FlutterViewController 遵循了它。

Binarymessenger 并不知道 Channel 的存在,它只和 BinaryMessageHandler 打交道。而 Channel 和 BinaryMessageHandler 则是一一对应的。由于 Channel 从 BinaryMessageHandler 接收到的消息是二进制格式数据,无法直接使用,故 Channel 会将该二进制消息通过 Codec(消息编解码器)解码为能识别的消息并传递给 Handler 进行处理。

当 Handler 处理完消息之后,会通过回调函数返回 result ,并将 result 通过编解码器编码为二进制格式数据,通过 BinaryMessenger 发送回 Flutter 端。

消息编解码器:Codec

1-20190813180321800.png

消息编解码器 Codec 主要用于将二进制格式的数据转化为 Handler 能够识别的数据,Flutter 定义了两种 Codec :MessageCodec 和 MethodCodec 。

MessageCodec

MessageCodec 用于二进制格式数据与基础数据之间的编解码。BasicMessageChannel 所使用的编解码器就是MessageCodec。

Android 中,MessageCodec 是一个接口,定义了两个方法:encodeMessage接收一个特定的数据类型 T,并将其编码为二进制数据 ByteBuffer ,而decodeMessage则接收二进制数据 ByteBuffer ,将其解码为特定数据类型 T。iOS 中,其名称为 FlutterMessageCodec ,是一个协议,定义了两个方法:encode接收一个类型为 id 的消息,将其编码为 NSData 类型,而decode接收 NSData 类型消息,将其解码为 id 类型数据。

MessageCodec 有多种不同的实现:

StandardMessageCodec

StandardMessageCodec 是 BasicMessageChannel 的默认编解码器,其支持基础数据类型、二进制数据、列表、字典。

BinaryCodec

BinaryCodec 是最为简单的一种 Codec ,因为其返回值类型和入参的类型相同,均为二进制格式( Android 中为 ByteBuffer ,iOS 中为 NSData )。实际上,BinaryCodec 在编解码过程中什么都没做,只是原封不动将二进制数据消息返回而已。或许你会因此觉得 BinaryCodec 没有意义,但是在某些情况下它非常有用,比如使用 BinaryCodec 可以使传递内存数据块时在编解码阶段免于内存拷贝。

StringCodec

StringCodec 用于字符串与二进制数据之间的编解码,其编码格式为UTF-8。

JSONMessageCodec

JSONMessageCodec 用于基础数据与二进制数据之间的编解码,其支持基础数据类型以及列表、字典。其在 iOS 端使用了 NSJSONSerialization 作为序列化的工具,而在 Android 端则使用了其自定义的 JSONUtil 与 StringCodec 作为序列化工具。

MethodCodec

MethodCodec 用于二进制数据与方法调用( MethodCall )和返回结果之间的编解码。MethodChannel 和 EventChannel 所使用的编解码器均为 MethodCodec 。

与 MessageCodec 不同的是,MethodCodec 用于 MethodCall 对象的编解码,一个 MethodCall 对象代表一次从 Flutter 端发起的方法调用。MethodCall 有2个成员变量:String 类型的method代表需要调用的方法名称,通用类型( Android 中为 Object ,iOS 中为 id )的arguments代表需要调用的方法入参。

由于处理的是方法调用,故相比于 MessageCodec ,MethodCodec 多了对调用结果的处理。当方法调用成功时,使用encodeSuccessEnvelope将 result 编码为二进制数据,而当方法调用失败时,则使用encodeErrorEnvelope将 error 的 code 、message 、detail 编码为二进制数据。

MethodCodec 有两种实现:

StandardMethodCodec

MethodCodec 的默认实现,StandardMethodCodec 的编解码依赖于 StandardMessageCodec ,当其编码 MethodCall 时,会将 method 和 args 依次使用 StandardMessageCodec 编码,写入二进制数据容器。其在编码方法的调用结果时,若调用成功,会先向二进制数据容器写入数值0(代表调用成功),再写入 StandardMessageCodec 编码后的 result 。而调用失败,则先向容器写入数据1(代表调用失败),再依次写入 StandardMessageCodec 编码后的 code ,message 和 detail 。

JSONMethodCodec

JSONMethodCodec 的编解码依赖于 JSONMessageCodec ,当其在编码 MethodCall 时,会先将 MethodCall 转化为字典{"method":method,"args":args}。其在编码调用结果时,会将其转化为一个数组,调用成功为[result],调用失败为[code,message,detail]。再使用 JSONMessageCodec 将字典或数组转化为二进制数据。

消息处理器:Handler

当我们接收二进制格式消息并使用 Codec 将其解码为 Handler 能处理的消息后,就该 Handler 上场了。Flutter 定义了三种类型的 Handler ,与 Channel 类型一一对应。我们向 Channel 注册一个 Handler 时,实际上就是向 BinaryMessager 注册一个与之对应的 BinaryMessageHandler 。当消息派分到 BinaryMessageHandler 后,Channel 会通过 Codec 将消息解码,并传递给 Handler 处理。

MessageHandler

MessageHandler 用户处理字符串或者半结构化的消息,其onMessage方法接收一个 T 类型的消息,并异步返回一个相同类型 result 。MessageHandler 的功能比较基础,使用场景较少,但是其配合 BinaryCodec 使用时,能够方便传递二进制数据消息。

MethodHandler

MethodHandler 用于处理方法的调用,其onMessage方法接收一个 MethodCall 类型消息,并根据 MethodCall 的成员变量method去调用对应的 API ,当处理完成后,根据方法调用成功或失败,返回对应的结果。

StreamHandler

1-20190813181928257.png

StreamHandler 与前两者稍显不同,用于事件流的通信,最为常见的用途就是 Platform 端向 Flutter 端发送事件消息。当我们实现一个 StreamHandler 时,需要实现其onListenonCancel方法。而在onListen方法的入参中,有一个 EventSink(其在 Android 是一个对象,iOS 端则是一个 block )。我们持有 EventSink 后,即可通过 EventSink 向 Flutter 端发送事件消息。

实际上,StreamHandler 工作原理并不复杂。当我们注册了一个 StreamHandler 后,实际上会注册一个对应的 BinaryMessageHandler 到 BinaryMessager 。而当 Flutter 端开始监听事件时,会发送一个二进制消息到 Platform 端。Platform 端用 MethodCodec 将该消息解码为 MethodCall ,如果 MethodCall 的 method 的值为 "listen" ,则调用 StreamHandler 的onListen方法,传递给 StreamHandler 一个 EventSink 。而通过 EventSink 向 Flutter 端发送消息时,实际上就是通过 BinaryMessager 的 send 方法将消息传递过去。

截屏2019-08-1515.08.19.png

Flutter & Native 交互实操

BasicMessageChannel

Flutter

static const messageChannel = const BasicMessageChannel('samples.flutter.io/message', StandardMessageCodec());
static const messageChannel2 = const BasicMessageChannel('samples.flutter.io/message2', StandardMessageCodec());

Future<String> sendMessage() async { 
    String reply = await messageChannel.send('发送给Native端的数据'); 
    print('reply: $reply'); return reply; 
} 

void receiveMessage() { 
    messageChannel2.setMessageHandler((message) async { 
        print('message: $message'); 
        return '返回Native端的数据'; 
    }); 
}

@override void initState() { 
    // TODO:
  implement initState super.initState(); 
  receiveMessage(); 
  sendMessage(); 
}

iOS

// 初始化定义
FlutterBasicMessageChannel* messageChannel = [FlutterBasicMessageChannel messageChannelWithName:@"samples.flutter.io/message" binaryMessenger:controller];

// 接收消息监听
[messageChannel setMessageHandler:^(id message, FlutterReply callback) {
    NSLog(message);
    callback(@"返回flutter端的数据");
}];

// 触发事件执行

FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
FlutterBasicMessageChannel* messageChannel2 = [FlutterBasicMessageChannel messageChannelWithName:@"samples.flutter.io/message2" binaryMessenger:controller];

// 发送消息
[messageChannel2 sendMessage:(@"发送给flutter的数据") reply:^(id reply) {
    NSLog(reply);
}];

Android

BasicMessageChannel<Object> messageChannel = new BasicMessageChannel<Object>(getFlutterView(), "samples.flutter.io/message", StandardMessageCodec.INSTANCE);

// 接收消息监听
messageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<Object>() {
    @Override
    public void onMessage(Object o, BasicMessageChannel.Reply<Object> reply) {
        System.out.println("onMessage: " + o);
        reply.reply("返回给flutter的数据");
    }
});

// 触发事件执行

BasicMessageChannel<Object> messageChannel2 = new BasicMessageChannel<Object>(getFlutterView(), "samples.flutter.io/message2", StandardMessageCodec.INSTANCE);

// 发送消息
messageChannel2.send("发送给flutter的数据", new BasicMessageChannel.Reply<Object>() {
    @Override
    public void reply(Object o) {

        System.out.println("onReply: " + o);
    }
});

MethodChannel

Flutter

 static const platform = const MethodChannel('samples.flutter.io/battery');
 
 Future<Null> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }
  
  // 执行_getBatteryLevel方法

iOS

FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
    
FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
                                        methodChannelWithName:@"samples.flutter.io/battery"
                                        binaryMessenger:controller];



[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    // TODO
    if ([@"getBatteryLevel" isEqualToString:call.method]) {
        int batteryLevel = [self getBatteryLevel];
        
        if (batteryLevel == -1) {

            result([FlutterError errorWithCode:@"UNAVAILABLE"
                                       message:@"Battery info unavailable"
                                       details:nil]);
        } else {
            result(@(batteryLevel));
        }
    } else {
        result(FlutterMethodNotImplemented);
    }
}];

- (int)getBatteryLevel {
    UIDevice* device = UIDevice.currentDevice;
    device.batteryMonitoringEnabled = YES;
    
    if (device.batteryState == UIDeviceBatteryStateUnknown) {
        return -1;
    } else {
        return (int)(device.batteryLevel * 100);
    }
}

Android

new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
    new MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall call, Result result) {
            // TODO
            if (call.method.equals("getBatteryLevel")) {
                int batteryLevel = getBatteryLevel();

                if (batteryLevel != -1) {
                    result.success(batteryLevel);
                } else {
                    result.error("UNAVAILABLE", "Battery level not available.", null);
                }
            } else {
                result.notImplemented();
            }
        }
    }
);


private int getBatteryLevel() {
    int batteryLevel = -1;
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
        BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
        batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
    } else {
        Intent intent = new ContextWrapper(getApplicationContext()).
                registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
                intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
    }

    return batteryLevel;
}

FlutterEventChannel

Flutter

static const EventChannel _eventChannel = const EventChannel('samples.flutter.io/test');

 void _onEvent(Object event) {
    print('返回的内容: $event');
  }

  void _onError(Object error) {
    print('返回的错误');
  }
  
  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    // 监听开始
    _eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
  }

iOS

FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController;
    
FlutterEventChannel* eventChannel = [FlutterEventChannel eventChannelWithName:@"samples.flutter.io/test" binaryMessenger:controller];
[eventChannel setStreamHandler:self];


FlutterEventSink     eventSink;
// // 这个onListen是Flutter端开始监听这个channel时的回调,第二个参数 EventSink是用来传数据的载体。
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)events {
    eventSink = events;
    // arguments flutter给native的参数
    // 回调给flutter, 建议使用实例指向,因为该block可以使用多次
    if (events) {
        events(@"主动发送通知到flutter");
    }

    // 监听电池状态
    [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(onBatteryStateDidChange:)
                                               name:UIDeviceBatteryStateDidChangeNotification
                                             object:nil];
    return nil;
}

/// flutter不再接收
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
    // arguments flutter给native的参数
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    eventSink = nil;
    return nil;
}

- (void)onBatteryStateDidChange:(NSNotification*)notification {
    if (eventSink == nil) return;
    UIDeviceBatteryState state = [[UIDevice currentDevice] batteryState];
    switch (state) {
            case UIDeviceBatteryStateFull:
            case UIDeviceBatteryStateCharging:
            eventSink(@"charging");
            break;
            case UIDeviceBatteryStateUnplugged:
            eventSink(@"discharging");
            break;
        default:
            eventSink([FlutterError errorWithCode:@"UNAVAILABLE"
                                           message:@"Charging status unavailable"
                                           details:nil]);
            break;
    }
}

Android

new EventChannel(getFlutterView(), CHANNEL2).setStreamHandler(
    new EventChannel.StreamHandler() {
        @Override
        public void onListen(Object o, EventChannel.EventSink eventSink) {
            this.eventSink = eventSink;

            handler.sendEmptyMessageDelayed(1, 1000);
        }

        @Override
        public void onCancel(Object o) {

        }

        private EventChannel.EventSink eventSink;
        private int count = 0;
        private Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                eventSink.success((count++) + "主动发送消息给flutter");
//              handler.sendEmptyMessageDelayed(1,1000);
            }
        };

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

推荐阅读更多精彩内容