YYImage源码剖析与学习

建议查看原文:https://www.jianshu.com/p/83edaeeb5851(不定时更新)

源码剖析学习系列:(不断更新)

1、FBKVOController源码剖析与学习
2、MJRefresh源码剖析与学习
3、YYImage源码剖析与学习


前言:

要看懂YYImage框架,最好先了解热身部分(具体的自行百度),如果懒得看,直接跨过该部分,等到下面部分有疑问,再回过头看这部分的知识,也是可以。

热身部分

移动端图片格式调研

1、Image I/O

Image I/O 学习笔记
Image I/O官方文档
GIF图添加文字Demo

使用 CGBitmapContextCreate 函数创建一个位图上下文;
使用 CGContextDrawImage 函数将原始位图绘制到上下文中;
使用 CGBitmapContextCreateImage 函数创建一张新的解压缩后的位图。

2、 CGBitmapContextCreate 中的参数

谈谈 iOS 中图片的解压缩

  • data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
  • widthheight :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
  • bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
  • bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
  • space颜色空间,一般使用 RGB 即可;
  • bitmapInfo :位图的布局信息。当图片不包含 alpha 的时候使用 kCGImageAlphaNoneSkipFirst ,否则使用 kCGImageAlphaPremultipliedFirst
3、信号量

信号量的讲解

/* 注意,正常的使用顺序是先降低然后再提高,这两个函数通常成对使用。 */
    dispatch_semaphore_wait(_framesLock, DISPATCH_TIME_FOREVER); //等待降低信号量
    // to do
    dispatch_semaphore_signal(_framesLock); //提高信号量

所用到的知识:
复合赋值运算符、Image I/O、CADisplayLink、willChangeValueForKey:、

一、YYImage总体介绍

YImage 结构

1、YYImage 源码
2、YYImage 源码的文字解析版本

1、YYImage 功能

  • 显示动画类型的图片
  • 播放帧动画
  • 播放 sprite sheet 动画
  • 图片类型探测
  • 图片解码、编码(最核心功能)

2、YYImage 主要类介绍

YYImage 类

它是一个完全兼容的“UIImage”子类。它扩展了UIImage
支持动画WebP, APNG和GIF格式的图像数据解码。它还
支持NSCoding协议,以存档和反存档多帧图像数据。

a、animatedImageMemorySize

如果所有帧图像都被加载到内存中,那么总内存使用(以字节为单位)。
如果图像不是从多帧图像数据创建的,则该值为0。

b、preloadAllAnimatedImageFrames

将此属性设置为“YES”将阻塞要解码的调用线程
所有动画帧图像到内存,设置为“NO”将释放预装帧。
如果图像被许多图像视图(如emoticon)共享,则预加载所有视图
帧将降低CPU成本。

YYAnimatedImageView 类

用于显示动画图像的图像视图。
可以用来播放多帧动画以及普通动画,可以控制、暂停动画
当设备有足够的空闲内存时,这个视图及时请求帧数据。
这个视图可以在内部缓冲区中缓存一些或所有未来的帧,以降低CPU成本。

3、YYImage 的意义(图片解码的原因)

从磁盘中加载一张图片,并将它显示到屏幕上,这个过程其实经历很多,非常耗性能。随着显示的图片增加,性能下降尤其明显。不管是 JPEG 还是 PNG 等图片,都是一种编码后(压缩)的位图图形格式。我们先看下显示到屏幕这个过程的工作流:

1、我们使用+[UIImage imageWithContentsOfFile:]方法从磁盘中加载一张图片。此时,图片还没有被解码,仍旧是编码状态下。
2、返回的图片被分配给UIImageView
3、接着一个隐式的 CATransaction 捕获到了图层树的变化;
4、在主线程的下一个 run loop到来时,Core Animation 提交了这个隐式的事务,可能会涉及copy这些图片(已经成为图层树中的图层内容的图片)。这个 copy 操作可能会涉及以下部分或全部步骤:

a.分配缓冲区来管理文件IO和解压缩操作。
b.文件数据从磁盘读取到内存。
c.将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
d.最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层

图层树:(个人理解)洋葱看过去有很多层,这就是洋葱的图层,而屏幕上显示的文字、图片啊,都可以理解成为图层,很多图层就形成了一个结构,这个很多图层的结构就叫做图层树。

因此,在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这就是为什么需要对图片解码的原因。

二、YYImage主要类调用逻辑

A、渲染GIF/WebP/PNG(APNG)方法调用顺序

1、YYImage *image = [YYImage imageNamed:name]; //传入图片名创建YYImage对象

2、[[self alloc] initWithData:data scale:scale];//用重写的方法初始化图像数据

3、YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];//创建解码类 YYImageDecoder 对象,紧接着更新数据

4、result = [self _updateData:data final:final];//根据图像的data算出图片的type以及其他信息,再根据不同type 的图像去分别更新数据

5、[self _updateSourceImageIO];// 计算出PNG、GIF等图片信息(图片的每一帧的属性,包括宽、高、方向、动画重复次数(gif类型)、持续时间(gif类型))

6、 YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image]; //把图片添加到 UIImageView 的子类,这个子类后面讲(第7点后都是它的核心),这里暂且当它为普通 ImageView 那样看。

7、[self setImage:image withType:YYAnimatedImageTypeImage];// 设置图片,类似Setter方法

8、[self imageChanged];//判断当前图片类型以及帧数,由CATransaction支持的显示事务去更新图层的 contentsRect,以及重置动画的参数,后面详解该方法。

9、[self resetAnimated];//重置动画多种参数;[self calcMaxBufferCount]; // 动态调整当前内存的缓冲区大小。

10、[self didMoved];// 窗口对象或者父视图对象改变,则开始控制动画的启动(停止),这是动画得以显示的关键

B、渲染帧动画方法调用顺序

1、UIImage *image = [[YYFrameImage alloc] initWithImagePaths:paths oneFrameDuration:0.1 loopCount:0]; //传入图片组的路径、每一个帧(每一个图片)的时间以及循环多少次,计算出总的durations
2、[self initWithImagePaths:paths frameDurations:durations loopCount:loopCount];// 把第一张图片解码后返回,并求出第一帧的大小,作为每一帧的大小
3、YYAnimatedImageView *imageView = [[YYAnimatedImageView alloc] initWithImage:image];
后面步骤跟 渲染GIF/WebP/PNG(APNG)方法调用顺序 第7点开始几乎一样

注意:由于代码过多,不可能面面俱到,所以下面只会摘取核心进行讲解。这样,读者看完此文以及看完我标注过的源码(),,去读源代码,也更容易理解。

三、核心代码

// 它接受一个原始的位图参数 imageRef ,最终返回一个新的解压缩后的位图 newImage
CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
    if (!imageRef) return NULL;
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    if (width == 0 || height == 0) return NULL;
    
    // 重新绘制解码(可能会失去一些精度)
    if (decodeForDisplay) { //decode with redraw (may lose some precision)
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        // BGRA8888 (premultiplied) or BGRX8888
        // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
        if (!context) return NULL;
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
        CGImageRef newImage = CGBitmapContextCreateImage(context);
        CFRelease(context);
        return newImage; // 返回一个新的解压缩后的位图 newImage
        
    } else {
    
    }
}

YYCGImageCreateDecodedCopy 是解压缩的核心,也就是渲染图片性能显著的原因。该方法首先求出图片的宽高,注意,这里的图片是指编码前的图片的每一帧图片。

- (void)imageChanged {
    YYAnimatedImageType newType = [self currentImageType];
    id newVisibleImage = [self imageForType:newType];
    NSUInteger newImageFrameCount = 0;
    BOOL hasContentsRect = NO;
    if ([newVisibleImage isKindOfClass:[UIImage class]] &&
        [newVisibleImage conformsToProtocol:@protocol(YYAnimatedImage)]) {
        // 求出有多少帧(如果是帧动画(由多张图组合的),相当于有多少张图)
        newImageFrameCount = ((UIImage<YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;
        if (newImageFrameCount > 1) { // 动态图
            hasContentsRect = [((UIImage<YYAnimatedImage> *) newVisibleImage) respondsToSelector:@selector(animatedImageContentsRectAtIndex:)];
        }
    }
    // 由CATransaction支持的显示事务去更新图层的 contentsRect, 但一般不用走这段代码。大都走的是 CATransaction 的隐式事务自己更新
    if (!hasContentsRect && _curImageHasContentsRect) {
        if (!CGRectEqualToRect(self.layer.contentsRect, CGRectMake(0, 0, 1, 1)) ) {
            [CATransaction begin];
            [CATransaction setDisableActions:YES];
            self.layer.contentsRect = CGRectMake(0, 0, 1, 1);
            [CATransaction commit];
        }
    }
    _curImageHasContentsRect = hasContentsRect;

    // YYSpriteSheetImage 类用到,先不理
    if (hasContentsRect) {
        CGRect rect = [((UIImage<YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:0];
        [self setContentsRect:rect forImage:newVisibleImage];
    }
    
    if (newImageFrameCount > 1) {
        [self resetAnimated]; // 重置动画多种参数,包括在后台释放图像,下面再赋值已经被重置过的动画参数
        _curAnimatedImage = newVisibleImage; // 当前动画图片
        _curFrame = newVisibleImage; // 当前帧
        _totalLoop = _curAnimatedImage.animatedImageLoopCount; // 总循环次数
        _totalFrameCount = _curAnimatedImage.animatedImageFrameCount; // 总帧数
        [self calcMaxBufferCount]; // 动态调整当前内存的缓冲区大小。
    }
    [self setNeedsDisplay]; // 标志需要重绘,会在下一个循环到来时刷新
    [self didMoved]; // 窗口对象或者父视图对象改变,则开始控制动画的启动(停止),这是动画得以显示的关键
}

图片改变的处理核心

主要做了以下几点:

  • 初始化动画参数 resetAniamted
  • 初始化或者重置后求出动画播放循环次数、当前帧、总帧数
  • 调用动态调整缓冲区方法 calcMaxBufferCount 、调用控制动画方法 didMoved
// init the animated params.
- (void)resetAnimated {
    if (!_link) {
        _lock = dispatch_semaphore_create(1);
        _buffer = [NSMutableDictionary new];
        
        // 添加到这种队列中的操作,就会自动放到子线程中执行。
        _requestQueue = [[NSOperationQueue alloc] init];
        /* maxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。
        为1时,队列为串行队列。只能串行执行。大于1时,队列为并发队列 */
        _requestQueue.maxConcurrentOperationCount = 1;
        /* 初始化一个新的 CADisplayLink 对象,在屏幕更新时调用。为了使显示循环与显示同步,应用程序使用addToRunLoop:forMode:方法将其添加到运行循环中
            一个计时器对象,允许应用程序将其绘图同步到显示的刷新率。
         */
        _link = [CADisplayLink displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:self] selector:@selector(step:)];
        if (_runloopMode) {
            [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:_runloopMode];
        }
        // 禁用通知
        _link.paused = YES;
        
        // 接受内存警告的通知
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
        // 接受返回后台的通知,返回后台时,记录即将显示的下一帧
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
    }
    
    [_requestQueue cancelAllOperations];
    
    LOCK(
         if (_buffer.count) {
             NSMutableDictionary *holder = _buffer;
             _buffer = [NSMutableDictionary new];
             dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
                 // Capture the dictionary to global queue,
                 // release these images in background to avoid blocking UI thread.
                 [holder class];  // 捕获字典到全局队列,在后台释放这些图像以避免阻塞UI线程。
                 
             });
         }
    );
    _link.paused = YES;
    _time = 0;
    if (_curIndex != 0) {
        [self willChangeValueForKey:@"currentAnimatedImageIndex"];
        _curIndex = 0; // 把索引值重置为0
        [self didChangeValueForKey:@"currentAnimatedImageIndex"];
    }
    _curAnimatedImage = nil; // 当前图像为空
    _curFrame = nil; // 当前帧
    _curLoop = 0; //当前循环次数
    _totalLoop = 0; // 总循环次数
    _totalFrameCount = 1; // 总帧数
    _loopEnd = NO; // 是否循环结尾
    _bufferMiss = NO; // 是否丢帧
    _incrBufferCount = 0; // 当前允许的缓存
}

重置图片的参数;
内存警告时释放内存;
初始化一个新的 CADisplayLink 对象,在屏幕更新时调用。

// 只有屏幕刷新累加时间不小于当前帧的动画播放时间才显示图片,播放下一帧。
// 播放 GIF 的关键
- (void)step:(CADisplayLink *)link {
    UIImage <YYAnimatedImage> *image = _curAnimatedImage;
    NSMutableDictionary *buffer = _buffer;
    // 下一张的图片
    UIImage *bufferedImage = nil;
    // 下一张要显示的索引
    NSUInteger nextIndex = (_curIndex + 1) % _totalFrameCount;
    BOOL bufferIsFull = NO;
    
    // // 当前无图像显示 返回
    if (!image) return;
    if (_loopEnd) { // view will keep in last frame // 结束循环 停留在最后帧
        [self stopAnimating]; // 如果动画播放循环结束了,就停止动画
        return;
    }
    
    NSTimeInterval delay = 0;
    if (!_bufferMiss) {
        // 屏幕刷新时间的累加
        _time += link.duration; // link.duration 屏幕刷新的时间,默认1/60 s
        delay = [image animatedImageDurationAtIndex:_curIndex]; // 返回当前帧的持续时间
        if (_time < delay) return;
        _time -= delay; // 减去上一帧播放的时间
        if (nextIndex == 0) {
            _curLoop++; // 增加一轮循环次数
            if (_curLoop >= _totalLoop && _totalLoop != 0) { // 已经到了循环次数,停止播放
                _loopEnd = YES;
                [self stopAnimating];
                [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
                return; // stop at last frame
            }
        }
        delay = [image animatedImageDurationAtIndex:nextIndex]; // 返回下一帧的的持续时间
        
        /**  */
        if (_time > delay) _time = delay; // do not jump over frame
    }
    LOCK(
         bufferedImage = buffer[@(nextIndex)];
         if (bufferedImage) {
             if ((int)_incrBufferCount < _totalFrameCount) {
                 [buffer removeObjectForKey:@(nextIndex)];
             }
             [self willChangeValueForKey:@"currentAnimatedImageIndex"];
             _curIndex = nextIndex; // 用KVO改变 当前索引值
             [self didChangeValueForKey:@"currentAnimatedImageIndex"];
             _curFrame = bufferedImage == (id)[NSNull null] ? nil : bufferedImage;
             
             // 实现YYSpriteSheetImage 的协议方法,才会进入该 if 语句
             if (_curImageHasContentsRect) {
                 _curContentsRect = [image animatedImageContentsRectAtIndex:_curIndex];
                 [self setContentsRect:_curContentsRect forImage:_curFrame];
             }
             nextIndex = (_curIndex + 1) % _totalFrameCount;
             _bufferMiss = NO;
             if (buffer.count == _totalFrameCount) {
                 bufferIsFull = YES; // 缓冲区已经满
             }
         } else {
             // 丢帧,某一帧没有办法找到显示
             _bufferMiss = YES;
         }
    )//LOCK
    
    if (!_bufferMiss) {
        // 刷新显示图像
        [self.layer setNeedsDisplay]; // let system call `displayLayer:` before runloop sleep
    }
    
    /* _YYAnimatedImageViewFetchOperation 为 NSOperation 的子类
        还未获取完所有图像,交给它获取下一张图像 */
    if (!bufferIsFull && _requestQueue.operationCount == 0) { // if some work not finished, wait for next opportunity
        _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];
        operation.view = self;
        operation.nextIndex = nextIndex;
        operation.curImage = image;
        [_requestQueue addOperation:operation]; //
    }
}

这是动画播放的关键,是 CADisplayLink对象 的方法,每 1/60s 也就是屏幕刷新一次就调用一次

- (void)calcMaxBufferCount {
    int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame; // 求出每一帧的字节数
    if (bytes == 0) bytes = 1024; // 如果为0,则给定1024
    
    int64_t total = _YYDeviceMemoryTotal(); // 获取设备的CPU物理内存
    int64_t free = _YYDeviceMemoryFree(); // 获取设备的容量
    int64_t max = MIN(total * 0.2, free * 0.6); // 比较内存的0.2倍以及容量的0.6倍最小值
    max = MAX(max, BUFFER_SIZE); // 如果不够 10 M,则以 10 M 作为最大缓冲区大小
    
    /** _maxBufferSize 内部帧缓冲区大小
     * 当设备有足够的空闲内存时,这个视图将请求并解码一些或所有未来的帧图像进入一个内部缓冲区。
     * 默认值为0 如果这个属性的值是0,那么最大缓冲区大小将根据当前的状态进行动态调整设备释放内存。否则,缓冲区大小将受到此值的限制。
     * 当收到内存警告或应用程序进入后台时,缓冲区将被立即释放
     */
    if (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max; //得出缓冲区的最大值
    
    double maxBufferCount = (double)max / (double)bytes;
    if (maxBufferCount < 1) maxBufferCount = 1;
    else if (maxBufferCount > 512) maxBufferCount = 512;
    _maxBufferCount = maxBufferCount; // 最大缓冲数
}

动态求出最大缓冲数--->参考

/* 从自定义的 start 方法中调用 main 方法
 调用[self didMoved]; 从而调用此方法
*/
- (void)main {
    __strong YYAnimatedImageView *view = _view;
    if (!view) return;
    if ([self isCancelled]) return;
    view->_incrBufferCount++;
    
    //动态调整当前内存的缓冲区大小。
    if (view->_incrBufferCount == 0) [view calcMaxBufferCount];
    
    if (view->_incrBufferCount > (NSInteger)view->_maxBufferCount) {
        view->_incrBufferCount = view->_maxBufferCount;
    }
    NSUInteger idx = _nextIndex; // 获取 Operation 中传过来的 下一个索引值
    NSUInteger max = view->_incrBufferCount < 1 ? 1 : view->_incrBufferCount; // 当前的缓冲区计数
    NSUInteger total = view->_totalFrameCount; // 总图片帧数
    view = nil;
    
    for (int i = 0; i < max; i++, idx++) {
        @autoreleasepool {
            if (idx >= total) idx = 0;
            if ([self isCancelled]) break;
            __strong YYAnimatedImageView *view = _view;
            if (!view) break;
            LOCK_VIEW(BOOL miss = (view->_buffer[@(idx)] == nil)); //  拿索引值去当前缓冲区取图片
            
            // 如果没有取到图片,则在子线程重新解码,得到解码后的图片
            if (miss) {
                // 等到当前还未解码的图片
                UIImage *img = [_curImage animatedImageFrameAtIndex:idx];
                NSLog(@"当前线程---%@", [NSThread currentThread]); // 打印当前线程,每次打印都是 name = (null),说明在异步线程
                // 在异步线程再次调用解码图片,如果无法解码或已经解码就返回self
                img = img.yy_imageByDecoded;
                if ([self isCancelled]) break;
                LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [NSNull null]); // 每次添加一张图片到 _buffer 数组
                view = nil;
            }
        }
    }
}

该方法负责把图片存入缓冲区中。(过程:取未解码图片-->解码存入缓冲区)

在此,对YYImage框架完毕了,希望大家都能从大神源码学到知识。




其他额外收获:

1、是否模拟器

- (BOOL)isSimulator {
    size_t size;
    sysctlbyname("hw.machine", NULL, &size, NULL, 0);
    char *machine = malloc(size);
    sysctlbyname("hw.machine", machine, &size, NULL, 0);
    NSString *model = [NSString stringWithUTF8String:machine];
    free(machine);
    return [model isEqualToString:@"x86_64"] || [model isEqualToString:@"i386"];
}

2、根据不同的系统 scale 选择图片

/** 一个NSNumber对象数组,根据不同的系统scale返回数组内部不同顺序的数字
e.g. iPhone3GS:@[@1,@2,@3] iPhone5:@[@2,@3,@1]  iPhone6 Plus:@[@3,@2,@1]
*/
static NSArray *_NSBundlePreferredScales() {
    static NSArray *scales;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CGFloat screenScale = [UIScreen mainScreen].scale;
        if (screenScale <= 1) {
            scales = @[@1,@2,@3];
        } else if (screenScale <= 2) {
            scales = @[@2,@3,@1];
        } else {
            scales = @[@3,@2,@1];
        }
    });
    return scales;
}

咋一看,这不是单例吗?保证初始化代码只执行一次,可移步单例相关文章

3、判断图片后缀

    NSArray *exts = ext.length > 0 ? @[ext] : @[@"", @"png", @"jpeg", @"jpg", @"gif", @"webp", @"apng"];
    NSArray *scales = _NSBundlePreferredScales();
    for (int s = 0; s < scales.count; s++) {
        scale = ((NSNumber *)scales[s]).floatValue;
        NSString *scaledName = _NSStringByAppendingNameScale(res, scale);
        for (NSString *e in exts) {
            path = [[NSBundle mainBundle] pathForResource:scaledName ofType:e];
            if (path) break;
        }
        if (path) break;
    }

如果图片没标明后缀,则遍历后缀数组,并添加后缀到传进来的图片名,最后到 mainBundle 里面取图片路径,取到地址则停止

CF_RETURNS_RETAINED 标记返回CF类型的函数,该类型需要调用方释放
NSDefaultRunLoopMode 保持gif 图在scrollView 拉动时不停止
|= 为按位或运算符 eg: a|=b; 相当于 a=a|b;

参考:
快速解决GIF图的锯齿问题

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

推荐阅读更多精彩内容

  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,094评论 5 13
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,462评论 6 30
  • 前几天,我听一个情感类节目,其中有位男士,暂且称他为A先生吧,打进电话和主持人谈心:“隔着电台,彼此不认识,我想讲...
    隽虹阅读 498评论 0 0
  • 抛开滴滴和监管之外,谈谈个人的一些建议: 对于女士: 在经历了上一次事件之后,对于个人,或者任何时候出门在外,都需...
    zh_way阅读 231评论 0 0
  • 姚建新 其实,你我都清楚 白就是白,黑就是黑 可是为什么会接受 那个黑白搅拌出来的灰 其实,你我都认得 鹿就是鹿,...
    姚建新阅读 226评论 0 0