Flutter混编方案

前言

为什么会有Flutter混编方案?其实这是一个很现实的问题。比如我们想要新写一个App,直接选用Flutter作为移动端开发的跨平台方案是非常好的一个选择。但是现实中是我们的App可能是已经开发了很多年的一个巨型工程,完全放弃原有的代码而使用Flutter重写App是不现实的。

开发过程中,我们最想要的是原生代码开发和Flutter共存:既不影响项目工程的原生开发,又能使用Flutter去统一iOS/Android技术栈。

混编方案

一般来说混编方案有以下两种:

flutter_01.png
  1. 统一管理方案:将iOS工程和Android工程作为Flutter工程的子工程,由Flutter统一管理。
  2. 三端分离方案:iOS工程、Android工程、Flutter工程是三个单独的项目工程,将Flutter工程的编译产物作为iOS工程和Android工程的依赖模块,原有工程的管理模式不变,对原生工程没有侵入性,无需额外配置工作。

1. 统一管理方案

统一管理方案是只有一个项目工程,这样的好处是代码集中,可以很方便的进行项目开发,每个开发同学都可以进行iOS、Android和Flutter的开发。当然缺点也非常明显:

  • 对原有项目的侵入性太大,项目对外部环境的依赖程度增加。
  • 每个人本地都要装有自己端的开发环境(iOS/Android)和Flutter的开发环境,并且Flutter SDK版本要保持一致。
  • 耦合度会越来越高。当项目越来越复杂后,整个项目的代码耦合度会越来越高,相关工具链耗时也会越来越长,导致开发效率降低。

2. 三端分离方案

三端分离方案是iOS、Android和Flutter分别作为三个独立项目存在,在远端各自有各自的代码仓库。这种方案需要单独创建Flutter项目,然后通过iOS(CocoaPods)和安卓的依赖管理工具将Flutter项目build出来的framework、资源包等放入Native工程以供使用。这种方式可以将iOS、Android和Flutter项目放在一个目录下面作为一个项目来管理,也可以不在同一目录下,关键是设置Flutter模块依赖时相对路径一定要设置正确,如下:

some/path/
  demo_android/
  demo_ios/
  demo_flutter/

以iOS端为例。

2.1 创建Flutter工程

要将Flutter集成到现有项目中,首先创建Flutter模块,命令行运行:

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

执行完后,在yourproject/path/my_flutter中生成了Flutter模块项目。在该目录中,你可以运行一些flutter命令,比如flutter run --debugflutter build ios

注意:

path可以自己定,但是一定要和后边Podfile文件的路径一致。

my_flutter目录结构如下:

.
├── .android                // Android部分工程文件
├── .gitignore              // 忽略项配置文件
├── .ios                    // iOS部分工程文件
│   └── Runner.xcworkspace  // iOS工程工作区
├── lib                     // 项目Dart源文件    
│   └── main.dart           // Flutter项目代码入口文件,类似iOS的main.m,RN的index.js文件
├── pubspec.yaml            // 项目依赖配置文件,类似于iOS的Podfile,RN的package.json文件
└── test                    // 项目测试文件
  • 在lib目录下放置自己的Dart代码
  • Flutter依赖项添加到my_flutter/pubspec.yaml,包括Flutter软件包和插件。
  • .ios包含一个Xcode工作区,自己的iOS代码不要添加到这里,这里的更改不会显示到已有的iOS项目中,并且可能会被Flutter覆盖。
  • .ios/.android/目录是自动生成的,不要对其进行源码控制。
  • 首次拉取到集成Flutter模块的项目代码后,构建项目之前,要现在my_flutter目录运行flutter pub get以生成对应的.ios/.android/目录。

使用Android Studio运行Flutter工程无误后,就可以将Flutter推到远端仓库,为后边的混编做好准备工作。

2.2 将Flutter工程集成到已有应用程序中

官方推荐使用CocoaPods依赖管理工具来安装Flutter SDK,这种方式要求当前项目的每个开发人员本地都必须安装Flutter SDK版本。

如果你的项目还没有使用CocoaPods,可以参考CocoaPods官网或者CocoaPods入门来给项目添加CocoaPods依赖管理工具。

① Podfile文件

我们要通过CocoaPods管理Flutter SDK,需要再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)

这里需要注意的是,如果你的Flutter模块目录结构与官方文档推荐的不一致,需要自己调整相对路径,以保证安装正确。Podfile详情案例如下:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
inhibit_all_warnings!

# path修改为调整后的相对路径
flutter_application_path = './Demo/Vendors/demo_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'MyApp' do

install_all_flutter_pods(flutter_application_path)

end
② 运行pod install

pod install主要做了以下事情:

  • 解析Generated.xcconfig文件,获取 Flutter工程配置信息,文件在my_flutter/.ios/Flutter/目录下,文件中包含了Flutter SDK路径、Flutter工程路径、Flutter工程入口、编译目录等。
  • 将Flutter SDK中的Flutter.framework通过pod添加到Native工程。
  • 使用post_install这个pod hooks来关闭Native工程的bitcode,并将Generated.xcconfig文件加入Native工程。

现在虽然进行了三端分离,但是项目之间是直接依赖的,仍然存在一些问题:

  • 对原有项目仍然有侵入性,需要在项目中配置Podfile文件和执行flutter命令。
  • 每个人本地仍然需要自己端的开发环境(iOS/Android)和Flutter的开发环境,并且Flutter SDK版本要保持一致。
2.3 构建Flutter模块

三端分离方案的关键是抽离Flutter工程,将Flutter项目的构建产物按照某种规则提供给原生工程使用,比如Android使用aar,iOS使用CocoaPods。

这种方案是将Flutter项目的构建产物作为原生工程的子模块,原有工程不需要本地安装Flutter开发环境,只需要关注原生开发即可。当我们的Flutter项目有了新功能或改动后,将其构建产物通过拖入或改造为依赖库的方式提供给原生项目使用。下面我们来一步步实现三端分离方案。

原生工程对Flutter的依赖主要分为两部分:

  1. Flutter库和引擎,也就是Flutter的framework库和引擎库。
  2. Flutter工程,也就是我们自己实现的Flutter模块功能,主要包含Flutter模块lib目录下的Dart代码和各种资源。
① 构建

iOS集成Flutter模块要稍微比Android麻烦一点。iOS项目工程对Flutter的依赖分别是:

  1. Flutter库和引擎,即Flutter.framework;
  2. Flutter项目产物,即App.framework。

iOS项目的Flutter模块依赖,实际上就是这两个产物,可以直接拖入项目工程,或者封装成一个CocoaPods私有库供原生项目引用。

如何构建Flutter产物呢,Flutter项目根目录下执行build命令:

flutter build ios --debug
flutter_02.png

这条命令执行完后,会生成上面说的构建产物:Flutter.frameworkApp.framework。如果想要release的产物,把--debug换成--release即可。

flutter_03.png
② 依赖使用

如果想和Android一样搞成依赖库,需要单独将构建产物封装成CocoaPods私有库,通过pod的方式给项目引入使用。如何构建私有库,这里就不介绍了,如果有兴趣可以参考CocoaPods入门

如果不想这么麻烦,也可以直接将产物拖进项目的某个目录下,直接引入使用。

原生iOS项目中,导入头文件#import <Flutter/Flutter.h>,直接使用FlutterViewController创建视图控制器即可,代码如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    FlutterViewController *vc = [[FlutterViewController alloc]init];
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];
    return YES;
}
2.4. iOS端集成方案
① 开发模式

开发模式最重要要求就是便于调试,这时可以采用上面1.2.2方案,仍然是三端分离,但是不使用Flutter构建产物。

首先在iOS指定目录下clone Flutter项目代码。之前的iOS项目也集成过React Native模块,为了便于统一管理,我们将Flutter模块放在React Native模块同一目录下。切到指定目录下执行clone命令:

git clone flutter项目地址
flutter_04.png

然后进入到Flutter模块根目录下,执行:

flutter pub get

这是管理Flutter packages的命令,会将项目依赖的Flutter package拉取到本地供项目使用。

flutter_05.png

此时Flutter模块的准备工作已经完成,下边需要将Flutter模块配置给iOS项目工程使用。Podfile文件中增加以下内容:

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '8.0'
inhibit_all_warnings!

# flutter模块路径配置,路径为你缩放至目录的相对路径
flutter_application_path = './Demo/Vendors/demo_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

target 'Demo' do

# iOS依赖库
pod 'AFNetworking', '3.2.1'

# flutter
install_all_flutter_pods(flutter_application_path)

end

修改完毕后,执行:

pod update
flutter_06.png

执行完毕后,Flutter.framework会通过Pod添加到iOS项目中,还有一些工程配置也会在这个命令中得以完成。

此时配置完毕,iOS项目添加如2.3所示native代码,运行项目,即可看到Flutter页面。

② 发布模式

开发模式主要是为了调试,所以开发同学本地必须有Flutter开发环境,但是当开发测试完需要打Release包发布或者代码提交的服务端使用打包服务机打包的时候,Flutter工程的代码是什么我们已经不关心了,只需要提供Flutter工程的编译产物给原生工程依赖使用即可。

所以此时进入到项目中的Flutter工程目录下执行flutter build ios --debugflutter build ios --release构建编译产物,待编译完成后有两种方式提供给原生工程使用:

  • 直接拖入原生工程,做相应工程配置后即可依赖使用。
flutter_07.png
  • 将构建产物使用CocoaPods制作成私有库供原生项目依赖使用,这也是目前比较推崇的方式。

具体关系图:

flutter_08.png

目前iOS端是直接将Flutter编译产物直接拖入项目使用,后续会将Flutter编译产物构建成上图中私有库的形式,使用Cocoapods做Flutter模块的依赖管理。

3. Native与Flutter通信

就像Native与H5交互相仿,Native与Flutter通信也是通过一个中间通信工具对象(Platform Channel)来完成的,有三种类型:

  • MethodChannel,最常用的传递对象,现在项目中使用的通信方式也是基于MethodChannel完成的。
  • BasicMessageChannel,用户数据信息的传递。
  • EventChannel,用于时间监听传递等场景。
3.1 Native模块

下面我们来看下iOS端代码实现,首先定义MethodChannel的name,初始化过程中会使用这些name创建通信对象。

static NSString *const kChannelFlutterToNative = @"com.demo.flutter/native";
static NSString *const kChannelNativeToFlutter = @"com.demo.flutter/flutter";
① Native模块传递信息给Flutter模块
// native to flutter
FlutterMethodChannel *flutterChannel = [FlutterMethodChannel methodChannelWithName:kChannelNativeToFlutter binaryMessenger:flutterVC.binaryMessenger];
NSString *serviceToken = @"token";
if (serviceToken.length > 0) {
    [flutterChannel invokeMethod:@"onActivetyResult" arguments:@{@"cookie" : serviceToken}
    ];
}
② Native模块接收Flutter模块传递信息
// flutter to native
FlutterMethodChannel *nativeChannel = [FlutterMethodChannel methodChannelWithName:kChannelFlutterToNative binaryMessenger:flutterVC.binaryMessenger];
[nativeChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
    if ([call.method isEqualToString:@"openWebViewPage"]) { 
        //打开webView
        NSString *url = [call.arguments[@"message"] description];
        self.loadWebView(url);
    } else if ([call.method isEqualToString:@"handleTrackingCrash"]) { 
        //触发native埋点
        [self trackingFlutterErrorWithData:call.arguments];
    }
}];
3.2 Flutter模块

Flutter工程代码实现,首先也要初始化MethodChannel对象,初始化用的字符串要与上面iOS Native工程使用的保持一致。

static final flutterToNativeChannel = const MethodChannel('com.demo.flutter/native');
static final nativeToFlutterChannel = const MethodChannel('com.demo.flutter/flutter');
① Flutter模块接收Native模块传递信息
Future<dynamic> handle(MethodCall call) async {
  switch(call.method) {
    case 'onActivetyResult':
      onDataChange(call.arguments);
      break;
  }
}
ConstantsUtil.nativeToFlutterChannel.setMethodCallHandler(handle);
② Flutter传递信息给Native模块
Widget renderBottomRow(i) {
  return GestureDetector(
    onTap: () => openWebView(ConstantsUtil.flutterToNativeChannel, listData[i]['url']),
    child: Container(
        ...
    ),
  );
}

Future<Null> openWebView(MethodChannel channel, String url) async {
  Map<String,String> param = {'message':url};
  await channel.invokeMethod('openWebViewPage', param);
}

4. 调试

作为软件开发,调试过程必不可少,那么在混编模式下有什么调试方案和技巧呢?

在2.4所提到的开发模式下,我们本地既有Native工程代码,又有Flutter工程代码,想要同时调试Native代码和Flutter代码,一般有两种方案:

4.1 iOS和Flutter同时调试,不支持断点方案

① Xcode打开iOS项目,运行项目并打开Flutter项目页面。

② 终端命令行输入:flutter devices,打印出已连接到计算机的设备。

flutter_09.png

③ Android Studio打开嵌在iOS项目中的Flutter项目,控制台选择Terminal选项卡,输入:

flutter attach -d 894DADC8-A12B-47FC-B8A7-EE29F0D2B086

flutter attach的作用是将当前Flutter项目连接到某个正在运行的应用程序上。回车后,控制台会输出:

Syncing files to device iPhone 11...                                    
 6,247ms (!) 

这表示连接成功,具体Android Studio项目详情截图如下:

flutter_10.png

Flutter项目代码修改后:

  • r是热加载,局部刷新,刷新所有改动的Flutter代码文件,此时就可以看到代码改动后的结果;
  • R是热重启,全部刷新,刷新所有的Flutter文件。如过Hot reload刷新无效,可以尝试使用Hot restart
  • dq都是终止连接,结束调试。

Hot reloadHot restart区别:

  • Hot reload,将所有代码更改加载到VM中,并重新构建Widget树,但是不会重新运行main()initState()
  • Hot restart,同样将所有代码更改加载到VM中,然后重新启动Flutter应用,从而丢失应用状态。
4.2 iOS和Flutter同时调试,支持断点方案

① Android Studio打开嵌在iOS项目中的Flutter项目,工具栏点击Flutter Attach

flutter_11.png

此时控制台Debug选项卡log输出:

Waiting for a connection from Flutter on iPhone 11...

② Xcode打开iOS项目,运行项目并打开Flutter项目页面。控制台Debug选项卡log如下输出代表连接完成,可以进行断点调试。

Debug service listening on ws://127.0.0.1:54615/cDjoWoEjEok=/ws
Syncing files to device iPhone 11...
flutter_12.png

同样在控制台上边也可以通过点击Hot reloadHot restart按钮来实现代码修改的更新操作。

5. 遇到的问题

5.1 找不到GeneratedPluginRegistrant文件

如果原生代码中使用了GeneratedPluginRegistrant类,还要从Flutter项目中将这个类文件拿出来和Flutter项目产物放在一块提供给原生项目使用,不然会报错找不到这个类。

5.2 启动崩溃,Library not loaded: @rpath/Flutter.framework/Flutter

项目启动崩溃,控制台log日志如下:

dyld: Library not loaded: @rpath/Flutter.framework/Flutter
  Referenced from: /Users/zzz/Library/Developer/CoreSimulator/Devices/F5A071EC-2F1A-47E8-9C71-8E1269E01568/data/Containers/Bundle/Application/72BC9387-1FFB-467F-97FE-21767A5861B0/Demo.app/Demo
  Reason: image not found

根据提示Flutter.framework没有被加载,我们点击Xocde的TARGETS -> General,找到 Frameworks,Libraries,and Embedded Content选项的Flutter.framework,将Embed值由Do Not Embed改为Embed Without Signing,重新运行项目即可。

另外App.framework也需要将Embed值由Do Not Embed改为Embed Without Signing,不然项目运行后进入flutter页面是看到你写的功能页面的。

5.3 项目嵌入Flutter编译产物后,使用模拟器运行项目报错
Building for iOS Simulator, but the linked and embedded framework 'App.framework' was built for iOS.
flutter_13.png

这是因为Xcode 11.4更改了框架的链接和嵌入方式,导致了在iOS设备和模拟器之间切换的问题。想要避免这个启动错误最简单的操作就是更改Workspace Settings。点击Xocde菜单栏File --> Workspace Settings...,在弹出对话框中将Build System值改为Legacy Build System即可。重新运行项目,即可正常在模拟器启动应用。

flutter_14.png
5.4 Android Studio运行Flutter项目,提示Waiting for another flutter command to release the startup lock...

项目异常关闭或者使用任务管理器强制关闭后一般会出现这个问题,原因是在Flutter编译运行过程中会创建一个文件锁lockfile,而异常关闭或者强制关闭或导致这个锁没有释放而一直存在,导致启动过程中出现waiting问题。

解决方案也很简单,找到这个文件删除即可。

rm ./flutter/bin/cache/lockfile
5.5 应用程序提交App Store时报错
Unsupported Architecture. Your executable contains unsupported architecture '[x86_64, i386]

意思比较明白,打包的应用程序包含了不被支持的模拟器架构(x86_64和 i386)。解决方案当然就是删掉这两个模拟器架构。

选择Xcode的Targets --> Build Phases,点击+号按钮选择New Run Script Phase,在输入框中填入以下代码:

APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"

# This script loops through the frameworks embedded in the application and
# removes unused architectures.

find "$APP_PATH" -name '*.framework' -type d | while read -r FRAMEWORK
do
    FRAMEWORK_EXECUTABLE_NAME=$(defaults read "$FRAMEWORK/Info.plist" CFBundleExecutable)
    FRAMEWORK_EXECUTABLE_PATH="$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME"
    echo "Executable is $FRAMEWORK_EXECUTABLE_PATH"

    EXTRACTED_ARCHS=()
    for ARCH in $ARCHS
    do
        echo "Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME"
        lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
        EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
    done

    echo "Merging extracted architectures: ${ARCHS}"
    lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
    rm "${EXTRACTED_ARCHS[@]}"

    echo "Replacing original executable with thinned version"
    rm "$FRAMEWORK_EXECUTABLE_PATH"
    mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"
done

这段脚本将会在打包过程中执行,删除掉Archive包中不被支持的模拟器架构(x86_64和 i386)。

5.6 配置Flutter环境变量后执行Flutter命令仍然报zsh: command not found: flutter问题

配置Flutter环境变量后执行Flutter命令正常,退出终端工具ZSH,再次打开终端工具ZSH执行Flutter命令,会提示zsh: command not found: flutter问题。

原因是如果你使用的是ZSH,终端启动时 ~/.bash_profile 将不会被加载,解决办法就是修改 ~/.zshrc ,在其中添加:source ~/.bash_profile

6. 异常监控

要想知道Flutter模块在原生应用中是否正常使用,异常监控绝对少不了。下面我们来看下Flutter异常如何收集和上报。

trackError方法是回调Native异常上报代码,在Native中上传。目前iOS端使用的是埋点上传的方式记录当前Flutter模块的异常,后边还会做进一步优化,上传到统一异常收集平台展示。

Future<Null> trackError (MethodChannel channel, String exception, String stack) async {
  Map<String,String> param = {'exception' : exception,
                              'stack' : stack};
  await channel.invokeMethod('handleTrackingCrash', param);
}
6.1 Dart异常

对于Dart异常,我们可以使用全局onError函数去捕获:

runZoned(() {
  runApp(MyApp());
  if (Platform.isAndroid) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
  }
}, onError: (error, stackTrace) {
  // This is a pure Dart error
  trackError(ConstantsUtil.flutterToNativeChannel, error.toString(), stackTrace.toString());
});

这里只要Dart代码有Error就会触发onError回调方法。

6.2 Flutter异常

除了Dart异常外,Flutter也能抛出其他异常,比如调用原生代码发生的平台异常,这种类型的异常也同样是需要上报的。

为了捕获 Flutter 异常,需要重写FlutterError.onError属性。在开发环境下,可以将异常格式化输出到控制台。在生产环境下,可以把异常信息传递Native模块做异常上报。

FlutterError.onError = (FlutterErrorDetails errorDetails) {
  trackError(ConstantsUtil.flutterToNativeChannel, errorDetails.exception.toString(), errorDetails.stack.toString());
};

如果想要了对异常上报做进一步了解,请点击实用教程查看。

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