跨端框架Hummer 是如何渲染的?(iOS 版)

image_t83BSP8Sgc.png

Hummer是目前滴滴货运司机端正在使用的跨端框架,之前在开发需求的时候碰到了诡异的布局问题,由于自己是刚接触,完全不知道从哪里下手排查问题,于是请来了兄弟部门 Hummer iOS 现在的维护者史广远,看着他一步一步地打断点,打日志,最终成功定位问题,给出了解决方案。心里敬佩得很。

所以既为了以后开发能够有独立排查问题的能力,也为了提高自己对于跨端技术的理解,这就来整理了下Hummer 的渲染机制,我看以后还有什么问题能难倒我?

image_APZMBxd5cB.png

从我们的题目开始:Hummer在 iOS 侧是如何渲染的?

如同把大象塞进冰箱,我分为 5 步:

  1. 创建:把组件实例化,创建出来
  2. 设置:设置它的 style 和属性
  3. 渲染:把准备好的组件添加到 yoga 节点树上,也是添加到屏幕上
  4. 标记:标记需要重新计算、刷新的组件
  5. 布局:调用 yoga 对组件进行布局

Hummer 既可以用 TypeScript 写,也可以用Tenon 写,本文采用第一种写法。

首先来看用TypeScript 写的业务代码:

import {Hummer,View, Text} from '@hummer/hummer-front'
class RootView extends View {
  constructor() {
    super();
    this.style = {
      width: '100%',
      height: '100%',
      alignItems: 'flex-start',
      justifyContent: 'center'
    }
    // 创建了一个 Text 组件
    var text = new Text()
    text.style = {
      fontSize: 20
    }
    text.text = '~ Hello Hummer ~'
    this.appendChild(text);
  }
}

// 根页面渲染
Hummer.render(new RootView());

简单,天真,几乎只写了一个 Text 组件。

1 实例化:「特别鸣谢你制造更欢乐的我」

在执行 var text = new Text() 时,在 Native 侧就得创建一个 HMLabel实例,并且要把实例装入一个 JS 对象中返回给 JS,它还得持有一下。这一步交给谁来做 ——hummerCreate: 函数。

1 调用 HumemrBase 的 constructor 方法
2 根据 name 属性获取 className & check cache
3调用 globalThis 的 hummerCreate 方法生成 native 对应的视图实例 & 被 this._ptivate 持有
——《跨端框架 Hummer 通信机制 - iOS 版》

JSValueRef _Nullable hummerCreate(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef _Nonnull arguments[], JSValueRef *exception) {
    HMAssertMainQueue(); 
    if (argumentCount < 2) {
        HMLogError(HUMMER_CREATE_ERROR);

        return NULL;
    }
    HMJSCExecutor *executor = (HMJSCExecutor *) [HMExecutorMap objectForKey:[NSValue valueWithPointer:JSContextGetGlobalContext(ctx)]];
    NSString *className = [executor convertValueRefToString:arguments[0] isForce:NO];
    if (className.length == 0) {
        HMLogError(HUMMER_CREATE_ERROR);
        return NULL;
    }
    // 获取导出类
    HMExportClass *exportClass = HMExportManager.sharedInstance.jsClasses[className];
    NSString *objcClassName = exportClass.className;
    if (objcClassName.length == 0) {
        HMLogError(HUMMER_CREATE_CLASS_NOT_FOUND, className);

        return NULL;
    }
    // className 是 Hummer 侧的类名(比如 Text)
    // objcClassName 是 Native 侧的类名(比如 HMLabel)
    Class clazz = NSClassFromString(objcClassName);
    if (!clazz) {
        HMLogError(HUMMER_CREATE_CLASS_NOT_FOUND, className);
        return NULL;
    }

    // jscall回调
    HMJSContext *context = [[HMJSGlobal globalObject] currentContext:executor];
    
    // 创建不透明指针
    NSObject *opaquePointer = NULL;
    NSMutableArray<HMBaseValue *> *argumentArray = nil;
    for (int i = 2; i < argumentCount; ++i) {
        HMBaseValue *value = [[HMJSCStrongValue alloc] initWithValueRef:arguments[i] executor:executor];
        if (!argumentArray) {
            argumentArray = [NSMutableArray arrayWithCapacity:argumentCount - 2];
        }
        if (value) {
            [argumentArray addObject:value];
        }
    }
#ifdef HMDEBUG
    [HMJSCallerInterceptor callNativeWithClassName:className functionName:@"constructor" objectRef:nil args:argumentArray context:context];
#endif
    HMCurrentExecutor = executor;
    // 支持 HMJSObject,如果不支持则回退 init
    // 不判断 argumentCount > 2,因为 UIView 必须调用 HMJSObject 初始化方法
    if ([clazz conformsToProtocol:@protocol(HMJSObject)]) {
        opaquePointer = (id) [(id) [clazz alloc] initWithHMValues:argumentArray];
    } else {
        HMOtherArguments = argumentArray.copy;
        opaquePointer = [[clazz alloc] init];
    }
    HMCurrentExecutor = nil;
    HMOtherArguments = nil;
    if (!opaquePointer) {
        HMLogError(HUMMER_CAN_NOT_CREATE_NATIVE_OBJECT, className);
        return NULL;
    }
    // 关联 hm_value
    opaquePointer.hmValue = [[HMJSCStrongValue alloc] initWithValueRef:arguments[1] executor:executor];
    // 引用计数 +1
    HMLogDebug(HUMMER_CREATE_TEMPLATE, className);
    JSClassDefinition hostObjectClassDef = kJSClassDefinitionEmpty;
    hostObjectClassDef.version = 0;
    hostObjectClassDef.attributes = kJSClassAttributeNoAutomaticPrototype;
    hostObjectClassDef.finalize = hummerFinalize;
    JSClassRef hostObjectClass = JSClassCreate(&hostObjectClassDef);

    // 填充不透明指针
    JSObjectRef objectRef = JSObjectMake(ctx, hostObjectClass, (__bridge void *) opaquePointer);
    if (objectRef) {
        CFRetain((__bridge CFTypeRef) opaquePointer);
    }
    JSClassRelease(hostObjectClass);

    return objectRef;
}

这个函数做了:

  1. 根据 Hummer 侧传过来的消息获取到了在 Native 侧注册的类
  2. 创建这个类
  3. 创建不透明指针,把这个类填到不透明指针当中
  4. 返回不透明指针

再次强调:最终返回的是JSObjectRef 对象,不是简单的 HMLabel 对象。

我猜测这样做的原因是 Native需要跟 JavaScriptCore 进行通信,只创建一个 HMLabel 你怎么跟 Hummer 侧进行交流,后续 Hummer 怎么拿到这个对象进行设置?

2.1设置 style:

HMLabel 创建出来后,就需要设置它的样式,比如 height、width等。Hummer 侧会一个发 setStyle 的信号,Native会调用hm_setStyle: 方法

这层转化是通过 HM_EXPORT_PROPERTY 这个宏实现的

- (void)hm_setStyle:(HMBaseValue *)style {
    // 转化交给 HMJSCExecutor 完成
    id styleObject = style.toDictionary;
    NSDictionary *styleDic = nil;
    if ([styleObject isKindOfClass:NSDictionary.class]) {
        styleDic = styleObject;
    }
    if (styleDic.count == 0) {
        HMLogError(@"style 必须有键值对");
        return;
    }
    // 把 style 信息划分成三个部分:布局、属性(样式)、过渡信息(动画相关)
    NSMutableDictionary<NSString *, NSObject *> *layoutInfo = NSMutableDictionary.dictionary;
    NSMutableDictionary<NSString *, NSObject *> *attributes = NSMutableDictionary.dictionary;
    NSMutableDictionary<NSString *, NSObject *> *transitions = NSMutableDictionary.dictionary;
    [styleDic enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
        if (![key isKindOfClass:NSString.class]) {
            return;
        }
        
        // 比如 left,flexDirection
        if ([[self hm_layoutInfoKeys] containsObject:key]) {
            layoutInfo[key] = obj;
        } else if ([[self hm_transtionInfoKeys] containsObject:key]) {
        // 比如 transitionDelay,
            transitions[key] = obj;
        } else {
        // 比如 backgroundColor
            attributes[key] = obj;
        }
        // 布局、属性和过渡信息可以看文档,看看有什么
        // https://hummer.didi.cn/doc#/zh-CN/normal_view_style
    }];

    NSMutableDictionary<NSString *, NSObject *> *mutableStyleDictionary = self.hm_styleStore.mutableCopy;
    if (!mutableStyleDictionary) {
        mutableStyleDictionary = [NSMutableDictionary dictionaryWithCapacity:layoutInfo.count + attributes.count + transitions.count];
    }
    // 把分好的三部分又装了回去,让 self.hm_styleStore 持有
    self.hm_styleStore = nil;
    [mutableStyleDictionary addEntriesFromDictionary:layoutInfo];
    [mutableStyleDictionary addEntriesFromDictionary:attributes];
    [mutableStyleDictionary addEntriesFromDictionary:transitions];
    self.hm_styleStore = mutableStyleDictionary;

    ...
    [attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSObject *obj, BOOL *stop) {
        // 过滤需要动画的属性,通过动画展示
        if (self.hm_transitionAnimation && [self.hm_transitionAnimation.needAnimations.allKeys containsObject:key]) {
            [transitionAnimations addEntriesFromDictionary:@{key: obj}];
        } else {
            // layoutInfo、attributes 就通过这个设置
            [self hm_configureWithTarget:self cssAttribute:key value:obj converterManager:HMAttrManager.sharedManager];
        }
    }];
    
    ...
    // 标记:该结点需要重新计算位置
    [self hm_markDirty];
}

用我易(粗)懂(暴)的语言说就是:

  1. HMJSCExecutor 把 Hummer 侧的 style 字典转成NSDictionary
  2. 把NSDictionary里的元素划分成三种不同的 style:布局、属性、过渡相关的
  3. 设置给hm_styleStore持有
  4. 通过hm_configureWithTarget: cssAttribute: value:converterManager: 函数实际去设置对应的 style 值

2.2 设置 Property (比如 Text 的 text 属性)

// 不管是 get 还是 set 都是调用这个方法
- (JSValueRef)hummerGetSetPropertyWithArgumentCount:(size_t)argumentCount arguments:(const JSValueRef _Nonnull[])arguments isSetter:(BOOL)isSetter {
    ...
    // 这里为什么要把 argument[0] 转成 JSObjectRef?
    JSObjectRef objectRef = NULL;
    if (JSValueIsObject(self.contextRef, arguments[0])) {
        JSValueRef exception = NULL;
        objectRef = JSValueToObject(self.contextRef, arguments[0], &exception);
        [self popExceptionWithErrorObject:&exception];
    }
    ...

    // 获取对应的类名
    NSString *className = [self convertValueRefToString:arguments[objectRef ? 1 : 0] isForce:NO];
    if (className.length == 0) {
        return NULL;
    }
    NSString *propertyName = [self convertValueRefToString:arguments[objectRef ? 2 : 1] isForce:NO];
    if (propertyName.length == 0) {
        return NULL;
    }

    id target = nil;
    SEL selector = nil;
    NSMethodSignature *methodSignature = nil;

    [self hummerExtractExportWithFunctionPropertyName:propertyName objectRef:objectRef target:&target selector:&selector methodSignature:&methodSignature isSetter:isSetter jsClassName:className];
    // 通过导出类、导出方法,获取到了 Native 类、对应的方法

    ...
    // 转发消息,调用原生方法
    return [self hummerCallNativeWithArgumentCount:argumentCount arguments:arguments target:target selector:selector methodSignature:methodSignature];
}

- (JSValueRef)hummerCallNativeWithArgumentCount:(size_t)argumentCount arguments:(JSValueRef const[])arguments target:(id)target selector:(SEL)selector methodSignature:(NSMethodSignature *)methodSignature {
    ...
    BOOL isClass = object_isClass(target);
    NSMutableArray<HMBaseValue *> *otherArguments = nil;
    HMCurrentExecutor = self;
    // 隐含着 numerOfArguments + 0/1 <= argumentCount
    for (NSUInteger i = methodSignature.numberOfArguments + (isClass ? 0 : 1); i < argumentCount; ++i) {
        // 多余的转数组
        HMBaseValue *hummerValue = [[HMJSCStrongValue alloc] initWithValueRef:arguments[i] executor:HMCurrentExecutor];
        if (!otherArguments) {
            otherArguments = [NSMutableArray arrayWithCapacity:argumentCount - methodSignature.numberOfArguments];
        }
        if (hummerValue) {
            [otherArguments addObject:hummerValue];
        }
    }
    // 存储额外参数
    HMOtherArguments = otherArguments.copy;
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
    invocation.target = target;
    invocation.selector = selector;
    // 后续做循环,都是临时变量,如果不做 retain,会导致野指针
    [invocation retainArguments];

    // 参数
    // 本质为 MIN(methodSignature.numberOfArguments, argumentCount - (isClass : 0 : 1)),主要为了防止无符号数字溢出
    for (NSUInteger i = 2; i < MIN(methodSignature.numberOfArguments + (isClass ? 0 : 1), argumentCount) - (isClass ? 0 : 1); ++i) {
        const char *objCType = [methodSignature getArgumentTypeAtIndex:i];
        HMEncodingType type = HMEncodingGetType(objCType);
        id param = nil;
        if (type == HMEncodingTypeBlock) {
            // Block
            param = [(HMJSCExecutor *) HMCurrentExecutor convertValueRefToFunction:arguments[i + (isClass ? 0 : 1)]];
        } else if (type == HMEncodingTypeObject) {
            // HMJSCValue
            param = [[HMJSCStrongValue alloc] initWithValueRef:arguments[i + (isClass ? 0 : 1)] executor:HMCurrentExecutor];
        } else if (HMEncodingTypeIsCNumber(type)) {
            // js 只存在 double 和 bool 类型,但原生需要区分具体类型。
            param = [(HMJSCExecutor *) HMCurrentExecutor convertValueRefToNumber:arguments[i + (isClass ? 0 : 1)] isForce:NO];
        } else {
            HMLogError(HUMMER_UN_SUPPORT_TYPE_TEMPLATE, objCType);
        }
        [invocation hm_setArgument:param atIndex:i encodingType:type];
    }
    // 拼装完毕,执行方法
    [invocation invoke];
    HMOtherArguments = nil;
    // 返回值
    JSValueRef returnValueRef = NULL;
    const char *objCReturnType = methodSignature.methodReturnType;
    HMEncodingType returnType = HMEncodingGetType(objCReturnType);
    if (returnType != HMEncodingTypeVoid && returnType != HMEncodingTypeUnknown) {
        id returnObject = [invocation hm_getReturnValueObject];
        if (returnObject) {
            returnValueRef = [(HMJSCExecutor *) HMCurrentExecutor convertObjectToValueRef:returnObject];
        }
    }
    HMCurrentExecutor = nil;

    return returnValueRef;
}

继续粗暴描述:

  1. 获取到 Native 侧已经注册的类名,获取到 getter 和 setter
  2. 拼装NSInvocation,处理参数,然后调用 invoke 方法执行Native 的 getter 和 setter
  3. 执行完成后,获取返回值
  4. 最后把JSValueRef返回给JSC(我理解就是 Hummer 侧)

3 render渲染

该创建的创了,该设置的设了,然后得把它们渲染到屏幕上

- (void)render:(HMBaseValue *)page {
    id<HMBaseExecutorProtocol> executor = page.context;
    HMJSContext *context = [HMJSGlobal.globalObject currentContext:executor ? executor : HMCurrentExecutor];
    context.didCallRender = YES;
    // 里面还是用 HMJSCExecutor 将 HMBaseValue 转成具体的类,比如 UIView
    NSObject *viewObject = page.toNativeObject;
    if (!viewObject || ![viewObject isKindOfClass:UIView.class]) {
        return;
    }
    UIView *view = (UIView *) viewObject;
    [context didRenderPage:page nativeView:view];
}

- (void)didRenderPage:(HMBaseValue *)page nativeView:(nonnull UIView *)view{
    
    self.componentView = page;
    // 把这个 View 添加到屏幕上(会调用 Yoga 把节点插入到节点树上)
    [self.rootView addSubview:view];
    self.rootView.isHmLayoutEnabled = YES;
    [self.rootView hm_markDirty];
    if ([self.delegate respondsToSelector:@selector(context:didRenderPage:)]) {
        [self.delegate context:self didRenderPage:page];
    }
    if (self.renderCompletion) {
        self.renderCompletion();
    }
    // 排序当前视图
    [UIView hm_reSortFixedView:self];
    ...
}

复杂地概括下:

  1. 将 page 转成 Native 的 UIView 对象
  2. 渲染 UIView
  3. 把这个 UIView 添加到 rootView 上,插入到 Yoga节点树

4 markDirty:

- (void)hm_markDirty {
    if (!self.isHmLayoutEnabled) {
        return;
    }

    // 1. 叶节点 -> 叶节点(脏),需要 markDirty + setNeedsLayout
    // 2. 容器节点 -> 容器节点(脏),只需要 setNeedsLayout
    // 3. 叶节点 -> 容器节点(脏),只需要 setNeedsLayout
    // 4. 容器节点 -> 叶节点(脏),只需要 setNeedsLayout
    // YGAttachNodesFromViewHierachy 会针对 2 3 4 情况自行做出标记脏节点的动作
    if (self.hm_renderObject.numberOfChildren == 0 && self.hm_renderObject.isLeaf) {
        // 原先是叶节点,现在也是叶节点
        [self.hm_renderObject markDirty];
    }
    NSAssert(NSThread.isMainThread, @"必须是主线程");
    if (!viewSet) {
        viewSet = NSHashTable.weakObjectsHashTable;
        dispatch_async(dispatch_get_main_queue(), ^{
            // 布局对应的 View(最终是通过 frame 的方式进行布局的)
            [UIView hm_layoutIfNeeded];
        });
    }
    [viewSet addObject:self];
}

5 layout 布局

+ (void)hm_layoutIfNeeded {
    if (viewSet.count == 0) {
        return;
    }
    NSHashTable<__kindof UIView *> *rootViewSet = nil;
    // viewSet 是静态变量,我猜测
    NSEnumerator<__kindof UIView *> *enumerator = viewSet.objectEnumerator;
    UIView *value = nil;
    // 找到 yoga 节点树的 rootView
    while ((value = enumerator.nextObject)) {
        UIView *rootView = hm_yoga_get_root_view(value);
        if (!rootViewSet) {
            rootViewSet = NSHashTable.weakObjectsHashTable;
        }
        [rootViewSet addObject:rootView];
    }
    viewSet = nil;
    enumerator = rootViewSet.objectEnumerator;
    // 调用 yoga 进行布局
    while ((value = enumerator.nextObject)) {
        [value hm_layoutYogaRootView];
    }
}

// 最终会调用这个函数:
// preserveOriginpreserveOrigin 为 YES,表示布局的时候继承上一次的 frame
- (void)applyLayoutPreservingOrigin:(BOOL)preserveOriginpreserveOrigin dimensionFlexibility:(HummerDimensionFlexibility)dimensionFlexibility view:(UIView *)view affectedShadowViews:(NSHashTable<HMRenderObject *> *)affectedShadowViews {
    // 把 View 插入到 yoga 节点树中
    [self attachRenderObjectFromViewHierarchyForRootView:view];

    if (!affectedShadowViews) {
        affectedShadowViews = [NSHashTable weakObjectsHashTable];
    }
    HMRenderObject *renderObject = (HMRenderObject *) view.hm_renderObject;
    HMRootRenderObject *rootRenderObject = [[HMRootRenderObject alloc] init];
    rootRenderObject.renderObject = renderObject;
    
    // 最小大小直接忽略
    CGSize oldMinimumSize = CGSizeMake(HMCoreGraphicsFloatFromYogaValue(YOGA_TYPE_WRAPPER(YGNodeStyleGetMinWidth)(renderObject.yogaNode), 0), HMCoreGraphicsFloatFromYogaValue(YOGA_TYPE_WRAPPER(YGNodeStyleGetMinHeight)(renderObject.yogaNode), 0));
    if (!CGSizeEqualToSize(oldMinimumSize, rootRenderObject.minimumSize)) {
        rootRenderObject.minimumSize = oldMinimumSize;
    }
    // 默认直接使用 rootView 大小
    rootRenderObject.availableSize = view.bounds.size;
    if (dimensionFlexibility & HummerDimensionFlexibilityWidth) {
        CGSize availableSize = rootRenderObject.availableSize;
        availableSize.width = CGFLOAT_MAX;
        rootRenderObject.availableSize = availableSize;
    }
    if (dimensionFlexibility & HummerDimensionFlexibilityWidth) {
        CGSize availableSize = rootRenderObject.availableSize;
        availableSize.height = CGFLOAT_MAX;
        rootRenderObject.availableSize = availableSize;
    }
    // 调用 yoga 进行布局
    [rootRenderObject layoutWithAffectedShadowViews:affectedShadowViews];

    if (affectedShadowViews.count <= 0) {
        // no frame change results in no UI update block
        return;
    }
    for (HMRenderObject *shadowView in affectedShadowViews) {
        HMLayoutMetrics layoutMetrics = shadowView.layoutMetrics;
        UIView *inlineView = shadowView.view;
        hm_safe_main_thread(^{
            inlineView.hm_animationPropertyBounds = inlineView.bounds;
            inlineView.hm_animationPropertyCenter = inlineView.center;
            CGRect frame = layoutMetrics.frame;
            // 只有在 HMDisplayTypeNone 或 visibility: hidden 情况下,隐藏视图
            BOOL isHidden = layoutMetrics.displayType == HMDisplayTypeNone || inlineView.hm_visibility == YES;
            if (inlineView.isHidden != isHidden) {
                inlineView.hidden = isHidden;
            }
            if (view == inlineView) {
                // 需要继承上次的 origin
                if (preserveOrigin) {
                    frame = (CGRect) {
                            .origin = {
                                    .x = (CGRectGetMinX(frame) + inlineView.frame.origin.x),
                                    .y = (CGRectGetMinY(frame) + inlineView.frame.origin.y),
                            },
                            .size = frame.size
                    };
                }
            }
            // 为 shadowView 设置 frame
            [inlineView hummerSetFrame:frame];
        });
    }
}

shadowView 是什么?——辅助类,负责在 shadow 线程进行布局相关的计算

至此,我们的 Text 组件就添加到了屏幕上,并最终显示了出来

image_GR5FJw9Zmq.png

image_BDt3lrxuia.png

总结

Hummer 的渲染机制跟 ReactNative 的比较像:

Hummer渲染_6CexzvtQEf.png

不过还是有很多问题浮现了出来:

  1. 如果组件发生了改变,Hummer 是如何刷新视图树的呢?
  2. hummerCreate 函数是怎么一步步从 Hummer 侧调用的呢?还有 render:?
  3. Native 和 JS 是如何通信的?
  4. Hummer 的启动流程都做了些什么?

这些问题我会在后续的文章中逐渐进行解答。

自测习题

根据《认知天性》,考试是最有效的学习策略之一,你能解答下面的问题吗:

为什么以下写法不能生效?

text.style.color = '#ffffff'
image_o7Bw64ThP5.png

请诸君思考,思考

解答:那种写法会调用 getter 而不是 setter,也就是hm_style: 方法,而不是hm_setStyle: 方法。当然不会生效。


为什么这行代码放在一开始就会导致render 不执行?

appendText(message:string){
    var text = new Text()

    // 放在这里, 练render 函数都不会调用了,为啥?
    text.style.color = '#666666'

    text.style = {
      fontSize: 20,
    }

    text.style = {
      color: '#999999'
    }

    // 放在这里,不生效,因为这行代码翻译过去是调用了 getter 而不是 setter
    text.style.color = '#666666'

    text.text = message
    this.appendChild(text);
  }

暂时没答案,我猜是因为调用 getter 后得到的返回值类型不对,导致后面的代码不执行了?

参考文献

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

推荐阅读更多精彩内容