成熟项目的Flutter快速引入以及Flutter、Native混合开发探究

阅读须知:

  • 1.本篇文章基于 Android 平台
  • 2.Flutter测试项目——测试、Flutter容器项目——容器

本文分为以下章节,读者可按需阅读:

  • 1.成熟项目的Flutter快速引入——在已有项目中无缝引入Flutter作为开发的一种方式
  • 2.Flutter、Native混合开发——在一个页面中同时使用 Flutter 与 Native 两种技术的开发探究
  • 3.尾巴

Flutter测试项目Github

Flutter容器项目Github

一、成熟项目的Flutter快速引入

现在很多教程都停留在创建一个新的 Flutter 项目然后开始介绍如何使用这个项目开发 Flutter。但是其实我们目前大部分使用 Flutter 的场景都是基于已经成熟的项目。我们不可能因为使用 Flutter 而将原来的项目推到重来。这一节我就来介绍一种成熟项目无缝接入 Flutter 的方式。本章需要大家结合上面提到的 Github 项目代码食用。

1.闲鱼以及美团的实践

  • 1.目前很多厂商都有着自己的成熟项目的 Flutter 接入实践,其中美团、闲鱼的实践应该已经运行的比较久了。他们的接入方式主要分下面几步:
    • 1.理清楚 Flutter App 的构建和运行方式。
    • 2.修改 Flutter 项目的 Gradle 文件,将 Flutter 项目打包成 AAR 文件。
    • 3.将 AAR 文件推送到 Maven 服务器上。
    • 4.主工程引入 Flutter 的 AAR 文件,和主工程一起编译生成主 App。
  • 2.美团的实践
  • 3.闲鱼的实践

2.我的实践

从上面的介绍来看,闲鱼、美团的实践方式似乎有着一些不方便之处。比如说不能动态更新 Flutter 代码、Flutter 的 AAR 和主工程一起编译太具有侵入性等等(这里只是我自己浅薄的看法,有异议的同学可以在评论区提出)。所以我这一节要介绍一种侵入性非常小的接入 Flutter 的方式,简单来说就一句话:动态加载 Flutter 生成的 Apk接下来我会结合前面提到的两个 github 项目里的代码进行讲解,大家一定要把这两个项目 clone 下来,当然能点个 star 就更好了。

(1).创建Flutter测试项目

image
  • 1.创建一个 Flutter Project,这个很简单,网上教程很多我就不复述了。
  • 2.创建好了之后如图1所示,我们需要在 app 目录下的 build.gradle 文件中添加一些代码,如代码块1所示。
----代码块1,本文发自简书、掘金:何时夕-----
project.afterEvaluate {
    android.applicationVariants.all { variant ->
        def variantName = variant.name.capitalize()
        def buildTask = project.tasks.findByName("assemble${variantName}")
        if (buildTask) {
            def outputApk = variant.outputs[0].outputFile.path
            def classEntry = "*.dex"
            def soEntry = "lib/*"
            def metaEntry = "META-INF/*"
            def licenseEntry = "assets/flutter_assets/LICENSE"

            buildTask.doLast {
                println variant.outputs[0].outputFile.length()
                exec {
                    commandine 'sh', '-c', "zip -d ${outputApk} ${classEntry}"
                }
                exec {
                    commandLine 'sh', '-c', "zip -d ${outputApk} ${soEntry}"
                }
                exec {
                    commandLine 'sh', '-c', "zip -d ${outputApk} ${metaEntry}"
                }
                exec {
                    commandLine 'sh', '-c', "zip -d ${outputApk} ${licenseEntry}"
                }
            }
        }
    }
}

  • 3.这个代码的主要功能是将 flutter 生成的 apk 中的 classes.dex、libflutter.so、META-INF 等等不需要的文件都删掉,因为我们最终只需要用到 apk 中的 Dart 代码与图片资源。
  • 4.代码加好之后,我们用命令行运行 flutter build apk --debug,这样就会生成一个 debug 版的 apk。其大小为 7.3 MB,没有添加代码块1中的代码之前 debug 版的 apk 大小为 33.5 MB。可以看见这个操作还是非常有有效果的。而如果是 release 版的 apk,其大小还会进一步缩小到 1.5 MB

(2).创建Flutter容器项目

image
  • 1.有了 Flutter 的精简 apk,接下来我们需要用一个容器来加载这个 Flutter apk。具体代码在前面我提到的Flutter 容器项目中,接下来大家就跟随我来看看这个容器是怎么加载 Flutter apk 的吧。
  • 2.如图2,项目中 Flutter 容器是以一个 Android Library 的形式存在的,这样也方便大家能把这个 lib 引入到自己的工程中。我们可以看见 lib 中直接引入的 Flutter.jar,这个 jar 分为 debug 版 和 release 版。jar 中包含了 Flutter 的 java 层代码,与 so 文件。debug 版本大小为 7.3MB ,release 版本则是 3.6MB。这就是最终我们的 apk 会增大的大小,还是可以接受的。而包含 Dart 代码和资源的 apk,我们可以通过动态下载来获取。
----代码块2,本文发自简书、掘金:何时夕-----
public class MainActivity extends AppCompatActivity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    RxPermissions permissions = new RxPermissions(this);
    permissions.setLogging(true);
    permissions.request(Manifest.permission.READ_EXTERNAL_STORAGE)
        .subscribe(aBoolean -> FlutterContainer.init(getApplication(), "/storage/emulated/0/flutter1.apk"));
    findViewById(R.id.aaa).setOnClickListener(v -> startActivity(new Intent(MainActivity.this, Main2Activity.class)));
  }
}

----代码块3,本文发自简书、掘金:何时夕-----
public class FlutterContainer {
  private static final String TAG = "FlutterContainer";
  private static boolean sInitialized = false;
  private static Context sApplicationContext;

  private static String sFlutterInstallPath = "";

  public static void init(@NonNull Application applicationContext,
                          @NonNull FlutterEngine.PrepareFlutterPackage prepareFlutterPackage) {
    init(applicationContext, null, prepareFlutterPackage, null);
  }

  public static void init(@NonNull Application applicationContext,
                          @NonNull String flutterInstallPath) {
    init(applicationContext, flutterInstallPath, null, null);
  }

  public static void init(@NonNull Application applicationContext,
                          @NonNull String flutterInstallPath,
                          @Nullable FlutterEngine.Callback startCallBack) {
    init(applicationContext, flutterInstallPath, null, startCallBack);
  }

  public static void init(@NonNull Application applicationContext,
                          @NonNull FlutterEngine.PrepareFlutterPackage prepareFlutterPackage,
                          @Nullable FlutterEngine.Callback startCallBack) {
    init(applicationContext, null, prepareFlutterPackage, startCallBack);
  }

  /**
   * 只能在 app 启动的时候初始化一次
   *
   * @param applicationContext
   */
  private static void init(@NonNull Application applicationContext, @Nullable String flutterInstallPath,
                           @Nullable FlutterEngine.PrepareFlutterPackage prepareFlutterPackage, @Nullable FlutterEngine.Callback startCallBack) {
    if (sInitialized) {
      return;
    }
    new FlutterManager(applicationContext);
    sInitialized = true;
    sApplicationContext = applicationContext;
    if (!TextUtils.isEmpty(flutterInstallPath)) {
      upgradeFlutterPackage(flutterInstallPath, startCallBack);
    } else if (prepareFlutterPackage != null) {
      upgradeFlutterPackage(prepareFlutterPackage, startCallBack);
    } else {
      Log.i(TAG, "FlutterContainer init no flutter package");
    }
  }

  /**
   * @param flutterInstallPath
   */
  public static void upgradeFlutterPackage(@NonNull String flutterInstallPath, @Nullable FlutterEngine.Callback startCallBack) {
    if (!sInitialized) {
      return;
    }
    FlutterManager.getInstance().resetFlutterPackage();
    sFlutterInstallPath = flutterInstallPath;
    FlutterManager.getInstance().getFlutterEngine().startFast(startCallBack);
  }

  • 3.接下来我们看代码块2,这是一个例子。可以看见 FlutterContainer 就是容器库暴露出来的 api,用于初始化 Flutter 环境以及升级 Flutter Apk。
  • 4.代码块2中调用了 init,所以我们来看看代码块3 FlutterContainer 中的 api。
    • 1.init:方法用于第一次需要初始化 Flutter apk 的时候调用一次,有多个不同的 api。
    • 2.upgradeFlutterPackage:则是用于重新加载 Flutter apk,比如我们需要发布新的 Flutter 版本,就可以使用这个 api 来重新加载一个新的 Flutter apk。
----代码块4,本文发自简书、掘金:何时夕-----
public class FlutterManager {

  private static FlutterManager sInstance;

  private final FlutterEngine mFlutterEngine;
  private final FlutterContextWrapper mFlutterContextWrapper;
  private final Context mContext;

  FlutterManager(Application context) {
    sInstance = this; // 简单单例, 线程并不安全, 逻辑保证
    mFlutterEngine = new FlutterEngine(context);
    mFlutterContextWrapper = new FlutterContextWrapper(context);
    mContext = context;
  }

  public static FlutterManager getInstance() {
    return sInstance;
  }

  public void registerChannel(BinaryMessenger messenger, String channel, BaseHandler handler) {
    new MethodChannel(messenger, channel + ".method").setMethodCallHandler(handler);
    if (handler.mEnableEventChannel) {
      new EventChannel(messenger, channel + ".event").setStreamHandler(handler);
    }
  }

  FlutterEngine getFlutterEngine() {
    return mFlutterEngine;
  }

  public FlutterContextWrapper getFlutterContextWrapper() {
    return mFlutterContextWrapper;
  }

  /**
   * 是否有 Flutter 包可用
   *
   * @return
   */
  public boolean isFlutterAvailable() {
    File activeApk = new File(FlutterContainer.getFlutterInstallPath());
    return activeApk.isFile();
  }

  /**
   * 如果要使用新的 Flutter 包,那么需要重置一下
   */
  void resetFlutterPackage() {
    mFlutterContextWrapper.reset();
  }
}

  • 5.FlutterContainer 相当于初始化 Flutter apk 的入口,那么 FlutterManager 就是具体做这件事情的类了。我们看代码块4,可以了解到 FlutterManager 是一个单例,FlutterContainer.init 中有一个步骤就是初始化这个单例。其中的 api 有下面这些功能:
    • 1.registerChannel:注册 java 和 dart 之间的通信 channel,这个在后面会详细讲解。
    • 2.getFlutterEngine:获取 FlutterEngine,其内部会调用 Flutter 真正加载 apk 的 api。
    • 3.getFlutterContextWrapper:一个 Context 的包装类,主要是为了让 Flutter 能顺利解压出 apk 里面的代码和资源。
----代码块5,本文发自简书、掘金:何时夕-----
public class FlutterContextWrapper extends ContextWrapper {

  private AssetManager sAssets;

  FlutterContextWrapper(Context base) {
    super(base);
  }

  public void reset() {
    sAssets = null; // 在每次安装flutter包之后,需要重新创建新的assets
  }

  @Override
  public Resources getResources() {
    return new Resources(getAssets(), super.getResources().getDisplayMetrics(),
        super.getResources().getConfiguration());
  }

  @Override
  public AssetManager getAssets() {
    if (sAssets != null) {
      return sAssets;
    }

    File activeApk = new File(FlutterContainer.getFlutterInstallPath());
    if (!activeApk.isFile()) {
      return super.getAssets();
    }

    sAssets = ReflectionUtil.newInstance(AssetManager.class);
    ReflectionUtil.callMethod(sAssets, "addAssetPath", activeApk.getPath());
    return sAssets;
  }

  @Override
  public PackageManager getPackageManager() {
    return new FlutterPackageManager(super.getPackageManager());
  }
}

  • 6.因为 Flutter 在 build apk 的时候会将 Dart 代码和资源都放在 asset 中,所以我们需要如代码块5中那样,创建一个 FlutterContextWrapper 来替换 AssetManager,使得 Flutter 加载 apk 时 asset 目录指向我们创建的 Flutter apk 中。
----代码块6,本文发自简书、掘金:何时夕-----
class FlutterEngine {

  private static boolean sInitialized; // 全局标记引擎已经启动
  private final Context mContext;

  FlutterEngine(Context context) {
    mContext = context;
  }

  /**
   * 快速启动模式,表示已经有包了
   */
  void startFast(@Nullable Callback callback) {
    if (sInitialized) {
      // 需要尽快启动,所以需要去重
      callback(callback, null);
      return;
    }
    if (FlutterManager.getInstance().isFlutterAvailable()) { // 当前有可用包
      startFlutterInitialization();
      ensureInitializationComplete();
      callback(callback, null);
    } else {
      DebugUtil.logError(new RuntimeException("startFast but no available package"));
    }
  }

  /**
   * 慢速启动模式, 表示没有报,需要准备
   */
  void startSlow(@Nullable Callback callback, @NonNull PrepareFlutterPackage prepareFlutterPackage) {
    Single.fromCallable(() -> {
      // 此处不去重, 不管是否sInitialized都重新初始化, 保证使用最新flutter包.
      prepareFlutterPackage.prepareFlutterPackage();
      return new Object();
    }).subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(o -> {
          startFlutterInitialization();
          ensureInitializationComplete();
          callback(callback, null);
        }, throwable -> {
          throwable.printStackTrace();
          callback(callback, throwable);
        });
  }

  private static void callback(@Nullable Callback callback, Throwable t) {
    if (callback != null) {
      callback.onCompleted(t);
    }
  }

  private void startFlutterInitialization() { // 不阻塞UI
    // Flutter SDK的start方法可以多次调用, 他的主要作用是解压资源, 因此不用做去重
    FlutterMain.startInitialization(FlutterManager.getInstance().getFlutterContextWrapper());
  }

  private void ensureInitializationComplete() {
    FlutterMain.ensureInitializationComplete(mContext, null);
    sInitialized = true; // 已经初始化
  }

  // 启动回调
  public interface Callback {

    /**
     * 初始化完成.
     *
     * @param e 成功为null,失败不为null.
     */
    void onCompleted(Throwable e);
  }

  // 准备 Flutter 包的回调
  public interface PrepareFlutterPackage {
    String prepareFlutterPackage();
  }
}

  • 7.顺接 FlutterContainer 的调用继续深入,我们会来到代码块6的 FlutterEngine 中,这里主要有两个 api:
    • 1.startFast:如方法名说的那样,这个方法表示快速加载 flutter apk。他只能被调用一次,多次调用会去重,一般来说我们如果已经准备好了 flutter apk 的话, 那么可以使用这个方法来加载 flutter apk。可以看见其内部最终会调用到 FlutterMain.startInitialization,这是 Flutter.jar 中的 api,主要用于解压和移动 Context 中的 Asset。因为我们前面创建了一个 FlutterContextWrapper,所以这里其实会解压 flutter apk 中的 Dart 代码和资源。
    • 2.startSlow:这个方法能调用多次,主要用于升级 apk,多次调用不会去重。如果我们没有准备好 apk,需要从网络中下载,可以使用这个方法。但是最终的原理和 startFast 一样,都是使用 FlutterMain.startInitialization 来解压和移动 Flutter apk 中的资源。
  • 8.到这里成熟项目中无缝引入 Flutter 就完成了。大家可以编译Flutter容器项目然后将Flutter测试项目生成的 apk adb push 到手机的 /storage/emulated/0/flutter1.apk 中,就能体验到动态加载 Flutter apk 的快感了。
  • 9.另外你还可以使用 flutter attch 来对 debug 版的 Flutter apk 进行 hot reload,享受到秒级代码更新的快感。

二、Flutter、Native混合开发

前面完了在成熟项目中无缝引入 Flutter 的方式,这一章我们再来说说 Flutter 和 Native 混合开发的方式。可能会混合开发不是很简单吗,直接嵌入一个 Flutter 的 Activity/Fragment 就能将其作为容器运行 Flutter 了。其实这样的想法太过理想化,如果我的一个 Acitivity/Fragment 中 Flutter 和 Native 都需要有呢?这一章我我就是要来解决这个问题,大家随我一起往下看。

1.Flutter、Native混合开发场景以及闲鱼的实践

  • 1.我们先来聊聊在什么情况下在 Activity/Fragment 中会需要 Flutter、Native 一起使用
    • 1.比如我的一个界面上需要嵌入地图 view,此时如果我需要在这个界面上使用 Flutter 的话,因为 Flutter 的组件远没有 Native 这么完善,像高德地图、百度地图目前都只有 Native 的版本,所以此时就需要 Flutter、Native 混合开发了。
    • 2.再拿目前比较火的短视频 App 们来做例子,例如抖音 App 的视频编辑功能,视频编辑的大部分功能都是基于 Native 层的视频编辑 sdk 来开发的。如果这种界面要上 Flutter 的话,整个视频编辑 sdk 需要提供一 Dart 的版本,这在短时间内都是无法实现的。
    • 3.有了上面两个例子,我们现在大概可以知道在什么场景下需要在一个界面上使用 Flutter、Native 进行混合开发了:Flutter 的控件还无法代替 Native 的控件时,如果某个界面需要上 Flutter 的话,就会出现这样的场景。虽然随着 Flutter 的慢慢发展,慢慢可能会有 Flutter 版的地图、Flutter 版的视频编辑 sdk,但是在最近一两年内,Flutter、Native 混合开发还是一个非常常见的场景。
  • 2.那么我们再来聊聊目前已经有的混合开发的实践,目前闲鱼有写过博客分享自己的混合开发实践:闲鱼的混合开发实践
    • 1.使用 Flutter 提供的 api 将 Android 端的 View 交给 Flutter。
    • 2.因为 Flutter 渲染的方式是 SurfaceView 或者 TextureView,所以 Android 端的 View 会生成一个 Texture(OpenGL的纹理),交给 Flutter 然后让 Flutter 一起渲染在 Surface/TextureVIew 上。
    • 3.相应的手势也由 Flutter 层传递给 Android 层。
  • 3.闲鱼的实践方式当然有它们的优势,例如是官方推荐的实践方式、通用性更好等等。但是其有不可忽视的缺点就是Android View 的 Texture 传递到 Flutter 的流程是 GPU->CPU->GPU,这是一套昂贵的方案。

2.我的实践

为了解决数据传递的昂贵耗损,我想了另外一个办法来绕过这个问题。本小结需要结合Flutter容器项目食用。

  • 1.我们首先得了解 Flutter 在 Android 端渲染的几个前置知识:

    • 1.Flutter 在开始运行之后,画面是渲染到 Android 端的 SurfaceView/TextureView 上面的。
    • 2.要深入了解 SurfaceView 和 TextureView,可以看这篇文章:Android绘制机制以及Surface家族源码全解析
    • 3.Flutter 如果用 SurfaceView 渲染,底层默认是黑的。
    • 4.Flutter 如果用 TextureView 渲染,底层默认是透明的。
    • 5.综上所述,如果当我们使用 TextureView 渲染 Flutter 的时候, 我们可以只将 Flutter 当做 Android 视图层级中的一个普通的 view,它可以在某些 View 的上面或者下面。这就是我们的解决方案:不再把 Flutter 当做一个 Activity 的全部,它只是 View 层级中的一份子,这样一来我们想对这个 View 做啥就做啥。
  • 2.在了解了混合开发的思想之后代码上就非常简单了。

    • 1.首先我们得知道除了 io.flutter.app.FlutterActivity,这个一般我们使用的 Acitivty 外。Flutter 还提供了另一个 io.flutter.embedding.android.FlutterActivity Acitvity,这个 Activity 渲染 Flutter 的方式之一就是使用 TexutreView。
    • 2.当然最后 io.flutter.embedding.android.FlutterAcitivity 还是通过 io.flutter.embedding.android.FlutterFragment 来将 TextureView 添加到 View 的层级中的。
----代码块7,本文发自简书、掘金:何时夕-----
public class FlutterTextureBaseFragment extends FlutterFragment {
  protected FlutterView mFlutterView;
  protected FlutterContextWrapper mFlutterContextWrapper;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = super.onCreateView(inflater, container, savedInstanceState);
    mFlutterView = ViewUtil.getFlutterView(view);
    mFlutterContextWrapper = new FlutterContextWrapper(getContext());
    return mFlutterView;
  }

  @Nullable
  public FlutterView getFlutterView() {
    return mFlutterView;
  }

  public static class TextureBuilder extends FlutterFragment.Builder {
    @NonNull
    public <T extends FlutterFragment> T build() {
      try {
        T frag = (T) FlutterTextureBaseFragment.class.newInstance();
        if (frag == null) {
          throw new RuntimeException("The FlutterFragment subclass sent in the constructor (" + FlutterTextureBaseFragment.class.getCanonicalName() + ") does not match the expected return type.");
        } else {
          Bundle args = this.createArgs();
          frag.setArguments(args);
          return frag;
        }
      } catch (Exception var3) {
        throw new RuntimeException("Could not instantiate FlutterFragment subclass (" + FlutterTextureBaseFragment.class.getName() + ")", var3);
      }
    }
  }

  @Override
  public Context getContext() {
    if (mFlutterContextWrapper == null) {
      return super.getContext();
    } else {
      return mFlutterContextWrapper;
    }
  }
}

  • 3.我们看代码块7,FlutterFragment.Builder 是构建 io.flutter.embedding.android.FlutterFragment 的 Buidler 类,我的 FlutterTextureBaseFragment 主要是为了提供 FlutterView 给外界使用。
----代码块8,本文发自简书、掘金:何时夕-----
public class FlutterTextureBaseActivity extends FlutterActivity {
  protected FlutterView mFlutterView;
  protected FlutterTextureBaseFragment mFlutterTextureBaseFragment;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @Nullable
  public ViewGroup getFlutterViewParent() {
    getFlutterView();
    if (mFlutterView == null) {
      return null;
    } else {
      return (ViewGroup) mFlutterView.getParent();
    }
  }

  @Nullable
  public FlutterView getFlutterView() {
    if (mFlutterTextureBaseFragment == null) {
      return null;
    } else if (mFlutterTextureBaseFragment.getView() != null) {
      mFlutterView = mFlutterTextureBaseFragment.getFlutterView();
      return mFlutterView;
    } else {
      return null;
    }
  }

  @Nullable
  public FlutterTextureBaseFragment getFlutterTextureBaseFragment() {
    return mFlutterTextureBaseFragment;
  }

  @NonNull
  protected FlutterTextureBaseFragment createFlutterFragment() {
    mFlutterTextureBaseFragment = (new FlutterTextureBaseFragment.TextureBuilder())
        .dartEntrypoint(this.getDartEntrypoint())
        .initialRoute(this.getInitialRoute())
        .appBundlePath(this.getAppBundlePath())
        .flutterShellArgs(FlutterShellArgs.fromIntent(this.getIntent()))
        .renderMode(FlutterView.RenderMode.texture)
        .transparencyMode(FlutterView.TransparencyMode.opaque)
        .build();
    return mFlutterTextureBaseFragment;
  }
}

  • 4.在看代码块8,FlutterTextureBaseActivity 继承了 io.flutter.embedding.android.FlutterActivity,主要工作是创建一个以 TexutreVIew 作为渲染方式的 FlutterTextureBaseFragment,然后提供 FlutterView 的 ParentView,以供外部使用。
  • 5.了解了上面的代码之后,大家要在一个 Activity 中进行混合开发也就非常简单了。例如我需要用 Flutter 仿写抖音 App 的视频编辑页,就可以有如下步骤:
    • 1.继承 FlutterTextureBaseActivity 后,将视频编辑 sdk 的 View 放在 FlutterView 的下面,此时 FlutterView 就会透出视频编辑 View。
    • 2.在 Flutter 中开发业务逻辑
    • 3.使用 Channel 让 Flutter 中的行为操作视频编辑 View。
  • 6.我使用我司的视频编辑 sdk 简单的实践了一下视频播放和暂停的功能,如下图3
    • 1.下面的视频播放器是 Android 端 Native 的代码。
    • 2.上面的两个 play 和 stop 的 button 是 Flutter 的代码。
    • 3.因为是公司内部代码,所以不能放在 github 上面,大家见谅。
image

作者:何时夕
链接:https://www.jianshu.com/p/9f578d50ae94
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

推荐阅读更多精彩内容