Flutter cached_network_image 圖片加載流程分析

前言

一天測試小姐姐拿著手機過來說,你這里圖片下載有問題呀,為什么沒有網(wǎng)絡(開飛行模式)也彈Toast提示下載成功呀缓艳?

下意識反應,肯定是Toast提示彈早了看峻,剛點擊按鈕阶淘,還沒開始下載就彈了Toast,趕緊拿手機過來操作驗證一波互妓。確實沒有網(wǎng)絡溪窒,彈了下載完成提示杰扫,去相冊檢查一下宣羊,嗯?圖片下載成功了日月,還有這種操作灼狰?趕緊檢查一下代碼惜浅,發(fā)現(xiàn)項目中使用的cached_network_image三方庫加載的圖片,從名字上可以判斷伏嗜,這是一個緩存網(wǎng)絡圖片的加載框架坛悉。所以應該是圖片顯示出來以后就被緩存到本地了,實際下載的流程并未走網(wǎng)絡請求承绸,為了驗證想法裸影,看了下框架加載圖片流程,總結出下文军熏。

使用

組件CachedNetworkImage可以支持直接使用或者通過ImageProvider轩猩。

引入依賴

dependencies:
  cached_network_image: ^3.1.0

執(zhí)行flutter pub get,項目中使用

Import it

import 'package:cached_network_image/cached_network_image.dart';

添加占位圖

CachedNetworkImage(
        imageUrl: "http://via.placeholder.com/350x150",
        placeholder: (context, url) => CircularProgressIndicator(),
        errorWidget: (context, url, error) => Icon(Icons.error),
     ),

進度條展示

CachedNetworkImage(
        imageUrl: "http://via.placeholder.com/350x150",
        progressIndicatorBuilder: (context, url, downloadProgress) => 
                CircularProgressIndicator(value: downloadProgress.progress),
        errorWidget: (context, url, error) => Icon(Icons.error),
     ),

原生組件Image配合

Image(image: CachedNetworkImageProvider(url))

使用占位圖并提供provider給其他組件使用

CachedNetworkImage(
  imageUrl: "http://via.placeholder.com/200x150",
  imageBuilder: (context, imageProvider) => Container(
    decoration: BoxDecoration(
      image: DecorationImage(
          image: imageProvider,
          fit: BoxFit.cover,
          colorFilter:
              ColorFilter.mode(Colors.red, BlendMode.colorBurn)),
    ),
  ),
  placeholder: (context, url) => CircularProgressIndicator(),
  errorWidget: (context, url, error) => Icon(Icons.error),
),

這樣就可以加載網(wǎng)絡圖片了荡澎,而且均践,圖片加載完成時,就被緩存到本地了摩幔,首先看下圖片的加載流程

官網(wǎng)說了彤委,它現(xiàn)在不包含緩存,緩存功能實際上是另一個庫flutter_cache_manager中實現(xiàn)的

原理

加載&顯示

這里我們僅梳理圖片加載和緩存的主流程或衡,對于一些其他分支流程焦影,或無關參數(shù)不做過多分析

首先,頁面上使用的構造函數(shù)接收了一個必傳參數(shù)imageUrl封断,用于生成ImageProvider提供圖片加載

class CachedNetworkImage extends StatelessWidget{
  /// image提供
  final CachedNetworkImageProvider _image;

  /// 構造函數(shù)
  CachedNetworkImage({
    Key key,
    @required this.imageUrl,
    /// 省略部分
    this.cacheManager,
    /// ...
  })  : assert(imageUrl != null),
        /// ...
        _image = CachedNetworkImageProvider(
          imageUrl,
          headers: httpHeaders,
          cacheManager: cacheManager,
          cacheKey: cacheKey,
          imageRenderMethodForWeb: imageRenderMethodForWeb,
          maxWidth: maxWidthDiskCache,
          maxHeight: maxHeightDiskCache,
        ),
        super(key: key);
  
  @override
  Widget build(BuildContext context) {
    var octoPlaceholderBuilder =
        placeholder != null ? _octoPlaceholderBuilder : null;
    var octoProgressIndicatorBuilder =
        progressIndicatorBuilder != null ? _octoProgressIndicatorBuilder : null;
    /// ...

    return OctoImage(
      image: _image,
      /// ...
    );
  }
}

這里可以看到斯辰,構造函數(shù)初始化了一個本地變量_image 類型是CachedNetworkImageProvider,它繼承ImageProvider提供圖片加載坡疼,看下它的構造函數(shù)

/// 提供網(wǎng)絡圖片加載Provider并緩存
abstract class CachedNetworkImageProvider
    extends ImageProvider<CachedNetworkImageProvider> {
  /// Creates an object that fetches the image at the given URL.
  const factory CachedNetworkImageProvider(
    String url, {
    int maxHeight,
    int maxWidth,
    String cacheKey,
    double scale,
    @Deprecated('ErrorListener is deprecated, use listeners on the imagestream')
        ErrorListener errorListener,
    Map<String, String> headers,
    BaseCacheManager cacheManager,
    ImageRenderMethodForWeb imageRenderMethodForWeb,
  }) = image_provider.CachedNetworkImageProvider;

  /// 可選cacheManager. 默認使用 DefaultCacheManager()
  /// 當運行在web時,cacheManager沒有使用.
  BaseCacheManager get cacheManager;

  /// 請求url.
  String get url;

  /// 緩存key
  String get cacheKey;
  
  /// ...

  @override
  ImageStreamCompleter load(
      CachedNetworkImageProvider key, DecoderCallback decode);
}

它的構造函數(shù)調用了image_provider.CachedNetworkImageProvider的實例在_image_provider_io.dart中是加載的具體實現(xiàn)類

/// IO implementation of the CachedNetworkImageProvider; the ImageProvider to
/// load network images using a cache.
class CachedNetworkImageProvider
    extends ImageProvider<image_provider.CachedNetworkImageProvider>
    implements image_provider.CachedNetworkImageProvider {
  /// Creates an ImageProvider which loads an image from the [url], using the [scale].
  /// When the image fails to load [errorListener] is called.
  const CachedNetworkImageProvider(
    this.url, {
  /// ...
  })  : assert(url != null),
        assert(scale != null);

  @override
  final BaseCacheManager cacheManager;
    /// ...

  @override
  Future<CachedNetworkImageProvider> obtainKey(
      ImageConfiguration configuration) {
    return SynchronousFuture<CachedNetworkImageProvider>(this);
  }

  /// 核心方法加載圖片入口
  @override
  ImageStreamCompleter load(
      image_provider.CachedNetworkImageProvider key, DecoderCallback decode) {
    final chunkEvents = StreamController<ImageChunkEvent>();
    /// 多圖加載
    return MultiImageStreamCompleter(
      codec: _loadAsync(key, chunkEvents, decode),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      informationCollector: () sync* {
        yield DiagnosticsProeperty<ImageProvider>(
          'Image provider: $this \n Image key: $key',
          this,
          style: DiagnosticsTreeStyle.errorProperty,
        );
      },
    );
  }

這里的load方法即是圖片加載的啟動入口彬呻,它會在頁面可見時被調用

它返回了一個MultiImageStreamCompleter傳入_loadAsync,看下這個方法

 /// 異步加載
  Stream<ui.Codec> _loadAsync(
    CachedNetworkImageProvider key,
    StreamController<ImageChunkEvent> chunkEvents,
    DecoderCallback decode,
  ) async* {
    assert(key == this);
    try {
      /// 默認緩存管理器
      var mngr = cacheManager ?? DefaultCacheManager();
      assert(
          mngr is ImageCacheManager || (maxWidth == null && maxHeight == null),
          'To resize the image with a CacheManager the '
          'CacheManager needs to be an ImageCacheManager. maxWidth and '
          'maxHeight will be ignored when a normal CacheManager is used.');

      /// 下載邏輯放在ImageCacheManager,得到下載stream
      var stream = mngr is ImageCacheManager
          ? mngr.getImageFile(key.url,
              maxHeight: maxHeight,
              maxWidth: maxWidth,
              withProgress: true,
              headers: headers,
              key: key.cacheKey)
          : mngr.getFileStream(key.url,
              withProgress: true, headers: headers, key: key.cacheKey);

      await for (var result in stream) {
        if (result is FileInfo) {
          var file = result.file;
          var bytes = await file.readAsBytes();
          var decoded = await decode(bytes);
          /// 下載完成返回結果
          yield decoded;
        }
      }
    } catch (e) {
      /// ...
    } finally {
      await chunkEvents.close();
    }
  }
}

這里我們看到了默認緩存管理器cacheManager創(chuàng)建的地方闸氮,為DefaultCacheManager剪况,那么它如何緩存的呢,后邊再分析湖苞。

下載的邏輯也是放在了ImageCacheManager下了拯欧,返回結果是一個stream完成多圖下載的支持详囤,下載完成通過yield 返回給ui解碼最終顯示财骨。

MultiImageStreamCompleter支持多圖加載繼承自ImageStreamCompleter

/// An ImageStreamCompleter with support for loading multiple images.
class MultiImageStreamCompleter extends ImageStreamCompleter {
  /// The constructor to create an MultiImageStreamCompleter. The [codec]
  /// should be a stream with the images that should be shown. The
  /// [chunkEvents] should indicate the [ImageChunkEvent]s of the first image
  /// to show.
  MultiImageStreamCompleter({
    @required Stream<ui.Codec> codec,
    @required double scale,
    Stream<ImageChunkEvent> chunkEvents,
    InformationCollector informationCollector,
  })  : assert(codec != null),
        _informationCollector = informationCollector,
        _scale = scale {
    /// 顯示邏輯
    codec.listen((event) {
      if (_timer != null) {
        _nextImageCodec = event;
      } else {
        _handleCodecReady(event);
      }
    }, onError: (dynamic error, StackTrace stack) {
      reportError(
        context: ErrorDescription('resolving an image codec'),
        exception: error,
        stack: stack,
        informationCollector: informationCollector,
        silent: true,
      );
    });
    /// ...
    }
  }
  /// 處理解碼完成
  void _handleCodecReady(ui.Codec codec) {
    _codec = codec;
    assert(_codec != null);

    if (hasListeners) {
      _decodeNextFrameAndSchedule();
    }
  }
  /// 解碼下一幀并繪制
  Future<void> _decodeNextFrameAndSchedule() async {
    try {
      _nextFrame = await _codec.getNextFrame();
    } catch (exception, stack) {
      reportError(
        context: ErrorDescription('resolving an image frame'),
        exception: exception,
        stack: stack,
        informationCollector: _informationCollector,
        silent: true,
      );
      return;
    }
    if (_codec.frameCount == 1) {
      // ImageStreamCompleter listeners removed while waiting for next frame to
      // be decoded.
      // There's no reason to emit the frame without active listeners.
      if (!hasListeners) {
        return;
      }

      // This is not an animated image, just return it and don't schedule more
      // frames.
      _emitFrame(ImageInfo(image: _nextFrame.image, scale: _scale));
      return;
    }
    _scheduleAppFrame();
  }
}

這里做了顯示邏輯,和最終轉化成flutter上幀的處理藏姐,_scheduleAppFrame完成發(fā)送幀的處理

下載&緩存

上邊的mngr調用了ImageCacheManager中的getImageFile方法現(xiàn)在就到了flutter_cache_manager這個三方庫當中隆箩,它是被隱式依賴的,文件是image_cache_manager.dart

mixin ImageCacheManager on BaseCacheManager {
    Stream<FileResponse> getImageFile(
        String url, {
        String key,
        Map<String, String> headers,
        bool withProgress,
        int maxHeight,
        int maxWidth,
      }) async* {
        if (maxHeight == null && maxWidth == null) {
          yield* getFileStream(url,
              key: key, headers: headers, withProgress: withProgress);
          return;
        }
      /// ...
     }
  /// ...
}

getFileStream方法實現(xiàn)在子類cache_manager.dart文件中的CacheManager

class CacheManager implements BaseCacheManager {
     /// 緩存管理
  CacheStore _store;

  /// Get the underlying store helper
  CacheStore get store => _store;

  /// 下載管理
  WebHelper _webHelper;

  /// Get the underlying web helper
  WebHelper get webHelper => _webHelper;
  
  /// 從下載或者緩存讀取file返回stream
  @override
  Stream<FileResponse> getFileStream(String url,
      {String key, Map<String, String> headers, bool withProgress}) {
    key ??= url;
    final streamController = StreamController<FileResponse>();
    _pushFileToStream(
        streamController, url, key, headers, withProgress ?? false);
    return streamController.stream;
  }
  
  Future<void> _pushFileToStream(StreamController streamController, String url,
      String key, Map<String, String> headers, bool withProgress) async {
    key ??= url;
    FileInfo cacheFile;
    try {
      /// 緩存判斷
      cacheFile = await getFileFromCache(key);
      if (cacheFile != null) {
        /// 有緩存直接返回
        streamController.add(cacheFile);
        withProgress = false;
      }
    } catch (e) {
      print(
          'CacheManager: Failed to load cached file for $url with error:\n$e');
    }
    /// 沒有緩存或者過期下載
    if (cacheFile == null || cacheFile.validTill.isBefore(DateTime.now())) {
      try {
        await for (var response
            in _webHelper.downloadFile(url, key: key, authHeaders: headers)) {
          if (response is DownloadProgress && withProgress) {
            streamController.add(response);
          }
          if (response is FileInfo) {
            streamController.add(response);
          }
        }
      } catch (e) {
        assert(() {
          print(
              'CacheManager: Failed to download file from $url with error:\n$e');
          return true;
        }());
        if (cacheFile == null && streamController.hasListener) {
          streamController.addError(e);
        }
      }
    }
    unawaited(streamController.close());
  }
}

緩存判斷邏輯在CacheStore提供兩級緩存


class CacheStore {
  Duration cleanupRunMinInterval = const Duration(seconds: 10);
    /// 未下載完成緩存
  final _futureCache = <String, Future<CacheObject>>{};
  /// 已下載完緩存
  final _memCache = <String, CacheObject>{};

  /// ...

  Future<FileInfo> getFile(String key, {bool ignoreMemCache = false}) async {
    final cacheObject =
        await retrieveCacheData(key, ignoreMemCache: ignoreMemCache);
    if (cacheObject == null || cacheObject.relativePath == null) {
      return null;
    }
    final file = await fileSystem.createFile(cacheObject.relativePath);
    return FileInfo(
      file,
      FileSource.Cache,
      cacheObject.validTill,
      cacheObject.url,
    );
  }

  Future<void> putFile(CacheObject cacheObject) async {
    _memCache[cacheObject.key] = cacheObject;
    await _updateCacheDataInDatabase(cacheObject);
  }

  Future<CacheObject> retrieveCacheData(String key,
      {bool ignoreMemCache = false}) async {
    /// 判斷是否已緩存過
    if (!ignoreMemCache && _memCache.containsKey(key)) {
      if (await _fileExists(_memCache[key])) {
        return _memCache[key];
      }
    }
    /// 未緩存的 已加入futureCache中的key直接返回
    if (!_futureCache.containsKey(key)) {
      final completer = Completer<CacheObject>();
      /// 未加入的添加到futureCache
      unawaited(_getCacheDataFromDatabase(key).then((cacheObject) async {
        if (cacheObject != null && !await _fileExists(cacheObject)) {
          final provider = await _cacheInfoRepository;
          await provider.delete(cacheObject.id);
          cacheObject = null;
        }

        _memCache[key] = cacheObject;
        completer.complete(cacheObject);
        unawaited(_futureCache.remove(key));
      }));
      _futureCache[key] = completer.future;
    }
    return _futureCache[key];
  }
    /// ...
  /// 更新到數(shù)據(jù)庫
  Future<dynamic> _updateCacheDataInDatabase(CacheObject cacheObject) async {
    final provider = await _cacheInfoRepository;
    return provider.updateOrInsert(cacheObject);
  }
}

_cacheInfoRepository緩存?zhèn)}庫是CacheObjectProvider使用的數(shù)據(jù)庫緩存對象

class CacheObjectProvider extends CacheInfoRepository
    with CacheInfoRepositoryHelperMethods {
  Database db;
  String _path;
  String databaseName;

  CacheObjectProvider({String path, this.databaseName}) : _path = path;

    /// 打開
  @override
  Future<bool> open() async {
    if (!shouldOpenOnNewConnection()) {
      return openCompleter.future;
    }
    var path = await _getPath();
    await File(path).parent.create(recursive: true);
    db = await openDatabase(path, version: 3,
        onCreate: (Database db, int version) async {
      await db.execute('''
      create table $_tableCacheObject ( 
        ${CacheObject.columnId} integer primary key, 
        ${CacheObject.columnUrl} text, 
        ${CacheObject.columnKey} text, 
        ${CacheObject.columnPath} text,
        ${CacheObject.columnETag} text,
        ${CacheObject.columnValidTill} integer,
        ${CacheObject.columnTouched} integer,
        ${CacheObject.columnLength} integer
        );
        create unique index $_tableCacheObject${CacheObject.columnKey} 
        ON $_tableCacheObject (${CacheObject.columnKey});
      ''');
    }, onUpgrade: (Database db, int oldVersion, int newVersion) async {
      /// ...
    return opened();
  }

  @override
  Future<dynamic> updateOrInsert(CacheObject cacheObject) {
    if (cacheObject.id == null) {
      return insert(cacheObject);
    } else {
      return update(cacheObject);
    }
  }

  @override
  Future<CacheObject> insert(CacheObject cacheObject,
      {bool setTouchedToNow = true}) async {
    var id = await db.insert(
      _tableCacheObject,
      cacheObject.toMap(setTouchedToNow: setTouchedToNow),
    );
    return cacheObject.copyWith(id: id);
  }

  @override
  Future<CacheObject> get(String key) async {
    List<Map> maps = await db.query(_tableCacheObject,
        columns: null, where: '${CacheObject.columnKey} = ?', whereArgs: [key]);
    if (maps.isNotEmpty) {
      return CacheObject.fromMap(maps.first.cast<String, dynamic>());
    }
    return null;
  }

  @override
  Future<int> delete(int id) {
    return db.delete(_tableCacheObject,
        where: '${CacheObject.columnId} = ?', whereArgs: [id]);
  }
}

可見數(shù)據(jù)庫緩存的是CacheObject對象羔杨,保存了url捌臊、key、relativePath等信息

class CacheObject {
  static const columnId = '_id';
  static const columnUrl = 'url';
  static const columnKey = 'key';
  static const columnPath = 'relativePath';
  static const columnETag = 'eTag';
  static const columnValidTill = 'validTill';
  static const columnTouched = 'touched';
  static const columnLength = 'length';
}

沒有緩存下調用了_webHelper.downloadFile方法

class WebHelper {
  WebHelper(this._store, FileService fileFetcher)
      : _memCache = {},
        fileFetcher = fileFetcher ?? HttpFileService();

  final CacheStore _store;
  @visibleForTesting
  final FileService fileFetcher;
  final Map<String, BehaviorSubject<FileResponse>> _memCache;
  final Queue<QueueItem> _queue = Queue();

  ///Download the file from the url
  Stream<FileResponse> downloadFile(String url,
      {String key,
      Map<String, String> authHeaders,
      bool ignoreMemCache = false}) {
    key ??= url;
    if (!_memCache.containsKey(key) || ignoreMemCache) {
      var subject = BehaviorSubject<FileResponse>();
      _memCache[key] = subject;
      /// 下載或者加入隊列
      unawaited(_downloadOrAddToQueue(url, key, authHeaders));
    }
    return _memCache[key].stream;
  }
  
  Future<void> _downloadOrAddToQueue(
    String url,
    String key,
    Map<String, String> authHeaders,
  ) async {
    //如果太多請求被執(zhí)行兜材,加入隊列等待
    if (concurrentCalls >= fileFetcher.concurrentFetches) {
      _queue.add(QueueItem(url, key, authHeaders));
      return;
    }

    concurrentCalls++;
    var subject = _memCache[key];
    try {
      await for (var result
          in _updateFile(url, key, authHeaders: authHeaders)) {
        subject.add(result);
      }
    } catch (e, stackTrace) {
      subject.addError(e, stackTrace);
    } finally {
      concurrentCalls--;
      await subject.close();
      _memCache.remove(key);
      _checkQueue();
    }
  }
  
   ///下載資源
  Stream<FileResponse> _updateFile(String url, String key,
      {Map<String, String> authHeaders}) async* {
    var cacheObject = await _store.retrieveCacheData(key);
    cacheObject = cacheObject == null
        ? CacheObject(url, key: key)
        : cacheObject.copyWith(url: url);
    /// 請求得到response
    final response = await _download(cacheObject, authHeaders);
    yield* _manageResponse(cacheObject, response);
  }
  
  
  Stream<FileResponse> _manageResponse(
      CacheObject cacheObject, FileServiceResponse response) async* {
    /// ...
    if (statusCodesNewFile.contains(response.statusCode)) {
      int savedBytes;
      await for (var progress in _saveFile(newCacheObject, response)) {
        savedBytes = progress;
        yield DownloadProgress(
            cacheObject.url, response.contentLength, progress);
      }
      newCacheObject = newCacheObject.copyWith(length: savedBytes);
    }
        /// 加入緩存
    unawaited(_store.putFile(newCacheObject).then((_) {
      if (newCacheObject.relativePath != oldCacheObject.relativePath) {
        _removeOldFile(oldCacheObject.relativePath);
      }
    }));

    final file = await _store.fileSystem.createFile(
      newCacheObject.relativePath,
    );
    yield FileInfo(
      file,
      FileSource.Online,
      newCacheObject.validTill,
      newCacheObject.url,
    );
  }
  
  Stream<int> _saveFile(CacheObject cacheObject, FileServiceResponse response) {
    var receivedBytesResultController = StreamController<int>();
    unawaited(_saveFileAndPostUpdates(
      receivedBytesResultController,
      cacheObject,
      response,
    ));
    return receivedBytesResultController.stream;
  }
  
  Future _saveFileAndPostUpdates(
      StreamController<int> receivedBytesResultController,
      CacheObject cacheObject,
      FileServiceResponse response) async {
    /// 根據(jù)路徑創(chuàng)建file
    final file = await _store.fileSystem.createFile(cacheObject.relativePath);

    try {
      var receivedBytes = 0;
      /// 寫文件
      final sink = file.openWrite();
      await response.content.map((s) {
        receivedBytes += s.length;
        receivedBytesResultController.add(receivedBytes);
        return s;
      }).pipe(sink);
    } catch (e, stacktrace) {
      receivedBytesResultController.addError(e, stacktrace);
    }
    await receivedBytesResultController.close();
  }

}

總結

cached_network_image圖片加載流程依賴ImageProvider理澎,緩存和下載邏輯放在另一個庫flutter_cache_manager下載文件在WebHelper中提供隊列管理,依賴傳入FileService做具體獲取文件方便擴展默認實現(xiàn)HttpFileService曙寡,下載完成后路徑保存在CacheObject保存在sqflite數(shù)據(jù)庫

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末糠爬,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子举庶,更是在濱河造成了極大的恐慌执隧,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件户侥,死亡現(xiàn)場離奇詭異镀琉,居然都是意外死亡,警方通過查閱死者的電腦和手機蕊唐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門屋摔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人替梨,你說我怎么就攤上這事凡壤。” “怎么了耙替?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵亚侠,是天一觀的道長。 經常有香客問我俗扇,道長硝烂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任铜幽,我火速辦了婚禮滞谢,結果婚禮上串稀,老公的妹妹穿的比我還像新娘。我一直安慰自己狮杨,他們只是感情好母截,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著橄教,像睡著了一般清寇。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上护蝶,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天华烟,我揣著相機與錄音,去河邊找鬼持灰。 笑死盔夜,一個胖子當著我的面吹牛,可吹牛的內容都是我干的堤魁。 我是一名探鬼主播喂链,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妥泉!你這毒婦竟也來了椭微?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤涛漂,失蹤者是張志新(化名)和其女友劉穎赏表,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匈仗,經...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡瓢剿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了悠轩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片间狂。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖火架,靈堂內的尸體忽然破棺而出鉴象,到底是詐尸還是另有隱情,我是刑警寧澤何鸡,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布纺弊,位于F島的核電站,受9級特大地震影響骡男,放射性物質發(fā)生泄漏淆游。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望犹菱。 院中可真熱鬧拾稳,春花似錦、人聲如沸腊脱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽陕凹。三九已至悍抑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捆姜,已是汗流浹背传趾。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工迎膜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留泥技,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓磕仅,卻偏偏與公主長得像珊豹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子榕订,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內容