Flutter 的渲染邏輯及和 Native 通信

在這篇文章中穆咐,我們主要了解兩個部分的內(nèi)容颤诀,一個是 Flutter 的基本渲染邏輯 另一個是 Flutter 和 Native 互通的方法,這里的 Native 是以 Android 為例对湃。然后使用案例分別進行演示着绊。

Flutter 渲染

Android 中,我們所說的 View 的渲染邏輯指的是 onMeasure(), onLayout(), onDraw(), 我們只要重寫這三個方法就可以自定義出符合我們需求的 View熟尉。其實归露,即使我們不懂 Android 中 View 的渲染邏輯,也能寫出大部分的 App斤儿,但是當系統(tǒng)提供的 View 滿足不了我們的需求的時候剧包,這時就需要我們自定義 View 了,而自定義 View 的前提就是要知道 View 的渲染邏輯往果。

Flutter 中也一樣疆液,系統(tǒng)提供的 Widget 可以滿足我們大部分的需求,但是在一些情況下我們還是得渲染自己的 Widget陕贮。

和 Android 類似堕油,F(xiàn)lutter 中的渲染也會經(jīng)歷幾個必要的階段,如下:

  • Layout : 布局階段肮之,F(xiàn)lutter 會確定每一個子 Widget 的大小和他們在屏幕中將要被放置的位置掉缺。
  • Paint : 繪制階段,F(xiàn)lutter 為每個子 Widget 提供一個 canvas戈擒,并讓他們繪制自己眶明。
  • Composite : 組合階段,F(xiàn)lutter 會將所有的 Widget 組合在一起筐高,并交由 GPU 處理搜囱。

上面三個階段中丑瞧,比較重要的就是 Layout 階段了,因為一切都始于布局蜀肘。

在 Flutter 中绊汹,布局階段會做兩個事情:父控件將 約束(Constraints) 向下傳遞到子控件;子控件將自己的 布局詳情(Layout Details) 向上傳遞給父控件扮宠。如下圖:

image

布局過程如下:

這里我們將父 widget 稱為 parent西乖;將子 widget 稱為 child

  1. parent 會將某些布局約束傳遞給 child,這些約束是每個 child 在 layout 階段必須要遵守的涵卵。如同 parent 這樣告訴 child :“只要你遵守這些規(guī)則,你可以做任何你想做的事”荒叼。最常見的就是 parent 會限制 child 的大小轿偎,也就是 child 的 maxWidth 或者 maxHeight。

  2. 然后 child 會根據(jù)得到的約束生成一個新的約束被廓,并將這個新的約束傳遞給自己的 child(也就是 child 的 child)坏晦,這個過程會一直持續(xù)到出現(xiàn)沒有 child 的 widget 為止。

  3. 之后嫁乘,child 會根據(jù) parent 傳遞過來的約束確定自己的布局詳情(Layout Details)昆婿。如:假設 parent 傳遞給 child 的最大寬度約束為 500px,child 可能會說:“好吧蜓斧,那我就用500px”仓蛆,或者 “我只會用 100px”。這樣挎春,child 就確定了自己的布局詳情看疙,并將其傳遞給 parent。

  4. parent 反過來做同樣的事情直奋,它根據(jù) child 傳遞回來的 Layout Details 來確定其自身的 Layout Details能庆,然后將這些 Layout Details 向上層的 parent 傳遞,直到到達 root widget (根 widget)或者遇到了某些限制脚线。

那我們上面所提到的 約束(Constraints)布局詳情(Layout Details) 都是什么呢搁胆?這取決于布局協(xié)議(Layout protocol)。Flutter 中有兩種主要的布局協(xié)議:Box ProtocolSliver Protocol邮绿,前者可以理解為類似于盒子模型協(xié)議渠旁,后者則是和滑動布局相關的協(xié)議。這里我們以前者為例船逮。

Box Protocol 中一死,parent 傳遞給 child 的約束都叫做 BoxConstraints 這些約束決定了每個 child 的 maxWidth 和 maxHeight 以及 minWidth 和 minHeight。如:parent 可能會將如下的 BoxConstraints 傳遞給 child傻唾。

image

上圖中投慈,淺綠色的為 parent承耿,淺紅色的小矩形為 child。
那么伪煤,parent 傳遞給 child 的約束就是 150 ≤ width ≤ 300, 100 ≤ height ≤ 無限大
而 child 回傳給 parent 的布局詳情就是 child 的尺寸(Size)加袋。

有了 child 的 Layout Details ,parent 就可以繪制它們了抱既。

在我們渲染自己的 widget 之前职烧,先來了解下另外一個東西 Render Tree

Render Tree

我們在 Android 中會有 View tree防泵,F(xiàn)lutter 中與之對應的為 Widget tree蚀之,但是 Flutter 中還有另外一種 tree,稱為 Render tree捷泞。

Flutter 中 我們常見的 widgetStatefulWidget足删,StatelessWidgetInheritedWidget 等等锁右。但是這里還有另外一種 widget 稱為 RenderObjectWidget失受,這個 widget 中沒有 build() 方法,而是有一個 createRenderObject() 方法咏瑟,這個方法允許創(chuàng)建一個 RenderObject 并將其添加到 render tree 中拂到。

RenderObject 是渲染過程中非常重要的組件,render tree 中的內(nèi)容都是 RenderObject码泞,每個 RenderObject 中都有許多用來執(zhí)行渲染的屬性和方法:

  • constraints : 從 parent 傳遞過來的約束兄旬。
  • parentData: 這里面攜帶的是 parent 渲染 child 的時候所用到的數(shù)據(jù)。
  • performLayout():此方法用于布局所有的 child余寥。
  • paint():這個方法用于繪制自己或者 child辖试。
  • 等等...

但是,RenderObject 是一個抽象類劈狐,他需要被子類繼承來進行實際的渲染罐孝。RenderObject 的兩個非常重要的子類是 RenderBoxRenderSliver 。這兩個類是所有實現(xiàn) Box ProtocolSliver Protocol 的渲染對象的父類肥缔。而且這兩個類還擴展了數(shù)十個和其他幾個處理特定場景的類莲兢,并且實現(xiàn)了渲染過程的細節(jié)。

現(xiàn)在我們開始渲染自己的 widget续膳,也就是創(chuàng)建一個 RenderObject改艇。這個 widget 需要滿足下面兩點要求:

  • 它只會給 child 最小的寬和高
  • 它會把它的 child 放在自己的右下角

如此 “小氣” 的 widget ,我們就叫他 Stingy 吧坟岔!Stingy 所屬的樹形結構如下:

MaterialApp
  |_Scaffold
    |_Container       // Stingy 的 parent
      |_Stingy        // 自定義的 RenderObject
        |_Container   // Stingy 的 child

代碼如下:

void main() {
  runApp(MaterialApp(
    home: Scaffold(
      body: Container(
        color: Colors.greenAccent,
        constraints: BoxConstraints(
            maxWidth: double.infinity,
            minWidth: 100.0,
            maxHeight: 300,
            minHeight: 100.0),
        child: Stingy(
          child: Container(
            color: Colors.red,
          ),
        ),
      ),
    ),
  ));
}

Stingy

class Stingy extends SingleChildRenderObjectWidget {
  Stingy({Widget child}) : super(child: child);

  @override
  RenderObject createRenderObject(BuildContext context) {
    // TODO: implement createRenderObject
    return RenderStingy();
  }
}

Stingy 繼承了 SingleChildRenderObjectWidget谒兄,顧名思義,他只能有一個 child
createRenderObject(...) 方法創(chuàng)建并返回了一個 RenderObjectRenderStingy 類的實例

RenderStingy

class RenderStingy extends RenderShiftedBox {
  RenderStingy() : super(null);

  // 繪制方法
  @override
  void paint(PaintingContext context, Offset offset) {
    // TODO: implement paint
    super.paint(context, offset);
  }

  // 布局方法
  @override
  void performLayout() {
    // 布局 child 確定 child 的 size
    child.layout(
        BoxConstraints(
            minHeight: 0.0,
            maxHeight: constraints.minHeight,
            minWidth: 0.0,
            maxWidth: constraints.minWidth),
        parentUsesSize: true);

    print('constraints: $constraints');


    // child 的 Offset
    final BoxParentData childParentData = child.parentData;
    childParentData.offset = Offset(constraints.maxWidth - child.size.width,
        constraints.maxHeight - child.size.height);
    print('childParentData: $childParentData');

    // 確定自己(Stingy)的大小 類似于 Android View 的 setMeasuredDimension(...)
    size = Size(constraints.maxWidth, constraints.maxHeight);
    print('size: $size');
  }
}

RenderStingy 繼承自 RenderShiftedBox社付,該類是繼承自 RenderBox承疲。RenderShiftedBox 實現(xiàn)了 Box Protocol 所有的細節(jié)邻耕,并且提供了 performLayout() 方法的實現(xiàn)。我們需要在 performLayout() 方法中布局我們的 child燕鸽,還可以設置他們的偏移量兄世。

我們在使用 child.layout(...) 方法布局 child 的時候傳遞了兩個參數(shù),第一個為 child 的布局約束啊研,而另外一個參數(shù)是 parentUserSize御滩, 該參數(shù)如果設置為 false,則意味著 parent 不關心 child 選擇的大小党远,這對布局優(yōu)化比較有用削解;因為如果 child 改變了自己的大小,parent 就不必重新 layout 了沟娱。但是在我們的例子中氛驮,我們的需要把 child 放置在 parent 的右下角,這意味著如果 child大谢ǔ痢(Size)一旦改變柳爽,則其對應的偏移量(Offset) 也會改變媳握,這就意味著 parent 需要重新布局碱屁,所以我們這里傳遞了一個 true

child.layout(...) 完成了以后蛾找,child 就確定了自己的 Layout Details娩脾。然后我們就還可以為其設置偏移量來將它放置到我們想放的位置。在我們的例子中為 右下角打毛。

最后柿赊,和 child 根據(jù) parent 傳遞過來的約束選擇了一個尺寸一樣,我們也需要為 Stingy 選擇一個尺寸幻枉,以至于 Stingyparent 知道如何放置它碰声。類似于在 Android 中我們自定義 View 重寫 onMeasure(...) 方法的時候需要調(diào)用 setMeasuredDimension(...) 一樣。

運行效果如下:

image

綠色部分為我們定義的 Stingy熬甫,紅色小方塊為 Stingy 的 child 胰挑,這里是一個 Container

代碼中的輸入如下 (iphone 6 尺寸):

flutter: constraints: BoxConstraints(100.0<=w<=375.0, 100.0<=h<=300.0)
flutter: childParentData: offset=Offset(275.0, 200.0)
flutter: size: Size(375.0, 300.0)

上述我們自定義 RenderBoxperformLayout() 中做的事情可大概分為如下三個步驟:

  • 使用 child.layout(...) 來布局 child,這里是為 child 根據(jù) parent 傳遞過來的約束選擇一個大小
  • child.parentData.offset , 這是在為 child 如何擺放設置一個偏移量
  • 設置當前 widgetsize

在我們的例子中椿肩,Stingychild 是一個 Container瞻颂,并且 Container 沒有 child,因此他會使用 child.layout(...) 中設置的最大約束郑象。通常贡这,每個 widget 都會以不同的方式來處理提供給他的約束。如果我們使用 RaiseButton 替換 Container

Stingy(  
  child: RaisedButton(  
    child: Text('Button'),
    onPressed: (){}
  )  
)

效果如下:

image

可以看到厂榛,RaisedButtonwidth 使用了 parent 給他傳遞的約束值 100盖矫,但是高度很明顯沒有 100丽惭,RaisedButton 的高度默認為 48 ,由此可見 RaisedButton 內(nèi)部對 parent 傳遞過來的約束做了一些處理炼彪。

我們上面的 Stingy 繼承的是 SingleChildRenderObjectWidget吐根,也就是只能有一個 child。那如果有多個 child 怎么辦辐马,不用擔心拷橘,這里還有一個 MultiChildRenderObjectWidget,而這個類有一個子類叫做 CustomMultiChildLayout喜爷,我們直接用這個子類就好冗疮。

先來看看 CustomMultiChildLayout 的構造方法如下:

/// The [delegate] argument must not be null.
CustomMultiChildLayout({
  Key key,
  @required this.delegate,
  List<Widget> children = const <Widget>[],
})
  • key:widget 的一個標記,可以起到標識符的作用
  • delegate:這個特別重要檩帐,注釋上明確指出這個參數(shù)一定不能為空术幔,我們在下會說
  • children:這個就很好理解了,他是一個 widget 數(shù)組湃密,也就是我們們需要渲染的 widget

上面的 delegate 參數(shù)類型如下:

  /// The delegate that controls the layout of the children.
  final MultiChildLayoutDelegate delegate;

可以看出 delegate 的類型為 MultiChildLayoutDelegate诅挑,并且注釋也說明了它的作用:控制 children 的布局。也就是說泛源,我們的 CustomMultiChildLayout 里面要怎么布局拔妥,完全取決于我們自定義的 MultiChildLayoutDelegate 里面的實現(xiàn)。所以 MultiChildLayoutDelegate 中也會有類似的 performLayout(..) 方法达箍。

另外没龙,CustomMultiChildLayout 中的每個 child 必須使用 LayoutId 包裹,注釋如下:

/// Each child must be wrapped in a [LayoutId] widget to identify the widget for  
/// the delegate.

LayoutId 的構造方法如下:

  /// Marks a child with a layout identifier.
  /// Both the child and the id arguments must not be null.
  LayoutId({
    Key key,
    @required this.id,
    @required Widget child
  })

注釋的大概意思說的是:使用一個布局標識來標識一個 child;參數(shù) child 和 參數(shù) id 不定不能為空。
我們在布局 child 的時候會根據(jù) childid 來布局攻柠。

下面我們來使用 CustomMultiChildLayout 實現(xiàn)一個用于展示熱門標簽的效果:

Container(
   child: CustomMultiChildLayout(
     delegate: _LabelDelegate(itemCount: items.length, childId: childId),
     children: items,
   ),
 )

我們的 _LabelDelegate 里面接受兩個參數(shù),一個為 itemCount筝家,還有是 childId

_LabelDelegate 代碼如下:

class _LabelDelegate extends MultiChildLayoutDelegate {

  final int itemCount;
  final String childId;

  // x 方向上的偏移量
  double dx = 0.0;
  // y 方向上的偏移量
  double dy = 0.0;

  _LabelDelegate({@required this.itemCount, @required this.childId});

  @override
  void performLayout(Size size) {
    // 獲取父控件的 width
    double parentWidth = size.width;

    for (int i = 0; i < itemCount; i++) {
      // 獲取子控件的 id
      String id = '${this.childId}$i';
      // 驗證該 childId 是否對應一個 非空的 child
      if (hasChild(id)) {
        // layout child 并獲取該 child 的 size
        Size childSize = layoutChild(id, BoxConstraints.loose(size));

        // 換行條件判斷
        if (parentWidth - dx < childSize.width) {
          dx = 0;
          dy += childSize.height;
        }
        // 根據(jù) Offset 來放置 child
        positionChild(id, Offset(dx, dy));
        dx += childSize.width;
      }
    }
  }

  /// 該方法用來判斷重新 layout 的條件
  @override
  bool shouldRelayout(_LabelDelegate oldDelegate) {
    return oldDelegate.itemCount != this.itemCount;
  }
}

_LabelDelegate 中邻辉,重寫了 performLayout(...) 方法溪王。方法中有一個參數(shù) size,這個 size 表示的是當前 widgetparentsize恩沛,在我們這個例子中也就表示 Containersize在扰。我們可以看看 performLayout(...)方法的注釋:

  /// Override this method to lay out and position all children given this
  /// widget's size.
  ///
  /// This method must call [layoutChild] for each child. It should also specify
  /// the final position of each child with [positionChild].
  void performLayout(Size size);

還有一個是 hasChild(...) 方法,這個方法接受一個 childId雷客,childId 是由我們自己規(guī)定的芒珠,這個方法的作用是判斷當前的 childId 是否對應著一個非空的 child

滿足 hasChild(...) 之后搅裙,接著就是 layoutChild(...) 來布局 child , 這個方法中我們會傳遞兩個參數(shù)皱卓,一個是 childId裹芝,另外一個是 child約束(Constraints),這個方法返回的是當前這個 childSize娜汁。

布局完成之后嫂易,就是如何擺放的問題了,也就是上述代碼中的 positionChild(..) 了掐禁,此方法接受一個 childId 和 一個當前 child 對應的 Offset怜械,parent 會根據(jù)這個 Offset 來放置當前的 child

最后我們重寫了 shouldRelayout(...) 方法用于判斷重新 Layout 的條件傅事。

完整源碼在文章末尾給出缕允。

效果如下:

image

Flutter 和 Native 的交互

我們這里說的 Native 指的是 Android 平臺。

那既然要相互通信蹭越,就需要將 Flutter 集成到 Android 工程中來障本,不清楚的如何集成可以看看這里

這里有一點需要注意,就是我們在 Android 代碼中需要初始化 Dart VM响鹃,不然我們在使用 getFlutterView() 來獲取一個 Flutter View 的時候會拋出如下異常:

Caused by: java.lang.IllegalStateException: ensureInitializationComplete must be called after startInitialization
        at io.flutter.view.FlutterMain.ensureInitializationComplete(FlutterMain.java:178)
...

我們有兩種方式來執(zhí)行初始化操作:一個是直接讓我們的 Application 繼承 FlutterApplication驾霜,另外一個是需要我們在我們自己的 Application 中手動初始化:

方法一:

public class App extends FlutterApplication {  
  
}

方法二:

public class App extends Application {  
  @Override  
  public void onCreate() {  
  super.onCreate();  
  // 初始化 Flutter
  Flutter.startInitialization(this);  
  }  
}

其實方法一中的 FlutterApplication 中在其 onCreate() 方法中干了同樣的事情,部分代碼如下:

public class FlutterApplication extends Application {

    ...
    
    @CallSuper
    public void onCreate() {
        super.onCreate();
        FlutterMain.startInitialization(this);
    }
    
    ...
}

如果我們的 App 只是需要使用 Flutter 在屏幕上繪制 UI买置,那么沒問題粪糙, Flutter 框架能夠獨立完成這些事情。但是在實際的開發(fā)中堕义,難免會需要調(diào)用 Native 的功能猜旬,如:定位脆栋,相機倦卖,電池等等。這個時候就需要 Flutter 和 Native 通信了椿争。

官網(wǎng)上有一個案例 是使用 MethodChannel來調(diào)用給本地的方法獲取手機電量怕膛。

其實我們還可以使用另外一個類進行通信,叫做 BasicMessageChannel秦踪,先來看看它如果創(chuàng)建:

// java
basicMessageChannel = new BasicMessageChannel<String>(getFlutterView(), "foo", StringCodec.INSTANCE);

BasicMessageChannel 需要三個參數(shù)褐捻,第一個是 BinaryMessenger;第二個是通道名稱椅邓,第三個是交互數(shù)據(jù)類型的編解碼器柠逞,我們接下來的例子中的交互數(shù)據(jù)類型為 String ,所以這里傳遞的是 StringCodec.INSTANCE景馁,F(xiàn)lutter 中還有其他類型的編解碼器BinaryCodec板壮,JSONMessageCodec等,他們都有一個共同的父類 MessageCodec合住。 所以我們也可以根據(jù)規(guī)則創(chuàng)建自己編解碼器绰精。

接下來創(chuàng)建的例子是:FlutterAndroid 發(fā)送一條消息撒璧,Android 收到消息之后給 Flutter 回復一條消息,反之亦然笨使。

先來看看 Android 端的部分代碼:

// 接收 Flutter 發(fā)送的消息
basicMessageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<String>() {
    @Override
    public void onMessage(final String s, final BasicMessageChannel.Reply<String> reply) {

        // 接收到的消息
        linearMessageContainer.addView(buildMessage(s, true));
        scrollToBottom();

        // 延遲 500ms 回復
        flutterContainer.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 回復 Flutter
                String replyMsg = "Android : " + new Random().nextInt(100);
                linearMessageContainer.addView(buildMessage(replyMsg, false));
                scrollToBottom();
                // 回復
                reply.reply(replyMsg);
            }
        }, 500);

    }
});

 // ----------------------------------------------
 
 // 向 Flutter 發(fā)送消息
 basicMessageChannel.send(message, new BasicMessageChannel.Reply<String>() {
     @Override
     public void reply(final String s) {
         linearMessageContainer.postDelayed(new Runnable() {
             @Override
             public void run() {
                 // Flutter 的回復
                 linearMessageContainer.addView(buildMessage(s, true));
                 scrollToBottom();
             }
         }, 500);

     }
 });

類似的卿樱,Flutter 這邊的部分代碼如下:

  // 消息通道
  static const BasicMessageChannel<String> channel =
      BasicMessageChannel<String>('foo', StringCodec());

 // ----------------------------------------------

 // 接收 Android 發(fā)送過來的消息,并且回復
 channel.setMessageHandler((String message) async {
   String replyMessage = 'Flutter: ${Random().nextInt(100)}';
   setState(() {
     // 收到的android 端的消息
     _messageWidgets.add(_buildMessageWidget(message, true));
     _scrollToBottom();
   });

   Future.delayed(const Duration(milliseconds: 500), () {
     setState(() {
       // 回復給 android 端的消息
       _messageWidgets.add(_buildMessageWidget(replyMessage, false));
       _scrollToBottom();
     });
   });

   // 回復
   return replyMessage;
 });
 
 // ----------------------------------------------
 
 // 向 Android 發(fā)送消息
 void _sendMessageToAndroid(String message) {
   setState(() {
     _messageWidgets.add(_buildMessageWidget(message, false));
     _scrollToBottom();
   });
   // 向 Android 端發(fā)送發(fā)送消息并處理 Android 端給的回復
   channel.send(message).then((value) {
     setState(() {
       _messageWidgets.add(_buildMessageWidget(value, true));
       _scrollToBottom();
     });
   });
 }

最后的效果如下:

屏幕的上半部分為 Android硫椰,下半部分為 Flutter

image

如果錯誤繁调,還請指出,謝謝靶草!

源碼地址:
flutter_rendering
flutter_android_communicate

參考:
Flutter’s Rendering Engine: A Tutorial?—?Part 1
Flutter's Rendering Pipeline

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涉馁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子爱致,更是在濱河造成了極大的恐慌烤送,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件糠悯,死亡現(xiàn)場離奇詭異帮坚,居然都是意外死亡,警方通過查閱死者的電腦和手機互艾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門试和,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人纫普,你說我怎么就攤上這事阅悍。” “怎么了昨稼?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵节视,是天一觀的道長。 經(jīng)常有香客問我假栓,道長寻行,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任匾荆,我火速辦了婚禮拌蜘,結果婚禮上,老公的妹妹穿的比我還像新娘牙丽。我一直安慰自己简卧,他們只是感情好,可當我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布烤芦。 她就那樣靜靜地躺著举娩,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晓铆,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天勺良,我揣著相機與錄音,去河邊找鬼骄噪。 笑死尚困,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的链蕊。 我是一名探鬼主播事甜,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼滔韵!你這毒婦竟也來了逻谦?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤陪蜻,失蹤者是張志新(化名)和其女友劉穎邦马,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宴卖,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡滋将,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了症昏。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片随闽。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖肝谭,靈堂內(nèi)的尸體忽然破棺而出掘宪,到底是詐尸還是另有隱情,我是刑警寧澤攘烛,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布魏滚,位于F島的核電站,受9級特大地震影響医寿,放射性物質(zhì)發(fā)生泄漏栏赴。R本人自食惡果不足惜蘑斧,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一靖秩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竖瘾,春花似錦沟突、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春职辅,著一層夾襖步出監(jiān)牢的瞬間棒呛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工域携, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留簇秒,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓秀鞭,卻偏偏與公主長得像趋观,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子锋边,可洞房花燭夜當晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容