深入理解Kingfisher(下)

六、ImageDownloader

下载功能的架构以及主要属性介绍

在 Kingfisher 内,该类负责网络图片的下载,是对底层 URLSession 的封装,通过设置 URLSession 并成为 NSURLSessionDataDelegate 来得到图片数据,其主要属性如下所示:

public class ImageDownloader: NSObject {
    
    class ImageFetchLoad {
        var callbacks = [CallbackPair]()
        var responseData = NSMutableData()
        var shouldDecode = false
        var scale = KingfisherManager.DefaultOptions.scale
    }
    
    // MARK: - Public property
    /// This closure will be applied to the image download request before it being sent. You can modify the request for some customizing purpose, like adding auth token to the header or do a url mapping.
    public var requestModifier: (NSMutableURLRequest -> Void)?

    /// The duration before the download is timeout. Default is 15 seconds.
    public var downloadTimeout: NSTimeInterval = 15.0
    
    /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. You can use this set to specify the self-signed site.
    public var trustedHosts: Set<String>?
    
    /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
    public var sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration()
    
    /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
    public weak var delegate: ImageDownloaderDelegate?
    
    // MARK: - Internal property
    let barrierQueue: dispatch_queue_t
    let processQueue: dispatch_queue_t
    
    typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHander: ImageDownloaderCompletionHandler?)
    
    var fetchLoads = [NSURL: ImageFetchLoad]()
    
    // MARK: - Public method
    /// The default downloader.
    public class var defaultDownloader: ImageDownloader {
        return instance
    }
    
    /**
    Init a downloader with name.
    
    - parameter name: The name for the downloader. It should not be empty.
    
    - returns: The downloader object.
    */
    public init(name: String) {
        if name.isEmpty {
            fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
        }
        
        barrierQueue = dispatch_queue_create(downloaderBarrierName + name, DISPATCH_QUEUE_CONCURRENT)
        processQueue = dispatch_queue_create(imageProcessQueueName + name, DISPATCH_QUEUE_CONCURRENT)
    }
    
    func fetchLoadForKey(key: NSURL) -> ImageFetchLoad? {
        var fetchLoad: ImageFetchLoad?
        dispatch_sync(barrierQueue, { () -> Void in
            fetchLoad = self.fetchLoads[key]
        })
        return fetchLoad
    }
}

这段代码的重点在于:
这里定义了一个嵌套类 ImageFetchLoad,用于处理每一个 NSURL 的对应下载数据;
每一个 URL 的 ImageFetchLoad 里都包含一个 callbacks: [CallbackPair],而 CallbackPair 是一个元组,其中又包含两个闭包,一个是 progressBlock,一个是 completionHander,progressBlock 在每次接收到数据时都会调用,当下载任务较大时用于展示进度条,completionHander 当最后数据接收完成之后会被调用,只被调用一次。
每次获得的新数据都会被添加入 responseData: NSMutableData 中,最后完整的图片数据也会保存在其中。
通常情况下,我们的 ImageDownloader 往往需要处理多个 URL,也就对应多个 ImageFetchLoad,fetchLoads 是 [NSURL: ImageFetchLoad] 类型的字典,用于存储不同 URL 及其 ImageFetchLoad 之间的对应关系,这就牵扯到了一个问题,当读取 ImageFetchLoad 的时候,我们希望该 ImageFetchLoad 不在被写,写的同时不能进行读操作,我们使用 barrierQueue 来完成该需求,利用 dispatch_sync 阻塞当前线程,完成 ImageFetchLoad 读操作后再返回。
其实针对这种需求 GCD 提供了专门的特性来处理,即 dispatch_barrier_async 方法,使用此方法提交的任务,会等待先于它提交的任务执行完成之后才开始执行,只有当该任务执行完成之后,晚于它提交的任务才会开始执行,确保一段时间内该队列只执行该任务。

下载方法以及 NSURLSession 的设置

这段主要是为 KingfisherManager 提供封装好下载方法以及设置用于下载的 NSURLSession,代码如下:

    internal func downloadImageWithURL(URL: NSURL,
                       retrieveImageTask: RetrieveImageTask?,
                                 options: KingfisherManager.Options,
                           progressBlock: ImageDownloaderProgressBlock?,
                       completionHandler: ImageDownloaderCompletionHandler?)
    {
        if let retrieveImageTask = retrieveImageTask where retrieveImageTask.cancelled {
            return
        }
        
        let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
        
        // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
        let request = NSMutableURLRequest(URL: URL, cachePolicy: .ReloadIgnoringLocalCacheData, timeoutInterval: timeout)
        request.HTTPShouldUsePipelining = true
        
        self.requestModifier?(request)
        
        // There is a possiblility that request modifier changed the url to `nil`
        if request.URL == nil {
            completionHandler?(image: nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.InvalidURL.rawValue, userInfo: nil), imageURL: nil, originalData: nil)
            return
        }
        
        setupProgressBlock(progressBlock, completionHandler: completionHandler, forURL: request.URL!) {(session, fetchLoad) -> Void in
            let task = session.dataTaskWithRequest(request)
            task.priority = options.lowPriority ? NSURLSessionTaskPriorityLow : NSURLSessionTaskPriorityDefault
            task.resume()
            
            fetchLoad.shouldDecode = options.shouldDecode
            fetchLoad.scale = options.scale
            
            retrieveImageTask?.downloadTask = task
        }
    }
    
    // A single key may have multiple callbacks. Only download once.
    internal func setupProgressBlock(progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?, forURL URL: NSURL, started: ((NSURLSession, ImageFetchLoad) -> Void)) {

        dispatch_barrier_sync(barrierQueue, { () -> Void in

            var create = false
            var loadObjectForURL = self.fetchLoads[URL]
            if  loadObjectForURL == nil {
                create = true
                loadObjectForURL = ImageFetchLoad()
            }
            
            let callbackPair = (progressBlock: progressBlock, completionHander: completionHandler)
            loadObjectForURL!.callbacks.append(callbackPair)
            self.fetchLoads[URL] = loadObjectForURL!
            
            if create {
                let session = NSURLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())
                started(session, loadObjectForURL!)
            }
        })
    }
    
    func cleanForURL(URL: NSURL) {
        dispatch_barrier_sync(barrierQueue, { () -> Void in
            self.fetchLoads.removeValueForKey(URL)
            return
        })
    }
}

这里首先值得一提的是 NSMutableURLRequest 的实例属性 HTTPShouldUsePipelining,首先一图流解释这个属性的价值:

HTTPShouldUsePipelining.png

将该属性设置为 true 可以极大的提高网络性能,但是 HTTPShouldUsePipelining 也有其局限性,就是服务器必须按照收到请求的顺序返回对应的数据,详细内容在这里

从某 URL 处下载图片时,通过 setupProgressBlock 以及传入的 started 闭包生成对应的 NSURLSession,并依据生成的 session 和之前的 request 生成 NSURLSessionDataTask,并保留引用在 retrieveImageTask?.downloadTask 里,为 KingfisherManager 提供任务终止方法。
当生成 NSURLSession 时所传入的 ephemeralSessionConfiguration() 配置参数意在不保留下载缓存,因为缓存操作已在我们的 ImageCache 文件中处理,所以此处需做如此设置以保证 ImageDownloader 正常工作。

NSURLSessionDataDelegate

let session = NSURLSession(configuration: self.sessionConfiguration, delegate: self, delegateQueue:NSOperationQueue.mainQueue())

在之前的这行代码里,我们将自身设为了生成的 NSURLSession 的 delegate,所以接下来我们要通过实现 NSURLSessionDataDelegate 来得到返回的图片数据。
代码如下:

// MARK: - NSURLSessionTaskDelegate
extension ImageDownloader: NSURLSessionDataDelegate {
    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
        
        completionHandler(NSURLSessionResponseDisposition.Allow)
    }
    
    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {

        if let URL = dataTask.originalRequest?.URL, fetchLoad = fetchLoadForKey(URL) {
            fetchLoad.responseData.appendData(data)
            
            for callbackPair in fetchLoad.callbacks {
                callbackPair.progressBlock?(receivedSize: Int64(fetchLoad.responseData.length), totalSize: dataTask.response!.expectedContentLength)
            }
        }
    }
    
    private func callbackWithImage(image: UIImage?, error: NSError?, imageURL: NSURL, originalData: NSData?) {
        if let callbackPairs = fetchLoadForKey(imageURL)?.callbacks {
            
            self.cleanForURL(imageURL)
            
            for callbackPair in callbackPairs {
                callbackPair.completionHander?(image: image, error: error, imageURL: imageURL, originalData: originalData)
            }
        }
    }
    
    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
        
        if let URL = task.originalRequest?.URL {
            if let error = error { // Error happened
                callbackWithImage(nil, error: error, imageURL: URL, originalData: nil)
            } else { //Download finished without error
                
                // We are on main queue when receiving this.
                dispatch_async(processQueue, { () -> Void in
                    
                    if let fetchLoad = self.fetchLoadForKey(URL) {
                        
                        if let image = UIImage.kf_imageWithData(fetchLoad.responseData, scale: fetchLoad.scale) {
                            
                            self.delegate?.imageDownloader?(self, didDownloadImage: image, forURL: URL, withResponse: task.response!)
                            
                            if fetchLoad.shouldDecode {
                                
                                self.callbackWithImage(image.kf_decodedImage(scale: fetchLoad.scale), error: nil, imageURL: URL, originalData: fetchLoad.responseData)
                            } else {
                                
                                self.callbackWithImage(image, error: nil, imageURL: URL, originalData: fetchLoad.responseData)
                            }
                            
                        } else {
                            // If server response is 304 (Not Modified), inform the callback handler with NotModified error.
                            // It should be handled to get an image from cache, which is response of a manager object.
                            if let res = task.response as? NSHTTPURLResponse where res.statusCode == 304 {
                                self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.NotModified.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                                return
                            }
                            
                            self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                        }
                    } else {
                        self.callbackWithImage(nil, error: NSError(domain: KingfisherErrorDomain, code: KingfisherError.BadData.rawValue, userInfo: nil), imageURL: URL, originalData: nil)
                    }
                })
            }
        }
    }

    /**
    This method is exposed since the compiler requests. Do not call it.
    */
    public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {

        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            if let trustedHosts = trustedHosts where trustedHosts.contains(challenge.protectionSpace.host) {
                let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
                completionHandler(.UseCredential, credential)
                return
            }
        }
        
        completionHandler(.PerformDefaultHandling, nil)
    }
    
}

其中最重要的是这两个函数:

public func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData)

前一个函数当每一次下载到数据的时候都会被调用,我们在该函数中,将每次得到的数据添加在当前 URL 所对应的 fetchLoad 的 responseData 中,之后,我们为传入的 progressBlock 提供当前下载进度。

public func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?)

该函数在下载任务完成时会被调用,Kingfisher 在其中进行了各种错误处理,若数据成功下载,callbackWithImage 方法会被调用,返回下载到的图片、图片的 URL、以及下载到的原始数据。
这里我们注意到在图片下载成功之后,自身 ImageDownloaderDelegate 的代理方法会被调用,但我通过翻阅源码发现,并没有其他类接受了这个代理,delegate 始终为 nil,所以不对其进行讲解。
而且大概因为图片的解码操作也比较费时,Kingfisher 将函数主体放在了 processQueue 中执行以避免阻塞主线程。

KingfisherOptionsInfo

该文件主要用于接收配置 Kingfisher 行为的各种参数,包括缓存、下载、加载动画以及之前 KingfisherOptions 所包含的所有属性,代码如下:

/**
*   KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem]. You can use the enum of option item with value to control some behaviors of Kingfisher.
*/
public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem]

/**
Item could be added into KingfisherOptionsInfo

- Options:     Item for options. The value of this item should be a KingfisherOptions.
- TargetCache: Item for target cache. The value of this item should be an ImageCache object. Kingfisher will use this cache when handling the related operation, including trying to retrieve the cached images and store the downloaded image to it.
- Downloader:  Item for downloader to use. The value of this item should be an ImageDownloader object. Kingfisher will use this downloader to download the images.
- Transition:  Item for animation transition when using UIImageView.
*/
public enum KingfisherOptionsInfoItem {
    case Options(KingfisherOptions)
    case TargetCache(ImageCache)
    case Downloader(ImageDownloader)
    case Transition(ImageTransition)
}

func ==(a: KingfisherOptionsInfoItem, b: KingfisherOptionsInfoItem) -> Bool {
    switch (a, b) {
    case (.Options(_), .Options(_)): return true
    case (.TargetCache(_), .TargetCache(_)): return true
    case (.Downloader(_), .Downloader(_)): return true
    case (.Transition(_), .Transition(_)): return true
    default: return false
    }
}

extension CollectionType where Generator.Element == KingfisherOptionsInfoItem {
    func kf_findFirstMatch(target: Generator.Element) -> Generator.Element? {
        
        let index = indexOf {
            e in
            return e == target
        }
        
        return (index != nil) ? self[index!] : nil
    }
}

这段代码之中有两个亮点,其一是借助了对 == 运算符的重载实现了判断传入参数类型的作用;其二是通过对 CollectionType 的拓展来为其添加迅速找出对应类型配置参数的功能,该函数中出现的第二个 == 运算符即使用到了上方对 == 的拓展用法,第一个 == 运算符用于判断 Generator.Element 的类型,其重载在 CollectionType 内部实现。

KingfisherManager

该类是 Kingfisher 的核心类,封装了之前讲到的 ImageCache、ImageDownloader 与 KingfisherOptionsInfo,集成了缓存以及下载两大功能,并直接为 UIImageView+Kingfisher 以及 UIButton+Kingfisher 提供操作方法。
该类的功能主要可以分为两部分:一是根据传入的 URL 返回对应的网络图片,二是解析传入的配置参数并对相关功能模块进行配置。
其中第二个部分又是第一个功能的组成部分,我们先来看第二部分,代码如下:

    func parseOptionsInfo(optionsInfo: KingfisherOptionsInfo?) -> (Options, ImageCache, ImageDownloader) {
        var options = KingfisherManager.DefaultOptions
        var targetCache = self.cache
        var targetDownloader = self.downloader
        
        guard let optionsInfo = optionsInfo else {
            return (options, targetCache, targetDownloader)
        }
        
        if let optionsItem = optionsInfo.kf_findFirstMatch(.Options(.None)), case .Options(let optionsInOptionsInfo) = optionsItem {
            
            let queue = optionsInOptionsInfo.contains(KingfisherOptions.BackgroundCallback) ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) : KingfisherManager.DefaultOptions.queue
            let scale = optionsInOptionsInfo.contains(KingfisherOptions.ScreenScale) ? UIScreen.mainScreen().scale : KingfisherManager.DefaultOptions.scale
            
            options = (forceRefresh: optionsInOptionsInfo.contains(KingfisherOptions.ForceRefresh),
                lowPriority: optionsInOptionsInfo.contains(KingfisherOptions.LowPriority),
                cacheMemoryOnly: optionsInOptionsInfo.contains(KingfisherOptions.CacheMemoryOnly),
                shouldDecode: optionsInOptionsInfo.contains(KingfisherOptions.BackgroundDecode),
                queue: queue, scale: scale)
        }
        
        if let optionsItem = optionsInfo.kf_findFirstMatch(.TargetCache(self.cache)), case .TargetCache(let cache) = optionsItem {
            targetCache = cache
        }
        
        if let optionsItem = optionsInfo.kf_findFirstMatch(.Downloader(self.downloader)), case .Downloader(let downloader) = optionsItem {
            targetDownloader = downloader
        }
        
        return (options, targetCache, targetDownloader)
    }

我们可以看到三个 if let 语句分别对应 KingfisherOptionsInfoItem 的前三种枚举配置类型,而配置 KingfisherOptions 相关参数的时候又用到了 OptionSetType 的协议拓展方法 contains,来获取对应属性的配置参数。对最后一种 ImageTransition 的配置, Kingfisher 放在了 UIImageView+Kingfisher 中进行。

接下来我们来讲第一部分,第一部分的主要功能函数有两个,代码如下:

    func downloadAndCacheImageWithURL(URL: NSURL,
                               forKey key: String,
                        retrieveImageTask: RetrieveImageTask,
                            progressBlock: DownloadProgressBlock?,
                        completionHandler: CompletionHandler?,
                                  options: Options,
                              targetCache: ImageCache,
                               downloader: ImageDownloader)
    {
        downloader.downloadImageWithURL(URL, retrieveImageTask: retrieveImageTask, options: options, progressBlock: { (receivedSize, totalSize) -> () in
            progressBlock?(receivedSize: receivedSize, totalSize: totalSize)
        }) { (image, error, imageURL, originalData) -> () in

            if let error = error where error.code == KingfisherError.NotModified.rawValue {
                // Not modified. Try to find the image from cache. 
                // (The image should be in cache. It should be guaranteed by the framework users.)
                targetCache.retrieveImageForKey(key, options: options, completionHandler: { (cacheImage, cacheType) -> () in
                    completionHandler?(image: cacheImage, error: nil, cacheType: cacheType, imageURL: URL)

                })
                return
            }
            
            if let image = image, originalData = originalData {
                targetCache.storeImage(image, originalData: originalData, forKey: key, toDisk: !options.cacheMemoryOnly, completionHandler: nil)
            }
            
            completionHandler?(image: image, error: error, cacheType: .None, imageURL: URL)
        }
    }

该函数负责下载传入URL所对应的网络图片并将其缓存,主要调用了 downloader.downloadImageWithURL 来下载所需图片数据,之后调用 targetCache.storeImage 来缓存数据。

public func retrieveImageWithResource(resource: Resource,
        optionsInfo: KingfisherOptionsInfo?,
        progressBlock: DownloadProgressBlock?,
        completionHandler: CompletionHandler?) -> RetrieveImageTask
    {
        let task = RetrieveImageTask()
        
        // There is a bug in Swift compiler which prevents to write `let (options, targetCache) = parseOptionsInfo(optionsInfo)`
        // It will cause a compiler error.
        let parsedOptions = parseOptionsInfo(optionsInfo)
        let (options, targetCache, downloader) = (parsedOptions.0, parsedOptions.1, parsedOptions.2)
        
        if options.forceRefresh {
            downloadAndCacheImageWithURL(resource.downloadURL,
                forKey: resource.cacheKey,
                retrieveImageTask: task,
                progressBlock: progressBlock,
                completionHandler: completionHandler,
                options: options,
                targetCache: targetCache,
                downloader: downloader)
        } else {
            let diskTaskCompletionHandler: CompletionHandler = { (image, error, cacheType, imageURL) -> () in
                // Break retain cycle created inside diskTask closure below
                task.diskRetrieveTask = nil
                completionHandler?(image: image, error: error, cacheType: cacheType, imageURL: imageURL)
            }
            let diskTask = targetCache.retrieveImageForKey(resource.cacheKey, options: options, completionHandler: { (image, cacheType) -> () in
                if image != nil {
                    diskTaskCompletionHandler(image: image, error: nil, cacheType:cacheType, imageURL: resource.downloadURL)
                } else {
                    self.downloadAndCacheImageWithURL(resource.downloadURL,
                        forKey: resource.cacheKey,
                        retrieveImageTask: task,
                        progressBlock: progressBlock,
                        completionHandler: diskTaskCompletionHandler,
                        options: options,
                        targetCache: targetCache,
                        downloader: downloader)
                }
            })
            task.diskRetrieveTask = diskTask
        }
        
        return task
    }

首先我们对传入的配置参数使用 parseOptionsInfo 方法进行解析,如果 options.forceRefresh,被设置为 true,便直接调用 downloadAndCacheImageWithURL 方法下载并缓存该 URL 所对应的图片,若被设置为 false,则先调用 targetCache.retrieveImageForKey 尝试从缓存中取出所需图片,如果取不到,说明缓存中没有对应图片,则调用 downloadAndCacheImageWithURL 下载并缓存对应图片。

UIImageView+Kingfisher 以及 UIButton+Kingfisher

这两个类主要是对 UIImageView 和 UIButton进行拓展,功能的实现部分均为对 KingfisherManager 内相应函数的调用,Kingfisher 的文档内详细介绍了这两个类的对外拓展接口,这里就不赘述了。
不过其内部仍包含一个值得我们学习的知识点。

Associated Objects

Associated Objects(关联对象)或者叫作关联引用(Associative References),是作为Objective-C 2.0 运行时功能被引入到 Mac OS X 10.6 Snow Leopard(及iOS4)系统。与它相关在<objc/runtime.h>中有3个C函数,它们可以让对象在运行时关联任何值:

  • objc_setAssociatedObject
  • objc_getAssociatedObject
  • objc_removeAssociatedObjects

我们并不能在类型拓展中放置存储属性,所以需要使用 Associated Objects 来向某些系统类(NSObject 的子类)中增添所需的属性。
这里将介绍其最简单的用法,代码如下:

private var lastURLKey: Void?
public extension UIImageView {
    /// Get the image URL binded to this image view.
    public var kf_webURL: NSURL? {
        get {
            return objc_getAssociatedObject(self, &lastURLKey) as? NSURL
        }
    }
    
    private func kf_setWebURL(URL: NSURL) {
        objc_setAssociatedObject(self, &lastURLKey, URL, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
    }
}

这里为 UIImageView 成功添加了 kf_webURL 属性,我们通过 kf_setWebURL 对其赋值,通过 kf_webURL 获取其值。
如果你想知道更多 Associated Objects 的相关内容,可以看这里

结语

由于笔者只是大三在校生,并没有工作经验,对iOS开发的学习也完全出于兴趣,所以文章出现纰漏之处在所难免,恳请前辈们批评指正,不胜感激。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,048评论 18 139
  • 序言 Kingfisher 是由 @onevcat 编写的用于下载和缓存网络图片的轻量级Swift工具库,其中涉及...
    我偏笑_NSNirvana阅读 20,140评论 7 84
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,230评论 0 15
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,551评论 4 58
  • 司马迁为被迫降敌的李陵辩护,武帝震怒,将他打入大牢,并施以宫刑.这无疑是弯道的来临,但同时也给他带来了生命的机遇,...
    hello贡阅读 234评论 0 0