React Native for Android 源码解析:Reload, Debug JS Remotely具体做了什么?

忽悠妹纸买的splatoon不会玩然后甩给我了,美滋滋

Reload, debug js remotely罪恶滔天,弄的百姓怨声载道

最近使用0.54.0版本开发有个调试的bug非常恶心,debug js remotely总是抛

DeltaPatcher.js:58 Uncaught (in promise) Error: DeltaPatcher should receive a fresh Delta when being initialized
                                                       at DeltaPatcher.applyDelta (DeltaPatcher.js:58)
                                                       at deltaUrlToBlobUrl (deltaUrlToBlobUrl.js:34)
                                                       at <anonymous>

想再次debug就得杀掉进程重新打开,官方解释在0.55版本会修复此问题,看了下pr改动都是js代码,随即更新版本修复此问题。若想以后碰到类似框架性的问题,想要自己能有排错纠错能力,还是老老实实啃源码吧

Reload

首先看看Reload,先从Activity下手,初始demo里MainActivity继承了ReactActivity,RN工程的初始化,加载jsbundle的触发都在这个ReactActivity中,然后具体业务逻辑又交给了它的代理类ReactActivityDelegate,里面做了初始化RN框架逻辑,框架初始化的流程先不管,主要看看reload流程

onKeyUp

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (getReactNativeHost().hasInstance() && getReactNativeHost().getUseDeveloperSupport()) {
      if (keyCode == KeyEvent.KEYCODE_MENU) {
        getReactNativeHost().getReactInstanceManager().showDevOptionsDialog();
        return true;
      }
      boolean didDoubleTapR = Assertions.assertNotNull(mDoubleTapReloadRecognizer)
        .didDoubleTapR(keyCode, getPlainActivity().getCurrentFocus());
      if (didDoubleTapR) {
        getReactNativeHost().getReactInstanceManager().getDevSupportManager().handleReloadJS();
        return true;
      }
    }
    return false;
  }

ReactActivity中侦听了物理按键,在keyCode为82即menu按键的时候,获取了RN主要的管理类ReactInstanceManager,然后调起了调试框DevOptionsDialog,具体业务逻辑在DevSupportManagerImpl这个类中,还可以看到有另外一个doubleTapR操作可以直接进行reload jsbundle,继续跟到DevSupportManagerImpl中,这里定义了调试dialog,跟到R.string.catalyst_reloadjs这个事件,触发了handleReloadJS,reload的流程入口就在这个方法中

handleReloadJS

@Override
  public void handleReloadJS() {

    UiThreadUtil.assertOnUiThread();

    ReactMarker.logMarker(
        ReactMarkerConstants.RELOAD,
        mDevSettings.getPackagerConnectionSettings().getDebugServerHost());

    // dismiss redbox if exists
    hideRedboxDialog();

    if (mDevSettings.isRemoteJSDebugEnabled()) {
      PrinterHolder.getPrinter()
          .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy");
      mDevLoadingViewController.showForRemoteJSEnabled();
      mDevLoadingViewVisible = true;
      reloadJSInProxyMode();
    } else {
      PrinterHolder.getPrinter()
          .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server");
      String bundleURL =
        mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName));
      reloadJSFromServer(bundleURL);
    }
  }

可以看到这个方法主要是在取bundleURL,还区分了debug js remotely模式,可以看到这里的mJSAppBundleName是在构造函里数获取的,然后构造函数用IDE的函数跳转功能并不能找到在哪里构造的,仔细观察DevSupportManagerImpl的接口DevSupportManager,可以看到在DevSupportManagerFactory这个工厂类中有使用,这里是用的反射进行构造的

public static DevSupportManager create(
    Context applicationContext,
    ReactInstanceManagerDevHelper reactInstanceManagerHelper,
    // 这个是mJSAppBundleName
    @Nullable String packagerPathForJSBundleName,
    boolean enableOnCreate,
    @Nullable RedBoxHandler redBoxHandler,
    @Nullable DevBundleDownloadListener devBundleDownloadListener,
    int minNumShakes) {
    if (!enableOnCreate) {
      return new DisabledDevSupportManager();
    }
    try {
      // ProGuard is surprisingly smart in this case and will keep a class if it detects a call to
      // Class.forName() with a static string. So instead we generate a quasi-dynamic string to
      // confuse it.
      String className =
        new StringBuilder(DEVSUPPORT_IMPL_PACKAGE)
          .append(".")
          .append(DEVSUPPORT_IMPL_CLASS)
          .toString();
      Class<?> devSupportManagerClass =
        Class.forName(className);
      Constructor constructor =
        devSupportManagerClass.getConstructor(
          Context.class,
          ReactInstanceManagerDevHelper.class,
          String.class,
          boolean.class,
          RedBoxHandler.class,
          DevBundleDownloadListener.class,
          int.class);
      return (DevSupportManager) constructor.newInstance(
        applicationContext,
        reactInstanceManagerHelper,
        packagerPathForJSBundleName,
        true,
        redBoxHandler,
        devBundleDownloadListener,
        minNumShakes);
    } catch (Exception e) {
      throw new RuntimeException(
        "Requested enabled DevSupportManager, but DevSupportManagerImpl class was not found" +
          " or could not be created",
        e);
    }
  }

跟到最后可以发现是在ReactNativeHost这个抽象类的getJSMainModuleName()方法拿到的,这个方法可以给用户重写进行自定义,再回到handleReloadJS方法,拼接出来的bundleURL长这样
http://localhost:8081/index.delta?platform=android&dev=true&minify=false,host就是我们本地Nodejs启动的服务器地址

public void reloadJSFromServer(final String bundleURL) {
    ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_START);

    mDevLoadingViewController.showForUrl(bundleURL);
    mDevLoadingViewVisible = true;

    final BundleDownloader.BundleInfo bundleInfo = new BundleDownloader.BundleInfo();
    // 触发下载任务
    mDevServerHelper.downloadBundleFromURL(
        // 侦听下载
        new DevBundleDownloadListener() {
          @Override
          public void onSuccess() {
            mDevLoadingViewController.hide();
            mDevLoadingViewVisible = false;
            synchronized (DevSupportManagerImpl.this) {
              mBundleStatus.isLastDownloadSucess = true;
              mBundleStatus.updateTimestamp = System.currentTimeMillis();
            }
            if (mBundleDownloadListener != null) {
              mBundleDownloadListener.onSuccess();
            }
            UiThreadUtil.runOnUiThread(
                new Runnable() {
                  @Override
                  public void run() {
                    ReactMarker.logMarker(ReactMarkerConstants.DOWNLOAD_END, bundleInfo.toJSONString());
                    mReactInstanceManagerHelper.onJSBundleLoadedFromServer();
                  }
                });
          }

          @Override
          public void onProgress(@Nullable final String status, @Nullable final Integer done, @Nullable final Integer total) {
            mDevLoadingViewController.updateProgress(status, done, total);
            if (mBundleDownloadListener != null) {
              mBundleDownloadListener.onProgress(status, done, total);
            }
          }

          @Override
          public void onFailure(final Exception cause) {
            mDevLoadingViewController.hide();
            mDevLoadingViewVisible = false;
            synchronized (DevSupportManagerImpl.this) {
              mBundleStatus.isLastDownloadSucess = false;
            }
            if (mBundleDownloadListener != null) {
              mBundleDownloadListener.onFailure(cause);
            }
            FLog.e(ReactConstants.TAG, "Unable to download JS bundle", cause);
            UiThreadUtil.runOnUiThread(
                new Runnable() {
                  @Override
                  public void run() {
                    if (cause instanceof DebugServerException) {
                      DebugServerException debugServerException = (DebugServerException) cause;
                      showNewJavaError(debugServerException.getMessage(), cause);
                    } else {
                      showNewJavaError(
                          mApplicationContext.getString(R.string.catalyst_jsload_error),
                          cause);
                    }
                  }
                });
          }
        },
        mJSBundleTempFile,
        bundleURL,
        bundleInfo);
  }

这个方法触发了下载任务和下载成功后续的操作,跟进mDevServerHelper.downloadBundleFromUR()方法,走到BundleDownloader类的downloadBundleFromURL方法

public void downloadBundleFromURL(
      final DevBundleDownloadListener callback,
      final File outputFile,
      final String bundleURL,
      final @Nullable BundleInfo bundleInfo) {

    // 实例化okhttp请求
    final Request request =
        new Request.Builder()
            .url(mBundleDeltaClient.toDeltaUrl(bundleURL))
            // FIXME: there is a bug that makes MultipartStreamReader to never find the end of the
            // multipart message. This temporarily disables the multipart mode to work around it,
            // but
            // it means there is no progress bar displayed in the React Native overlay anymore.
            // .addHeader("Accept", "multipart/mixed")
            .build();
    mDownloadBundleFromURLCall = Assertions.assertNotNull(mClient.newCall(request));
    mDownloadBundleFromURLCall.enqueue(
        new Callback() {
          @Override
          public void onFailure(Call call, IOException e) {
            // ignore callback if call was cancelled
            if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
              mDownloadBundleFromURLCall = null;
              return;
            }
            mDownloadBundleFromURLCall = null;

            callback.onFailure(
                DebugServerException.makeGeneric(
                    "Could not connect to development server.",
                    "URL: " + call.request().url().toString(),
                    e));
          }

          @Override
          public void onResponse(Call call, final Response response) throws IOException {
            // ignore callback if call was cancelled
            if (mDownloadBundleFromURLCall == null || mDownloadBundleFromURLCall.isCanceled()) {
              mDownloadBundleFromURLCall = null;
              return;
            }
            mDownloadBundleFromURLCall = null;

            final String url = response.request().url().toString();

            // Make sure the result is a multipart response and parse the boundary.
            String contentType = response.header("content-type");
            Pattern regex = Pattern.compile("multipart/mixed;.*boundary=\"([^\"]+)\"");
            Matcher match = regex.matcher(contentType);
            try (Response r = response) {
              if (match.find()) {
                processMultipartResponse(
                  url, r, match.group(1), outputFile, bundleInfo, callback);
              } else {
                // In case the server doesn't support multipart/mixed responses, fallback to normal
                // download.
                processBundleResult(
                  url,
                  r.code(),
                  r.headers(),
                  Okio.buffer(r.body().source()),
                  outputFile,
                  bundleInfo,
                  callback);
              }
            }
          }
        });
  }

先看看这个方法的形参

  • DevBundleDownloadListener callback:jsbundle下载回调
  • File outputFile:Bundle缓存地址,我这里具体为
    /data/data/com.socketclientrn/files/ReactNativeDevBundle.js
  • String bundleURL:下载jsbundle的URL

再看函数具体逻辑,内部使用了okhttp进行下载,下载成功后,onResponse回调中对返回数据进行了缓存。

private void processBundleResult(
      String url,
      int statusCode,
      Headers headers,
      BufferedSource body,
      File outputFile,
      BundleInfo bundleInfo,
      DevBundleDownloadListener callback)
      throws IOException {
    // Check for server errors. If the server error has the expected form, fail with more info.
    if (statusCode != 200) {
      String bodyString = body.readUtf8();
      DebugServerException debugServerException = DebugServerException.parse(bodyString);
      if (debugServerException != null) {
        callback.onFailure(debugServerException);
      } else {
        StringBuilder sb = new StringBuilder();
        sb.append("The development server returned response error code: ").append(statusCode).append("\n\n")
          .append("URL: ").append(url).append("\n\n")
          .append("Body:\n")
          .append(bodyString);
        callback.onFailure(new DebugServerException(sb.toString()));
      }
      return;
    }

    if (bundleInfo != null) {
      populateBundleInfo(url, headers, bundleInfo);
    }

    File tmpFile = new File(outputFile.getPath() + ".tmp");

    boolean bundleUpdated;

    if (BundleDeltaClient.isDeltaUrl(url)) {
      // If the bundle URL has the delta extension, we need to use the delta patching logic.
      bundleUpdated = mBundleDeltaClient.storeDeltaInFile(body, tmpFile);
    } else {
      mBundleDeltaClient.reset();
      bundleUpdated = storePlainJSInFile(body, tmpFile);
    }

    if (bundleUpdated) {
      // If we have received a new bundle from the server, move it to its final destination.
      if (!tmpFile.renameTo(outputFile)) {
        throw new IOException("Couldn't rename " + tmpFile + " to " + outputFile);
      }
    }

    callback.onSuccess();
  }

内部具体的流操作使用了okio,具体缓存的时候在参数outputFile后面加了个.tmp然后进行存储,存储ok后回调DevBundleDownloadListener
再回到DevSupportManagerImplreloadJSFromServer方法,可以在onSuccess回调中看到判空mBundleDownloadListener然后调用的逻辑,这个回调是初始化DevSupportManagerImpl传进来的,调用链跟到最后是在ReactNativeHostcreateReactInstanceManager方法中构建ReactInstanceManager时传递的,这个方法开发者是可以重写的,提供给开发者侦听jsbundle下载是否成功与失败

createCachedBundleFromNetworkLoader

private ReactInstanceManagerDevHelper createDevHelperInterface() {
    return new ReactInstanceManagerDevHelper() {
      @Override
      public void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) {
        ReactInstanceManager.this.onReloadWithJSDebugger(jsExecutorFactory);
      }

      @Override
      public void onJSBundleLoadedFromServer() {
        ReactInstanceManager.this.onJSBundleLoadedFromServer();
      }

      @Override
      public void toggleElementInspector() {
        ReactInstanceManager.this.toggleElementInspector();
      }

      @Override
      public @Nullable Activity getCurrentActivity() {
        return ReactInstanceManager.this.mCurrentActivity;
      }
    };
  }

跟着调用链,最后走到了createCachedBundleFromNetworkLoader方法里

public static JSBundleLoader createCachedBundleFromNetworkLoader(
      final String sourceURL,
      final String cachedFileLocation) {
    return new JSBundleLoader() {
      @Override
      public String loadScript(CatalystInstanceImpl instance) {
        try {
          instance.loadScriptFromFile(cachedFileLocation, sourceURL, false);
          return sourceURL;
        } catch (Exception e) {
          throw DebugServerException.makeGeneric(e.getMessage(), e);
        }
      }
    };
  }

createCachedBundleFromNetworkLoader构造完JSBundleLoader后,就开始调用CatalystInstanceImpl去加载jsbundle了,CatalystInstance是Java,C,JavaScript三端通信的入口。

/* package */ void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
    mSourceURL = sourceURL;
    jniLoadScriptFromFile(fileName, sourceURL, loadSynchronously);
  }

  private native void jniLoadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously);

可以看到最终加载jsbundle是在C里面完成的

Reload总流程

reload总的流程可以总结为:点击reload -> DevSupportManagerImpl拼接URL,触发下载 -> BundleDownloader请求服务器下载jsbundle -> 回调DevSupportManagerImpl -> 调用CatalystInstanceImpl通知C加载新的jsbundle

Debug JS Remotely

onKeyUp

先看看Debug JS Remotely的点击事件,

options.put(
        remoteJsDebugMenuItemTitle,
        new DevOptionHandler() {
          @Override
          public void onOptionSelected() {
            mDevSettings.setRemoteJSDebugEnabled(!mDevSettings.isRemoteJSDebugEnabled());
            handleReloadJS();
          }
        });

先设置反了一下remote_js_debug这个key,使用SharedPreference存储,然后就走到handleReloadJS方法里

handleReloadJS

if (mDevSettings.isRemoteJSDebugEnabled()) {
      PrinterHolder.getPrinter()
          .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Proxy");
      mDevLoadingViewController.showForRemoteJSEnabled();
      mDevLoadingViewVisible = true;
      reloadJSInProxyMode();
    } else {
      PrinterHolder.getPrinter()
          .logMessage(ReactDebugOverlayTags.RN_CORE, "RNCore: load from Server");
      String bundleURL =
        mDevServerHelper.getDevServerBundleURL(Assertions.assertNotNull(mJSAppBundleName));
      reloadJSFromServer(bundleURL);
    }

这里区分了debug js remotely模式与普通开发模式,主要看看reloadJSInProxyMode方法

private void reloadJSInProxyMode() {
    // When using js proxy, there is no need to fetch JS bundle as proxy executor will do that
    // anyway
    mDevServerHelper.launchJSDevtools();

    JavaJSExecutor.Factory factory = new JavaJSExecutor.Factory() {
      @Override
      public JavaJSExecutor create() throws Exception {
        WebsocketJavaScriptExecutor executor = new WebsocketJavaScriptExecutor();
        SimpleSettableFuture<Boolean> future = new SimpleSettableFuture<>();
        executor.connect(
            mDevServerHelper.getWebsocketProxyURL(),
            getExecutorConnectCallback(future));
        // TODO(t9349129) Don't use timeout
        try {
          future.get(90, TimeUnit.SECONDS);
          return executor;
        } catch (ExecutionException e) {
          throw (Exception) e.getCause();
        } catch (InterruptedException | TimeoutException e) {
          throw new RuntimeException(e);
        }
      }
    };
    mReactInstanceManagerHelper.onReloadWithJSDebugger(factory);
  }

先调用了launchJSDevtools方法,里面仅仅做了一个简单的request,URL为
http://localhost:8081/launch-js-devtools,目的应该是打开调试网页,然后实例化了一个实现JavaJSExecutor.Factory接口的匿名类,create方法会在调用recreateReactContextInBackground方法里的子线程中调用,跟进到connectInternal方法

private void connectInternal(
      String webSocketServerUrl,
      final JSExecutorConnectCallback callback) {
    final JSDebuggerWebSocketClient client = new JSDebuggerWebSocketClient();
    final Handler timeoutHandler = new Handler(Looper.getMainLooper());
    client.connect(
        webSocketServerUrl, new JSDebuggerWebSocketClient.JSDebuggerCallback() {
          // It's possible that both callbacks can fire on an error so make sure we only
          // dispatch results once to our callback.
          private boolean didSendResult = false;

          @Override
          public void onSuccess(@Nullable String response) {
            client.prepareJSRuntime(
                new JSDebuggerWebSocketClient.JSDebuggerCallback() {
                  @Override
                  public void onSuccess(@Nullable String response) {
                    timeoutHandler.removeCallbacksAndMessages(null);
                    mWebSocketClient = client;
                    if (!didSendResult) {
                      callback.onSuccess();
                      didSendResult = true;
                    }
                  }

                  @Override
                  public void onFailure(Throwable cause) {
                    timeoutHandler.removeCallbacksAndMessages(null);
                    if (!didSendResult) {
                      callback.onFailure(cause);
                      didSendResult = true;
                    }
                  }
                });
          }

          @Override
          public void onFailure(Throwable cause) {
            timeoutHandler.removeCallbacksAndMessages(null);
            if (!didSendResult) {
              callback.onFailure(cause);
              didSendResult = true;
            }
          }
        });
    timeoutHandler.postDelayed(
        new Runnable() {
          @Override
          public void run() {
            client.closeQuietly();
            callback.onFailure(
                new WebsocketExecutorTimeoutException(
                    "Timeout while connecting to remote debugger"));
          }
        },
        CONNECT_TIMEOUT_MS);
  }

这里使用了websocket与本地服务器进行连接,服务器URL为:
ws://localhost:8081/debugger-proxy?role=client
继续跟到JSDebuggerWebSocketClientconnect方法

public void connect(String url, JSDebuggerCallback callback) {
    if (mHttpClient != null) {
      throw new IllegalStateException("JSDebuggerWebSocketClient is already initialized.");
    }
    mConnectCallback = callback;
    mHttpClient = new OkHttpClient.Builder()
      .connectTimeout(10, TimeUnit.SECONDS)
      .writeTimeout(10, TimeUnit.SECONDS)
      .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
      .build();

    Request request = new Request.Builder().url(url).build();
    mHttpClient.newWebSocket(request, this);
  }

这里是使用okhttp来和本地服务器进行长连接,建立起连接后可以看到JSDebuggerWebSocketClientonMessagesendMessage方法与服务器通信的逻辑。这里我们先回到reloadJSInProxyMode方法,跟到onReloadWithJSDebugger方法

private void onReloadWithJSDebugger(JavaJSExecutor.Factory jsExecutorFactory) {
    Log.d(ReactConstants.TAG, "ReactInstanceManager.onReloadWithJSDebugger()");
    recreateReactContextInBackground(
        new ProxyJavaScriptExecutor.Factory(jsExecutorFactory),
        JSBundleLoader.createRemoteDebuggerBundleLoader(
            mDevSupportManager.getJSBundleURLForRemoteDebugging(),
            mDevSupportManager.getSourceUrl()));
  }

这里逻辑与普通debug模式差不多,都是构造JSBundleLoaderJavaScriptExecutorFactory,跟到createRemoteDebuggerBundleLoader方法中

createRemoteDebuggerBundleLoader

/**
   * This loader is used when proxy debugging is enabled. In that case there is no point in fetching
   * the bundle from device as remote executor will have to do it anyway.
   */
  public static JSBundleLoader createRemoteDebuggerBundleLoader(
      final String proxySourceURL,
      final String realSourceURL) {
    return new JSBundleLoader() {
      @Override
      public String loadScript(CatalystInstanceImpl instance) {
        instance.setSourceURLs(realSourceURL, proxySourceURL);
        return realSourceURL;
      }
    };
  }

 /**
   * This API is used in situations where the JS bundle is being executed not on
   * the device, but on a host machine. In that case, we must provide two source
   * URLs for the JS bundle: One to be used on the device, and one to be used on
   * the remote debugging machine.
   *
   * @param deviceURL A source URL that is accessible from this device.
   * @param remoteURL A source URL that is accessible from the remote machine
   * executing the JS.
   */
  /* package */ void setSourceURLs(String deviceURL, String remoteURL) {
    mSourceURL = deviceURL;
    jniSetSourceURL(remoteURL);
  }

可以从注释中看出,此时jsbundle也是从本地服务器下载的

跳出逻辑看看JSBundleLoader,暴露了四个方法

  • createAssetLoader 从asset目录中创建loader
  • createFileLoader 从具体某个文件中创建loader
  • createCachedBundleFromNetworkLoader 从URL中加载
  • createRemoteDebuggerBundleLoader 同上

所以加载JSBundle可以归类为以上三种方式

finally

开头的问题是js层面的,好像跟我分析的Java层并没什么卵关系。。

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

推荐阅读更多精彩内容