iOS 性能监控方案 Wedjat(下篇)

作者:敖志敏
本文为原创文章,转载请注明作者及出处

image.png

前文地址:iOS 性能监控方案 Wedjat(上篇)

国内移动网络环境非常复杂,WIFI、4G、3G、2.5G(Edge)、2G 等多种移动网络并存,用户的网络可能会在 WIFI/4G/3G/2.5G/2G 类型之间切换,这是移动网络和传统网络一个很大的区别,被称作是 Connection Migration 问题。此外,还存在国内运营商网络的 DNS 解析慢、失败率高、DNS 被劫持的问题;还有国内运营商互联和海外访问国内带宽低传输慢等问题。这些网络问题令人非常头疼。移动网络的现状造成了用户在使用过程中经常会遇到各种网络问题,网络问题将直接导致用户无法在 App 进行操作,当一些关键的业务接口出现错误时,甚至会直接导致用户的大量流失。网络问题不仅给移动开发带来了巨大的挑战,同时也给网络监控带来了全新的机遇。以往要解决这些问题,只能靠经验和猜想,而如果能站在 App 的视角对网络进行监控,就能更有针对性地了解产生问题的根源。

网络监控一般通过 NSURLProtocol 和代码注入(Hook)这两种方式来实现,由于 NSURLProtocol 作为上层接口,使用起来更为方便,因此很自然选择它作为网络监控的方案,但是 NSURLProtocol 属于 URL Loading System 体系中,应用层的协议支持有限,只支持 FTPHTTPHTTPS 等几个应用层协议,对于使用其他协议的流量则束手无策,所以存在一定的局限性。监控底层网络库 CFNetwork 则没有这个限制。

下面是网络采集的关键性能指标:

  • TCP 建立连接时间
  • DNS 时间
  • SSL 时间
  • 首包时间
  • 响应时间
  • HTTP 错误率
  • 网络错误率

NSURLProtocol

//为了避免 canInitWithRequest 和 canonicalRequestForRequest 出现死循环
static NSString * const HJHTTPHandledIdentifier = @"hujiang_http_handled";

@interface HJURLProtocol () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>

@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSOperationQueue     *sessionDelegateQueue;
@property (nonatomic, strong) NSURLResponse        *response;
@property (nonatomic, strong) NSMutableData        *data;
@property (nonatomic, strong) NSDate               *startDate;
@property (nonatomic, strong) HJHTTPModel          *httpModel;

@end

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if (![request.URL.scheme isEqualToString:@"http"] &&
        ![request.URL.scheme isEqualToString:@"https"]) {
        return NO;
    }
    
    if ([NSURLProtocol propertyForKey:HJHTTPHandledIdentifier inRequest:request] ) {
        return NO;
    }
    return YES;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    [NSURLProtocol setProperty:@YES
                        forKey:HJHTTPHandledIdentifier
                     inRequest:mutableReqeust];
    return [mutableReqeust copy];
}

- (void)startLoading {
    self.startDate                                        = [NSDate date];
    self.data                                             = [NSMutableData data];
    NSURLSessionConfiguration *configuration              = [NSURLSessionConfiguration defaultSessionConfiguration];
    self.sessionDelegateQueue                             = [[NSOperationQueue alloc] init];
    self.sessionDelegateQueue.maxConcurrentOperationCount = 1;
    self.sessionDelegateQueue.name                        = @"com.hujiang.wedjat.session.queue";
    NSURLSession *session                                 = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.sessionDelegateQueue];
    self.dataTask                                         = [session dataTaskWithRequest:self.request];
    [self.dataTask resume];

    httpModel                                             = [[NEHTTPModel alloc] init];
    httpModel.request                                     = self.request;
    httpModel.startDateString                             = [self stringWithDate:[NSDate date]];

    NSTimeInterval myID                                   = [[NSDate date] timeIntervalSince1970];
    double randomNum                                      = ((double)(arc4random() % 100))/10000;
    httpModel.myID                                        = myID+randomNum;
}

- (void)stopLoading {
    [self.dataTask cancel];
    self.dataTask           = nil;
    httpModel.response      = (NSHTTPURLResponse *)self.response;
    httpModel.endDateString = [self stringWithDate:[NSDate date]];
    NSString *mimeType      = self.response.MIMEType;
    
    // 解析 response,流量统计等
}

#pragma mark - NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (!error) {
        [self.client URLProtocolDidFinishLoading:self];
    } else if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
    } else {
        [self.client URLProtocol:self didFailWithError:error];
    }
    self.dataTask = nil;
}

#pragma mark - NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    completionHandler(NSURLSessionResponseAllow);
    self.response = response;
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    if (response != nil){
        self.response = response;
        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
}

Hertz 使用的是 NSURLProtocol 这种方式,通过继承 NSURLProtocol,实现 NSURLConnectionDelegate 来实现截取行为。

Hook

如果我们使用手工埋点的方式来监控网络,会侵入到业务代码,维护成本会非常高。通过 Hook 将网络性能监控的代码自动注入就可以避免上面的问题,做到真实用户体验监控(RUM: Real User Monitoring),监控应用在真实网络环境中的性能。

AOP(Aspect Oriented Programming,面向切面编程),是通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态添加功能的一种技术。其核心思想是将业务逻辑(核心关注点,系统的主要功能)与公共功能(横切关注点,如日志、事物等)进行分离,降低复杂性,提高软件系统模块化、可维护性和可重用性。其中核心关注点采用 OOP 方式进行代码的编写,横切关注点采用 AOP 方式进行编码,最后将这两种代码进行组合形成系统。AOP 被广泛应用在日志记录,性能统计,安全控制,事务处理,异常处理等领域。

在 iOS 中 AOP 的实现是基于 Objective-CRuntime 机制,实现 Hook 的三种方式分别为:Method SwizzlingNSProxyFishhook。前两者适用于 Objective-C 实现的库,如 NSURLConnectionNSURLSessionFishhook 则适用于 C 语言实现的库,如 CFNetwork

下图是阿里百川码力监控给出的三类网络接口需要 hook 的方法

image.png

接下来分别来讨论这三种实现方式:

Method Swizzling

Method swizzling 是利用 Objective-C Runtime 特性把一个方法的实现与另一个方法的实现进行替换的技术。每个 Class 结构体中都有一个 Dispatch Table 的成员变量,Dispatch Table 中建立了每个 SEL(方法名)和对应的 IMP(方法实现,指向 C 函数的指针)的映射关系,Method Swizzling 就是将原有的 SELIMP映射关系打破,并建立新的关联来达到方法替换的目的。

image.png

因此利用 Method swizzling 可以替换原始实现,在替换的实现中加入网络性能埋点行为,然后调用原始实现。

NSProxy

NSProxy is an abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet. Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.

这是 Apple 官方文档给 NSProxy 的定义,NSProxyNSObject 一样都是根类,它是一个抽象类,你可以通过继承它,并重写 -forwardInvocation:-methodSignatureForSelector: 方法以实现消息转发到另一个实例。综上,NSProxy 的目的就是负责将消息转发到真正的 target 的代理类。

Method swizzling 替换方法需要指定类名,但是 NSURLConnectionDelegateNSURLSessionDelegate 是由业务方指定,通常来说是不确定,所以这种场景不适合使用 Method swizzling。使用 NSProxy 可以解决上面的问题,具体实现:proxy delegate 替换 NSURLConnectionNSURLSession 原来的 delegate,当 proxy delegate 收到回调时,如果是要 hook 的方法,则调用 proxy 的实现,proxy 的实现最后会调用原来的 delegate;如果不是要 hook 的方法,则通过消息转发机制将消息转发给原来的 delegate。下图示意了整个操作流程。

image.png

Fishhook

fishhook 是一个由 Facebook 开源的第三方框架,其主要作用就是动态修改 C 语言的函数实现,我们可以使用 fishhook 来替换动态链接库中的 C 函数实现,具体来说就是去替换 CFNetworkCoreFoundation 中的相关函数。后面会在讲监控 CFNetwork 详细说明,这里不再赘述。

讲解完 iOS 上 hook 的实现技术,接下来讨论在 NSURLConnectionNSURLSessionCFNetwork 中,如何将上面的三种技术应用到实践中。

NSURLConnection

image.png

NSURLSession

image.png

CFNetwork

概述

NeteaseAPM 作为案例来讲解如何通过 CFNetwork 实现网络监控,它是通过使用代理模式来实现的,具体来说,是在 CoreFoundation Framework 的 CFStream 实现一个 Proxy Stream 从而达到拦截的目的,记录通过 CFStream 读取的网络数据长度,然后再转发给 Original Stream,流程图如下:

image.png

详细描述

由于 CFNetwork 都是 C 函数实现,想要对 C 函数 进行 Hook 需要使用 Dynamic Loader Hook 库函数 - fishhook

Dynamic Loader(dyld)通过更新 Mach-O 文件中保存的指针的方法来绑定符号。借用它可以在 Runtime 修改 C 函数调用的函数指针。fishhook 的实现原理:遍历 __DATA segment 里面 __nl_symbol_ptr__la_symbol_ptr 两个 section 里面的符号,通过 Indirect Symbol Table、Symbol Table 和 String Table 的配合,找到自己要替换的函数,达到 hook 的目的。

CFNetwork 使用 CFReadStreamRef 做数据传递,使用回调函数来接收服务器响应。当回调函数收到流中有数据的通知后,将数据保存到客户端的内存中。显然对流的读取不适合使用修改字符串表的方式,如果这样做的话也会 hook 系统也在使用的 read 函数,而系统的 read 函数不仅仅被网络请求的 stream 调用,还有所有的文件处理,而且 hook 频繁调用的函数也是不可取的。

使用上述方式的缺点就是无法做到选择性的监控和 HTTP 相关的 CFReadStream,而不涉及来自文件和内存的 CFReadStreamNeteaseAPM 的解决方案是在系统构造 HTTP Stream 时,将一个 NSInputStream 的子类 ProxyStream 桥接为 CFReadStream 返回给用户,来达到单独监控 HTTP Stream 的目的。

image.png

具体的实现思路就是:首先设计一个继承自 NSObject 并持有 NSInputStream 对象的 Proxy 类,持有的 NSInputStream 记为 OriginalStream。将所有发向 Proxy 的消息转发给 OriginalStream 处理,然后再重写 NSInputStreamread:maxLength: 方法,如此一来,我们就可以获取到 stream 的大小了。
XXInputStreamProxy 类的代码如下:

- (instancetype)initWithStream:(id)stream {
    if (self = [super init]) {
        _stream = stream;
    }
    return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [_stream methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:_stream];
}
                                                        

继承 NSInputStream 并重写 read:maxLength: 方法:

- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len {
    NSInteger readSize = [_stream read:buffer maxLength:len];
    // 记录 readSize
    return readSize;
}                                                   

XX_CFReadStreamCreateForHTTPRequest 会被用来替换系统的 CFReadStreamCreateForHTTPRequest 方法


static CFReadStreamRef (*original_CFReadStreamCreateForHTTPRequest)(CFAllocatorRef __nullable alloc,
                                                                    CFHTTPMessageRef request);
                         
/**
 XXInputStreamProxy 持有 original CFReadStreamRef,转发消息到 original CFReadStreamRef,
 在 read 方法中记录获取数据的大小
 */
static CFReadStreamRef XX_CFReadStreamCreateForHTTPRequest(CFAllocatorRef alloc,
                                                           CFHTTPMessageRef request) {
    // 使用系统方法的函数指针完成系统的实现
    CFReadStreamRef originalCFStream = original_CFReadStreamCreateForHTTPRequest(alloc, request);
    // 将 CFReadStreamRef 转换成 NSInputStream,并保存在 XXInputStreamProxy,最后返回的时候再转回 CFReadStreamRef
    NSInputStream *stream = (__bridge NSInputStream *)originalCFStream;
    XXInputStreamProxy *outStream = [[XXInputStreamProxy alloc] initWithClient:stream];
    CFRelease(originalCFStream);
    CFReadStreamRef result = (__bridge_retained CFReadStreamRef)outStream;
    return result;
}                                                             
                                                        

使用 fishhook 替换函数地址

void save_original_symbols() {
    original_CFReadStreamCreateForHTTPRequest = dlsym(RTLD_DEFAULT, "CFReadStreamCreateForHTTPRequest");
}                                                      
rebind_symbols((struct rebinding[1]){{"CFReadStreamCreateForHTTPRequest", XX_CFReadStreamCreateForHTTPRequest, (void *)& original_CFReadStreamCreateForHTTPRequest}}, 1);                                                    

根据 CFNetwork API 的调用方式,使用 fishhook 和 Proxy Stream 获取 C 函数的设计模型如下:

image.png

NSURLSessionTaskMetrics/NSURLSessionTaskTransactionMetrics

Apple 在 iOS 10 的 NSURLSessionTaskDelegate 代理中新增了 -URLSession: task:didFinishCollectingMetrics: 方法,如果实现这个代理方法,就可以通过该回调的 NSURLSessionTaskMetrics 类型参数获取到采集的网络指标,实现对网络请求中 DNS 查询/TCP 建立连接/TLS 握手/请求响应等各环节时间的统计。

/*
 * Sent when complete statistics information has been collected for the task.
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

NSURLSessionTaskMetrics

NSURLSessionTaskMetrics 对象封装了 session task 的指标,每个 NSURLSessionTaskMetrics 对象有 taskIntervalredirectCount 属性,还有在执行任务时产生的每个请求/响应事务中收集的指标。

  • transactionMetrics:transactionMetrics 数组包含了在执行任务时产生的每个请求/响应事务中收集的指标。

    /*
     * transactionMetrics array contains the metrics collected for every request/response transaction created during the task execution.
     */
    @property (copy, readonly) NSArray<NSURLSessionTaskTransactionMetrics *> *transactionMetrics;
    
  • taskInterval:任务从创建到完成花费的总时间,任务的创建时间是任务被实例化时的时间;任务完成时间是任务的内部状态将要变为完成的时间。

    /*
     * Interval from the task creation time to the task completion time.
     * Task creation time is the time when the task was instantiated.
     * Task completion time is the time when the task is about to change its internal state to completed.
     */
    @property (copy, readonly) NSDateInterval *taskInterval;
    
  • redirectCount:记录了被重定向的次数。

    /*
     * redirectCount is the number of redirects that were recorded.
     */
    @property (assign, readonly) NSUInteger redirectCount;
    

NSURLSessionTaskTransactionMetrics

NSURLSessionTaskTransactionMetrics 对象封装了任务执行时收集的性能指标,包括了 requestresponse 属性,对应 HTTP 的请求和响应,还包括了从 fetchStartDate 开始,到 responseEndDate 结束之间的指标,当然还有 networkProtocolNameresourceFetchType 属性。

  • request:表示了网络请求对象。

    /*
     * Represents the transaction request.
     */
    @property (copy, readonly) NSURLRequest *request;
    
  • response:表示了网络响应对象,如果网络出错或没有响应时,responsenil

    /*
     * Represents the transaction response. Can be nil if error occurred and no response was generated.
     */
    @property (nullable, copy, readonly) NSURLResponse *response;
    
  • networkProtocolName:获取资源时使用的网络协议,由 ALPN 协商后标识的协议,比如 h2, http/1.1, spdy/3.1。

    @property (nullable, copy, readonly) NSString *networkProtocolName;
    
  • isProxyConnection:是否使用代理进行网络连接。

    /*
     * This property is set to YES if a proxy connection was used to fetch the resource.
     */
    @property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;
    
  • isReusedConnection:是否复用已有连接。

    /*
     * This property is set to YES if a persistent connection was used to fetch the resource.
     */
    @property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;
    
  • resourceFetchType:NSURLSessionTaskMetricsResourceFetchType 枚举类型,标识资源是通过网络加载,服务器推送还是本地缓存获取的。

    /*
     * Indicates whether the resource was loaded, pushed or retrieved from the local cache.
     */
    @property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;
    

对于下面所有 NSDate 类型指标,如果任务没有完成,所有相应的 EndDate 指标都将为 nil。例如,如果 DNS 解析超时、失败或者客户端在解析成功之前取消,domainLookupStartDate 会有对应的数据,然而 domainLookupEndDate 以及在它之后的所有指标都为 nil

这幅图示意了一次 HTTP 请求在各环节分别做了哪些工作

image.png

如果是复用已有的连接或者从本地缓存中获取资源,下面的指标都会被赋值为 nil

  • domainLookupStartDate

  • domainLookupEndDate

  • connectStartDate

  • connectEndDate

  • secureConnectionStartDate

  • secureConnectionEndDate

  • fetchStartDate:客户端开始请求的时间,无论资源是从服务器还是本地缓存中获取。

    @property (nullable, copy, readonly) NSDate *fetchStartDate;
    
  • domainLookupStartDate:DNS 解析开始时间,Domain -> IP 地址。

    /*
     * domainLookupStartDate returns the time immediately before the user agent started the name lookup for the resource.
     */
    @property (nullable, copy, readonly) NSDate *domainLookupStartDate;
    
  • domainLookupEndDate:DNS 解析完成时间,客户端已经获取到域名对应的 IP 地址。

    /*
     * domainLookupEndDate returns the time after the name lookup was completed.
     */
    @property (nullable, copy, readonly) NSDate *domainLookupEndDate;
    
  • connectStartDate:客户端与服务器开始建立 TCP 连接的时间。

    /*
     * connectStartDate is the time immediately before the user agent started establishing the connection to the server.
     *
     * For example, this would correspond to the time immediately before the user agent started trying to establish the TCP connection.
     */
    @property (nullable, copy, readonly) NSDate *connectStartDate;
    
    • secureConnectionStartDate:HTTPS 的 TLS 握手开始时间。

      /*
       * If an encrypted connection was used, secureConnectionStartDate is the time immediately before the user agent started the security handshake to secure the current connection.
       *
       * For example, this would correspond to the time immediately before the user agent started the TLS handshake.
       *
       * If an encrypted connection was not used, this attribute is set to nil.
       */
      @property (nullable, copy, readonly) NSDate *secureConnectionStartDate;
      
    • secureConnectionEndDate:HTTPS 的 TLS 握手结束时间。

      /*
       * If an encrypted connection was used, secureConnectionEndDate is the time immediately after the security handshake completed.
       *
       * If an encrypted connection was not used, this attribute is set to nil.
       */
      @property (nullable, copy, readonly) NSDate *secureConnectionEndDate;
      
  • connectEndDate:客户端与服务器建立 TCP 连接完成时间,包括 TLS 握手时间。

    /*
     * connectEndDate is the time immediately after the user agent finished establishing the connection to the server, including completion of security-related and other handshakes.
     */
    @property (nullable, copy, readonly) NSDate *connectEndDate;
    
  • requestStartDate:开始传输 HTTP 请求的 header 第一个字节的时间。

    /*
     * requestStartDate is the time immediately before the user agent started requesting the source, regardless of whether the resource was retrieved from the server or local resources.
     *
     * For example, this would correspond to the time immediately before the user agent sent an HTTP GET request.
     */
    @property (nullable, copy, readonly) NSDate *requestStartDate;
    
  • requestEndDate:HTTP 请求最后一个字节传输完成的时间。

    /*
     * requestEndDate is the time immediately after the user agent finished requesting the source, regardless of whether the resource was retrieved from the server or local resources.
     *
     * For example, this would correspond to the time immediately after the user agent finished sending the last byte of the request.
     */
    @property (nullable, copy, readonly) NSDate *requestEndDate;
    
  • responseStartDate:客户端从服务器接收到响应的第一个字节的时间。

    /*
     * responseStartDate is the time immediately after the user agent received the first byte of the response from the server or from local resources.
     *
     * For example, this would correspond to the time immediately after the user agent received the first byte of an HTTP response.
     */
    @property (nullable, copy, readonly) NSDate *responseStartDate;
    
  • responseEndDate:客户端从服务器接收到最后一个字节的时间。

    /*
     * responseEndDate is the time immediately after the user agent received the last byte of the resource.
     */
    @property (nullable, copy, readonly) NSDate *responseEndDate;
    

Wrap up

iOS 的网络监控有两种实现方式:NSURLProtocol 和代码注入(Hook),文中给出了通过 NSURLProtocol 实现监控的具体实现,然后分别介绍了在 iOS 中如何使用 Method SwizzlingNSProxyFishhook 进行 AOP Hook,文章也给出了三种 AOP Hook 技术在 NSURLConnectionNSURLSessionCFNetwork 的案例。最后详细介绍在 iOS 10 中新引入的 NSURLSessionTaskMetricsNSURLSessionTaskTransactionMetrics 类,它们可以被用于获取网络相关的元数据,比如 DNS 查询、TLS 握手、请求响应等环节的耗时,这些数据可以帮助开发人员更好地分析网络性能。

参考资料

![]](http://upload-images.jianshu.io/upload_images/6560691-ec9194547993f9d1.gif?imageMogr2/auto-orient/strip)

更多有关 iOS 技术文章,请关注「沪江技术学院」微信公众号。

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

推荐阅读更多精彩内容