Flutter集成到已有iOS項目中-Flutter module相關

Flutter

集成

系統要求

你的開發環境必須滿足 Flutter 對 macOS 系統的版本要求1已經安裝 Xcode2,Flutter 支持 iOS 8.0 及以上。

創建 Flutter module

為了將 Flutter 集成到你的既有應用里,第一步要創建一個 Flutter module。

在命令行中執行:

cd some/path/flutter 
create --template module my_flutter 

Flutter module 會創建在 some/path/my_flutter/ 目錄。在這個目錄中,你可以像在其它 Flutter 項目中一樣,執行 flutter 命令。比如 flutter run --debug 或者 flutter build ios

你也同樣可以在 Android Studio/IntelliJ3 或者 VS Code4 中運行這個模塊,并附帶 Flutter 和 Dart 插件。在集成到既有應用前,這個項目在 Flutter module 中包含了一個單視圖的示例代碼,對 Flutter 側代碼的測試會有幫助。

模塊組織

my_flutter 模塊,目錄結構和普通 Flutter 應用類似:

my_flutter/
 ├─.ios/│ 
 ├─Runner.xcworkspace│
   └─Flutter/podhelper.rb├─lib/│ 
   └─main.dart├─test/
   └─pubspec.yaml 

添加你的 Dart 代碼到 lib/ 目錄。

添加 Flutter 依賴到 my_flutter/pubspec.yaml,包括 Flutter packages 和 plugins。

.ios/ 隱藏文件夾包含了一個 Xcode workspace,用于單獨運行你的 Flutter module。它是一個獨立啟動 Flutter 代碼的殼工程,并且包含了一個幫助腳本,用于編譯 framewroks 或者使用 CocoaPods 將 Flutter module 集成到你的既有應用。

iOS 代碼要添加到你的既有應用或者 Flutter plugin 中,而不是 Flutter module 的 .ios/ 目錄下。.ios/ 下的改變不會集成到你的既有應用。在 my_flutter 執行 flutter clean 或者 flutter pub get 會重新生成這個目錄。

在你的既有應用中集成 Flutter module

這里有兩種方式可以將 Flutter 集成到你的既有應用中。

? 使用 CocoaPods 依賴管理和已安裝的 Flutter SDK 。(推薦)

? 把 Flutter engine 、你的 dart 代碼和所有 Flutter plugin 編譯成 framework 。然后用 Xcode 手動集成到你的應用中,并更新編譯設置。

你的應用將不能在模擬器上運行 Release 模式,因為 Flutter 還不支持將 Dart 代碼編譯成 x86 ahead-of-time (AOT) 模式的二進制文件。你可以在模擬機和真機上運行 Debug 模式,在真機上運行 Release 模式。

使用 Flutter 會 增加應用體積5

選項 A - 使用 CocoaPods 和 Flutter SDK 集成

這個方法需要你的項目的所有開發者,都在本地安裝 Flutter SDK。只需要在 Xcode 中編譯應用,就可以自動運行腳本來集成 dart 代碼和 plugin。這個方法允許你使用 Flutter module 中的最新代碼快速迭代開發,而無需在 Xcode 以外執行額外的命令。

下面的示例假設你的既有應用和 Flutter module 在相鄰目錄。如果你有不同的目錄結構,需要適配到對應的路徑。

some/path/├── my_flutter/│ 
  └── .ios/│     
  └── Flutter/│     
  └── podhelper.rb
  └── MyApp/   
  └── Podfile 

如果你的應用(MyApp)還沒有 Podfile,根據 CocoaPods getting started guide 來在項目中添加 Podfile

? 在 Podfile 中添加下面代碼:

# MyApp/Podfile flutter_application_path = '../my_flutter'load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb') 

? 每個需要集成 Flutter 的 Podfile target,執行install_all_flutter_pods(flutter_application_path)

# MyApp/Podfile target 'MyApp' do    install_all_flutter_pods(flutter_application_path)end 

? 運行 pod install

當你在 my_flutter/pubspec.yaml 改變了 Flutter plugin 依賴,需要在 Flutter module 目錄運行 flutter pub get,來更新會被podhelper.rb 腳本用到的 plugin 列表,然后再次在你的應用目錄 some/path/MyApp 運行 pod install.

podhelper.rb 腳本會把你的 plugins,Flutter.framework,和 App.framework 集成到你的項目中。

你應用的 Debug 和 Release 編譯配置,將會集成相對應的 Debug 或 Release 的 編譯產物6

可以增加一個 Profile 編譯配置用于在 profile 模式下測試應用。

Flutter.framework 是 Flutter engine 的框架,App.framework 是你的 Dart 代碼的編譯產物。

在 Xcode 中打開 MyApp.xcworkspace ,你現在可以使用 ?B 編譯項目了。

選項 B - 在 Xcode 中集成 frameworks

除了上面的方法,你也可以創建必備的 frameworks,手動修改既有 Xcode 項目,將他們集成進去。當你組內其它成員們不能在本地安裝 Flutter SDK 和 CocoaPods,或者你不想使用 CocoaPods 作為既有應用的依賴管理時,這種方法會比較合適。但是每當你在 Flutter module 中改變了代碼,都必須運行 flutter build ios-framework

如果你使用前面的 “使用 CocoaPods 和 Flutter SDK 集成” ,你可以跳過本步驟。

下面的示例假設你想在 some/path/MyApp/Flutter/ 目錄下創建 frameworks:

flutter build ios-framework --output=some/path/MyApp/Flutter/ 
some/path/MyApp/└── Flutter/    ├── Debug/    │   ├── Flutter.framework    │   ├── App.framework    │   ├── FlutterPluginRegistrant.framework    │   └── example_plugin.framework (each plugin with iOS platform code is a separate framework)      ├── Profile/      │   ├── Flutter.framework      │   ├── App.framework      │   ├── FlutterPluginRegistrant.framework      │   └── example_plugin.framework      └── Release/          ├── Flutter.framework          ├── App.framework          ├── FlutterPluginRegistrant.framework          └── example_plugin.framework 

在 Xcode 11 中, 你可以添加 --xcframework --no-universal 參數來生成 XCFrameworks,而不是通用 framework。

在 Xcode 中將生成的 frameworks 集成到你的既有應用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目錄拖拽 frameworks 到 你的應用 target 編譯設置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中選擇 "Embed & Sign"。

[圖片上傳失敗...(image-35602-1603942843514)]

在 target 的編譯設置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/

[圖片上傳失敗...(image-aa8c6b-1603942843514)]

在 Xcode 項目中即成 frameworks 有很多方法 —— 選擇最適合你的項目的。

你現在可以在 Xcode中使用 ?B 編譯項目。

如果你想在 Debug 編譯配置下使用 Debug 版本的 Flutter frameworks,在 Release 編譯配置下使用 Release 版本的 Flutter frameworks,在 MyApp.xcodeproj/project.pbxproj 文件中,嘗試在所有 Flutter 相關 frameworks 上使用 path = "Flutter/$(CONFIGURATION)/example.framework"; 替換 path = Flutter/Release/example.framework; (注意添加引號 ")。

你也必須在 Framework Search Paths 編譯設置中使用 $(PROJECT_DIR)/Flutter/$(CONFIGURATION)

開發

下面我們在既有 iOS 應用中添加單個 Flutter 頁面。

啟動 FlutterEngine 和 FlutterViewController

為了在既有 iOS 應用中展示 Flutter 頁面,請啟動 FlutterEngine7FlutterViewController8

FlutterEngine 充當 Dart VM 和 Flutter 運行時的主機;FlutterViewController 依附于 FlutterEngine,給 Flutter 傳遞 UIKit 的輸入事件,并展示被 FlutterEngine 渲染的每一幀畫面。

FlutterEngine 的壽命可能與 FlutterViewController 相同,也可能超過 FlutterViewController

通常建議為您的應用預熱一個“長壽”的 FlutterEngine 是因為:

? 當展示 FlutterViewController 時,第一幀畫面將會更快展現;

? 你的 Flutter 和 Dart 狀態將比一個FlutterViewController 存活更久;

? 在展示 UI 前,你的應用和 plugins 可以與 Flutter 和 Dart 邏輯交互。

加載順序和性能9 里有更多關于預熱 engine 的延遲和內存取舍的分析。

創建一個 FlutterEngine

創建 FlutterEngine 的合適位置取決于您的應用。作為示例,我們將在應用啟動的 app delegate 中創建一個 FlutterEngine, 并作為屬性暴露給外界。

如果你使用 Objective-C 在 ****AppDelegate.h:

// AppDelegate.h @import UIKit;@import Flutter; @interface AppDelegate : FlutterAppDelegate // 以下有關于 FlutterAppDelegate 的更多信息@property (nonatomic,strong) FlutterEngine *flutterEngine;@end

在 ****AppDelegate.m:

// AppDelegate.m #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Used to connect plugins. #import "AppDelegate.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application    didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {  self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];  // 使用默認 Flutter 路由運行默認 Dart 入口  [self.flutterEngine run];  [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];  return [super application:application didFinishLaunchingWithOptions:launchOptions];} @end 

如果你使用 Swift

在 ****AppDelegate.swift:

// AppDelegate.swift import UIKitimport Flutterimport FlutterPluginRegistrant // 用于連接 plugins @UIApplicationMainclass AppDelegate: FlutterAppDelegate { // FlutterAppDelegate 有更多信息  lazy var flutterEngine = FlutterEngine(name: "my flutter engine")   override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {    // 使用默認 Flutter 路由運行默認 Dart 入口    flutterEngine.run();    GeneratedPluginRegistrant.register(with: self.flutterEngine);    return super.application(application, didFinishLaunchingWithOptions: launchOptions);  }} 

使用 FlutterEngine 展示 FlutterViewController

下面的例子展示了一個普通的 ViewController,包含一個 present FlutterViewController 的按鈕。

如果你使用 Objective-C

// ViewController.m @import Flutter;#import "AppDelegate.h"#import "ViewController.h" @implementation ViewController- (void)viewDidLoad {    [super viewDidLoad];     // 制作一個按鈕,當點擊的時候調用 showFlutter 方法    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];    [button addTarget:self               action:@selector(showFlutter)     forControlEvents:UIControlEventTouchUpInside];    [button setTitle:@"Show Flutter!" forState:UIControlStateNormal];    button.backgroundColor = UIColor.blueColor;    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);    [self.view addSubview:button];} - (void)showFlutter {    FlutterEngine *flutterEngine =        ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;    FlutterViewController *flutterViewController =        [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];    [self presentViewController:flutterViewController animated:YES completion:nil];}@end 

如果你使用 Swift

// ViewController.swift import UIKitimport Flutter class ViewController: UIViewController {  override func viewDidLoad() {    super.viewDidLoad()     // 制作一個按鈕,當點擊的時候調用 showFlutter 方法    let button = UIButton(type:UIButton.ButtonType.custom)    button.addTarget(self, action: #selector(showFlutter), for: .touchUpInside)    button.setTitle("Show Flutter!", for: UIControl.State.normal)    button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)    button.backgroundColor = UIColor.blue    self.view.addSubview(button)  }   @objc func showFlutter() {    let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine    let flutterViewController =        FlutterViewController(engine: flutterEngine, nibName: nil, bundle: nil)    present(flutterViewController, animated: true, completion: nil)  }} 

現在,你的 iOS 應用中集成了一個 Flutter 頁面。

在上一個例子中,你的默認 Dart 庫的默認入口函數 main(),將會在 AppDelegate 創建 FlutterEngine 并調用 run 方法時調用。

或者 —— 使用隱式 FlutterEngine 創建 FlutterViewController

上一個示例還有另一個選擇,你可以讓 FlutterViewController 隱式創建它自己的 FlutterEngine,而不用提前預熱 engine。

不過不建議這樣做,因為按需創建FlutterEngine 的話,在 FlutterViewController 被 present 出來之后,第一幀圖像渲染完之前,將會引入明顯的延遲。但是當 Flutter 頁面很少被展示時,當對決定何時啟動 Dart VM 沒有好的啟發時,當 Flutter 無需在頁面(view controller)之間保持狀態時,此方式可能會有用。

為了不使用已經存在的 FlutterEngine 來展現 FlutterViewController,省略 FlutterEngine 的創建步驟,并且在創建 FlutterViewController 時,去掉 engine 的引用。

如果你使用 Objective-C

// "ViewController.m // 省略已經存在的代碼- (void)showFlutter {  FlutterViewController *flutterViewController =      [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];  [self presentViewController:flutterViewController animated:YES completion:nil];}@end 

如果你使用 Swift

// ViewController.swift // 省略已經存在的代碼func showFlutter() {  let flutterViewController = FlutterViewController(project: nil, nibName: nil, bundle: nil)  present(flutterViewController, animated: true, completion: nil)} 

查看 加載順序和性能 了解更多關于延遲和內存使用的探索。

使用 FlutterAppDelegate

推薦讓你應用的 UIApplicationDelegate 繼承 FlutterAppDelegate,但不是必須的。

FlutterAppDelegate 有這些功能:

? 傳遞應用的回調,例如 openURL, 到 Flutter plugins,例如 local_auth;

? 傳遞狀態欄點擊(這只能在 AppDelegate 中檢測)到 Flutter 的點擊置頂行為。

如果你的 app delegate 不能直接繼承FlutterAppDelegate,讓你的 app delegate 實現 FlutterAppLifeCycleProvider 協議,來確保 Flutter plugins 接收到必要的回調。否則,依賴這些事件的 plugins 將會有無法預估的行為。

例如:

// AppDelegate.h @import Flutter;@import UIKit;@import FlutterPluginRegistrant; @interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>@property (strong, nonatomic) UIWindow *window;@property (nonatomic,strong) FlutterEngine *flutterEngine;@end

App delegate 的實現中,應該最大化地委托給 FlutterPluginAppLifeCycleDelegate

// AppDelegate.m @interface AppDelegate ()@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;@end@implementation AppDelegate- (instancetype)init {    if (self = [super init]) {        _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];    }    return self;} - (BOOL)application:(UIApplication*)applicationdidFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>*))launchOptions {    self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];    [self.flutterEngine runWithEntrypoint:nil];    [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];    return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];} // 返回 key window 的 rootViewController, 如果它是一個 FlutterViewController// Otherwise, returns nil.- (FlutterViewController*)rootFlutterViewController {    UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;    if ([viewController isKindOfClass:[FlutterViewController class]]) {        return (FlutterViewController*)viewController;    }    return nil;} - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {    [super touchesBegan:touches withEvent:event];     // 傳遞狀態欄的點擊到 key window 上 Flutter 的 rootViewController    if (self.rootFlutterViewController != nil) {        [self.rootFlutterViewController handleStatusBarTouches:event];    }} - (void)application:(UIApplication*)applicationdidRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {    [_lifeCycleDelegate application:applicationdidRegisterUserNotificationSettings:notificationSettings];} - (void)application:(UIApplication*)applicationdidRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {    [_lifeCycleDelegate application:applicationdidRegisterForRemoteNotificationsWithDeviceToken:deviceToken];} - (void)application:(UIApplication*)applicationdidReceiveRemoteNotification:(NSDictionary*)userInfofetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {    [_lifeCycleDelegate application:application       didReceiveRemoteNotification:userInfo             fetchCompletionHandler:completionHandler];} - (BOOL)application:(UIApplication*)application            openURL:(NSURL*)url            options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {    return [_lifeCycleDelegate application:application openURL:url options:options];} - (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {    return [_lifeCycleDelegate application:application handleOpenURL:url];} - (BOOL)application:(UIApplication*)application            openURL:(NSURL*)url  sourceApplication:(NSString*)sourceApplication         annotation:(id)annotation {    return [_lifeCycleDelegate application:application                                   openURL:url                         sourceApplication:sourceApplication                                annotation:annotation];} - (void)application:(UIApplication*)applicationperformActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem  completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {    [_lifeCycleDelegate application:application       performActionForShortcutItem:shortcutItem                  completionHandler:completionHandler];} - (void)application:(UIApplication*)applicationhandleEventsForBackgroundURLSession:(nonnull NSString*)identifier  completionHandler:(nonnull void (^)(void))completionHandler {    [_lifeCycleDelegate application:applicationhandleEventsForBackgroundURLSession:identifier                  completionHandler:completionHandler];} - (void)application:(UIApplication*)applicationperformFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {    [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];} - (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {    [_lifeCycleDelegate addDelegate:delegate];}@end 

啟動選項

例子中展示了使用默認啟動選項運行 Flutter。

為了定制化你的 Flutter 運行時,你也可以置頂 Dart 入口、庫和路由。

Dart 入口

FlutterEngine 上調用 run,默認將會調用你的 lib/main.dart 文件里的 main() 函數。

你也可以使用另一個入口方法 runWithEntrypoint,并使用 NSString 字符串指定一個不同的 Dart 入口。

使用 main() 以外的 Dart 入口函數,必須使用下面的注解,防止被 tree-shaking 優化掉,而沒有編譯。

// main.dart @pragma('vm:entry-point')void myOtherEntrypoint() { ... }; 

Dart 庫

另外,在指定 Dart 函數時,你可以指定特定文件的特定函數。

下面的例子使用 lib/other_file.dart 文件的 myOtherEntrypoint() 函數取代 lib/main.dartmain() 函數:

如果你使用 Objective-C

[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"]; 

如果你使用 Swift

flutterEngine.run(withEntrypoint: "myOtherEntrypoint", libraryURI: "other_file.dart") 

路由

當構建 engine 時,可以為你的 Flutter WidgetsApp 設置一個初始路由。

如果你使用 Objective-C

FlutterEngine *flutterEngine =    [[FlutterEngine alloc] initWithName:@"my flutter engine"];[[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute"                                      arguments:@"/onboarding"];[flutterEngine run]; 

如果你使用 Swift

let flutterEngine = FlutterEngine(name: "my flutter engine")flutterEngine.navigationChannel.invokeMethod("setInitialRoute", arguments:"/onboarding")flutterEngine.run() 

這段代碼使用 "/onboarding" 取代 "/",作為你的 dart:uiwindow.defaultRouteName

請注意:

navigationChannel 上的 "setInitialRoute"必須在啟動 FlutterEngine 前調用,才能在 Flutter 的第一幀中顯示期望的路由。特別是,它必須在運行 Dart 入口函數前被調用。入口函數可能會引起一系列的事件,因為 runApp 搭建了一個 Material/Cupertino/WidgetsApp,進而隱式創建了一個 Navigator,Navigator 又可能在第一次初始化 NavigatorState 時讀取 window.defaultRouteName

運行 engine 后設置初始化路由,將不會有作用.

另外

如果在 FlutterEngine 啟動后,迫切得需要在平臺側改變你當前的 Flutter 路由,可以使用 FlutterViewController 里的 pushRoute,或者popRoute。

在 Flutter 側推出 iOS 路由,調用 SystemNavigator.pop()

查看 路由和導航了解更多 Flutter 路由的內容。

其它

之前的例子僅僅展示了怎樣定制 Flutter 實例初始化的幾種方式,通過 撰寫雙端平臺代碼,你可以在 FlutterViewController 展示 Flutter UI 之前,自由地選擇你喜歡的,推入數據和準備 Flutter 環境的方式。

參考文獻

[1]https://flutter.cn/docs/get-started/install/macos#system-requirements
[2]https://flutter.cn/docs/get-started/install/macos#install-xcode
[3]https://flutter.cn/docs/development/tools/android-studio
[4]https://flutter.cn/docs/development/tools/vs-code
[5]https://flutter.cn/docs/resources/faq#how-big-is-the-flutter-engine
[6]https://flutter.cn/docs/testing/build-modes
[7]https://api.flutter-io.cn/objcdoc/Classes/FlutterEngine.html
[8]https://api.flutter-io.cn/objcdoc/Classes/FlutterViewController.html
[9]https://flutter.cn/docs/development/add-to-app/performance
[10]https://blog.csdn.net/olsq93038o99s/article/details/104177294

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