[译]Flutter Platform Channels(二)

版本所有,转载请注明出处。

原文地址
配套视频

本文仅供自己学习,公开是为了方便部分朋友共同学习,不喜欢勿喷。

Method channels:标准化信封

method channel.png

Method channels是platform channels的一种,用于调用Dart和Java / Kotlin或Objective-C / Swift中的命名代码段。 方法通道利用标准化消息“信封”来传递从发送方到接收方的方法名称和参数,并区分相关答复中的成功和错误结果。 信封和支持的有效负载由单独的方法编解码器类定义,类似于message channels 如何使用消息编解码器。

Method channels所做的就是:将通道名称与编解码器组合在一起。

特别地,对于在接收到Method channels上的消息时执行什么代码没有做任何假设。 即使消息表示方法调用,你也不必调用方法。 你可以只打开方法名称并为每种情况执行几行代码。

边注。缺乏对方法及其参数的默示或自动绑定可能会令你失望。 那很好,失望也能产生积极的影响。 我想你可以使用注释处理和代码器生成从头开始构建这样的解决方案,或者你可以重用现有RPC框架的一部分。 Flutter是开源的,随时贡献! 如果符合条件,Method channels可以使用代码生成来实现。 同时,它们在“手工模式”中也很有用。

Method channels是Flutter团队对定义可行通信API的挑战的回答,以供当时并不存在的插件生态系统使用。 我们想要一些插件作者可以立即开始使用的东西,而不需要很多样板或复杂的构建设置。 我认为method channel的概念是一个不错的答案,但如果它仍然是唯一的答案,我会感到惊讶。


下面演示是简单情况下你如何从Dart端使用method channel调用一些平台代码。 代码与名称bar相关联,在这种情况下,该名称bar不是方法名称,但可能是。 它所做的就是构造一个问候语字符串并将其返回给调用者,因此我们可以在合理的假设下对平台调用进行编码,这将不会失败(我们将在下面进一步讨论错误处理):

// 简单情况下调用平台方法
// Dart side.
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);
// Android side.
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
  when (call.method) {
    "bar" -> result.success("Hello, ${call.arguments}")
    else -> result.notImplemented()
  }
}
// iOS side.
let channel = FlutterMethodChannel(
  name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
  (call: FlutterMethodCall, result: FlutterResult) -> Void in
  switch (call.method) {
  case "bar": result("Hello, \(call.arguments as! String)")
  default: result(FlutterMethodNotImplemented)
  }
}

通过向switch添加分支条件,我们可以轻松扩展上述内容以处理多种方法。 default子句处理调用未知方法的情况(很可能是由于编程错误)。

上面的Dart代码等效于以下内容:

const codec = StandardMethodCodec();
final ByteData reply = await BinaryMessages.send(
  'foo',
  codec.encodeMethodCall(MethodCall('bar', 'world')),
);
if (reply == null)
  throw MissingPluginException();
else
  print(codec.decodeEnvelope(reply));

MethodChannel在AndroidiOS)上的实现同样是对BinaryMessage的简单封装。 空回复用来表示“未实现”。 这使得接收者并不关心方法的调用在switch-case语句中是否出现贯穿到default中的现象,也不会关心根本没有向通道注册方法调用处理程序。

示例中的参数值是单个字符串string。 但是,默认方法编解码器,恰当地命名为“standard method codec”,使用standard message codec来编码有效负载值。 这意味着前面描述的“类JSON”值都支持作为方法参数和(成功)结果。 特别是,异构列表支持多个参数,而异构映射支持命名参数。 默认参数值为null。 几个例子:

await channel.invokeMethod('bar');
await channel.invokeMethod('bar', <dynamic>['world', 42, pi]);
await channel.invokeMethod('bar', <String, dynamic>{
  name: 'world',
  answer: 42,
  math: pi,
}));

Flutter SDK包含了两种method codec:

  • [StandardMethodCodec](https://docs.flutter.io/flutter/services/StandardMethodCodec-class.html) which by default delegates the encoding of payload values to StandardMessageCodec. Because the latter is extensible, so is the former.
  • [JSONMethodCodec](https://docs.flutter.io/flutter/services/JSONMethodCodec-class.html) which delegates the encoding of payload values to JSONMessageCodec.
  • StandardMethodCodec,默认情况下将有效负载值的编码委托给StandardMessageCodec。 因为后者是可扩展的,前者也是如此。
  • JSONMethodCodec ,它将有效负载值的编码委托给JSONMessageCodec

您可以使用任何方法编解码器配置method channels,包括自定义编解码器。 为了完全理解实现编解码器所涉及的内容,让我们通过使用易错的baz方法扩展上面的示例来查看如何在method channels API级别处理错误:

// Method calls with error handling.
// Dart side.
const channel = MethodChannel('foo');
// Invoke a platform method.
const name = 'bar'; // or 'baz', or 'unknown'
const value = 'world';
try {
  print(await channel.invokeMethod(name, value));
} on PlatformException catch(e) {
  print('$name failed: ${e.message}');
} on MissingPluginException {
  print('$name not implemented');
}
// Receive method invocations from platform and return results.
channel.setMethodCallHandler((MethodCall call) async {
  switch (call.method) {
    case 'bar':
      return 'Hello, ${call.arguments}';
    case 'baz':
      throw PlatformException(code: '400', message: 'This is bad');
    default:
      throw MissingPluginException();
  }
});

// Android side.
val channel = MethodChannel(flutterView, "foo")
// Invoke a Dart method.
val name = "bar" // or "baz", or "unknown"
val value = "world"
channel.invokeMethod(name, value, object: MethodChannel.Result {
  override fun success(result: Any?) {
    Log.i("MSG", "$result")
  }
  override fun error(code: String?, msg: String?, details: Any?) {
    Log.e("MSG", "$name failed: $msg")
  }
  override fun notImplemented() {
    Log.e("MSG", "$name not implemented")
  }
})
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler { call, result ->
  when (call.method) {
    "bar" -> result.success("Hello, ${call.arguments}")
    "baz" -> result.error("400", "This is bad", null)
    else -> result.notImplemented()
  }
}

// iOS side.
let channel = FlutterMethodChannel(
  name: "foo", binaryMessenger: flutterView)
// Invoke a Dart method.
let name = "bar" // or "baz", or "unknown"
let value = "world"
channel.invokeMethod(name, arguments: value) {
  (result: Any?) -> Void in
  if let error = result as? FlutterError {
    os_log("%@ failed: %@", type: .error, name, error.message!)
  } else if FlutterMethodNotImplemented.isEqual(result) {
    os_log("%@ not implemented", type: .error, name)
  } else {
    os_log("%@", type: .info, result as! NSObject)
  }
}
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler {
  (call: FlutterMethodCall, result: FlutterResult) -> Void in
  switch (call.method) {
  case "bar": result("Hello, \(call.arguments as! String)")
  case "baz": result(FlutterError(
    code: "400", message: "This is bad", details: nil))
  default: result(FlutterMethodNotImplemented)
}

错误是由三部分组成的(cod,消息,详细信息),其中code和消息是字符串。 message旨在供人使用,code就是code。 错误详细信息是一些自定义值,通常为null,受编解码器支持的值类型的约束。

要点

异常。 Dart或Android方法调用处理程序中抛出的任何未捕获的异常都会被channel捕获,并记录,并将错误结果返回给调用者。 结果处理程序中抛出的未捕获异常会被记录。

信封编码。 方法编解码器如何对信封细节的编码就像消息编解码器如何将消息转换为字节一样。 例如,方法编解码器可能使list:方法调用可以编码为双元素list[方法名称,参数]; 成功结果作为单元素list[结果]; 错误结果为三元素list[代码,消息,详细信息]。 然后,这种方法编解码器可以简单地通过委托给支持至少list,字符串和null的基础消息编解码器来实现。 方法调用时的参数,成功结果以及错误详细信息将是该消息编解码器支持的任意值。

API differences. The code examples above highlight that method channels deliver results very differently across Dart, Android, and iOS:
API不同。 上面的代码示例突出显示method channels 在Dart,Android和iOS上返回处理结果的方式很不一样:

  • 在Dart方面,调用由返回值为Future的方法处理。 Future在成功的时候返回结果,发现错误的时候会出现PlatformException,在没有实现对应方法的时候会抛出MissingPluginException异常。
  • 在Android上,调用由参数为回调的方法处理。 回调接口定义了三种方法,根据结果调用其中的一种方法。 客户端代码实现回调接口,以定义成功,出错和未实现时应该发生的事情。
  • 在iOS上,调用类似地由采用回调参数的方法处理。 但是在这里,回调是一个单参数函数,它给出了FlutterError实例,FlutterMethodNotImplemented常量,或者,如果成功,则给出调用的结果。 客户端代码根据需要提供具有条件逻辑的块以处理不同的情况。

These differences, mirrored also in the way message call handlers are written, arose as concessions to the styles of the programming languages (Dart, Java, and Objective-C) used for the Flutter SDK method channel implementations. Redoing the implementations in Kotlin and Swift might remove some of the differences, but care must be taken to avoid making it harder to use method channels from Java and Objective-C.

这些差异也反映在消息调用处理程序的编写方式中,这些差异是对在FlutterSDK中实现method channel的编程语言(Dart,Java和Objective-C)的让步。 重做Kotlin和Swift中的实现可能会消除一些差异,但必须注意避免使用Java和Objective-C中的方法通道变得更加困难。

Event channels: 流

image.png

event channel是一个专用平台的通道,用于将平台事件作为Dart流暴露给Flutter的用例。 Flutter SDK目前不支持将Dart流暴露给对应该平台代码,但如果需要,可以构建它。

以下是你在Dart端使用平台事件流的方法:

// Consuming events on the Dart side.
const channel = EventChannel('foo');
channel.receiveBroadcastStream().listen((dynamic event) {
  print('Received event: $event');
}, onError: (dynamic error) {
  print('Received error: ${error.message}');
});

下面的代码显示了如何使用Android上的传感器事件作为示例在对应平台端生成流事件。 主要关注的是确保我们正在监听来自平台源(在这种情况下为传感器管理器)的事件,并在以下情况下通过事件通道发送它们:1)Dart侧至少有一个流监听器和2)activity正在运行。 在单个类中打包必要的逻辑会增加正确执行此操作的可能性:

// Producing sensor events on Android.
// SensorEventListener/EventChannel adapter.
class SensorListener(private val sensorManager: SensorManager) :
  EventChannel.StreamHandler, SensorEventListener {
  private var eventSink: EventChannel.EventSink? = null

  // EventChannel.StreamHandler methods
  override fun onListen(
    arguments: Any?, eventSink: EventChannel.EventSink?) {
    this.eventSink = eventSink
    registerIfActive()
  }
  override fun onCancel(arguments: Any?) {
    unregisterIfActive()
    eventSink = null
  }

  // SensorEventListener methods.
  override fun onSensorChanged(event: SensorEvent) {
    eventSink?.success(event.values)
  }
  override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW)
      eventSink?.error("SENSOR", "Low accuracy detected", null)
  }
  // Lifecycle methods.
  fun registerIfActive() {
    if (eventSink == null) return
    sensorManager.registerListener(
      this,
      sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
      SensorManager.SENSOR_DELAY_NORMAL)
  }
  fun unregisterIfActive() {
    if (eventSink == null) return
    sensorManager.unregisterListener(this)
  }
}
// Use of the above class in an Activity.
class MainActivity: FlutterActivity() {
  var sensorListener: SensorListener? = null

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
    sensorListener = SensorListener(
      getSystemService(Context.SENSOR_SERVICE) as SensorManager)
    val channel = EventChannel(flutterView, "foo")
    channel.setStreamHandler(sensorListener)
  }

  override fun onPause() {
    sensorListener?.unregisterIfActive()
    super.onPause()
  }

  override fun onResume() {
    sensorListener?.registerIfActive()
    super.onResume()
  }
}

如你在你的app中使用了 android.arch.lifecycle包, 你可以通过使用LifecycleObserver来更加方便地管理SensorListener

要点

stream handler的生命周期。 在平台端,stream handler有两个方法onListenonCancel,只要Dart流的侦听器数分别从0变为1和或者从1变为0,就会调用它们。 这可能会发生多次。 stream handler实现应该在调用前者(onListen)时开始将事件倾注到事件接收器中,并在调用后者(onCancel)时停止。 此外,stream handler应该在程序处于非活跃状态时暂停。 上面的代码提供了一个典型示例。在底层上,stream handler当然只是一个二进制消息处理程序,使用事件通道的名称在Flutter视图中注册。

编解码器。 event channel 配置有方法编解码器,允许我们区分成功和失败结果,这和method channel区分成功和失败是一样的。

Stream handler的参数和错误。 Stream Hanlder中onListenonCancel的调用是通过调用method channel的实现的。 因此,我们从Dart到平台的控制方法调用和反向的事件消息都在同一个逻辑通道上。 此设置允许将参数中继到两种控制方法以及要报告的任何错误。 在Dart端,参数(如果有的话)在receiveBroadcastStream的调用中给出。 这意味着它们只被指定一次,无论在流的生命周期中发生的onListen和onCancel的调用次数如何。 返回的任何错误都会被记录。

End of stream. An event sink has an endOfStream method that can be invoked to signal that no additional success or error events will be sent. The null binary message is used for this purpose. On receipt on the Dart side, the stream is closed.

流的终止。 eventSink有个方法叫endOfStream,可以调用该方法以表示不会发送其他成功或错误事件。 为了这个目的实际上是使用了一个空的二进制消息。 在Dart侧收到后,流将关闭。

Life of a stream. The Dart stream is backed by a stream controller fed from the incoming platform channel messages. A binary message handler is registered using the event channel’s name to receive incoming messages only while the stream has listeners.

流的生命周期。 在Dart中,stream由stream controller控制的,其消息来源于平台通道消息。仅当stream有listener的时候,使用envent channel名称的binary message handler才会被注册,以用接收消息。

使用指南

使用域名作为channel名称前缀以确保唯一性

Channel名称只是字符串,但在我们的应用中必须保证所有的channel名称是唯一的,无论channel是出于什么目的。 你可以使用任何合适的命名方案来实现这一点。 但是,插件中为了避免channel重名的推荐方法是使用域名和插件名称前缀,例如some.body.example.com/sensors/foo是用于some.bodyexample.com中开发的名为foo通道sensors插件。 这样做允许插件使用者在他们的应用程序中组合任意数量的插件,而不会有channel名称冲突的风险。

考虑将平台通道视为模块内通信

在分布式系统中调用远程过程调用的代码看起来与使用method channels的代码类似:你调用字符串给出的方法并序列化你的参数和结果。 由于分布式系统组件通常是独立开发和部署的,因此强大的请求和回复检查至关重要,通常在网络两端以检查和日志方式完成。

Platform channels on the other hand glue together three pieces of code that are developed and deployed together, in a single component.

另一方面,在单一组件中,Platform channels将开发的三端代码和部署粘合在一起。

Java/Kotlin ↔ Dart ↔ Objective-C/Swift

实际上,将单独的代码模块打包这样的三元组通常来说是有意义的,例如Flutter插件。 这意味着,对通过method channel调用的参数与结果的检查的是必要性如同在同一模块中使用正常方法时对参数和结果的检查是一样的。

在模块内部,我们主要关心的是防止编程错误,而这些错误超出了编译器的静态检查范围,并且在运行时没有被检测到,直到它们在时间或空间上造成非本地的破坏。 一种合理的编码方便是使用指定类型或断言使假设明确,从而使我们能够快速而干净地失败,例如: 异常。 当然,细节因编程语言而异。 例子:

  • 如果希望通过platform channel传递的数据有一个具体的类型,请立即将该类型分配给它。
  • 如果希望通过平台通道接收到的值是非空(non-null)的,那么可以设置一些参数使其立即取消引用,或者在存储数据之前断言它是非空的。 根据你的编程语言,你可以将其分配给非可空类型的变量。

两个简单的例子:

// Dart: 我们期望接收到一个非空的整型list。
for (final int n in await channel.invokeMethod('getFib', 100)) {
  print(n * n);
}
// Android: 我们期望异步地来处理的非空的name和age参数,用键为字符串的map来交付。
channel.setMethodCallHandler { call, result ->
  when (call.method) {
    "bar" -> {
      val name : String = call.argument("name")
      val age : Int = call.argument("age")
      process(name, age, result)
    }
    else -> result.notImplemented()
  }
}
:
fun process(name: String, age: Int, result: Result) { ... }

Android代码利用MethodCall的泛型<T> T argument(String key)方法,该方法在参数中查找键(假设为map),并将找到的值转换为目标(调用者)类型。如果由于任何原因失败,则抛出适当的异常。从method call handler抛出时,它将被记录下来,并将错误结果发送到Dart端。

不要 mock platform channels

(Pun intended.) When writing unit tests for Dart code that uses platform channels, a knee jerk reaction may be to mock the channel object, as you would a network connection.

(双关语)。当为使用platform channels的Dart代码编写单元测试时,一个下意识的反应可能是模拟channel对象,就像模拟网络连接一样。

You can certainly do that, but channel objects don’t actually need to be mocked to play nicely with unit tests. Instead, you can register mock message or method handlers to play the role of the platform during a particular test. Here is a unit test of a function hello that is supposed to invoke the bar method on channel foo:

你当然可以这样做,但实际上channel对象不需要为了迎合单元测试被模拟。 相反,你可以注册模拟消息或method handlers,以在特定测试期间扮演平台的角色。 这是一个名为hello的函数的单元测试,它应该在名为foo的channel上调用bar方法:

test('gets greeting from platform', () async {
  const channel = MethodChannel('foo');
  channel.setMockMethodCallHandler((MethodCall call) async {
    if (call.method == 'bar')
      return 'Hello, ${call.arguments}';
    throw MissingPluginException();
  });
  expect(await hello('world'), 'Platform says: Hello, world');
});

To test code that sets up message or method handlers, you can synthesize incoming messages using BinaryMessages.handlePlatformMessage. At present, this method is not mirrored on platform channels, though that could easily be done as indicated in the code below. The code defines a unit test of a class Hello that is supposed to collect incoming arguments of calls to method bar on channel foo, while returning greetings:

要测试设置消息或 method handlers的代码,可以使用BinaryMessages.handlePlatformMessage合成传入消息。目前,这个方法在platform channels上还没有镜像,不过可以像下面的代码中所示的那样轻松地实现。这段代码定义了一个类名为Hello的单元测试,它应该收集名在为foo的chnnael上名为bar方法的传入参数,同时返回greeting:

test('collects incoming arguments', () async {
  const channel = MethodChannel('foo');
  final hello = Hello();
  final String result = await handleMockCall(
    channel,
    MethodCall('bar', 'world'),
  );
  expect(result, contains('Hello, world'));
  expect(hello.collectedArguments, contains('world'));
});
// Could be made an instance method on class MethodChannel.
Future<dynamic> handleMockCall(
  MethodChannel channel,
  MethodCall call,
) async {
  dynamic result;
  await BinaryMessages.handlePlatformMessage(
    channel.name,
    channel.codec.encodeMethodCall(call),
    (ByteData reply) {
      if (reply == null)
        throw MissingPluginException();
      result = channel.codec.decodeEnvelope(reply);
    },
  );
  return result;
}

上面的两个例子都在单元测试中声明了channel对象。这工作得很好——除非你担心重复的通道名称和编解码器——因为所有具有相同名称和编解码器的通道对象都是等价的。你可以通过将channel声明为const,使其对生产代码和测试都可见,从而避免重复。

你不需要的是提供一种将模拟通道注入生产代码的方法。

考虑对平台交互进行自动化测试

Platform channels非常简单,但是通过由单独的Java / Kotlin和Objective-C / Swift实现支持的自定义Dart API从Flutter UI获取所有内容确实需要一些小心。 在实际操作中,保持设置正常运行将需要自动化测试以防止回归。 单独使用单元测试无法实现这一点,因为你需要一个运行 platform channels 的真实应用程序来实际与平台通信。

Flutter comes with the flutter_driver integration test framework that allows you to test Flutter applications running on real devices and emulators. But flutter_driver is not currently integrated with other frameworks to enable testing across Flutter and platform components. I am confident this is one area where Flutter will improve in the future.

Flutter附带了flutter_driver集成测试框架,允许你在真实设备和模拟器上测试运行的Flutter应用程序。但是,flutter_driver目前还没有与其他框架集成,以支持跨Flutter 和平台组件进行测试。我相信这是Flutter 在未来将得到改善的一个领域。

在某些情况下,你可以按原样使用flutter_driver来测试平台通道使用情况。 这要求你的Flutter用户界面可用于触发任何平台交互,然后以足够的细节进行更新,以使你的测试能够确定交互的结果。

如果你不处于这种情况,或者你将你的platform channel打包为flutter插件,并且你需要一个模块进行测试,那么你可以编写一个简单的Flutter应用程序用于测试。 该应用程序应具有上述特征,然后可以使用flutter_driver执行。 你会在Flutter GitHub(https://github.com/flutter/flutter/tree/master/dev/integration_tests/platform_interaction)中找到一个例子。

保持平台端准备好接收同步调用

platform channel只能是异步的。 但是有很多平台API却需要同步调用宿主程序的组件,询问信息或提供帮助或提供选择窗口。 一个例子是Android上的Activity.onSaveInstanceState。 同步意味着必须在即将到来的调用返回之前完成所有操作。 现在,你可能希望在此类处理中包含来自Dart端的信息,但是一时当主UI线程上的同步调用已经处于活动状态时,开始发送异步消息就已经来不及了。

The approach used by Flutter, most notably for semantics/accessibility information, is to proactively send updated (or updates to) information to the platform side whenever the information changes on the Dart side. Then, when the synchronous call arrives, the information from the Dart side is already present and available to platform side code.
Flutter使用的方法,尤其是语义/可访问性( semantics/accessibility )信息,是在Dart端信息发生变化时主动向平台端发送更新(或更新)信息。 然后,当进行同步调用时,来自Dart的信息已经存在并且对平台代码可用。

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

推荐阅读更多精彩内容