Flutter Plugin 的編寫使用

Flutter Plugin 資料

[TOC]

前言

在Flutter中,如果我們需要打印日志,如果不進行自定義,我們只能使用自帶的print()或者debugPrint()方法進行打印,
但是這兩種打印,日志都是默認Info層級的日志,很不友好,所以如果需要日志打印層級分明,我們就需要自定義一個
日志打印組件,但是我希望這個打日志的組件也可以以后在其他項目里直接拿來使用.這就需要我們來開發一個日志的插件了,
再比如我們想在Flutter里面獲取Android設備的信息,或者就是想用Native實現一個功能,然后能在Flutter里面使用. 等等...

什么是插件

在flutter中,一個插件叫做一個package,使用packages的目的就是為了達到模塊化,可以創建出可被復用和共享的代碼,
這和大多數編程語言中的模塊、包的概念相同。創建出來的package可以在pubspec.yaml中直接依賴。

一個最小化的package包含了兩部分:

  • 一個pubspec.yaml文件:一個元數據文件,聲明了聲明了package的名稱、版本、作者等信息。

  • 一個lib文件夾:包含里package的公開代碼,文件夾至少需要存在<pakcage-name>.dart這個文件。

注意:<pakcage-name>.dart這個文件必須存在,因為這是方便使用的人快速import這個package來使用它,可以把它理解成一種必須要遵守的規則。

package的種類

package可以分為兩種:純dart代碼的package和帶有特定平臺代碼的package。

  • Dart packages:這是一個只有dart代碼的package,里面包含了flutter的特定功能,所以它依賴于flutter的framework,也決定了它只能用在flutter上。
  • plugin packages:這是一個既包含了dart代碼編寫的api,又包含了平臺(Android/IOS)特定實現的package,可以被Android和ios調用。

上面應該很好理解,可以理解成java jar包和Android sdk的區別。而要開發的日志插件就是第二種。

開發步驟

開發 Dart packages

要創建Dart包,請使用--template=package 來執行 flutter create

flutter create --template=package hello

這將在hello/文件夾下創建一個具有以下專用內容的package工程:

  • lib/hello.dart:
    • Package的Dart代碼
  • test/hello_test.dart:
    • Package的單元測試代碼.
  • 實現package
    • 對于純Dart包,只需在主lib/<package name>.dart文件內或lib目錄中的文件中添加功能 。
    • 要測試軟件包,請在test目錄中添加unit tests。

開發 plugin packages

Step 1: 創建 package

  • 要創建插件包,請使用--template=plugin參數執行flutter create

  • 使用--org選項指定您的組織,并使用反向域名表示法。該值用于生成的Android和iOS代碼中的各種包和包標識符。

flutter create --org com.example --template=plugin hello

  • 這將在hello/文件夾下創建一個具有以下專用內容的插件工程:

  • lib/hello.dart:

    • 插件包的Dart API.
  • android/src/main/java/com/yourcompany/?hello/HelloPlugin.java:

    • 插件包API的Android實現.
  • ios/Classes/HelloPlugin.m:

    • 插件包API的ios實現.
  • example/:

    • 一個依賴于該插件的Flutter應用程序,來說明如何使用它
      默認情況下,插件項目針對iOS代碼使用Objective-C,Android代碼使用Java。如果您更喜歡Swift或Kotlin,則可以使用-i 或 -a 為iOS或Android指定語言。例如:

flutter create --template=plugin -i swift -a kotlin hello

Step 2: 實現包 package

  • 由于插件包中包含用多種編程語言編寫的多個平臺的代碼,因此需要一些特定的步驟來確保順暢的體驗。

  • Step 2a: 定義包API(.dart)
    插件包的API在Dart代碼中定義。打開主文件夾hello/ 。找到lib/hello.dart

  • Step 2b: 添加Android平臺代碼(.java / .kt)
    我們建議您使用Android Studio編輯Android代碼。

在Android Studio中編輯Android平臺代碼之前,首先確保代碼至少已經構建過一次(例如,從IntelliJ運行示例應用程序或在終端執行cd hello/example; flutter build apk)

  1. 啟動Android Studio
  2. 在’Welcome to Android Studio’對話框選擇 ‘Import project’, 或者在菜單欄 ‘File > New > Import Project…‘,然后選擇hello/example/android/build.gradle文件.
  3. 在’Gradle Sync’ 對話框, 選擇 ‘OK’.
  4. 在’Android Gradle Plugin Update’ 對話框, 選擇 ‘Don’t remind me again for this project’.
    您插件的Android平臺代碼位于 hello/java/com.yourcompany.hello/?HelloPlugin.

您可以通過按下 ? 按鈕從Android Studio運行示例應用程序.

  • Step 2d: 連接API和平臺代碼
    最后,您需要將用Dart代碼編寫的API與平臺特定的實現連接起來。這是通過platform channels完成的。

添加文檔

建議將以下文檔添加到所有軟件包:

  1. README.md:介紹包的文件
  2. CHANGELOG.md 記錄每個版本中的更改
  3. LICENSE 包含軟件包許可條款的文件

發布 packages

一旦你實現了一個包,你可以在Pub上發布它 ,這樣其他開發人員就可以輕松使用它

在發布之前,檢查pubspec.yamlREADME.md以及CHANGELOG.md文件,以確保其內容的完整性和正確性。

然后, 運行 dry-run 命令以查看是否都準備OK了:

flutter packages pub publish --dry-run

最后, 運行發布命令:

flutter packages pub publish

Plugin 通信原理

  • 在介紹Plugin前,我們先簡單了解一下Flutter:
flutter.png
  • Flutter框架包括:Framework和Engine,他們運行在各自的Platform上。
  • Framework是Dart語言開發的,包括Material Design風格的Widgets和Cupertino(iOS-style)風格的Widgets,以及文本、圖片、按鈕等基礎Widgets;還包括渲染、動畫、繪制、手勢等基礎能力。
  • Engine是C++實現的,包括Skia(二維圖形庫);Dart VM(Dart Runtime);Text(文本渲染)等。

實際上,Flutter的上層能力都是Engine提供的。Flutter正是通過Engine將各個Platform的差異化抹平。而我們今天要講的Plugin,正是通過Engine提供的Platform Channel實現的通信。

  • Plugin其實就是一個特殊的Package。Flutter Plugin提供Android或者iOS的底層封裝,在Flutter層提供組件功能,使Flutter可以較
    方便的調取Native的模塊。很多平臺相關性或者對于Flutter實現起來比較復雜的部分,都可以封裝成Plugin。其原理如下
flutter_channel.png

通過上圖,我們看到Flutter App是通過Plugin創建的Platform Channel調用的Native APIs。

Platform Channel:

  1. Flutter App (Client),通過MethodChannel類向Platform發送調用消息;
  2. Android Platform (Host),通過MethodChannel類接收調用消息;
  3. iOS Platform (Host),通過FlutterMethodChannel類接收調用消息。

PS:消息編解碼器,是JSON格式的二進制序列化,所以調用方法的參數類型必須是可JSON序列化的。
PS:方法調用,也可以反向發送調用消息。

  • Android Platform

    FlutterActivity,是Android的Plugin管理器,它記錄了所有的Plugin,并將Plugin綁定到FlutterView。

理解Platform Channel工作原理

  • Flutter定義了三種不同類型的Channel,它們分別是

  • BasicMessageChannel:用于傳遞字符串和半結構化的信息。

  • MethodChannel:用于傳遞方法調用(method invocation)。

  • EventChannel: 用于數據流(event streams)的通信。

三種Channel之間互相獨立,各有用途,但它們在設計上卻非常相近。每種Channel均有三個重要成員變量:

  • name: String類型,代表Channel的名字,也是其唯一標識符。
  • messager:BinaryMessenger類型,代表消息信使,是消息的發送與接收的工具。
  • codec: MessageCodec類型或MethodCodec類型,代表消息的編解碼器。

Channel name

? 一個Flutter應用中可能存在多個Channel,每個Channel在創建時必須指定一個獨一無二的name,Channel之間使用name來區分彼此。當有消息從Flutter端發送到Platform端時,會根據其傳遞過來的channel name找到該Channel對應的Handler(消息處理器)。

消息信使:BinaryMessenger

binary_messenger.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

codec.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有多種不同的實現:

BinaryCodec

BinaryCodec是最為簡單的一種Codec,因為其返回值類型和入參的類型相同,均為二進制格式(Android中為ByteBuffer,iOS中為NSData)。實際上,BinaryCodec在編解碼過程中什么都沒做,只是原封不動將二進制數據消息返回而已。或許你會因此覺得BinaryCodec沒有意義,但是在某些情況下它非常有用,比如使用BinaryCodec可以使傳遞內存數據塊時在編解碼階段免于內存拷貝。

StringCodec

StringCodec用于字符串與二進制數據之間的編解碼,其編碼格式為UTF-8。

  • JSONMessageCodec
    • JSONMessageCodec用于基礎數據與二進制數據之間的編解碼,其支持基礎數據類型以及列表、字典。其在iOS端使用了NSJSONSerialization作為序列化的工具,而在Android端則使用了其自定義的JSONUtil與StringCodec作為序列化工具。
  • StandardMessageCodec
    • StandardMessageCodec是BasicMessageChannel的默認編解碼器,其支持基礎數據類型、二進制數據、列表、字典,其工作原理會在下文中詳細介紹。
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有兩種實現:

  • JSONMethodCodec
    • JSONMethodCodec的編解碼依賴于JSONMessageCodec,當其在編碼MethodCall時,會先將MethodCall轉化為字典{"method":method,"args":args}。其在編碼調用結果時,會將其轉化為一個數組,調用成功為[result],調用失敗為[code,message,detail]。再使用JSONMessageCodec將字典或數組轉化為二進制數據。
  • StandardMethodCodec
    • MethodCodec的默認實現,StandardMethodCodec的編解碼依賴于StandardMessageCodec,當其編碼MethodCall時,會將method和args依次使用StandardMessageCodec編碼,寫入二進制數據容器。其在編碼方法的調用結果時,若調用成功,會先向二進制數據容器寫入數值0(代表調用成功),再寫入StandardMessageCodec編碼后的result。而調用失敗,則先向容器寫入數據1(代表調用失敗),再依次寫入StandardMessageCodec編碼后的code,message和detail。

消息處理器: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
streamhandler.png
  • StreamHandler與前兩者稍顯不同,用于事件流的通信,最為常見的用途就是Platform端向Flutter端發送事件消息。當我們實現一個StreamHandler時,需要實現其onListen和onCancel方法。而在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方法將消息傳遞過去。

理解消息編解碼過程

? 在官方文檔《Writing custom platform-specific code with platform channels》中的獲取設備電量的例子中我們發現,Android端的返回值是java.lang.Integer類型的,而iOS端返回值則是一個NSNumber類型的(通過NSNumber numberWithInt:獲取)。而到了Flutter端時,這個返回值自動"變成"了dart語言的int類型。那么這中間發生了什么呢?
? Flutter官方文檔表示,standard platform channels使用standard messsage codec對message和response進行序列化和反序列化,message與response可以是booleans, numbers, Strings, byte buffers,List, Maps等等,而序列化后得到的則是二進制格式的數據。
? 所以在上文提到的例子中,java.lang.Integer或NSNumber類型的返回值先是被序列化成了一段二進制格式的數據,然后該數據傳遞到傳遞到flutter側后,被反序列化成了dart語言中的int類型的數據。
? Flutter默認的消息編解碼器是StandardMessageCodec,其支持的數據類型如下:

  • 平臺通道數據類型支持和解碼器
  • 標準平臺通道使用標準消息編解碼器,以支持簡單的類似JSON值的高效二進制序列化,例如 booleans,numbers, Strings, byte buffers, List, Maps(請參閱StandardMessageCodec了解詳細信息)。 當您發送和接收值時,這些值在消息中的序列化和反序列化會自動進行。

下表顯示了如何在宿主上接收Dart值,反之亦然:

Dart Android iOS
null null nil (NSNull when nested)
bool java.lang.Boolean NSNumber numberWithBool:
int java.lang.Integer NSNumber numberWithInt:
int, if 32 bits not enough java.lang.Long NSNumber numberWithLong:
int, if 64 bits not enough java.math.BigInteger FlutterStandardBigInteger
double java.lang.Double NSNumber numberWithDouble:
String j ava.lang.String NSString
Uint8List byte[] FlutterStandardTypedData typedDataWithBytes:
Int32List int[] FlutterStandardTypedData typedDataWithInt32:
Int64List long[] FlutterStandardTypedData typedDataWithInt64:
Float64List double[] FlutterStandardTypedData typedDataWithFloat64:
List java.util.ArrayList NSArray
Map java.util.HashMap NSDictionary
  • 當message或response需要被編碼為二進制數據時,會調用StandardMessageCodec的writeValue方法,該方法接收一個名為value的參數,并根據其類型,向二進制數據容器(NSMutableData或ByteArrayOutputStream)寫入該類型對應的type值,再將該數據轉化為二進制表示,并寫入二進制數據容器。
    ? 而message或者response需要被解碼時,使用的是StandardMessageCodec的readValue方法,該方法接收到二進制格式數據后,會先讀取一個byte表示其type,再根據其type將二進制數據轉化為對應的數據類型。
    ? 在獲取設備電量的例子中,假設設備的電量為100,當這個值被轉化為二進制數據時,會先向二進制數據容器寫入int類型對應的type值:3,再寫入由電量值100轉化而得的4個byte。而當Flutter端接收到該二進制數據時,先讀取第一個byte值,并根據其值得出該數據為int類型,接著,讀取緊跟其后的4個byte,并將其轉化為dart類型的int。
msg_encode.png
  • 對于字符串、列表、字典的編碼會稍微復雜一些。字符串使用UTF-8編碼得到的二進制數據是長度不定的,因此會在寫入type后,先寫入一個代表二進制數據長度的size,再寫入數據。列表和字典則是寫入type后,先寫入一個代表列表或字典中元素個數的size,再遞歸調用writeValue方法將其元素依次寫入。

理解消息傳遞過程

? 消息是如何從Flutter端傳遞到Platform端的呢?接下來我們以一次MethodChannel的調用為例,去理解消息的傳遞過程。

  • 消息傳遞:從Flutter到Platform
Dart層

? 當我們在Flutter端使用MethodChannel的invokeMethod方法發起一次方法調用時,就開始了我們的消息傳遞之旅。invokeMethod方法會將其入參message和arguments封裝成一個MethodCall對象,并使用MethodCodec將其編碼為二進制格式數據,再通過BinaryMessages將消息發出。(注意,此處提到的類名與方法名均為dart層的實現)
? 上述過程最終會調用到ui.Window的_sendPlatformMessage方法,該方法是一個native方法,其實現在native層,這與Java的JNI技術非常類似。我們向native層發送了三個參數:

  • name,String類型,代表Channel名稱
  • data,ByteData類型,即之前封裝的二進制數據
  • callback,Function類型,用于結果回調
Native層

? 到native層后,window.cc的SendPlatformMessage方法接受了來自dart層的三個參數,并對它們做了一定的處理:dart層的回調callback封裝為native層的PlatformMessageResponseDart類型的response;dart層的二進制數據data轉化為std::vector<uint8_t>類型數據data;根據response,data以及Channel名稱name創建一個PlatformMessage對象,并通過dart_state->window()->client()->HandlePlatformMessage方法處理PlatformMessage對象。
? dart_state->window()->client()是一個WindowClient,而其具體的實現為RuntimeController,RuntimeController會將消息交給其代理RuntimeDelegate處理。
? RuntimeDelegate的實現為Engine,Engine在處理Message時,會判斷該消息是否是為了獲取資源(channel等于"flutter/assets"),如果是,則走獲取資源邏輯,否則調用Engine::Delegate的OnEngineHandlePlatformMessage方法。
? Engine::Delegate的具體實現為Shell,其OnEngineHandlePlatformMessage接收到消息后,會向PlatformTaskRunner添加一個Task,該Task會調用PlatformView的HandlePlatformMessage方法。值得注意的是,Task中的代碼執行在Platform Task Runner中,而之前的代碼均執行在UI Task Runner中。

dart_2_native.png
消息處理

? PlatformView的HandlePlatformMessage方法在不同平臺有不同的實現,但是其基本原理是相同的。

. PlatformViewAndroid

? PlatformViewAndroid的是Platformview的子類,也是其在Android端的具體實現。當PlatformViewAndroid接收到PlatformMessage類型的消息時,如果消息中有response(類型為PlatformMessageResponseDart),則生成一個自增長的response_id,并以response_id為key,response為value存入字典pending_responses_中。接著,將channel和data均轉化為Java可識別的數據,通過JNI向Java層發起調用,將response_id、channel和data傳遞過去。
? Java層中,被調用的代碼為FlutterNativeView (BinaryMessager的具體實現)的handlePlatformMessage,該方法會根據channel找到對應的BinaryMessageHandler并將消息傳遞給它處理。其具體處理過程我們已經在上文中詳細分析過了,此處不再贅述。
? BinaryMessageHandler處理完成后,FlutterNativeView會通過JNI調用native的方法,將response_data和response_id傳遞到native層。
? native層,PlatformViewAndroid的InvokePlatformMessageResponseCallback接收到了respond_id和response_data。其先將response_data轉化為二進制結果,并根據response_id,從panding_responses_中找到對應的PlatformMessageResponseDart對象,調用其Complete方法將二進制結果返回。

plat_android.png
PlatformViewIOS

? PlatformViewIOS是PlatformView的子類,也是其在iOS端的具體實現,當PlatformViewIOS接收到message時會交給PlatformMessageRouter處理。
? PlatformMessageRouter通過PlatformMessage中的channel找到對應的FlutterBinaryMessageHandler,并將二進制消息其處理,消息處理完成后,直接調用PlatformMessage對象中的PlatformMessageResponseDart對象的Complete方法將二進制結果返回。

結果回傳:從Platform到Flutter

? PlatformMessageResponseDart的Complete方法向UI Task Runner添加了一個新的Task,這個Task的作用是將二進制結果從native的二進制數據類型轉化為Dart的二進制數據類型response,并調用dart的callback將response傳遞到Dart層。
? Dart層接收到二進制數據后,使用MethodCodec將數據解碼,并返回給業務層。至此,一次從Flutter發起的方法調用就完整結束了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370