梳理ReactNative中oc到js的数据流动

以添加CalendarManager为例

.h

#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

#import <Foundation/Foundation.h>

@interface CalendarManager : NSObject<RCTBridgeModule>

@end

.m


@implementation CalendarManager

- (instancetype)init {
  if (self = [super init]) {
    
  }
  return self;
}
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
{
  // Date is ready to use!
  NSLog(@"location1-----%@",location);
}
RCT_EXPORT_METHOD(fangshufeng:(NSString *)name testTwo:(NSString *)location)
{
  // Date is ready to use!
  NSLog(@"location3-----%@",location);
}
RCT_REMAP_METHOD(fangshufengJsName,
                 addEvent:(NSString *)name testTwo:(NSString *)location hh:(NSString*)jj) {
  NSLog(@"location4-----%@",location);
}
@end

运行xcode,在下面的地方打上断点

15033061710856.jpg

输出configJSON,只保留CalendarManager相关的并且json一下就是

{
    "remoteModuleConfig": [
        [
            "CalendarManager",
            null,
            [
                "addEvent", // 这里对应的就是第一个方法addEvent:(NSString *)name location:(NSString *)location 下面两个分别对应第二第三个
                "fangshufeng",
                "fangshufengJsName"
            ],
            null,
            null
        ],
        ...
    ]
}

这是在debug环境的情况下传的是完整配置信息,而在release环境下这个配置信息就只有module信息,而其他的方法信息都是通过懒加载的形式得到

oc通过方法将jsonstring传给js

  [_javaScriptExecutor injectJSONText:configJSON
                  asGlobalObjectNamed:@"__fbBatchedBridgeConfig"
                             callback:onComplete];

js如何接收呢

NativeModules.js

 const bridgeConfig = global.__fbBatchedBridgeConfig;

通过一个global的key__fbBatchedBridgeConfig将数据给到js

let NativeModules : {[moduleName: string]: Object} = {};

可以知道NativeModules本身是一个对象

我们再debugger

(bridgeConfig.remoteModuleConfig || []).forEach((config: ModuleConfig, moduleID: number) => {
    // Initially this config will only contain the module name when running in JSC. The actual
    // configuration of the module will be lazily loaded.
    const info = genModule(config, moduleID);
    debugger;
    if (!info) {
      return;
    }

    if (info.module) {
      NativeModules[info.name] = info.module;
    }
    // If there's no module config, define a lazy getter
    else {
      defineLazyObjectProperty(NativeModules, info.name, {
        get: () => loadModule(info.name, moduleID)
      });
    }
  });
}

可以过滤出CalendarManager如下图所示

具体看看这个方法做的事情可以知道每个item都会经过genModule方法,先来看看

function genModule(config: ?ModuleConfig, moduleID: number): ?{name: string, module?: Object} {
  if (!config) {
    return null;
  }

  const [moduleName, constants, methods, promiseMethods, syncMethods] = config;
  invariant(!moduleName.startsWith('RCT') && !moduleName.startsWith('RK'),
    'Module name prefixes should\'ve been stripped by the native side ' +
    'but wasn\'t for ' + moduleName);

  if (!constants && !methods) {
    // Module contents will be filled in lazily later
    return { name: moduleName };
  }

  const module = {};
  methods && methods.forEach((methodName, methodID) => {
    const isPromise = promiseMethods && arrayContains(promiseMethods, methodID);
    const isSync = syncMethods && arrayContains(syncMethods, methodID);
    invariant(!isPromise || !isSync, 'Cannot have a method that is both async and a sync hook');
    const methodType = isPromise ? 'promise' : isSync ? 'sync' : 'async';
    module[methodName] = genMethod(moduleID, methodID, methodType);
  });
  Object.assign(module, constants);

  if (__DEV__) {
    BatchedBridge.createDebugLookup(moduleID, moduleName, methods);
  }

  return { name: moduleName, module };
}

这个又用到了genMethod方法,好吧先来看看

function genMethod(moduleID: number, methodID: number, type: MethodType) {
  let fn = null;
  if (type === 'promise') {
    fn = function(...args: Array<any>) {
      return new Promise((resolve, reject) => {
        BatchedBridge.enqueueNativeCall(moduleID, methodID, args,
          (data) => resolve(data),
          (errorData) => reject(createErrorFromErrorData(errorData)));
      });
    };
  } else if (type === 'sync') {
    fn = function(...args: Array<any>) {
      return global.nativeCallSyncHook(moduleID, methodID, args);
    };
  } else {
    fn = function(...args: Array<any>) {
      console.log('----', args , 'moduleID:--' +  moduleID , 'methodID:---' + methodID);
      const lastArg = args.length > 0 ? args[args.length - 1] : null;
      const secondLastArg = args.length > 1 ? args[args.length - 2] : null;
      const hasSuccessCallback = typeof lastArg === 'function';
      const hasErrorCallback = typeof secondLastArg === 'function';
      hasErrorCallback && invariant(
        hasSuccessCallback,
        'Cannot have a non-function arg after a function arg.'
      );
      const onSuccess = hasSuccessCallback ? lastArg : null;
      const onFail = hasErrorCallback ? secondLastArg : null;
      const callbackCount = hasSuccessCallback + hasErrorCallback;
      args = args.slice(0, args.length - callbackCount);
      BatchedBridge.enqueueNativeCall(moduleID, methodID, args, onFail, onSuccess);
    };
  }
  fn.type = type;
  return fn;
}

方法分为三种,由oc传给js的时候也有体现的这里我们先直接看aync情况的,这里做了三件事情

  1. 定义了一个接收多个参数额方法;
  2. 在参数列表中取出最后的两个作为js的回调;
  3. 最终调用的是原生的方法这里又引出了另一个方法BatchedBridge.enqueueNativeCall
enqueueNativeCall(moduleID: number, methodID: number, params: Array<any>, onFail: ?Function, onSucc: ?Function) {
    if (onFail || onSucc) {
      if (__DEV__) {
        const callId = this._callbackID >> 1;
        this._debugInfo[callId] = [moduleID, methodID];
        if (callId > DEBUG_INFO_LIMIT) {
          delete this._debugInfo[callId - DEBUG_INFO_LIMIT];
        }
      }
      onFail && params.push(this._callbackID);
      /* $FlowFixMe(>=0.38.0 site=react_native_fb,react_native_oss) - Flow error
       * detected during the deployment of v0.38.0. To see the error, remove
       * this comment and run flow */
      this._callbacks[this._callbackID++] = onFail;
      onSucc && params.push(this._callbackID);
      /* $FlowFixMe(>=0.38.0 site=react_native_fb,react_native_oss) - Flow error
       * detected during the deployment of v0.38.0. To see the error, remove
       * this comment and run flow */
      this._callbacks[this._callbackID++] = onSucc;
    }

    if (__DEV__) {
      global.nativeTraceBeginAsyncFlow &&
        global.nativeTraceBeginAsyncFlow(TRACE_TAG_REACT_APPS, 'native', this._callID);
    }
    this._callID++;

    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);

    if (__DEV__) {
      // Any params sent over the bridge should be encodable as JSON
      JSON.stringify(params);

      // The params object should not be mutated after being queued
      deepFreezeAndThrowOnMutationInDev((params:any));
    }
    this._queue[PARAMS].push(params);

    const now = new Date().getTime();
    if (global.nativeFlushQueueImmediate &&
        (now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS ||
         this._inCall === 0)) {
      var queue = this._queue;
      this._queue = [[], [], [], this._callID];
      this._lastFlush = now;
      global.nativeFlushQueueImmediate(queue);
    }
    Systrace.counterEvent('pending_js_to_native_queue', this._queue[0].length);
    if (__DEV__ && this.__spy && isFinite(moduleID)) {
      this.__spy(
        { type: TO_NATIVE,
          module: this._remoteModuleTable[moduleID],
          method: this._remoteMethodTable[moduleID][methodID],
          args: params }
      );
    } else if (this.__spy) {
      this.__spy({type: TO_NATIVE, module: moduleID + '', method: methodID, args: params});
    }
  }

这个方法一共做了以下几件事情:

  1. 判断如果后面两个参数有回调的话就会为每个回调生成一个唯一的ID这里叫做_callbackID,并将js方法装进一个_callbacks的数组中;

  2. 这里有个_queue对象他的结构如下

        _queue: [Array<number>, Array<number>, Array<any>, number];
        
        在js中分别以`MODULE_IDS` `METHOD_IDS` 和 `PARAMS`来表示数组的第0、1、2号元素,之所以这样命名是达到为了见名之意的效果
    

    完成了第一步的_callbackID的映射,在分别吧对应的模块idmoduleID、方法idmethodID和参数params分别装进_queue对应的数组中

    this._queue[MODULE_IDS].push(moduleID);
    this._queue[METHOD_IDS].push(methodID);
    this._queue[PARAMS].push(params);
    
  3. 回调原生的global.nativeFlushQueueImmediate方法,对应原生的

```
context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls){
  RCTJSCExecutor *strongSelf = weakSelf;
  if (!strongSelf.valid || !calls) {
    return;
  }
  
  RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"nativeFlushQueueImmediate", nil);
  [strongSelf->_bridge handleBuffer:calls batchEnded:NO];
  RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"js_call");
};

```

值得注意的是这里只是做方法的申明,并没有调用。

那我们在回到最开始的genModule方法,也就是说经过genModule方法以后以我们的CalendarManager来说的话也就是返回一个下面形式的对象

{
    name:"CalendarManager",
    {
       addEvent: {
             fn(...){},
             type:"async"
        },
        fangshufeng: {
             fn(...){},
             type:"async"
        },
        fangshufengJsName: {
             fn(...){},
             type:"async"
        }
    }
}

再回到一开始的方法

if (info.module) {
      NativeModules[info.name] = info.module;
    }

从而经过一大圈的变换最后得到的结果就是返回一个NativeModules对象,还是以CalendarManager为例,得到的NativeModules的对象如下

CalendarManager: 
    {
       addEvent: {
             fn(...){},
             type:"async"
        },
        fangshufeng: {
             fn(...){},
             type:"async"
        },
        fangshufengJsName: {
             fn(...){},
             type:"async"
        }
    }

我们自定义的CalendarManager作为了NativeModules的一个属性,CalendarManager所对应的方法列表成了CalendarManager属性
这里只是用CalendarManager为例子来讲的,实际上最后NativeModules是有很多的类似CalendarManager的属性

15033248335271.jpg

也就是说每一个原生模块都对应NativeModules一个属性

到这里也就说完了原生创建一个模块到js的演变过程,我们来总结一下,当我们要创建一个原生模块的时候需要做些什么;

  1. 要继承<RCTBridgeModule>这个协议;

  2. 要在.m中加入RCT_EXPORT_MODULE();还记得之前那个发给js的那个原生模块的jsonString吧这个目的其实是要将当前模块加入配置表中也就是下面的这个方法

    void RCTRegisterModule(Class moduleClass)
    {
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
        RCTModuleClasses = [NSMutableArray new];
      });
    
      RCTAssert([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)],
                @"%@ does not conform to the RCTBridgeModule protocol",
                moduleClass);
    
      // Register module
      [RCTModuleClasses addObject:moduleClass];
    }
    
  3. 对于要暴露给js的oc方法需要使用RCT_EXPORT_METHOD这个宏

    RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)
    

    展开以后就是下面这个样子

    + (NSArray<NSString *> *)__rct_export__300 { 
            return @[@"", 
            @"addEvent:(NSString *)name location:(NSString *)location "]; 
            } 
    - (void)addEvent:(NSString *)name location:(NSString *)location;
    

    到后面要查找方法列表的时候就是通过__rct_export__前缀来查找的,查找的方法如下

            - (NSArray<id<RCTBridgeMethod>> *)methods
{
  if (!_methods) {
    NSMutableArray<id<RCTBridgeMethod>> *moduleMethods = [NSMutableArray new];

    if ([_moduleClass instancesRespondToSelector:@selector(methodsToExport)]) {
      [moduleMethods addObjectsFromArray:[self.instance methodsToExport]];
    }

    unsigned int methodCount;
    Class cls = _moduleClass;
    while (cls && cls != [NSObject class] && cls != [NSProxy class]) {
      Method *methods = class_copyMethodList(object_getClass(cls), &methodCount);

      for (unsigned int i = 0; i < methodCount; i++) {
        Method method = methods[i];
        SEL selector = method_getName(method);
        if ([NSStringFromSelector(selector) hasPrefix:@"__rct_export__"]) {
          IMP imp = method_getImplementation(method);
          NSArray<NSString *> *entries =
            ((NSArray<NSString *> *(*)(id, SEL))imp)(_moduleClass, selector);
          id<RCTBridgeMethod> moduleMethod =
            [[RCTModuleMethod alloc] initWithMethodSignature:entries[1]
                                                JSMethodName:entries[0]
                                                 moduleClass:_moduleClass];

          [moduleMethods addObject:moduleMethod];
        }
      }

      free(methods);
      cls = class_getSuperclass(cls);
    }

    _methods = [moduleMethods copy];
  }
  return _methods;
}

这个方法做了两个事情,一个是查找已__rct_export__开头的方法,一个是以__rct_export__方法开头的第一个参数为js方法的名称。

  1. 由3可知以这个宏RCT_EXPORT_METHOD的话默认给js的方法名称是参数的第一位,可以知道都是空的,可是我们最后是需要给js一个方法一个名称的这里oc做了默认处理了
    - (NSString *)JSMethodName
{
  NSString *methodName = _JSMethodName;
  if (methodName.length == 0) {
    methodName = _methodSignature;
    NSRange colonRange = [methodName rangeOfString:@":"];
    if (colonRange.location != NSNotFound) {
      methodName = [methodName substringToIndex:colonRange.location];
    }
    methodName = [methodName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    RCTAssert(methodName.length, @"%@ is not a valid JS function name, please"
              " supply an alternative using RCT_REMAP_METHOD()", _methodSignature);
  }
  return methodName;
}

也就是如果JSMethodName的长度为空的话就会截取第二个参数的:以前的字符串作为方法,也就是
RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)对应addEventRCT_EXPORT_METHOD(fangshufeng:(NSString *)name testTwo:(NSString *)location)对应fangshufeng,那这样的话问题就来了,万一还有个这样的方法RCT_EXPORT_METHOD(fangshufeng:(NSString *)name)按照默认的规则岂不是就和RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location)冲突了吗,rn为了避免这种情况,我们可以使用这个宏RCT_REMAP_METHOD,也就是这种用法

    RCT_REMAP_METHOD(fangshufengJsName,
                 addEvent:(NSString *)name testTwo:(NSString *)location hh:(NSString*)jj) {
  NSLog(@"location4-----%@",location);
}

这样的话之前那个数组的参数第一个值就有值了对应fangshufengJsName

  1. 在研究js源码的时候我们发现,js对参数的解析是有要注意的地方的,如果原生想要使用js的回调的话只能放在参数的后两位
    RCT_EXPORT_METHOD(addEvent:(NSString *)name location:(NSString *)location action:(RCTResponseSenderBlock)action)
{
  // Date is ready to use!
  NSLog(@"location1-----%@",location);
}

这样的话就是错的

    RCT_EXPORT_METHOD(addEvent:(NSString *)name   action:(RCTResponseSenderBlock)action location:(NSString *)location)
    {
      // Date is ready to use!
      NSLog(@"location1-----%@",location);
    }

接下来将要讲的是当自定义的模块方法被调用时数据是如何流动的。

首先我们先把CalendarManager用起来

import  React,{Component} from 'react';
import {
    NativeModules,
    Text,
    TouchableOpacity,
    NativeAppEventEmitter,
    View,
} from 'react-native'

import  FadeInView from './FadeInView'

let CalendarManager = NativeModules.CalendarManager;

export  default  class TestAnimal extends  Component {

    render() {
        return (
            <TouchableOpacity
                 onPress={() => {CalendarManager.fangshufeng('name','ggg')}}
            >
            <FadeInView style={{width: 250, height: 50, backgroundColor: 'powderblue'}}

            >
                <Text style={{fontSize: 28, textAlign: 'center', margin: 10}}>Fading in</Text>
            </FadeInView>
            </TouchableOpacity>
        )
    }

}

运行的结果

15033660664537.jpg

点击fading in也就是调用fangshufeng方法,之前我们说过enqueueNativeCall方法只是做方法申明,这个时候就是调用之前定义的方法,在我的这个工程里面可以debug到,fangshufeng这个方法的moduleID1methodID1

15033664429808.jpg

然后debug到enqueueNativeCall

15033665264435.jpg

可以看到最终传给oc的queue是什么

15033665894650.jpg

如果只看CalendarManager的话当你点击Fading in的时候传给oc的queue就是

[[1],[1],[['name','ggg']]]

然后通过nativeFlushQueueImmediate方法给到oc,会进到下面的这个方法

- (void)handleBuffer:(NSArray *)buffer
{
  NSArray *requestsArray = [RCTConvert NSArray:buffer];

  NSArray<NSNumber *> *moduleIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldRequestModuleIDs]];
  NSArray<NSNumber *> *methodIDs = [RCTConvert NSNumberArray:requestsArray[RCTBridgeFieldMethodIDs]];
  NSArray<NSArray *> *paramsArrays = [RCTConvert NSArrayArray:requestsArray[RCTBridgeFieldParams]];

  int64_t callID = -1;

  if (requestsArray.count > 3) {
    callID = [requestsArray[RCTBridgeFieldCallID] longLongValue];
  }

    // 1
  NSMapTable *buckets = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory
                                                  valueOptions:NSPointerFunctionsStrongMemory
                                                      capacity:_moduleDataByName.count];

    //2
  [moduleIDs enumerateObjectsUsingBlock:^(NSNumber *moduleID, NSUInteger i, __unused BOOL *stop) {
    RCTModuleData *moduleData = self->_moduleDataByID[moduleID.integerValue];
    dispatch_queue_t queue = moduleData.methodQueue;
    NSMutableOrderedSet<NSNumber *> *set = [buckets objectForKey:queue];
    if (!set) {
      set = [NSMutableOrderedSet new];
      [buckets setObject:set forKey:queue];
    }
    [set addObject:@(i)];
  }];

    //3
  for (dispatch_queue_t queue in buckets) {
    dispatch_block_t block = ^{
      NSOrderedSet *calls = [buckets objectForKey:queue];
      @autoreleasepool {
        for (NSNumber *indexObj in calls) {
          NSUInteger index = indexObj.unsignedIntegerValue;
          [self callNativeModule:[moduleIDs[index] integerValue]
                          method:[methodIDs[index] integerValue]
                          params:paramsArrays[index]];
        }
      }
    };
        [self dispatchBlock:block queue:queue];
    }

  _flowID = callID;
}

在需要的解释的地方做了个标记

  1. 由于下面要用到object->object 所以这里使用map也就是NSMapTable来保存数据;
  2. 这个地方可能有点绕,如果对数据结构不清楚的话比较难以理解,还是以CalendarManager为例子,先来假设其他的数据如下所示:
    假设js传给oc的queue是这个样子的
    [
        [38,19,41,41,1], // 最后一个1表示`CalendarManager`模块
        [19,1,4,3,1], // 最后一个1表示addEvent: location: 方法
        [[...],[...],[...],[...],['name','ggg']] //['name','ggg']表示方面方法的入参
    ]

我们通过断点来跟踪一下数据

`modules`
    <__NSArrayI 0x7b048e30>(
    38,
    19,
    41,
    41,
    1
    )
    

buckets

    po buckets
    NSMapTable {
    [10] <OS_dispatch_queue: com.facebook.react.CalendarManagerQueue[0x7b652310]> -> {(
        4
    )}
    [46] <OS_dispatch_queue: com.facebook.react.ShadowQueue[0x7b19cb90]> -> {(
        0,
        2,
        3
    )}
    [47] <null> -> {(
        1
    )}
    }
    po methodIDs
    <__NSArrayI 0x7b048da0>(
    19,
    1,
    4,
    3,
    1
    )
    po paramsArrays
    <__NSArrayI 0x7ee5f7c0>(
    <__NSArray0 0x7a648bd0>(
    
    )
    ,
    <__NSSingleObjectArrayI 0x7b04c8d0>(
    47
    )
    ,
    <__NSSingleObjectArrayI 0x7b04daf0>(
    3
    )
    ,
    <__NSArrayI 0x7ee55570>(
    4,
    3,
    {
        frames =     (
            0,
            "0.008888888888888889",
            "0.03555555555555556",
            "0.08000000000000002",
            "0.1422222222222222",
            "0.2222222222222223",
            "0.3200000000000001",
            "0.4355555555555557",
            "0.5644444444444444",
            "0.6799999999999999",
            "0.7777777777777777",
            "0.8577777777777778",
            "0.9199999999999999",
            "0.9644444444444443",
            "0.9911111111111111",
            1,
            1
        );
        iterations = 1;
        toValue = 1;
        type = frames;
    },
    13
    )
    ,
    <__NSArrayI 0x7b2630e0>(
    name,
    ggg
    )
    

做的事情就是:

遍历模块的数组 -> 在_moduleDataByID取出moduleData -> 以moduleData中的串行队列作为key set作为value,而set中装着的对应的索引,由于module、method、Params都是索引值是一一对应的,这个索引值即是当前module在modules数组的索引值,也是method、Params的索引值。

  1. 上面已经给出了bucket的具体内容,循环后也就是调用下面的方法
    [self callNativeModule:[moduleIDs[index] integerValue]
                              method:[methodIDs[index] integerValue]
                              params:paramsArrays[index]];
    
    // 具体就是
    [self callNativeModule:1 //可以知道这个1就是`CalendarManager`
                    method:1 // 这个1就是表示addEvent: location: 方法
                    params:['name','ggg'] //表示方面方法的入参
                    ];

    //具体上面怎么解析的下面会做分析

oc拿到数据得到模块名称、方法名称、参数,然后执行

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

推荐阅读更多精彩内容