组件化工具BeeHive(一):事件分发

前言

BeeHive是阿里开源的一个组件化框架工具,其内部是使用Spring框架Service的理念来实现模块解耦的,实际上就是使用protocol-class的方案。另外,在组件化的基础上,BeeHive还增加了一个事件分发的功能来配合使用。

目录

  • 1. 概览
  • 2. 事件分发的作用
  • 3. 事件分发的配置过程
  • 4. 注册Module的几种方式
  • 5. 事件分发的内部实现
  • 6. 事件种类

1. 概览

官方文档中的架构图:


从上图可以看出,BeeHive的工作分为两部分:

  1. 事件分发
    BeeHive本身会监听一些系统事件和应用事件,比如App生命周期、推送、handoff等,当事件发生时,BeeHive将其分发给各个模块,然后各个业务模块就可以在自己的Module类中调用各自的响应方法。

  2. 组件化
    这部分是指在组件化的情况下,实现模块间调用,也就是说,各个模块是相互解耦的,BeeHive使用protocol-class的方案实现这一点。

2. 事件分发的作用

当一个事件被触发时,其对应的响应方法需要被执行,比如界面更新、数据存储等。在这个过程中,会涉及到响应方法的调用和实现这两部分。

首先需要确定的是响应方法的实现都是由模块来完成的(不属于现有模块的响应方法可以看做是属于一个全局模块),针对调用响应方法的位置,这里就有两种调用方式,一个是直接在事件触发点调用(通常是在AppDelegate),另一个是通过BeeHive将事件分发给各个模块,在具体模块的Module类中调用。

为了对比这两种调用方法的差别,下面以一个例子来说明:

一个场景

用户在spotlight搜索一个关键字testA,点击搜索结果,app需要跳转到模块A中的一个界面;
搜索关键字testB,点击搜索结果,app需要跳到模块B中的一个界面。

具体操作如下:


在这个场景中,当用户在spotlight中点击一个搜索结果后,会触发一个handoff事件,这个事件会被AppDelegate接受到,可以把AppDelegate当做是事件的触发点。
这个事件期望的响应是,根据点击的搜索结果,跳转到对应的模块界面中。

2.1. 直接调用

直接调用事件的响应方法,代码如下:(完整项目代码可以查看BeeHive_demo1

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler{
    
    if ([userActivity.activityType isEqualToString:@"com.company.app.moduleA.one"]) {
        
        id<ModuleAServiceProtocol> moduleAService = [[BeeHive shareInstance] createService:@protocol(ModuleAServiceProtocol)];
        [moduleAService pushToModuleAOneViewController];
        
    }else if ([userActivity.activityType isEqualToString:@"com.company.app.moduleB.one"]) {
        
        id<ModuleBServiceProtocol> moduleBService = [[BeeHive shareInstance] createService:@protocol(ModuleBServiceProtocol)];
        [moduleBService pushToModuleBOneViewController];
        
    }
    return YES;
}

上述代码中BeeHive的createService:方法是用来获取对应的模块句柄(下文会具体讲到),使用这个句柄可以直接调用模块的响应方法,跳转到模块对应的界面。
根据userActivity的类型来判断app是跳转到moduleA的界面还是moduleB的界面。

直接调用存在的问题

  • 在事件触发点处(AppDelegate)直接调用模块的响应方法,会将事件的响应代码全都堆积在触发点处,当事件的类别越来越多,这个地方会存在许多判断语句,不便于阅读和维护;
    更重要的是会导致触发点对各个模块产生依赖,继而会影响触发点的稳定性,只要模块稍有改动,触发点也要跟着变动。

  • 在执行事件的响应方法的过程中,会涉及到响应方法的调用逻辑和实现逻辑这两部分。响应方法的实现逻辑通常是在模块中完成的,采用第一种方式,响应方法的调用逻辑会在触发点处完成,整个事件的处理过程会被分割在触发点和模块这两部分中,当需要对事件的响应逻辑做出变动时,则需要在这两部分同时做出改变。
    事件触发点一般是位在主工程中,在大型项目中,模块和主工程一般是分开开发的,并且可能是由不同的开发者开发的,一个事件最好不要同时涉及到这两部分,因为这样会导致这两部分存在某种耦合,不易于维护。

2.2. 事件分发

在使用BeeHive之后,BeeHive会监听这些事件,事件触发后,它会遍历已注册模块对应的Module类,然后调用这些类对应的事件响应方法,这样,BeeHive就将一个事件分发给了所有的Module类。
换句话说,就是给每一个需要响应事件的模块都新建一个对应的Module类,在这个Module类中完成对响应方法的调用,这个Module类和模块将由同一个开发者创建和维护。

这样,一个事件的整个响应过程就都是由模块负责,主工程只需要负责事件分发。事件的处理被隔离在模块内部,即便以后需要修改事件的响应逻辑,也只需要改动模块,主工程不需要任何改动。

3. 事件分发的配置过程

使用BeeHive实现事件分发,共需要三步,下面还是以上文的场景来作为例子进行讲解:(完整项目代码可以查看BeeHive_demo2

  1. 初始化
    BeeHive内部有一个类BHAppDelegate,它的作用就是监听事件的触发,实际项目中的AppDelegate需要继承这个类。
//AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    [BHContext shareInstance].application = application;
    [BHContext shareInstance].launchOptions = launchOptions;
    [BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";
    [BHContext shareInstance].serviceConfigName = @"BeeHive.bundle/BHService";
    
    [BeeHive shareInstance].enableException = YES;
    [[BeeHive shareInstance] setContext:[BHContext shareInstance]];
    
    [super application:application didFinishLaunchingWithOptions:launchOptions];
    ...
    ...
    return YES;
}
  1. 创建并注册Module
    每一个需要响应事件的模块,都需要新建一个对应的Module类,且Module类需要遵守协议BHModuleProtocol,然后使用BeeHive提供的方法将这个Module类注册到BeeHive中,这样,BeeHive才能将事件转发给这个模块。
#import "ModuleAModule.h"
#import "BHService.h"

//注册
@BeeHiveMod(ModuleAModule)
@interface ModuleAModule() <BHModuleProtocol>
@end

@implementation ModuleAModule
...
...
@end

本例中使用宏BeeHiveMod来注册ModuleAModule类,代码如下:

@BeeHiveMod(ModuleAModule)
  1. 调用响应方法
    Module类中,调用事件对应的响应方法,每个Module类只应该处理和本模块相关的事件。
    在[步骤2]创建的Module类中添加响应方法:
//handoff事件响应
- (void)modContinueUserActivity:(BHContext *)context{
    
    NSUserActivity *userActivity = context.userActivityItem.userActivity;
    if ([userActivity.activityType isEqualToString:@"com.company.app.moduleA.one"]) {
    
        id<ModuleAServiceProtocol> moduleAService = [[BeeHive shareInstance] createService:@protocol(ModuleAServiceProtocol)];
        [moduleAService pushToModuleAOneViewController];
    }
}

4. 注册Module的几种方式

BeeHive提供了四种方式来注册成为Module类,其内部实现大多数都是间接调用:

[[BHModuleManager sharedManager] registerDynamicModule:moduleClass]
4.1. 方式一

通过BeeHive类的类方法+ (void)registerDynamicModule:(Class) moduleClass;
其方法实现是直接将消息转发给BHModuleManager类:

+ (void)registerDynamicModule:(Class)moduleClass
{
    [[BHModuleManager sharedManager] registerDynamicModule:moduleClass];
}
4.2. 方式二

使用在协议BHModuleProtocol中定义的宏BH_EXPORT_MODULE,其定义为:

#define BH_EXPORT_MODULE(isAsync) \
+ (void)load { [BeeHive registerDynamicModule:[self class]]; } \
-(BOOL)async { return [[NSString stringWithUTF8String:#isAsync] boolValue];}

这种方式是对第一种方式的调用,同时还增加了对-[async]方法的定义,返回YES表示这个Module类将会被异步加载,用来优化启动时间。Module类都需要遵守这个协议BHModuleProtocol,所以可以在其内部直接使用这个宏。

4.3. 方式三

使用在BHAnnotation类中定义的宏BeeHiveMod,这个宏需要一个类名作为参数,其用法如下:

@BeeHiveMod(ModuleAModule)

只需要这一句代码,ModuleAModule类就会被注册成功。

如何实现?

使用这个宏@BeeHiveMod(ModuleAModule),整个注册过程会分为三步

  1. 存储需要注册的类名
    BeeHiveMod的作用就是存储类名,它会在项目mach-o文件的segment:__DATA中添加一个名为BeehiveMods的section,并将字符串"ModuleAModule"添加到这个section中。

  2. 取出存储的类名
    从项目mach-o文件的(__DATA,BeehiveMods)中,获取所有的类名。

  3. 注册
    使用[步骤2]的类名,调用BHModuleManager类的registerDynamicModule:方法来注册。

4.3.1. 存储类名

BeeHiveMod的定义

#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";
#define BeeHiveDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))

在编译的时,预处理器会根据分段标志将被定义的字符串进行分段,然后每一段都会和宏的参数对比,如果相同就会被替换,空格、括号、运算符和##等都属于一种分段标志。
##一般被用在被替换段和其他段直接接触的情况下,比如替换变量名的一部分,上述宏定义中,就是使用两个##将一个变量名隔离成三段,kname_mod,然后使用参数替换name那一段。

上述宏定义中,还用到了#符号,#后面跟着的必须是宏的参数,它的作用是将参数的值符号化,也就是用一对引号""将参数包围起来。
另外,需要注意的是在使用#进行符号化的时候,其前面和后面的引号的数量必须是偶数,否则,预处理器不会替换和符号化参数。

下面使用预处理命令来验证一下:

新建一个macro.c文件,将上述宏定义写入,并调用

//macro.c

#define BeeHiveDATA(sectname) __attribute((used, section("__DATA, "#sectname" ")))

#define BeeHiveMod(name) \
class BeeHive; char * k##name##_mod BeeHiveDATA(BeehiveMods) = ""#name"";


@BeeHiveMod(ModuleAModule)

在终端输入如下预处理处理命令,预处理阶段会进行宏解析

clang -E macro.c 

输出的关键部分

@class BeeHive; char * kModuleAModule_mod __attribute((used, section("__DATA, ""BeehiveMods"" "))) = """ModuleAModule""";

上述输出中,__attribute((used, section("__DATA, ""BeehiveMods"" ")))表示在项目的mach-o文件的名字为__DATA的segment中添加一个名字为BeehiveMods的section,并将其值设置为字符串"ModuleAModule"

下面使用otool命令来验证一下

首先找到本文项目BeeHive-demo2生成的mach-o文件,在终端执行如下命令来输出这个mach-o文件的所有segment和section:

otool -l BeeHive-demo2 

下面是这个命令的部分输出:

......
Section
  sectname BeehiveMods
   segname __DATA
      addr 0x000000010002b3b8
      size 0x0000000000000010
    offset 177080
     align 2^3 (8)
    reloff 0
    nreloc 0
     flags 0x00000000
 reserved1 0
 reserved2 0
......

在mach-o文件中确实存在BeehiveMods这个section,接下来,继续验证这个section中的值是否是字符串"ModuleAModule"

使用下来命令查看section的内容:

otool -s __DATA BeehiveMods BeeHive-demo2

__DATA表示segment,BeehiveMods表示section,其输出为:

BeeHive-demo2:
Contents of (__DATA,BeehiveMods) section
000000010002b3b8    0001ea48 00000001 0001ead2 00000001 

上述输出表示,section内部包含了两个地址,分别为010001ea48010001ead2,这两个地址的其中一个就是指向字符串"ModuleAModule"

为了找到这两个地址指向的具体值,可以使用下列命名查看mach-o文件中segment为__TEXT,section为__cstring的内容

otool -V -s __TEXT __cstring BeeHive-demo2 

截取输出的开头部分

BeeHive-demo2:
Contents of (__TEXT,__cstring) section
000000010001ea48  ModuleBModule
000000010001ea56  com.company.app.moduleB.one
000000010001ea72  hash
000000010001ea77  TQ,R
000000010001ea7c  superclass
000000010001ea87  T#,R
000000010001ea8c  description
000000010001ea98  T@\"NSString\",R,C
000000010001eaa9  debugDescription
000000010001eaba  moduleA
000000010001eac2  moduleB
000000010001eaca  moduleC
000000010001ead2  ModuleAModule
000000010001eae0  com.company.app.moduleA.one

可以看到010001ea48010001ead2这两个地址分别在上述输出中的第三行和倒数第二行,对应的字符串分别为"ModuleBModule""ModuleAModule"

这样,类名就被存储在mach-o文件的section中了。

4.3.2. 取出类名

BeeHive在文件BHAnnotation.m中注册了一个函数dyld_callback,代码如下:

__attribute__((constructor))
void initProphet() {
    _dyld_register_func_for_add_image(dyld_callback);
}

当一个函数被__attribute__((constructor))修饰时,表示这个函数是这个image的初始化函数,在image被加载时,首先会调用这个函数。(image指的是mach-o和动态共享库,在工程运行时,可以使用lldb命令image list查看这个工程中加载的所有image。)
上述代码表示initProphet函数被指定为mach-o的初始化函数,当dyld(动态链接器)加载mach-o时,执行initProphet函数,其执行时机在man函数和类的load方法之前。

_dyld_register_func_for_add_image(dyld_callback);被执行时,如果已经加载了image,则每存在一个已经加载的image就执行一次dyld_callback函数,在此之后,每当有一个新的image被加载时,也会执行一次dyld_callback函数。
dyld_callback函数在image的初始化函数之前被调用,mach-o是第一个被加载的image,调用顺序是:load mach-o -> initProphet -> dyld_callback -> load other_image -> dyld_callback -> other_image_initializers -> ......)

所以,当程序启动时,会多次调用dyld_callback函数。

dyld_callback函数中,使用下列函数来获取[步骤2]中存储的类名

extern uint8_t *getsectiondata(
    const struct mach_header_64 *mhp,
    const char *segname,
    const char *sectname,
    unsigned long *size);

segname的值为__DATA,sectname的值为BeehiveMods

4.4.3. 注册

dyld_callback函数中,调用BHModuleManager的注册方法,并传入上文中回去的类名

[[BHModuleManager sharedManager] registerDynamicModule:cls];

4.4. 方式四

使用plist文件注册,首先需要指定plist文件的路径,使用如下代码来指定路径:

[BHContext shareInstance].moduleConfigName = @"BeeHive.bundle/BeeHive";

这一句代码一般是在初始化BeeHive时调用

plist文件的格式:

需要注意的是BeeHive.bundle必须添加到项目的主工程的target上,因为BeeHive内部是在[NSBundle mainBundle]的目录下寻找BeeHive.bundle。
当使用cocoapods来加载BeeHive时,默认情况下,BeeHive.bundle是存在于BeeHive.framework中,这个时候使用[NSBundle mainBundle]时获取不到BeeHive.bundle的,解决办法是改用[NSBundle bundleForClass:self.class]或将BeeHive.bundle添加到项目的主工程的target上。

5. 事件分发的内部实现

在BeeHive中,使用BHModuleManager类来负责事件分发,其主要步骤是:

  1. 注册
    存储Module类的对象和事件对应的响应方法
  2. 触发
    通过事件类型,获取需要响应的Module类对象和对应的响应方法,使用performSelector:withObject:执行响应方法。
5.1. 注册

BHModuleManager使用四个实例属性来存储Module类的对象和对应的响应方法

@property(nonatomic, strong) NSMutableArray<NSDictionary *>     *BHModuleInfos;
@property(nonatomic, strong) NSMutableArray     *BHModules;

@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSMutableArray<id<BHModuleProtocol>> *> *BHModulesByEvent;
@property(nonatomic, strong) NSMutableDictionary<NSNumber *, NSString *> *BHSelectorByEvent;

BHModules属性存储了所有注册的Module类的对象。

BHModuleInfos属性保存了对应Module类的一些状态,比如kModuleInfoHasInstantiatedKey表示Module类是否已创建,每一个Module类对应其内部的一个字典。

BHModulesByEvent属性表示每当一个事件触发时,哪些Module类需要响应这个事件。它是一个字典类型,以事件类型eventType作为key,响应这个事件的Module类的对象组成的数组作为value。

BHSelectorByEvent属性存储了事件类型和响应方法名的映射关系,它是一个字典类型,以事件类型eventType作为key,事件的响应方法的字符串名作为value。

相对比较重要的是后面两个属性,当事件触发时,会使用这两个属性。

这四个属性的值都是在注册Module类时设置的,从上文可知,注册Module类时,其内部是调用BHModuleManager类的registerDynamicModule:方法。
registerDynamicModule:方法的内部,首先将传入的Module类参数实例化得到一个对象moduleInstance,然后将其添加到BHModules中,并创建一个对应的字典添加到BHModuleInfos中。(这里就存储好了BHModulesBHModuleInfos这两个属性。)

最后,调用registerEventsByModuleInstance:来给moduleInstance对象注册响应事件。

- (void)registerEventsByModuleInstance:(id<BHModuleProtocol>)moduleInstance
{
    NSArray<NSNumber *> *events = self.BHSelectorByEvent.allKeys;
    [events enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self registerEvent:obj.integerValue withModuleInstance:moduleInstance andSelectorStr:self.BHSelectorByEvent[obj]];
    }];
}

BHSelectorByEvent属性存储了所有事件类型和响应方法名的映射关系,遍历这个属性的所有事件类型,通过事件类型拿到BHModulesByEvent属性对应的响应方法列表,然后将moduleInstance添加到事件的响应方法列表中。(这里就存储好了BHModulesByEvent属性)

如果需要添加自定义事件类型,也就是在BHSelectorByEvent属性中添加一个映射关系,可以使用方法registerCustomEvent:withModuleInstance:andSelectorStr:

5.2. 触发

使用BeeHive时,事件是如何被触发的?

这里还是以handoff为例,查看BeeHive中的BHAnnotation类的handoff代理方法:

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler
{
    if([UIDevice currentDevice].systemVersion.floatValue >= 8.0f){
        //保存参数
        [[BeeHive shareInstance].context.userActivityItem setUserActivity: userActivity];
        [[BeeHive shareInstance].context.userActivityItem setRestorationHandler: restorationHandler];
        //分发事件
        [[BHModuleManager sharedManager] triggerEvent:BHMContinueUserActivityEvent];
    }
    return YES;
}

可以看出,事件是通过BHModuleManager类的triggerEvent:方法分发出去的。triggerEvent:方法接受了一个参数BHMContinueUserActivityEvent,用来表示事件的类型。

查看triggerEvent:的内部实现,发现最终会调用BHModuleManager类的方法handleModuleEvent:forTarget:withSeletorStr:andCustomParam:

- (void)handleModuleEvent:(NSInteger)eventType
                forTarget:(id<BHModuleProtocol>)target
           withSeletorStr:(NSString *)selectorStr
           andCustomParam:(NSDictionary *)customParam
{
    BHContext *context = [BHContext shareInstance].copy;
    context.customParam = customParam;
    context.customEvent = eventType;
    if (!selectorStr.length) {
        selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
    }
    SEL seletor = NSSelectorFromString(selectorStr);
    if (!seletor) {
        selectorStr = [self.BHSelectorByEvent objectForKey:@(eventType)];
        seletor = NSSelectorFromString(selectorStr);
    }
    NSArray<id<BHModuleProtocol>> *moduleInstances;
    if (target) {
        moduleInstances = @[target];
    } else {
        moduleInstances = [self.BHModulesByEvent objectForKey:@(eventType)];
    }
    [moduleInstances enumerateObjectsUsingBlock:^(id<BHModuleProtocol> moduleInstance, NSUInteger idx, BOOL * _Nonnull stop) {
        if ([moduleInstance respondsToSelector:seletor]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
            [moduleInstance performSelector:seletor withObject:context];
#pragma clang diagnostic pop
            
            [[BHTimeProfiler sharedTimeProfiler] recordEventTime:[NSString stringWithFormat:@"%@ --- %@", [moduleInstance class], NSStringFromSelector(seletor)]];
            
        }
    }];
}

这个方法的目的是执行[moduleInstance performSelector:seletor withObject:context],其中moduleInstanceseletor表示事件对应的Module类对象和响应方法,它们分别可以由参数target和参数selectorStr来指定,由于在本例中并没有传入这两个参数,只传入了BHMContinueUserActivityEvent作为参数eventType的值,所以它们需要根据参数eventTypeself.BHModulesByEventself.BHSelectorByEvent这两个属性中获取,这里的self指的是BHModuleManager,它们都是在注册时就设置好了。

6. 事件类型

官方的系统事件流程图:


上图中的事件包含了Application生命周期事件和BeeHive自己扩展的三个事件,ModSetup、ModInit、ModSplash。除了这些事件,BeeHive还监听了推送、3D-Touch等相关的事件。

其完整的事件如下:

typedef NS_ENUM(NSInteger, BHModuleEventType)
{
    //通用事件
    BHMSetupEvent = 0,
    BHMInitEvent,
    BHMTearDownEvent,
    BHMSplashEvent,
    
    //3D-Touch
    BHMQuickActionEvent,
    
    //生命周期
    BHMWillResignActiveEvent,
    BHMDidEnterBackgroundEvent,
    BHMWillEnterForegroundEvent,
    BHMDidBecomeActiveEvent,
    BHMWillTerminateEvent,
    
    //未使用
    BHMUnmountEvent,
    BHMOpenURLEvent,
    BHMDidReceiveMemoryWarningEvent,

    //推送相关事件
    BHMDidFailToRegisterForRemoteNotificationsEvent,
    BHMDidRegisterForRemoteNotificationsEvent,
    BHMDidReceiveRemoteNotificationEvent,
    BHMDidReceiveLocalNotificationEvent,
    BHMWillPresentNotificationEvent,
    BHMDidReceiveNotificationResponseEvent,

    //handoff和相关事件
    BHMWillContinueUserActivityEvent,
    BHMContinueUserActivityEvent,
    BHMDidFailToContinueUserActivityEvent,
    BHMDidUpdateUserActivityEvent,

    //watchApp请求事件
    BHMHandleWatchKitExtensionRequestEvent,

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

推荐阅读更多精彩内容