Swift:用NSURLSession下載iTunes歌曲

在 swift 中使用 NSURLSession 時,看到了一篇 文章 使用 NSURLSession 從 iTunes 下載歌曲,也包含暫停、繼續下載、模擬進度、取消下載的功能。但文章中一些技術細節稍微老舊了些,故,在這里重新整理一下,方便日后學習。
完整項目地址 TracksDownload-iTunes

準備工作
  • Xcode 版本要求 7.3 及以上,我用的Xcode7.3,OS X 版本要求 10.11.0 及以上
  • 這里 下載基礎工程。解壓縮,運行程序,會看到一個基本的界面,界面上有個 SearchBar 和空的 TableView,如下圖
NSRULSession 簡介

NSRULSession 在技術上不僅是一個類,而且也是一套處理基于 HTTP/HTTPS 請求的類。通過下圖來了解一下它的構成

NSRULSession 是收、發 HTTP 請求的關鍵對象,它可以通過一個配置體 NSURLSessionConfiguration 創建。這個配置體可以設置 session 的超時時間,緩存策略,以及 HTTP headers ,它可以由三種方式創建:

  • defaultSessionConfiguration:通過這個方法生成的對象,會用默認的方式管理上傳和下載的任務 ,并本地持久化 cache,cookie 和 信任證書
  • ephemeralSessionConfiguration:和上面的方法類似,區別在于它會把會話相關的數據最優化的存儲在內存中,并從內存中取這些數據
  • backgroundSessionConfiguration:系統會把上傳或下載任務放在單獨的進程,允許這些任務在后臺進行,及時這個 app 被后臺掛起或終止,session 的傳輸也不會停止(如果你雙擊home鍵,向上滑動 app 進行關閉,那么所有的 session 都會中斷)

NSRULSession 的所有的任務都需要關聯一個任務 NSURLSessionTask對象,這個對象是任務的實際執行者,進行數據的獲取,下載或上傳文件。這個對象有三種類型:

  • NSURLSessionDataTask:用這種類型的對象做 HTTP GET 請求,從服務器檢索數據,并存到內存中
  • NSURLSessionUploadTask:用這種類型的對象把磁盤中的文件上傳到服務器,典型地,通過 HTTP POST 或 PUT 方法
  • NSURLSessionDownloadTask:用這種類型的對象從服務器下載文件,并存到一個臨時的文件地址

你可以暫停、繼續和取消一個任務。NSURLSessionDownloadTask 支持任務暫停,并在以后繼續下載

一般地,NSURLSession 通過兩種方式返回數據:一. 任務完成或失敗后,通過一個 completionHandler 塊返回數據;二. 在創建 session 時,指定一個代理方法,任務結束后通過回調方法返回數據

了解了 NSURLSession 的基本知識后,接下來開始實際操作

查詢歌曲

要查詢歌曲,需要借助 iTunes Search API ,在 UISearchBar 中,輸入關鍵字,然后點擊回車,進行搜索。

首先,在 SearchViewController.swift 中,在

var searchResults = [TrackModel]()

下面 添加以下代碼:

// 歌曲查詢 session 和 task
let session_queryTracks = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
var task_queryTracks: NSURLSessionTask?
  • 第一句,我們通過默認的 configuration 生成了一個 NSURLSession 對象
  • 第二句,聲明一個 NSURLSessionTask 類型變量,用它進行 HTTP GET 請求,從 iTunes 的服務器查詢歌曲。每次用戶發起新的查詢時,這個變量都會被重新初始化并循環使用

然后,需要借助 UISearchBar 的代理方法 searchBarSearchButtonClicked(_:),來捕獲用戶的搜索行為。在 SearchViewController.swift 中找到這個代理方法,更新為如下代碼:

func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    
    dismissKeyboard()

    let searchString = searchBar.text!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())

    if !searchString.isEmpty {
        // 1
        if task_queryTracks != nil {
            task_queryTracks?.cancel()
        }
        // 2   
        UIApplication.sharedApplication().networkActivityIndicatorVisible = true
        
        // 3 設置允許包含在搜索關鍵詞中的字符
        let expectedCharSet = NSCharacterSet.URLQueryAllowedCharacterSet()
        let searchTerm = searchString.stringByAddingPercentEncodingWithAllowedCharacters(expectedCharSet)
        // 4
        let urlString = "http://itunes.apple.com/search?media=music&entity=song&term=\(searchTerm!)"
        let url = NSURL(string: urlString)
        // 5 生成查詢任務對象
        task_queryTracks = session_queryTracks.dataTaskWithURL(url!, completionHandler: { [unowned self](data, response, error) in
            // 6
            dispatch_async(dispatch_get_main_queue(), {
                UIApplication.sharedApplication().networkActivityIndicatorVisible = false
            })
            // 7
            if let error = error {
                print(error.localizedDescription)
            }
            else if let httpResponse = response as? NSHTTPURLResponse {
            
                if httpResponse.statusCode == 200 {
                    self.updateSearchResults(data)
                }
            }
        })
        // 8 開始查詢
        task_queryTracks?.resume()
    }
  }

按著上面的注釋標號,依次說明一下:

  • //1. 每次用戶查詢時,都會檢查 task_queryTracks 是否已經初始化,如果初始化了,那么就取消上一次搜索任務,以便開始新的任務搜索,并重新利用 task_queryTracks
  • //2. 在狀態欄顯示小菊花,告訴用戶,系統正在進行網絡任務
  • //3. 搜索的關鍵字被傳入 URL 前,把一些不被允許的字符過濾掉
  • //4. 根據 iTunes Search API ,把處理過的內容當做 GET 請求的參數,生成一個 NSURL 對象
  • //5. 初始化一個 NSURLSessionDataTask 對象,來處理 HTTP GET 請求,任務完成后,數據會在 completionHandler 塊中返回
  • //6. 在主線程隱藏狀態欄的菊花,表明網絡任務結束
  • //7. 如果成功了,則調用方法 updateSearchResults(_:) 來處理收到的 NSData 數據,并更新 TableView
  • //8. 調用 resume() 開始搜索任務

運行 app,可以搜索任意一首歌,比如輸入 Swift,回車搜索,會出現下圖的效果:

準備下載歌曲

下載歌曲時,為了允許用戶暫停、繼續、取消下載,并且能顯示下載進度,我們建立一個下載的 Model ,來保存下載狀態。在 Model 文件夾下,新建類文件,命名為 DownloadModel 如圖:

在文件 DownloadModel.swift 中,添加以下代碼:

class DownloadModel {
    
    var downloadUrl: String
    var isDownloading = false
    var downloadProgress = 0.0
    
    var downloadTask: NSURLSessionDownloadTask?
    var downloadResumeData: NSData?
    
    init(downloadUrl: String) {
        self.downloadUrl = downloadUrl
    }
}

簡單介紹一下這些屬性:

  • downloadUrl :歌曲的下載地址,唯一標識一個 DownloadModel
  • isDownloading : 歌曲是否正在下載
  • downloadProgress:歌曲下載進度,0.0~1.0
  • downloadTask:歌曲下載的一個 Task 對象
  • downloadResumeData:暫停時,得到的恢復數據,包含繼續下載的信息(iTunes 服務器支持斷點下載)
建立下載任務

有了這個 Model 之后,為了追蹤每一個下載任務,切換到 SearchViewController.swift 文件,找到

var searchResults = [TrackModel]()

在它下面一行,添加以下代碼:

var trackDownload = [String: DownloadModel]()
lazy var session_downloadTracks: NSURLSession = {
        
        let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
        let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
        return session
    }()
  • 第一句是做了一個下載的映射,唯一的 url 對應一個下載 Model,來追蹤歌曲的下載狀態
  • 第二句生成下載歌曲的 Session,這個 Session 只用于生成下載歌曲用的 NSURLSessionDownloadTask。其中,設置了代理,來處理與 Session 相關的事件,比如可以在代理方法中得到下載的進度,數據等。我們設置 delegateQueue 為 nil,默認的,系統會在一個串行隊列中進行代理方法的調用以及執行的結果方法調用
  • 使用 lazy 關鍵字,系統不立刻生成 session_downloadTracks 這個對象,而是我們使用它時,系統才去創建

接下來,來實現 NSURLSession 的代理方法。在文件 SearchViewController.swift 的最底部,加入以下代碼:

extension SearchTracksViewController: NSURLSessionDownloadDelegate {
    
    func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
        
        print("下載結束")
    }
}
  • NSURLSessionDownloadDelegate 定義了使用 NSURLSession 下載某些任務時,用到的代理方法。一個下載任務結束的時候,方法 URLSession(_:downloadTask:didFinishDownloadingToURL:) 都會被調用。

我們來觸發下載任務。當用戶點擊 Download 按鈕時,會調用方法 startDownload(_:),在此方法中執行下載任務,找到這個方法,更新為以下代碼:

func startDownload(track: TrackModel) {
        
        if let urlString = track.trackPreviewUrl, url = NSURL(string:urlString) {
            
            let download = DownloadModel(downloadUrl: urlString)
            
            download.downloadTask = session_downloadTracks.downloadTaskWithURL(url)
            download.downloadTask?.resume()
            download.isDownloading = true
    
            trackDownload[urlString] = download
        }
    }
  • 當用戶點擊下載的時候,在此方法中生成一個 DownloadModel 對象,保存了下載中的歌曲狀態,并映射到 字典trackDownload

運行這個 app ,搜索任意一首歌,點擊下載,過一會就會收到一條打印信息:"下載結束"。表示下載結束。

保存并播放歌曲

歌曲下載完之后,會調用方法 URLSession(_:downloadTask:didFinishDownloadingToURL:) 。方法里有個參數 URL,是文件的臨時存放地址,我們要做到的就是把這個文件拷貝一個指定的地址(本地持久化)。然后,我們需要把已經完成的任務從字典 trackDownload 中移除,并更新相應的 tableViewCell。

為了方便找到對應的 cell,我們在 SearchViewController.swift 文件中添加一個輔助方法,用來返回 cell 所在的索引 index。代碼如下:

func cellIndexOfDownloadTrack(downloadTrack:NSURLSessionDownloadTask) -> Int? {
        
        if let url = downloadTrack.originalRequest?.URL?.absoluteString {
            
            for (index, track) in searchResults.enumerate() {
                
                if url == track.trackPreviewUrl {
                    return index
                }
            }
        }
        return nil
    }

下一步就要開始把文件拷貝到我們指定的地址。更新代理方法 URLSession(_:downloadTask:didFinishDownloadingToURL:) 如下:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
        
        // 1
        let originalURL: String? = downloadTask.originalRequest?.URL?.absoluteString
        if let url = originalURL, destinationURL = localFilePathForUrl(url) {
            
            print(destinationURL)
            
            // 2
            let fileManager = NSFileManager.defaultManager()
            do {
                try fileManager.removeItemAtURL(destinationURL)
            } catch {
                //
            }
            
            do {
                try fileManager.copyItemAtURL(location, toURL: destinationURL)
            } catch let error as NSError {
                print("Could not copy file to disk:\(error.localizedDescription)")
            }
        }
        
        // 3
        if let url = originalURL {
            
            trackDownload[url] = nil
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask) {
                dispatch_async(dispatch_get_main_queue(), {
                    
                    [unowned self] in
                    self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index,inSection: 0)], withRowAnimation: .None)
                })
            }
        }
    }

對于上面代碼標注的關鍵步驟,做一個簡單說明:

  • //1. 我們提取出下載任務的原始 URL,然后找到 app 的 Documents 路徑,在這個路徑后拼接原始 URL的lastPathComponent,得到一個新的路徑,就是我們需要的目標路徑
  • //2. 把文件從臨時路徑 location 拷貝到目標路徑之前,先使用 NSFileManager 清理目標路徑下的數據,然后再執行拷貝
  • //3. 從數據結構中刪除這個不再需要的 downloadTask 對象
  • //4. 根據索引,更新相應的 tableviewCell

運行 app,搜索一首歌,點擊下載,稍等片刻就會收到一條打印信息:


下載按鈕也會消失,點擊已經下載的歌曲,就會彈出 MPMoviePlayerViewController 進行播放,如圖:

模擬下載進度

模擬下載進度時,我們需要知道兩點:

  • 已接收的數據量
  • 總數據量

協議 NSURLSessionDownloadDelegate 的代理方法中,有一個方法帶有我們需要的這兩個參數,在文件 SearchViewController.swift 中,找到對這個協議的擴展,添加下面的方法:

func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
        // 1
        if let url = downloadTask.originalRequest?.URL?.absoluteString, trackDownload = trackDownload[url] {
            
            // 2
            trackDownload.downloadProgress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
            // 3
            let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: .Binary)
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask), trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as? TrackCell {
                
                dispatch_async(dispatch_get_main_queue(), { 
                    trackCell.v_progress.progress = trackDownload.downloadProgress
                    trackCell.lb_progress.text = String(format: "%.1f%% of %@", trackDownload.downloadProgress*100,totalSize)
                })
            }
        }
    }

接下來一步步分析代碼中的標注:

  • //1. 使用參數 downloadTask,提取其中的 URL,然后根據 URL 找到對應的下載 Model
  • //2. 這一步是關鍵,參數 totalBytesWritten 代表已經接收并寫入臨時文件的數據,參數 totalBytesExpectedToWrite 代表總數據量,兩個值求商就是當前的下載比例。然后保存到下載 Model 的 downloadProgress 屬性中
  • //3. NSByteCountFormatter 可以把數據量轉換為人們易懂的字節數,比如轉換后變為 50 KB
  • //4. 最后找到這首歌曲對應的 cell,然后更新 cell 上的進度等

為了在 cell 上正確的顯示下載狀態,找到方法 tableView(_:cellForRowAtIndexPath:),在

let track = searchResults[indexPath.row]

下面添加代碼:

        var showDownloadControls = false
        if let download = trackDownload[track.trackPreviewUrl!] {
            
            showDownloadControls = true
            cell.v_progress.progress = download.downloadProgress
            cell.lb_progress.text = download.isDownloading ? "Downloading..." : "Paused"
        }
        cell.v_progress.hidden = !showDownloadControls
        cell.lb_progress.hidden = !showDownloadControls

對于將要下載的歌曲,顯示 “Downloading...”,暫停的顯示 “Paused”,并且根據下載狀態隱藏or顯示 v_progress 和 lb_progress。對于正在下載的歌曲,下載按鈕也要隱藏,所以,這句代碼

cell.btn_download.hidden = trackHaveDownloaded

更新為

cell.btn_download.hidden = trackHaveDownloaded || showDownloadControls

運行 app,下載一首歌,看一下下載效果,如圖所示:


暫停、繼續、取消下載

......

暫停

......

暫停時,會產生恢復數據 resume data,根據這里面的數據,可以在以后繼續下載,前提是服務器支持斷點下載。

并且不是所有的條件下都可以繼續下載的,具體哪些情況可以繼續下載,請參考 文檔

找到方法 pauseDownload(_:),更新為以下代碼:

func pauseDownload(track: TrackModel) {
        
        if let url = track.trackPreviewUrl, download = trackDownload[url] {
            
            if download.isDownloading {
                download.downloadTask?.cancelByProducingResumeData({ (data) in
                    
                    if data != nil {
                        download.downloadResumeData = data
                    }
                })
                download.isDownloading = false
            }
        }
    }

上面的代碼中,通過調用方法 cancelByProducingResumeData(_:),得到了 resume data,然后把這個 data 保存到相應的下載 Model 中,方便以后繼續下載。并更新 Model 中的屬性 isDownloading,表示停止下載。

......

繼續

......

找到方法 resumeDownload(_:) ,更新為以下代碼:

func resumeDownload(track: TrackModel) {
        
        if let previewUrl = track.trackPreviewUrl, download = trackDownload[previewUrl] {
            
            if let resumeData = download.downloadResumeData {
                
                download.downloadTask = session_downloadTracks.downloadTaskWithResumeData(resumeData)
                download.downloadTask!.resume()
                download.isDownloading = true
            }
            else if let url = NSURL(string: download.downloadUrl) {
                
                download.downloadTask = session_downloadTracks.downloadTaskWithURL(url)
                download.downloadTask!.resume()
                download.isDownloading = true
            }
        }
    }

在這個方法中,我們判斷如果有 resume data,那么調用方法 downloadTaskWithResumeData(_:) 來繼續下載。如果沒有,就重新下載 。兩種情況下,都更新下載狀態為 true。

......

取消

......

取消下載就比較簡單了,找到方法 cancelDownload(_:) ,更新為以下代碼:

func cancelDownload(track: TrackModel) {
        
        if let url = track.trackPreviewUrl, download = trackDownload[url] {
            
            download.downloadTask?.cancel()
            trackDownload[url] = nil
        }
    }

在這個方法中,找到需要取消的下載任務,然后調用方法 cancel() 就會取消下載,并從字典中刪掉這個任務。

最后要做的就是更新 cell 的工作了。回到方法 tableView(_:cellForRowAtIndexPath:) ,在 if 塊中,添加下面的代碼:

let title = download.isDownloading ? "Pause" : "Resume"
cell.btn_pause.setTitle(title, forState: .Normal)

cell.lb_progress.hidden = !showDownloadControls

下面添加以下代碼:

cell.btn_pause.hidden = !showDownloadControls
cell.btn_cancel.hidden = !showDownloadControls

整個工作到此結束,運行 app,下載幾首歌,并進行暫停,恢復,取消,效果如下圖所示:

總結

在建立 DownloadModel 的時候,里面的 DownloadModel 最好是個 class 類型,而不要聲明為 struct 類型,正如本項目中建立的一樣。因為 struct 類型是 value typeclass 類型是 reference type

它們之間的區別請查看 Swift: 概念解釋

本項目中,會對 DownloadModel 的對象所持有的屬性,比如 isDownloading 等進行多次的修改。如果 DownloadModel 是 struct 類型,那么每次修改過之后,都需要再更新一遍字典 trackDownload 中對應的 model,因為 struct 類型的對象在傳遞的過程中,是重新拷貝一份的,拷貝后得到的數據并不指向原始地址。而 class 類型是 引用類型,故在傳遞過程中,這個對象都是指向原始地址的,對它的修改,也會影響原始數據。

我們可以對比一下 DownloadModl 為 class 類型和 struct 類型兩種情況下,代碼的差異性:

  • DownloadModl 為 class 類型:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
        // 1
        if let url = downloadTask.originalRequest?.URL?.absoluteString, trackDownload = trackDownload[url] {
            
            // 2
            trackDownload.downloadProgress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
            // 3
            let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: .Binary)
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask), trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as? TrackCell {
                
                dispatch_async(dispatch_get_main_queue(), { 
                    trackCell.v_progress.progress = trackDownload.downloadProgress
                    trackCell.lb_progress.text = String(format: "%.1f%% of %@", trackDownload.downloadProgress*100,totalSize)
                })
            }
        }
    }
  • DownloadModl 為 struct 類型:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        
        // 1
        if let url = downloadTask.originalRequest?.URL?.absoluteString {
            
            download = trackDownload[url]! as DownloadModl
            // 2
            download.downloadProgress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
            // 3
            let totalSize = NSByteCountFormatter.stringFromByteCount(totalBytesExpectedToWrite, countStyle: .Binary)
            // 4
            if let index = cellIndexOfDownloadTrack(downloadTask), trackCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0)) as? TrackCell {
                
                dispatch_async(dispatch_get_main_queue(), { 
                    trackCell.v_progress.progress = download.downloadProgress
                    trackCell.lb_progress.text = String(format: "%.1f%% of %@", download.downloadProgress*100,totalSize)
                })
            }
           trackDownload[url] = download
        }
    }

注意區分上面兩種情況下,使用 struct 類型會方便很多,不然,類似的還有方法 pauseDownload(_:)resumeDownload(_:)等,都需要做相應調整。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容