Flutter 应用程序性能优化建议

视频

https://www.bilibili.com/video/BV1ht421L7mP/

前言

原文 https://ducafecat.com/blog/boosting-flutter-performance-top-tips-for-developers

Flutter应用程序默认已经具有良好的性能,因此您只需要避免常见的陷阱,就可以获得出色的性能。

您设计和实现应用程序的用户界面的方式可能会对其运行效率产生重大影响。

本文这些最佳实践建议将帮助您编写性能最佳的Flutter应用程序。

那么让我们开始吧!

正文

代码结构拆分合理

干净架构

细致的拆分

https://marketplace.visualstudio.com/items?itemName=ducafecat.getx-template

使用状态管理

需要一套规范来耦合所有的内容

常见的优秀状态管理有:

  • provider
  • bloc
  • getx
  • riverpod

可以看下各种状态管理文章 https://ducafecat.com/blog/flutter-state-management-libraries-2024

使用代码分析工具

代码分析工具,如Flutter分析器和Lint,对于提高代码质量和减少错误和漏洞的风险非常有帮助。这些工具可以帮助识别潜在问题,防止它们成为问题,并提供改进代码结构和可读性的建议。

flutter analyze lib/

使用 Flutter Inspector 进行调试

flutter run --debug
图片.png

之前录过一个 dev tools 性能调优的视频

https://www.bilibili.com/video/BV1Tb4y1p7t9

https://ducafecat.tech/2022/03/17/2022/flutter-devtools-performance/

懒加载和分页

一次获取和渲染大量数据可能会显著影响性能。实现延迟加载和分页,根据需要加载数据,特别是对于长列表或数据密集的视图。

ListView.builder 方式

List<Item> loadItems(int pageNumber) {
}

ListView.builder(
  itemCount: totalPages,
  itemBuilder: (context, index) {
    return FutureBuilder(
      future: loadItems(index),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          // Build your list item here.
        } else {
          return CircularProgressIndicator();
        }
      },
    );
  },
);

pull_to_refresh_flutter 方式

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SmartRefresher(
        enablePullDown: true,
        enablePullUp: true,
        header: WaterDropHeader(),
        footer: CustomFooter(
          builder: (BuildContext context,LoadStatus mode){
            Widget body ;
            if(mode==LoadStatus.idle){
              body =  Text("pull up load");
            }
            else if(mode==LoadStatus.loading){
              body =  CupertinoActivityIndicator();
            }
            else if(mode == LoadStatus.failed){
              body = Text("Load Failed!Click retry!");
            }
            else if(mode == LoadStatus.canLoading){
                body = Text("release to load more");
            }
            else{
              body = Text("No more Data");
            }
            return Container(
              height: 55.0,
              child: Center(child:body),
            );
          },
        ),
        controller: _refreshController,
        onRefresh: _onRefresh, // 下拉刷新
        onLoading: _onLoading, // 上拉载入
        child: ListView.builder(
          itemBuilder: (c, i) => Card(child: Center(child: Text(items[i]))),
          itemExtent: 100.0,
          itemCount: items.length,
        ),
      ),
    );
  }

https://pub-web.flutter-io.cn/packages/pull_to_refresh_flutter3

压缩图片

你获取到一张宽 40000 的图片,如果你直接打印,真的噩梦了。

你需要本地压缩后再显示 flutter_image_compress。

https://pub.dev/packages/flutter_image_compress

  Future<Uint8List> testCompressFile(File file) async {
    var result = await FlutterImageCompress.compressWithFile(
      file.absolute.path,
      minWidth: 2300,
      minHeight: 1500,
      quality: 94,
      rotate: 90,
    );
    print(file.lengthSync());
    print(result.length);
    return result;
  }

优化动画

避免使用对应用程序性能产生影响的繁重或复杂的动画,尤其是在旧设备上。谨慎使用动画,并考虑使用Flutter内置的动画,如 AnimatedContainerAnimatedOpacity 等。

// 1 秒太长了
AnimatedContainer(
  duration: Duration(seconds: 1),
  height: _isExpanded ? 300 : 1000,
  color: Colors.blue,
);

// 缩短动画时长
AnimatedContainer(
  duration: Duration(milliseconds: 500),
  height: _isExpanded ? 300 : 100,
  color: Colors.blue,
);

优化应用程序启动时间

通过优化初始化过程来减少应用程序的启动时间。使用 flutter_native_splash 包在应用程序加载时显示启动画面,并延迟非必要组件的初始化直到应用程序启动后。

https://pub.dev/packages/flutter_native_splash

多些组件抽取

不要去写层次很深的代码, 多些代码抽取。

  // 主视图
  Widget _buildView() {
    List<Widget> ws = [];
    
    // 标题
    if (title != null) {
      ws.add(_buildTitle(title!));
    }

    // 统计栏
    ws.add(_buildTotalBar(win, draw, lose, winAvg, loseAvg));

    // 视图
    for (var item in fixtures) {
      // 栏
      if (item.league?.id != lastLeagueId) {
        lastLeagueId = item.league?.id ?? 0;
        ws.add(_buildLeagueBar(lastLeagueId, item.league?.name ?? ""));
      }

      // 行
      ws.add(_buildRow(item));

      // 分隔符
      ws.add(Container(
        height: 0.5,
        color: AppColors.surfaceVariant,
      ));
    }

    return ws.toColumn();
  }

使用级联(..)

如果你刚开始使用Flutter,你可能还没有使用过这个运算符,但当你想在同一个对象上执行某些任务时,它非常有用。

//Bad
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

//Good
var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

使用扩展运算符(...)

其它语言也有,可以用来合并集合,只是下面的用法太神奇。

// 很啰嗦
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        isTrue ? const Text('One') : Container(),
        isTrue ? const Text('Two') : Container(),
        isTrue ? const Text('Three') : Container(),
      ],
    ),
  );
}

// 才知道可以这样用 ... 符号
@override
Widget build(BuildContext context) {
  bool isTrue = true;
  return Scaffold(
    body: Column(
      children: [
        if(isTrue)...[
          const Text('One'),
          const Text('Two'),
          const Text('Three')
        ]
      ],
    ),
  );
}

抽取你的样式定义

// 繁琐
Column(
  children: const [
    Text(
      'One',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
    Text(
      'Two',
      style: TextStyle(
        fontSize: 14,
        fontWeight: FontWeight.normal,
      ),
    ),
  ],
)

// 复用 重构
Column(
  children: [
    Text(
      'One',
      style: Theme.of(context).textTheme.subtitle1,
    ),
    Text(
      'Two',
      style: Theme.of(context).textTheme.subtitle1,
    ),
  ],
),

局部刷新

StatefulBuilder 方式

 int a = 0;
 int b = 0;

 // 1、定义一个叫做“aState”的StateSetter类型方法;
 StateSetter? aState;

 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           // 2、将第一个“ElevatedButton”组件嵌套在“StatefulBuilder”组件内;
           StatefulBuilder(
             builder: (BuildContext context, StateSetter setState) {
               aState = setState;
               return ElevatedButton(
                 onPressed: () {
                   a++;
                   // 3、调用“aState”方法对“StatefulBuilder”内部进行刷新;
                   aState(() {});
                 },
                 child: Text('a : $a'),
               );
             },
           ),
           ElevatedButton(
             onPressed: () {
               b++;
               setState(() {});
             },
             child: Text('b : $b'),
           ),
         ],
       ),
     ),
   );
 }

也可以用 getx GetBuilder 这种状态组件实现局部刷新。

定义 GetBuilder,设置 id 名称

  @override
  Widget build(BuildContext context) {
    return GetBuilder<HomeIndexController>(
      init: HomeIndexController(),
      id: "home_index",
      builder: (_) {
        return Scaffold(
          appBar: appBarWidget(...),
          body: _buildView(),
        );
      },
    );
  }

控制器触发, 制定 id 名称,可以是一个列表

update(["home_index"]);

多使用 Widget 抽取组件,而不是函数

您可以节省CPU周期,并使用const构造函数,在仅在需要时进行重建,并获得更多的好处(例如重用等)。

// 定义成 widget
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      HeaderWidget(), 
      SubHeaderWidget(), 
      ContentWidget()
    ]
  );
}

使用 final

使用 final 关键字可以极大地提高您的应用程序的性能。当一个值被声明为 final 时,它只能被设置一次,之后不会再改变。这意味着框架不需要不断地检查变化,从而提高了性能。

  final String tag;
  final Color? color;
  final Size? size;
  final double? radius;
  final Color? fontColor;

使用 const

如果已经定义了,您可以使用相同的 Widget 来节省RAM。 const widgets 在编译时创建,因此在运行时更快。

x = const Container(); 
y = const Container(); 

使用 const 类构造函数

这有助于 Flutter 仅重新构建应更新的 Widget。

class FbTagWidget extends StatelessWidget {
  const FbTagWidget(this.tag,
      {super.key, this.color, this.size, this.radius, this.fontColor});

尽可能使用 private 关键词

这更像是 Dart 的最佳实践,而不是性能。

但是,最佳实践可以在某种程度上提高性能,比如理解代码,减少复杂性等等。

class Student{
  String _name;
  String _address;
  Student({
    required String name,
    required String address,
  }): 
  _name = name,
  _address = address;
}

使用nil代替const Container()

零消耗

// 原来
text != null ? Text(text) : const Container()

// 后来
text != null ? Text(text) : const SizedBox()

// 现在
text != null ? Text(text) : nil 

在ListView中使用itemExtent来处理长列表

这有助于Flutter计算滚动位置,而不是计算每个 Widget 的高度,并使滚动动画更加高效。

默认情况下,每个子项都必须确定其范围,这在性能方面是非常昂贵的。显式设置值可以节省大量的CPU周期。列表越长,使用此属性可以获得更多的速度提升。

final List<int> _listItems = <int>[1, 2, 3, 4, 5, 6, 7, 8, 9];

@override
Widget build(BuildContext context) {
  return ListView.builder(
    itemExtent: 150,
    itemCount: _listItems.length, 
    itemBuilder: (context, index) {
      var item = _listItems[index];
      return Center(
        child: Text(item.toString())
      );
    }
}

避免在 setState 中使用 AnimationController

错误的方式,将 addListener 去掉。

void initState() {
  _controller = AnimationController(
    vsync: this,
    duration: Duration(seconds: 1), 
  )..addListener(() => setState(() {})); 
}

Column( 
  children: [
    Placeholder(), // rebuilds 
    Placeholder(), // rebuilds 
    Placeholder(), // rebuilds 
    Transform.translate( // rebuilds
      offset: Offset(100 * _controller.value, 0),
      child: Placeholder(), 
    ),
  ], 
),

使用Keys来加速Flutter性能

在Flutter中,使用Keys可以帮助加速性能并优化应用程序的重建过程。Keys在Flutter中有多种用途,其中一项重要的功能是帮助Flutter识别小部件树中的特定小部件,从而在进行重建时更有效地更新小部件。以下是一些示例,说明如何使用Keys来加速Flutter性能:

保留状态:使用GlobalKey作为Key的一种常见用法是在需要保留小部件状态的情况下。通过在重建时将相同的GlobalKey分配给相同类型的小部件,可以确保小部件在重建后保留其先前的状态,而不会丢失用户的输入或滚动位置。

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      // Widget content
    );
  }
}

列表中的重用:在ListViewGridView等可滚动列表中,使用Key可以帮助Flutter跟踪列表项并在数据源更改时有效地更新列表项,而无需重新创建整个列表。

ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) {
    return ListTile(
      key: Key(items[index].id.toString()),
      title: Text(items[index].title),
    );
  },
)

动态添加或移除小部件:在动态添加或移除小部件时,使用Key可以帮助Flutter正确识别要添加或移除的小部件,而不会影响其他部分的布局。

List<Widget> widgets = [
  Container(key: Key('1'), child: Text('Widget 1')),
  Container(key: Key('2'), child: Text('Widget 2')),
];

// Add a new widget
widgets.add(Container(key: Key('3'), child: Text('Widget 3')));

// Remove a widget
widgets.removeWhere((widget) => widget.key == Key('2'));

通过使用Keys,开发人员可以更精确地控制Flutter小部件树的重建过程,避免不必要的重建,提高应用程序的性能和响应性。

使用 ListView 列表视图时优化内存

ListView.builder( 
  ...
  addAutomaticKeepAlives: false (true by default)
  addRepaintBoundaries: false (true by default) 
);
  • addAutomaticKeepAlives 当这个属性设置为true时,Flutter会尝试在滚动列表时保留列表项的状态。这意味着即使列表项在屏幕外被移除,它们的状态仍然会被保留,以便在滚回到它们时可以保持其状态。
  • addRepaintBoundaries 当这个属性设置为true时,Flutter会尝试在列表项之间创建重绘边界。这意味着在滚动列表时,只有在需要时才会重绘列表项,而不是每次滚动都重绘所有内容。

使用 for/while 代替 foreach/map

如果你要处理大量的数据,使用正确的循环可能会对你的性能产生影响。

预缓存您的图片和图标

图片

precacheImage(
  AssetImage(imagePath), 
  context
);

svg

precachePicture( 
  ExactAssetPicture(SvgPicture.svgStringDecoderBuilder, iconPath), 
  context
);

使用SKSL预热

如果一个应用在第一次运行时的动画不流畅,但后来相同的动画变得流畅,那很可能是由于着色器编译引起的不流畅。

flutter run --profile --cache-sksl --purge-persistent-cache
flutter build apk --cache-sksl --purge-persistent-cache

使用 RepaintBoundary

RepaintBoundary是一个 Widget ,用于将其子部件的绘制内容分离为单独的绘制层。这样做的主要目的是减少不必要的重绘操作,提高应用程序的性能。当RepaintBoundary包裹一个子部件时,该子部件及其所有子部件将被视为一个整体,即使其中的其他部分发生重绘,RepaintBoundary内的内容也不会重绘。

RepaintBoundary的主要作用包括:

  1. 减少重绘范围:通过将子部件包裹在RepaintBoundary中,可以将其视为一个整体,仅在该部件内部发生重绘时才重新绘制,而不会影响到其他部分。

  2. 性能优化:避免不必要的重绘操作,可以提高应用程序的性能,特别是在具有复杂界面或动态内容的情况下。

  3. 避免全局重绘:在某些情况下,只需要更新特定部分的UI,而不是整个界面。通过使用RepaintBoundary,可以限制重绘的范围,避免全局重绘。

  4. 边界控制:可以通过RepaintBoundary来控制重绘的边界,确保只在需要时才进行重绘操作,而不会影响到其他部分。

RepaintBoundary是一个有用的工具,可以帮助优化Flutter应用程序的性能,特别是在需要控制重绘范围和避免不必要重绘操作的情况下。在开发复杂界面或需要动态更新的应用程序时,合理使用RepaintBoundary可以提高应用程序的性能和用户体验。

class RepaintBoundaryExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text('This is inside RepaintBoundary'),
          SizedBox(height: 20),
          CustomPaint(
            size: Size(200, 200),
            painter: MyPainter(),
          ),
        ],
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.blue
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke;

    canvas.drawRect(Rect.fromLTWH(50, 50, 100, 100), paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return false;
  }
}

使用 Listview.builder

Listview.builder()

用了之后不出现在屏幕上的元素,不渲染。

不要使用 ShrinkWrap 来包裹可滚动 Widget

ShrinkWrap 动态确定子组件大小。

包裹滚动组件后可能有布局错误、性能问题。

处理高消耗操作时用 isolates

比如处理非常大的 json 文件、视频压缩。

这样不会卡主线程。

可以用一些 Dart 包简化代码。

https://pub-web.flutter-io.cn/packages/flutter_isolate

不要过度使用 isolates

如果你在每个最小的操作中都使用 isolates,你的应用程序可能会非常卡顿。

这是因为生成一个 isolates 并不是一项廉价的操作。它需要时间和资源。

释放你不用的内存数据

比如你载入一个图片数据进行加工,如加文字、加二维码,不用的时候请释放。

压缩数据处理

为了节省内存,请压缩您的数据。

比如你载入了百兆的 json 文件,你可以压缩起来放在内存中。

final response = await rootBundle.loadString('assets/en_us.json');

final original = utf8.encode(response); 

final compressed = gzip.encode(original); 
final decompress = gzip.decode(compressed);

final enUS = utf8.decode(decompress);

保持 Flutter 新稳定版本

在每个版本中,Flutter都变得越来越快。

所以不要忘记及时更新你的Flutter版本,并继续创作出令人惊艳的作品!

注意用稳定版。

https://docs.flutter.dev/release/archive?tab=macos

请多准备几台真机调试

始终在真实设备上测试您的应用程序性能,包括较旧的型号,以便发现在模拟器或较新设备上可能不明显的性能问题。

使用StatelessWidget而不是StatefulWidget

一个 StatelessWidget 比一个 StatefulWidget 更快,因为它不需要像其名称所暗示的那样管理状态。

所以如果可能的话,你应该优先选择它。

不要使用OpacityWidget

Opacity Widget在与动画一起使用时可能会导致性能问题,因为 Opacity Widget的所有子Widget都会在每个新帧中重新构建。在这种情况下,最好使用 AnimatedOpacity 。如果您想要淡入一张图片,请使用FadeInImageWidget。如果您想要具有不透明度的颜色,请绘制具有不透明度的颜色。

//不推荐
Opacity(opacity: 0.5, child: Container(color: Colors.red))

//推荐
Container(color: Color.fromRGBO(255, 0, 0, 0.5))

使用SizedBox而不是Container

一个 Container Widget非常灵活。例如,您可以自定义填充或边框,而无需将其嵌套在另一个Widget中。但是,如果您只需要一个具有特定高度和宽度的框,最好使用 SizedBox Widget。它可以被设置为const,而 Container 则不行。

Row/Column, 中添加空格时,更倾向于使用 SizedBox 而不是 Container

@override
Widget build(BuildContext context) {
return Column(
  children: [ 
      Text(header),
      const SizedBox(height: 10), 
      Text(subheader), 
      Text(content)
    ]
  ); 
}

不要用 Clip

Clip是一项非常昂贵的操作,当你的应用程序变慢时应该避免使用。如果Clip行为设置为 Clip.antiAliasWithSaveLayer ,它的代价会更高。尝试找到其他不需要Clip的方法来实现你的目标。例如,可以使用 borderRadius 属性来创建带有圆角边框的矩形,而不是使用Clip。

Container(
  width: 100,
  height: 100,
  decoration: BoxDecoration(
    borderRadius: BorderRadius.circular(50),
    image: DecorationImage(
      image: NetworkImage('https://example.com/image.jpg'),
      fit: BoxFit.cover,
    ),
  ),
)

使用Offstage

OffstageWidget允许您隐藏一个Widget,而不需要从Widget树中移除它。这对于提高性能很有用,因为框架不需要重新构建隐藏的Widget。

Offstage(
  offstage: !showWidget,
  child: MyWidget(),
)

在Flutter中, Offstage Widget用于在布局中隐藏子Widget,同时仍然是树的一部分。它可以用于有条件地显示或隐藏子Widget,而无需重新构建整个树。

Opacity Widget用于控制子Widget的透明度。它接受一个介于0.0和1.0之间的值,其中0.0表示完全透明,1.0表示完全不透明。然而,重要的是要注意它可能会影响性能,所以只在必要时使用。

Visibility Widget用于控制子Widget的可见性。它可以用于有条件地显示或隐藏子Widget,而无需重新构建整个树。

所有三个Widget都用于控制子Widget的显示,但它们的方式不同。

Offstage控制布局,Opacity控制透明度,Visibility控制可见性。

使用 addPostFrameCallback

在某些情况下,我们需要在帧渲染后执行某些操作。不要尝试使用任何延迟函数,也不要创建自定义回调!我们可以使用 WidgetsBinding.instance.addPostFrameCallback 方法来实现。这个回调将在帧渲染后被调用,并通过避免不必要的重建来提高性能。

WidgetsBinding.instance.addPostFrameCallback((_) {
 // ...
});

使用 AutomaticKeepAliveClientMixin

当使用 ListViewGridView 时,子部件可以被多次构建。为了避免这种情况,我们可以使用 AutomaticKeepAliveClientMixin 来处理子部件。这将保持子部件的状态并提高性能。

class MyChildWidget extends StatefulWidget {
  @override
  _MyChildWidgetState createState() => _MyChildWidgetState();
}

class _MyChildWidgetState extends State<MyChildWidget> with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;
  
  @override
  Widget build(BuildContext context) {
    return Text("I am a child widget");
  }
}

在这个例子中, MyChildWidget 类使用 AutomaticKeepAliveClientMixin 混入,并且 wantKeepAlive 属性被设置为 true 。这将保持 MyChildWidget 的状态,并防止它被多次重建,从而提高性能。

避免使用 MediaQuery.of(context).size

当你在Flutter中使用MediaQuery.of(context).size时,Flutter会将你的小部件与MediaQuery的大小相关联。这意味着每次调用MediaQuery.of(context).size时,Flutter会检测MediaQuery的大小是否发生变化,从而可能导致不必要的重建(rebuilds)。

使用MediaQuery.sizeOf(context)来避免这些不必要的重建,从而提高应用程序的响应性。通过使用MediaQuery.sizeOf(context),你可以绕过与MediaQuery大小相关的重建过程,从而减少不必要的性能开销。

类似的优化方法也适用于其他MediaQuery方法。举例来说,建议使用MediaQuery.platformBrightnessOf(context)而不是MediaQuery.of(context).platformBrightness,以避免不必要的重建,从而提高应用的响应性。

不要在调试模式下测量性能

一个用于性能和内存测量的特殊模式,即Profile模式。您可以通过Android Studio或Visual Studio Code等IDE运行它,也可以通过执行以下CLI命令来运行:

flutter run -profile

不要在模拟器中测量性能

多用真机性能调试

小结

优化Flutter应用的性能对于提供无缝的用户体验至关重要。通过实施这些提示,您可以进一步优化Flutter应用的性能。请记住,性能优化是一个持续的过程,定期进行分析和测试是确保应用程序保持高性能标准的关键。

感谢阅读本文

如果有什么建议,请在评论中让我知道。我很乐意改进。


© 猫哥
ducafecat.com

end

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

推荐阅读更多精彩内容