从Masonry原理解析使用注意点

Masonry的原理解析以及使用

Masonry应该是目前使用最为广泛的对于AutoLayout的封装(Swift版本叫做SnapKit),但是大家对于Masonry的使用只是停留在基础的方式,很少人会去理解Masonry内部去调用AutoLayout的具体原理,致使在UI上容易产生很多的冲突,导致Masonry的Crash等等情况;所以这篇文章主要是来解决上面提出的问题;

关于AutoLayout

要讲Masonry必须从iOS的布局历史开始,系统的UI布局大致分为3类:

  • Frame Layout
  • Auto Resizing
  • Auto Layout

所谓FrameLayout即通过设置view的frame属性值从而控制view的位置以及大小;

Auto Resizing其实也是属于FrameLayout的范畴,目的就是为了让子view可以跟随superview进行大小的调整;但是不足点就是Auto Resizing无法处理同级间的view布局以及无法让superview根据子view进行反向的数据调整;

于是就出现了Autolayout,它是一种基于约束的布局系统;简单来说Autolayout的本质其实就是解析一组多元一次方程,当要确定一个视图的位置,也是需要确定视图的横纵坐标以及宽度和高度的,只是这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果;

view_formula_2x.png

约束的本质就是两个view的线性关系,上图就是一个基本的关系方程式RedView的位置其实是通过BlueView的位置来固定的;这里不做过多的讲解有兴趣的朋友可以去看官方文档

AutoLayout的使用

官方对于AutoLayout的使用提供了3种方法;但是其中两种本质都是其实都是使用NSLayoutConstraint对象进行约束;

  1. 使用xib以及storyboard进行布局,不过这种方式基本不用,因为不好用;
  2. 使用VFL语法进行约束,VFL简直就是一种又臭又长的语法,非常不好用也很难记,需要扫盲的同学可以查看官方文档,我就不多讲了,因为我自己也不是很懂;
/* Create an array of constraints using an ASCII art-like visual format string.
 */
+ (NSArray<NSLayoutConstraint *> *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(nullable NSDictionary<NSString *, NSNumber *> *)metrics views:(NSDictionary<NSString *, id> *)views;

  1. 使用NSLayoutConstraint纯代码添加,这种方式的缺点就是会大大的增加代码量,平均一个约束就需要写大量的代码,造成开发的效率大大降低;
/* Create constraints explicitly.  Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant" 
 If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute.
 Use of this method is not recommended. Constraints should be created using anchor objects on views and layout guides.
 */
+ (instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;

上面曾经说过三种Autolayout的布局方法有两种的本质是一样的,就是因为VFL的本质其实就是返回多个NSLayoutConstraint对象,而不需要直接一个一个的创建NSLayoutConstraint对象大量减少代码量;个人认为要不是VFL的语法太过于变态,如果简单好用也就没有Masonry什么事了;

Masonry的本质其实就是通过链式的语法将一个一个约束关系记录下来,然后通过创建一个一个NSLayoutConstraint对象进行布局约束,Masonry内部的本质其实这样并不复杂,只是存在很多细节点,导致直接使用的它的人会存在许多疑惑点,约束间的关系理不清;

关于Masonry中的宏定义

一下有两段代码,其中一个使用了mas_定义另一个没有使用mas_定义,我想绝大部分人在使用的过程中肯定会充满疑惑,好像不管使用哪一个都没有问题;

// 使用宏定义
[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(self.mas_top).mas_offset(20.f);
    make.leading.mas_equalTo(self.mas_leading).mas_offset(20.f);
    make.size.mas_equalTo(CGSizeMake(100.f, 100.f));
}];

// 不使用宏定义
[view makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.top).offset(20.f);
    make.leading.equalTo(self.leading).offset(20.f);
    make.size.equalTo(CGSizeMake(100.f, 100.f));
}];

其实上述两种布局最后的效果都是一样的;原因就是非mas_定义的本质调用还是调用mas_定义的声明;

- (NSArray *)makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    return [self mas_makeConstraints:block];
}

- (NSArray *)updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    return [self mas_updateConstraints:block];
}

- (NSArray *)remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    return [self mas_remakeConstraints:block];
}

对于makeConstraints(updateConstraints && remakeConstraints)三个函数来说,本质还是调用了mas_makeConstraints(mas_updateConstraints && mas_remakeConstraints);

@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;

#define MAS_ATTR_FORWARD(attr)  \
- (MASViewAttribute *)attr {    \
    return [self mas_##attr];   \
}

MAS_ATTR_FORWARD(leading);

对于leading这些属性来说,leading的调用实际调用的还是mas_leading;

#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...)    greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...)       lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...)                  valueOffset(MASBoxValue((__VA_ARGS__)))


#ifdef MAS_SHORTHAND_GLOBALS

#define equalTo(...)                     mas_equalTo(__VA_ARGS__)
#define greaterThanOrEqualTo(...)        mas_greaterThanOrEqualTo(__VA_ARGS__)
#define lessThanOrEqualTo(...)           mas_lessThanOrEqualTo(__VA_ARGS__)

#define offset(...)                      mas_offset(__VA_ARGS__)

#endif

/**
 *  Sets the constraint relation to NSLayoutRelationEqual
 *  returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *  see readme for more details.
 */
- (MASConstraint * (^)(id attr))equalTo;

/**
 *  Sets the constraint relation to NSLayoutRelationGreaterThanOrEqual
 *  returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *  see readme for more details.
 */
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;

/**
 *  Sets the constraint relation to NSLayoutRelationLessThanOrEqual
 *  returns a block which accepts one of the following:
 *    MASViewAttribute, UIView, NSValue, NSArray
 *  see readme for more details.
 */
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;

/**
 *  Modifies the NSLayoutConstraint constant based on a value type
 */
- (MASConstraint * (^)(NSValue *value))valueOffset;

我们可以发现对于equalTo函数默认接受的应该是一个NSObject的对象,但是我们却可以传入一个CGSize以及CGFloat这类参数,原因就是因为存在一个equalTo的宏定义调用了mas_equalTo,而mas_equalTo实际调用了equalTo函数只是将参数转换成了NSObject对象;
所以我们可以得到以下结果:
无论传入的参数是否是NSObject对象,equalTo和mas_equalTo最后调用的都是equalTo函数,需要注意的是equalTo宏定义和equalTo函数虽然本质调用一样,但是是属于不同函数;

关于Masonry的数据转换

通过上面的宏定义我们存在一个疑惑点,为一个非NSObject对象可以被equalTo接受呢?原因就是下面这个函数噶会的作用;

/**
 *  Given a scalar or struct value, wraps it in NSValue
 *  Based on EXPObjectify: https://github.com/specta/expecta
 */
static inline id _MASBoxValue(const char *type, ...) {
    va_list v;
    va_start(v, type);
    id obj = nil;
    if (strcmp(type, @encode(id)) == 0) {
        id actual = va_arg(v, id);
        obj = actual;
    } else if (strcmp(type, @encode(CGPoint)) == 0) {
        CGPoint actual = (CGPoint)va_arg(v, CGPoint);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(CGSize)) == 0) {
        CGSize actual = (CGSize)va_arg(v, CGSize);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(double)) == 0) {
        double actual = (double)va_arg(v, double);
        obj = [NSNumber numberWithDouble:actual];
    } else if (strcmp(type, @encode(float)) == 0) {
        float actual = (float)va_arg(v, double);
        obj = [NSNumber numberWithFloat:actual];
    } else if (strcmp(type, @encode(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } else if (strcmp(type, @encode(long)) == 0) {
        long actual = (long)va_arg(v, long);
        obj = [NSNumber numberWithLong:actual];
    } else if (strcmp(type, @encode(long long)) == 0) {
        long long actual = (long long)va_arg(v, long long);
        obj = [NSNumber numberWithLongLong:actual];
    } else if (strcmp(type, @encode(short)) == 0) {
        short actual = (short)va_arg(v, int);
        obj = [NSNumber numberWithShort:actual];
    } else if (strcmp(type, @encode(char)) == 0) {
        char actual = (char)va_arg(v, int);
        obj = [NSNumber numberWithChar:actual];
    } else if (strcmp(type, @encode(bool)) == 0) {
        bool actual = (bool)va_arg(v, int);
        obj = [NSNumber numberWithBool:actual];
    } else if (strcmp(type, @encode(unsigned char)) == 0) {
        unsigned char actual = (unsigned char)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedChar:actual];
    } else if (strcmp(type, @encode(unsigned int)) == 0) {
        unsigned int actual = (unsigned int)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedInt:actual];
    } else if (strcmp(type, @encode(unsigned long)) == 0) {
        unsigned long actual = (unsigned long)va_arg(v, unsigned long);
        obj = [NSNumber numberWithUnsignedLong:actual];
    } else if (strcmp(type, @encode(unsigned long long)) == 0) {
        unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
        obj = [NSNumber numberWithUnsignedLongLong:actual];
    } else if (strcmp(type, @encode(unsigned short)) == 0) {
        unsigned short actual = (unsigned short)va_arg(v, unsigned int);
        obj = [NSNumber numberWithUnsignedShort:actual];
    }
    va_end(v);
    return obj;
}

对于所有传入的参数无论是否是NSObject对象,都会通过_MASBoxValue这个函数进行数据的封装,将所有参数都转换成一个NSObject对象

- (void)setLayoutConstantWithValue:(NSValue *)value {
    if ([value isKindOfClass:NSNumber.class]) {
        self.offset = [(NSNumber *)value doubleValue];
    } else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
        CGPoint point;
        [value getValue:&point];
        self.centerOffset = point;
    } else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
        CGSize size;
        [value getValue:&size];
        self.sizeOffset = size;
    } else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets insets;
        [value getValue:&insets];
        self.insets = insets;
    } else {
        NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);
    }
}

最后对于那些原本非NSObject对象在进行反向解析,设置对应的值;

关于Masonry的结构

下图是网上一张很详细介绍Masonry结构的一张结构图,这里引用一下,因为我不想很具体的去介绍每一句代码,只把最核心的几点告诉大家;


1829891-2157823aedb4e4dc.png

上图的大致流程其实很通俗易懂,因为我们这样想,我们最主要的目的无非就是讲一个一个NSLayoutConstraint约束抽象成我们能够简单通俗的编写方式,所以Masonry的主要流程其实就是每个view提供给用户一个MASConstraintMaker对象,让用户不断在MASConstraintMaker对象上添加一个一个MASConstraint的约束结构,最后将所有的MASConstraint转化成一个一个NSLayoutConstraint对象添加在相应的view上面;

接下来的很多概念都需要用到上面的结构;

关于mas_makeConstraints,mas_updateConstraints,mas_remakeConstraints的区别理解

我相信上面三个函数大家一定不会陌生,而且应该也知道对应的使用场景;
mas_makeConstraints就是创建一个新的约束
mas_updateConstraints就是更新一个原有的约束
mas_remakeConstraints就是移除现有的约束,添加新的约束;

介绍一下主要的原理,每个MASConstraintMaker对象有两个updateExisting && removeExisting属性,用来保存当前的maker的约束方式

// MASConstraintMaker
/**
 *  Whether or not to check for an existing constraint instead of adding constraint
 */
@property (nonatomic, assign) BOOL updateExisting;

/**
 *  Whether or not to remove existing constraints prior to installing
 */
@property (nonatomic, assign) BOOL removeExisting;

所以当调用mas_makeConstraints && mas_updateConstraints && mas_remakeConstraints这三个函数的时候,最后都会去执行install这个操作,而install里面本身就会判断如果是remakeConstraints那么它就会移除所有旧的约束,然后添加新的约束;对于updateConstraints && makeConstraints只是
添加新的约束,但是MASConstraint本身会保存当前的约束是更新约束还是新加约束;

// MASConstraintMaker
- (NSArray *)install {
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}

那么对于updateConstraints && makeConstraints内部对于updateExisting的区别其实很简单,如果updateExisting为true,那么就从当前的view去找是否存在和当前约束一样的约束,然后更新约束的constant,我们可以从layoutConstraintSimilarTo函数可以看到,判断约束是否存在的标准就是除了constant以外的所有属性;如果updateExisting为false,那么就是直接添加新的约束;

// MASConstraint
/**
 *  Whether or not to check for an existing constraint instead of adding constraint
 */
@property (nonatomic, assign) BOOL updateExisting;

////////////////////////////////////////////////
MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
    existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
    // just update the constant
    existingConstraint.constant = layoutConstraint.constant;
    self.layoutConstraint = existingConstraint;
} else {
    [self.installedView addConstraint:layoutConstraint];
    self.layoutConstraint = layoutConstraint;
    [firstLayoutItem.mas_installedConstraints addObject:self];
}

- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
    // check if any constraints are the same apart from the only mutable property constant

    // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints
    // and they are likely to be added first.
    for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
        if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
        if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
        if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
        if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
        if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
        if (existingConstraint.relation != layoutConstraint.relation) continue;
        if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
        if (existingConstraint.priority != layoutConstraint.priority) continue;

        return (id)existingConstraint;
    }
    return nil;
}

关于Masonry为什么可以链式调用

其实Masonry可以链式调用无非就是为了缩减代码量,没有其他任何原因;从我们之前的结构图可以看到MASConstraintMaker对象包函了大量的MASConstraint属性对象,而MASConstraint属性对象里面还是定义大量的MASConstraint属性,于是就可以不断返回MASConstraint的对象;于是问题来了,它是怎么做到将每个MASConstraint对象都保存起来呢?

// MASConstraint
/**
 *  Usually MASConstraintMaker but could be a parent MASConstraint
 */
@property (nonatomic, weak) id<MASConstraintDelegate> delegate;

// MASConstraintMaker
@interface MASConstraintMaker () <MASConstraintDelegate>

@property (nonatomic, weak) MAS_VIEW *view;
@property (nonatomic, strong) NSMutableArray *constraints;

@end

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

@protocol MASConstraintDelegate <NSObject>

/**
 *  Notifies the delegate when the constraint needs to be replaced with another constraint. For example
 *  A MASViewConstraint may turn into a MASCompositeConstraint when an array is passed to one of the equality blocks
 */
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint;

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;

@end

我们可以发现MASConstraint对象都有一个MASConstraintDelegate的代理,而MASConstraintMaker实现了这个代理,所以所有生成MASConstraint的任务其实最后都是通过MASConstraintMaker来实现的,并通过constraints进行保存;

关于Masonry如何正确添加对应的View关系

可能很多人存在一个疑惑点,Masonry是如何正确的添加每个约束关系到对应的View上呢?不理解我这句话的同学可以自行写一个约束,然后打印对应的view的constraints可以发现,每个约束都有自己的对应关系,有的添加在superview上面,有些是添加在自己的view上面;那么Masonry是怎么做的呢?

MASLayoutConstraint *layoutConstraint
    = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                    attribute:firstLayoutAttribute
                                    relatedBy:self.layoutRelation
                                       toItem:secondLayoutItem
                                    attribute:secondLayoutAttribute
                                   multiplier:self.layoutMultiplier
                                     constant:self.layoutConstant];
    
layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;
    
if (self.secondViewAttribute.view) {
    MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
    NSAssert(closestCommonSuperview,
             @"couldn't find a common superview for %@ and %@",
             self.firstViewAttribute.view, self.secondViewAttribute.view);
    self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
    self.installedView = self.firstViewAttribute.view;
} else {
    self.installedView = self.firstViewAttribute.view.superview;
}

我们发现对于存在secondView的情况,那么firstView和secondView的最近公共view就是约束需要添加的view,如果firstView是设置size的大小(包括单独的宽高),那么需要添加约束的就是自身的view,其他情况一律都是firstView的superview(比如make.center.offset(10)这类操作);

关于Masonry的一些缺省写法

很多人会在代码中会写如下的代码,以下两段代码实现效果是一样的,但是其中一段是缺省代码,那么为什么缺省的写法也是可以正确实现呢?

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view).offset(20.f);
    make.top.equalTo(self.view).offset(20.f);
    make.size.equalTo(CGSizeMake(120.f, 120.f));
}];

[view mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view.left).offset(20.f);
    make.top.equalTo(self.view.top).offset(20.f);
    make.size.equalTo(CGSizeMake(120.f, 120.f));
}];

原因就在于Masonry会对系统的缺省值进行补充,如果在equalTo的时候传入secondViewAttribute是UIView对象,那么使用的约束类型就是该firstView的约束属性,如果传入的secondViewAttribute是secondView的约束属性,那么就直接使用;

- (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        MASViewAttribute *attr = secondViewAttribute;
        if (attr.layoutAttribute == NSLayoutAttributeNotAnAttribute) {
            _secondViewAttribute = [[MASViewAttribute alloc] initWithView:attr.view item:attr.item layoutAttribute:self.firstViewAttribute.layoutAttribute];;
        } else {
            _secondViewAttribute = secondViewAttribute;
        }
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}

Masonry细节解析

大家想看Masonry具体细节原理具体看这篇文章,代码上没有什么难度只要理解思想基本就能看明白;

关于Masonry的属性优先级注意点

这里要说的主要是关于View自身内容尺寸(Intrinsic Content Size),抗压缩抗拉伸(Compression-Resistance and Content-Hugging)这两种属性的概念介绍,这点是需要好好理解的,这些主要影响到的就是关于约束的优先级关系,我这里不做多讲有兴趣可以去看这篇文章

关于Masonry的性能

其实AutoLayout的性能不是很好,想想也知道要在主线程解析多元一次方程,对于复杂的界面布局效率可想而知具体可以参见这篇文章;

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

推荐阅读更多精彩内容