iOS后臺(tái)模式開發(fā)指南

自從古老的iOS4以來,當(dāng)用戶點(diǎn)擊home建的時(shí)候,你可以使你的APP們在內(nèi)存中處于suspended(掛起)狀態(tài).即使APP仍停留在內(nèi)存中,它的所有操作是被暫停的直到用戶再次運(yùn)行它.

當(dāng)然這個(gè)規(guī)則中有例外情況.在特定的情況下,這個(gè)APP仍然可以在后臺(tái)中執(zhí)行某些操作.這個(gè)教程會(huì)教你在什么時(shí)候怎么去用最常用的一些后臺(tái)操作.

每一次iOS的發(fā)布都會(huì)在后臺(tái)操作和細(xì)節(jié)上的放寬限制先口,以此提升用戶體驗(yàn)和延長電池壽命.對于在iOS中實(shí)現(xiàn)"真正"的多任務(wù)來說,后臺(tái)模式不是一個(gè)神奇的解決辦法.當(dāng)用戶切換到其他的APP應(yīng)用時(shí),大多數(shù)的APP應(yīng)用仍然會(huì)完全的暫停運(yùn)行.你的應(yīng)用只被允許在很特殊的情況下才能在后臺(tái)中繼續(xù)運(yùn)行.例如,這些包括播放音頻,獲取位置更新,或者從服務(wù)器獲取最新內(nèi)容的情況.

iOS7之前,APP應(yīng)用在真正暫停之前會(huì)有連續(xù)10分鐘的時(shí)間去完成它們當(dāng)前的操作.隨著NSURLSession的出現(xiàn),有了一種更為優(yōu)雅的方式去應(yīng)對大量的網(wǎng)絡(luò)切換.因此,對于可用的后臺(tái)運(yùn)行時(shí)間已經(jīng)減少到只有幾分鐘,而且不再必須為連續(xù)的.

這樣的后臺(tái)模式可能不適合你.但如果合適,請繼續(xù)閱讀!

接下來的學(xué)習(xí)中,將會(huì)有幾個(gè)幾個(gè)后臺(tái)模式提供給你.在本教程中你將建立一個(gè)關(guān)于簡單標(biāo)簽應(yīng)用的工程,來探索從連續(xù)播放視頻到周期性的獲取更新內(nèi)容的四種常見模式.

開始

在深入這個(gè)工程之前,這里有一個(gè)iOS可用的基礎(chǔ)后臺(tái)模式的快速預(yù)覽.在Xcode 6中,你通過點(diǎn)擊目標(biāo)程序的Capabilities(功能)選項(xiàng)卡能夠看到如下列表:

打開后臺(tái)模式功能列表(1)在項(xiàng)目導(dǎo)航欄中選擇項(xiàng)目(2)選擇目標(biāo)應(yīng)用(3)選擇功能選項(xiàng)卡(4)把后臺(tái)模式開關(guān)打開.

在這個(gè)教程中,你會(huì)研究四種后臺(tái)進(jìn)程處理方式.

*視頻播放:APP可以在后臺(tái)播放或錄制視頻

*獲取位置更新:該應(yīng)用會(huì)隨著設(shè)備位置的改變繼續(xù)回調(diào)結(jié)果.

*執(zhí)行一定的任務(wù):通常在沒有限制的情況下,這時(shí)APP會(huì)在有限的時(shí)間內(nèi)運(yùn)行任意的代碼.

*后臺(tái)獲取:通過iOS的更新計(jì)劃獲取最細(xì)的內(nèi)容.

這個(gè)教程將按照上面的順序,在本教程的每個(gè)部分中介紹如何使用這四個(gè)模板.

從這個(gè)像觀光車一樣的工程開始型奥,通過它熟悉一下iOS后臺(tái)機(jī)制,首先下載這個(gè)上手工程.有個(gè)好消息:用戶界面已經(jīng)為你預(yù)配置好了.

運(yùn)行這個(gè)示例項(xiàng)目,檢查一下你的四個(gè)選項(xiàng)卡.

這些選項(xiàng)卡是本教程剩余部分的路線圖.第一站:后臺(tái)視頻

提示:為了使后臺(tái)模式充分發(fā)揮作用,你應(yīng)該使用一個(gè)真正的設(shè)備.根據(jù)我的經(jīng)驗(yàn),如果你忘記配置設(shè)置,該APP在模擬器的后臺(tái)能很好的運(yùn)行運(yùn)行.然而,當(dāng)你切換到真正的設(shè)備時(shí),它將不會(huì)運(yùn)行.

音頻播放

這里有iOS播放音頻的幾種方法,他們中的大部分需要實(shí)現(xiàn)回調(diào)函數(shù)去提供更多用來播放的音頻數(shù)據(jù).當(dāng)用戶使你的APP做某些事情,會(huì)調(diào)用回調(diào)函數(shù)(比如委托模型),在這種情況下,會(huì)把音波存儲(chǔ)在內(nèi)存緩存區(qū)中.

如果你想播放流數(shù)據(jù)中的音頻,你可以開啟一個(gè)網(wǎng)絡(luò)連接,連接的這些回調(diào)函數(shù)提供連續(xù)的音頻數(shù)據(jù).

當(dāng)你激活音頻后臺(tái)模式后,即使你的APP現(xiàn)在沒在活動(dòng),iOS將繼續(xù)執(zhí)行這些回調(diào)函數(shù).音頻后臺(tái)模式是自動(dòng)的,這么說很正確.你只是激活它,恰好為管理它提供了基礎(chǔ)設(shè)備.

對于我們這些有點(diǎn)小心思的人來說,如果你的APP確實(shí)為用戶播放音頻,你應(yīng)該只使用后臺(tái)音頻模式.如果你嘗試使用這個(gè)模式只是為了獲取當(dāng)程序安靜運(yùn)行的時(shí)候使用CPU的時(shí)長,蘋果將拒絕你APP的運(yùn)行.

在這部分,你將在你的APP中添加一個(gè)音頻播放器,打開后臺(tái)模式,為你演示它的運(yùn)行過程.

為了獲取到音頻播放裝置,你需要學(xué)習(xí) AV Foundation.打開AudioViewController.swift,在文件頂部import UIKit后面添加引用.

import AVFoundation

Override viewDidLoad() with the following implementation: 用下面的實(shí)現(xiàn)代碼重寫viewDidLoad()

override func viewDidLoad() {

super.viewDidLoad()

var error: NSError?

var success = AVAudioSession.sharedInstance().setCategory(

AVAudioSessionCategoryPlayAndRecord,

withOptions: .DefaultToSpeaker, error: &error)

if !success {

NSLog("Failed to set audio session category.? Error: \(error)")

}

}

這使用了音頻回話的單例模式sharedInstance()去設(shè)置播放的類別,也確保了聲音是通過手機(jī)揚(yáng)聲器而不是通過手機(jī)聽筒傳播的.如果它執(zhí)行了,他會(huì)檢查調(diào)用是否失敗并記錄錯(cuò)誤.一個(gè)真正的APP在發(fā)生錯(cuò)誤后會(huì)顯示一個(gè)隊(duì)伍的對話框,作為對錯(cuò)誤的回應(yīng),但是我們不需要因?yàn)檫@些小細(xì)節(jié)而糾結(jié).

接下來,你要把播放器這個(gè)成員屬性添加到AudioViewController中:

var player: AVQueuePlayer!

這是個(gè)隱式的可拓展的屬性,最初為nil,你將在viewDidLoad()對它進(jìn)行初始化.

這個(gè)上手項(xiàng)目包含來自主要收納免版權(quán)稅的音樂網(wǎng)站incompetech.com的音頻文件.認(rèn)證之后你可以免費(fèi)的使用它上面的音樂.你這里使用的全部歌曲來自incompetech.com 上Kevin MacLeod的作品.謝謝Kevin!

返回viewDidLoad(),在此函數(shù)的末尾處添加如下方法:

let songNames = ["FeelinGood", "IronBacon", "WhatYouWant"]

let songs = songNames.map {

AVPlayerItem(URL: NSBundle.mainBundle().URLForResource($0, withExtension: "mp3"))

}

player = AVQueuePlayer(items: songs)

player.actionAtItemEnd = .Advance

這樣可以獲取到歌曲的列表,把它們映射到主程序包的路徑中并把它們轉(zhuǎn)化為可以在AVQueuePlayer上播放的AVPlayerItems.此外,這個(gè)隊(duì)列被設(shè)置為循環(huán)播放.

為了在隊(duì)列進(jìn)程中更新歌曲名字,你需要觀察播放器中的currentItem.為了達(dá)到上述目的,需要在viewDidLoad()的末尾處添加如下代碼:

player.addObserver(self, forKeyPath: "currentItem", options: .New | .Initial , context: nil)

這使得每當(dāng)播放器中currentItem改變,類觀察者的回調(diào)被初始化.

現(xiàn)在你可以添加觀察者模式方法.把下面代碼放到viewDidLoad()下面.

override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {

if keyPath == "currentItem", let player = object as? AVPlayer,

currentItem = player.currentItem?.asset as? AVURLAsset {

songLabel.text = currentItem.URL?.lastPathComponent ?? "Unknown"

}

}

當(dāng)這個(gè)函數(shù)被調(diào)用的時(shí)候,你首先要確保這個(gè)被更新的屬性是你所關(guān)注的.在這種情形下,它不是那么重要了因?yàn)橹挥幸粋€(gè)屬性被觀察,但是在你之后添加更多的觀察者的情況下去檢查,是個(gè)不錯(cuò)的方法.如果它是currentItem鍵,你將使用它通過文件名更新songLabel.如果由于某些原因,當(dāng)前項(xiàng)的URL不能獲取到,它將使songLabel顯示字符串"Unknown".

你也需要一個(gè)去更新timeLabel的方法來顯示當(dāng)前播放項(xiàng)消耗的時(shí)間.使用addPeriodicTimeObserverForInterval(_:queue:usingBlock:)是達(dá)到當(dāng)前目的最好的方法,該函數(shù)講調(diào)用給定的隊(duì)列當(dāng)中提供的塊.在viewDidLoad()的末尾處添加如下代碼:

player.addPeriodicTimeObserverForInterval(CMTimeMake(1, 100), queue: dispatch_get_main_queue()) {

[unowned self] time in

let timeString = String(format: "%02.2f", CMTimeGetSeconds(time))

if UIApplication.sharedApplication().applicationState == .Active {

self.timeLabel.text = timeString

} else {

println("Background: \(timeString)")

}

}

這添加給播放器一個(gè)周期性的觀察者,如果這個(gè)APP在前臺(tái),這個(gè)觀察者每一秒的1/100就會(huì)被調(diào)用一次并且更新UI.

重要提示:由于你想在結(jié)束時(shí)更新UI,你必須確保這些代碼在主隊(duì)列中被調(diào)用.這就是你指定dispatch_get_main_queue()參數(shù)的原因.

在這里暫停一下,思考應(yīng)用的狀態(tài).

你的應(yīng)用處于下面五個(gè)狀態(tài)之一中.簡單地說,他們是:

*未運(yùn)行:你的APP在開啟之前處于這個(gè)狀態(tài).

*激活:一旦你的APP被開啟,它變成活躍狀態(tài).

*未激活:當(dāng)你的APP正在運(yùn)行,但是一些事情打斷它的動(dòng)作,比如有電話打進(jìn)來,它變成inactive狀態(tài).休眠意味著這個(gè)APP仍然在前臺(tái)運(yùn)行,只是它沒有接收事件.

*后臺(tái):在這個(gè)狀態(tài)下,你的APP不在前臺(tái)顯示了但是它仍然在執(zhí)行代碼.

*掛起:你的APP進(jìn)入不再運(yùn)行代碼的狀態(tài).

如果你想更深入的了解這些狀態(tài)之間的區(qū)別,蘋果網(wǎng)站的Execution States for Apps對此有很詳細(xì)介紹.

你可以通過讀取UIApplication.sharedApplication().applicationState來檢查APP的轉(zhuǎn)臺(tái).記住:你只能獲取三種狀態(tài)的返回值: .Active, .Inactive, and .Background.當(dāng)你的APP在執(zhí)行代碼的時(shí)候,掛起狀態(tài)和未運(yùn)行狀態(tài)很明顯不可能出現(xiàn).

讓我們將目光繼續(xù)放在之前代碼上,如果該應(yīng)用處于激活狀態(tài),你需要更新音樂標(biāo)題欄.在后臺(tái)中,你仍然能夠更新這個(gè)label的文字,但是這點(diǎn)知識證明了當(dāng)你APP在后臺(tái)的時(shí)候繼續(xù)接受回調(diào).

現(xiàn)在,把剩余的代碼添加到playPauseAction(:)的實(shí)現(xiàn)中,讓播放/暫停按鈕工作.在AudioViewController中,把下面代碼添加到playPauseAction(:)的實(shí)現(xiàn)中:

@IBAction func playPauseAction(sender: UIButton) {

sender.selected = !sender.selected

if sender.selected {

player.play()

} else {

player.pause()

}

}

很好,這是你全部的代碼.創(chuàng)建并運(yùn)行,你將看到下面的樣子:

現(xiàn)在,點(diǎn)播放,音樂將開始.很好!

測試后臺(tái)模式是否起作用.按home按鈕(如果你正在使用模擬器,按Cmd-Shift-H).如果你在真正的設(shè)備上運(yùn)行(不是Xcode 6.1的模擬器)音樂將停止.這是為什么呢?還有很重要的一塊落下了!)

對于大多數(shù)的后臺(tái)模式("Whatever"模式除外)你需要在Info.plist中添加一個(gè)key用來指明APP在后臺(tái)中運(yùn)行的代碼.幸運(yùn)的是,在Xcode6可以通過復(fù)選框進(jìn)行選擇.

回到Xcode碉京,按照以下步驟進(jìn)行操作:

1.在項(xiàng)目管理器中點(diǎn)擊工程

2.點(diǎn)擊目標(biāo)TheBackgrounder

3.點(diǎn)擊功能標(biāo)簽

4.滑動(dòng)背景模式并設(shè)置為ON

5.選中 Audio和AirPlay

重新編譯并且運(yùn)行.開始運(yùn)行音樂并且點(diǎn)擊home鍵厢汹,盡管這個(gè)APP在后臺(tái)運(yùn)行,這次你就會(huì)依舊能夠聽到音樂.

You should also see the time updates in your Console output in Xcode, proof that your code is still working even though the app is in the background. You can download the partially finished sample project up to this point.

在Xcode的輸出里你也能夠在控制臺(tái)看到實(shí)時(shí)的更新收夸,著就證明了雖然你的APP在后臺(tái)運(yùn)行坑匠,但是你的代碼依舊在工作.現(xiàn)在你可以下載部分完成的示例代碼了.

以上第一個(gè)模式結(jié)束了,如果你想學(xué)完整個(gè)教程--那就繼續(xù)往下讀吧!

接收位置更新

當(dāng)在后臺(tái)模式進(jìn)行定位時(shí),你的APP依舊會(huì)隨著用戶更新位置而接收到位置信息卧惜,甚至APP在后臺(tái)的時(shí)候.你可以控制這些位置更新的準(zhǔn)確性,甚至改變精度.

如果你的app真正需要這些信息來為用戶提供價(jià)值厘灼,你只能使用后臺(tái)模式.如果你使用這個(gè)模式并且Apple看到用戶將要獲得這些信息,你的應(yīng)用程序?qū)?huì)被拒絕.有時(shí)蘋果也將要求你向app添加一個(gè)警告的描述說明app將導(dǎo)致增加電量的使用.

第二步是為了位置更新咽瓷,打開LocationViewController.swift并且向里面增加一些屬性用來初始化LocationViewController.

var locations = [MKPointAnnotation]()

lazy var locationManager: CLLocationManager! = {

let manager = CLLocationManager()

manager.desiredAccuracy = kCLLocationAccuracyBest

manager.delegate = self

manager.requestAlwaysAuthorization()

return manager

}()

你將使用locations來存儲(chǔ)能夠繪制在地圖上的位置信息.CLLocationManager可以使你能夠從設(shè)備上獲取位置更新.你使用延遲的方法實(shí)例化它,所以當(dāng)你第一次訪問該屬性被調(diào)用的函數(shù)時(shí),它才被初始化.

代碼可以設(shè)置位置管理器的精確度來實(shí)現(xiàn)最高的精確设凹,你可以調(diào)節(jié)到你的app所需要的精確度.你會(huì)了解更多關(guān)于其他精度設(shè)置和它們的重要性.注意你也可以調(diào)用requestAlwaysAuthorization().這是在IOS8中的要求,并且為用戶提供了接口來允許用戶在后臺(tái)使用位置.

現(xiàn)在你可以填寫空的accuracyChanged(_:)的實(shí)現(xiàn)在LocationViewController里:

@IBAction func accuracyChanged(sender: UISegmentedControl) {

let accuracyValues = [

kCLLocationAccuracyBestForNavigation,

kCLLocationAccuracyBest,

kCLLocationAccuracyNearestTenMeters,

kCLLocationAccuracyHundredMeters,

kCLLocationAccuracyKilometer,

kCLLocationAccuracyThreeKilometers]

locationManager.desiredAccuracy = accuracyValues[sender.selectedSegmentIndex];

}

accuracyValues是由CLLocationManager的desiredAccuracy可能值構(gòu)成的數(shù)組.這些變量控制了你的位置的精確度.

你可能認(rèn)為這種方式是愚蠢的.為什么位置管理器不能夠給你最精確的位置信息呢茅姜?最重要的原因是為了節(jié)省電量.低精確意味著耗電量較低.

這就意味著你應(yīng)該選擇最少的值實(shí)現(xiàn)你的app可以承受的最低限度的精確度.你隨時(shí)可以修改這些值在你的需求.

另一個(gè)性能就是你可以控制你的app接收位置更新的頻率闪朱,忽視desiredAccuracy: distanceFilter的值.當(dāng)你的設(shè)備移動(dòng)到了一定的值(以米計(jì)算)時(shí),這個(gè)性能告訴位置管理器你只想接收位置更新.這個(gè)值應(yīng)該最大限度的節(jié)省你的電池消耗.

現(xiàn)在你可以在enabledChanged(_:)中添加代碼來實(shí)現(xiàn)獲取位置更新:

@IBAction func enabledChanged(sender: UISwitch) {

if sender.on {

locationManager.startUpdatingLocation()

} else {

locationManager.stopUpdatingLocation()

}

}

這個(gè)代碼示例有一個(gè)與動(dòng)作相關(guān)的UISwitch钻洒,這個(gè)UISwitch實(shí)現(xiàn)了位置跟蹤的開啟與關(guān)閉.

下一步你可以通過添加一個(gè)CLLocationManagerDelegate方法來接收位置更新.添加以下方法到LocationViewController中.

// MARK: - CLLocationManagerDelegate

func locationManager(manager: CLLocationManager!, didUpdateToLocation newLocation: CLLocation!, fromLocation oldLocation: CLLocation!) {

// Add another annotation to the map.

let annotation = MKPointAnnotation()

annotation.coordinate = newLocation.coordinate

// Also add to our map so we can remove old values later

locations.append(annotation)

// Remove values if the array is too big

while locations.count > 100 {

let annotationToRemove = locations.first!

locations.removeAtIndex(0)

// Also remove from the map

mapView.removeAnnotation(annotationToRemove)

}

if UIApplication.sharedApplication().applicationState == .Active {

mapView.showAnnotations(locations, animated: true)

} else {

NSLog("App is backgrounded. New location is %@", newLocation)

}

}

如果app的狀態(tài)是激活狀態(tài)奋姿,這些代碼將更新地圖.如果這個(gè)app在后臺(tái)運(yùn)行,你應(yīng)該在xcode的控制臺(tái)來看位置更新的log.

現(xiàn)在你已經(jīng)知道了后臺(tái)模式素标,現(xiàn)在你不應(yīng)該犯以前的相同的錯(cuò)誤了.現(xiàn)在你可以在Location updates中設(shè)置使得ios知道你的app想在后臺(tái)運(yùn)行時(shí)繼續(xù)接受位置更新.

除了更改這個(gè)之外称诗,你應(yīng)該在你的Info.plist中設(shè)置一個(gè)關(guān)鍵詞來允許你向使用者解釋為什么后臺(tái)更行數(shù)據(jù)是需要的.如果不被允許后臺(tái)更新,位置更新就會(huì)慢慢地失敗.

步驟如下:

1.選擇Info.plist文件

2.點(diǎn)擊+號來添加一個(gè)關(guān)鍵詞

3.點(diǎn)擊這個(gè)關(guān)鍵詞的名字:NSLocationAlwaysUsageDescription

4.描述為什么你需要在后臺(tái)位置更新头遭,能夠另使用者信服.

現(xiàn)在你可以編譯并且運(yùn)行你的程序了.切換到第二個(gè)選項(xiàng)卡并打開開關(guān).

當(dāng)你第一次運(yùn)行的時(shí)候寓免,你會(huì)看到你寫入到Info.plist中的信息.點(diǎn)擊allow出去走走癣诱,或者圍繞你周圍的建筑轉(zhuǎn)一轉(zhuǎn).這時(shí)候你就開始看到位置信息的更新,在模擬器里也可以實(shí)現(xiàn).

過一會(huì)袜香,你將會(huì)看到如下的一些東西:

如果你在后臺(tái)運(yùn)行你的app撕予,你將會(huì)在你的控制臺(tái)log看到你的app位置更新信息.重新打開你的app,你就會(huì)發(fā)現(xiàn)地圖上有所有的位置點(diǎn)蜈首,這些就是你的app在后臺(tái) 運(yùn)行時(shí)候更新的數(shù)據(jù).

如果你使用的是模擬器实抡,你也可以使用這個(gè)app來模擬這個(gè)動(dòng)作.打開菜單Debug \ Location:

設(shè)置location選項(xiàng)為Freeway Drive然后點(diǎn)擊home按鈕.這時(shí)候你就會(huì)看到在控制臺(tái)打印出你的程序運(yùn)行的狀態(tài),就像你在模擬你開車在加利福尼亞的高速公路上.

2014-12-21 20:05:13.334 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33482105,-122.03350886> +/- 5.00m (speed 15.90 mps / course 255.94) @ 12/21/14, 8:05:13 PM Pacific Standard Time

2014-12-21 20:05:14.813 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33477977,-122.03369603> +/- 5.00m (speed 17.21 mps / course 255.59) @ 12/21/14, 8:05:14 PM Pacific Standard Time

2014-12-21 20:05:15.320 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33474691,-122.03389325> +/- 5.00m (speed 18.27 mps / course 257.34) @ 12/21/14, 8:05:15 PM Pacific Standard Time

2014-12-21 20:05:16.330 TheBackgrounder[21591:674586] App is backgrounded. New location is <+37.33470894,-122.03411085> +/- 5.00m (speed 19.27 mps / course 257.70) @ 12/21/14, 8:05:16 PM Pacific Standard Time

現(xiàn)在你可以下載這個(gè)示例程序了,到第三個(gè)選項(xiàng)卡和第三個(gè)后臺(tái)模式.

執(zhí)行有限長任務(wù)等

下一個(gè)后臺(tái)模式在可以正式的稱為后臺(tái)執(zhí)行有限長的任務(wù)(Executing a Finite-Length Task in the Background).

嚴(yán)格的說這并不是真正意義上的后臺(tái)模式欢策,因?yàn)槟悴]有在Info.plist中聲明在你的app中使用這個(gè)模式(或者在復(fù)選框中使用Background Mode).相反澜术,它只是一個(gè)api你可以讓你的任意代碼運(yùn)行有限的時(shí)間,當(dāng)你的app在后臺(tái)運(yùn)行的時(shí)候.

在過去猬腰,這個(gè)模式只是在上傳或者下載或者運(yùn)行某一段時(shí)間來完成某一項(xiàng)任務(wù).但是如果這個(gè)鏈接很緩慢或者這個(gè)進(jìn)行一直不結(jié)束怎么辦?它會(huì)讓你的應(yīng)用程序在一個(gè)奇怪的狀態(tài),你必須添加大量的代碼來處理錯(cuò)誤使得程序穩(wěn)健地工作. 因?yàn)檫@樣的原因猜敢,Apple介紹了NSURLSession.

NSURLSession在面對后臺(tái)運(yùn)行甚至設(shè)備重啟時(shí)具有魯棒性姑荷,并且以減少設(shè)備能耗的方式完成任務(wù).如果你想處理大規(guī)模的下載,請查看我們的NSURLSession tutorial.

這種后臺(tái)運(yùn)行模式對完成一些長時(shí)間的任務(wù)還是一種非常有效的方法缩擂,比如在相機(jī)相冊中進(jìn)行渲染和寫入一個(gè)視頻.

但是這只是一個(gè)例子.你可以運(yùn)行的代碼是任意的,你可以用這個(gè)api來實(shí)現(xiàn)任意的事情:運(yùn)行長時(shí)間的計(jì)算鼠冕,將過濾器應(yīng)用到圖像處理,渲染一個(gè)復(fù)雜3 d網(wǎng)格...whatever胯盯!只要是你想在長時(shí)間運(yùn)行你的程序你都可以用這個(gè)api.

你的app在后臺(tái)運(yùn)行的時(shí)間取決于ios系統(tǒng).對于后臺(tái)運(yùn)行時(shí)間你可以在UIApplication中查詢backgroundTimeRemaining懈费,它將會(huì)告訴你剩余多長時(shí)間.

一般來說你會(huì)有3分鐘時(shí)間來實(shí)現(xiàn).但是在api文檔中并沒有給一個(gè)大約的時(shí)間,所以你不能依賴這個(gè)時(shí)間博脑,可能是5分鐘也可能是5秒.所以你的app需要準(zhǔn)備發(fā)生的任何事情.

這里給一個(gè)計(jì)算機(jī)學(xué)生都熟悉的任務(wù):斐波納契數(shù)列.

這里的意義是,你會(huì)在后臺(tái)計(jì)算這些數(shù)字!

打開WhateverViewController.swift并且在WhateverViewController里面添加屬性.

var previous = NSDecimalNumber.one()

var current = NSDecimalNumber.one()

var position: UInt = 1

var updateTimer: NSTimer?

var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid

NSDecimalNumbers將保存序列中的前兩個(gè)數(shù)的值.NSDecimalNumbers可以保存大的數(shù)據(jù)憎乙,因此非常適合你的目標(biāo).Position只是一個(gè)計(jì)數(shù)器來告訴你這個(gè)這個(gè)數(shù)在當(dāng)前序列中的位置.

你將使用updateTimer證明甚至計(jì)時(shí)器繼續(xù)使用這個(gè)API時(shí),也稍微放慢速度的計(jì)算,這樣你就可以觀察他們.

在WhateverViewController中添加一些實(shí)用方法來重置斐波那契計(jì)算,啟動(dòng)和停止能夠后臺(tái)運(yùn)行的任務(wù):

func resetCalculation() {

previous = NSDecimalNumber.one()

current = NSDecimalNumber.one()

position = 1

}

func registerBackgroundTask() {

backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {

[unowned self] in

self.endBackgroundTask()

}

assert(backgroundTask != UIBackgroundTaskInvalid)

}

func endBackgroundTask() {

NSLog("Background task ended.")

UIApplication.sharedApplication().endBackgroundTask(backgroundTask)

backgroundTask = UIBackgroundTaskInvalid

}

現(xiàn)在到了重要部分,在didTapPlayPause(_:)添加空的實(shí)現(xiàn):

@IBAction func didTapPlayPause(sender: UIButton) {

sender.selected = !sender.selected

if sender.selected {

resetCalculation()

updateTimer = NSTimer.scheduledTimerWithTimeInterval(0.5, target: self,

selector: "calculateNextNumber", userInfo: nil, repeats: true)

registerBackgroundTask()

} else {

updateTimer?.invalidate()

updateTimer = nil

if backgroundTask != UIBackgroundTaskInvalid {

endBackgroundTask()

}

}

}

按鈕改變選擇狀態(tài)取決于計(jì)算已經(jīng)停止叉趣,應(yīng)該開始或者是計(jì)算已經(jīng)開始泞边,應(yīng)該停止.

首先你必須設(shè)置斐波那契序列變量.然后你可以創(chuàng)建一個(gè)NSTimer,沒秒啟動(dòng)兩次疗杉,并且調(diào)用 calculateNextNumber()函數(shù).

現(xiàn)在到了一個(gè)重要的時(shí)刻:調(diào)用registerBackgroundTask()函數(shù)阵谚,反過來調(diào)用beginBackgroundTaskWithExpirationHandler(_:).這個(gè)方法告訴了ISO你需要時(shí)間在后臺(tái)運(yùn)行你的app.這些調(diào)用完成之后,在你調(diào)用endBackgroundTask()之前你的app會(huì)一直獲取cpu時(shí)間.

嗯,差不多.如果你的app在后臺(tái)運(yùn)行一段時(shí)間后沒有調(diào)用endBackgroundTask()烟具,IOS將調(diào)用關(guān)閉程序定義梢什,這是在你調(diào)用beginBackgroundTaskWithExpirationHandler(_:)時(shí)給你機(jī)會(huì)來停止執(zhí)行代碼.所以調(diào)用endBackgroundTask()告訴IOS你已經(jīng)完成工作了是非常好的一個(gè)主意.如果你不執(zhí)行上面所說的而是繼續(xù)執(zhí)行你的代碼,你的app將會(huì)終止.

第二部分關(guān)于if的語句是很簡單的:它只是使定時(shí)器失效朝聋,并且調(diào)用endBackgroundTask()來告訴ios不再需要額外的CPU 時(shí)間.

在你每次調(diào)用beginBackgroundTaskWithExpirationHandler(:)時(shí)調(diào)用endBackgroundTask()是非常重要的.如果你在一個(gè)任務(wù)里調(diào)用 beginBackgroundTaskWithExpirationHandler(:)兩次而只調(diào)用endBackgroundTask()一次嗡午,你將仍然獲取cpu時(shí)間,直到你在運(yùn)行第二次的后臺(tái)任務(wù)是調(diào)用endBackgroundTask()才能結(jié)束.這就是為什么你需要backgroundTask.

現(xiàn)在你可以實(shí)現(xiàn)簡單的計(jì)算機(jī)程序方法.在WhateverViewController添加以下的方法:

func calculateNextNumber() {

let result = current.decimalNumberByAdding(previous)

let bigNumber = NSDecimalNumber(mantissa: 1, exponent: 40, isNegative: false)

if result.compare(bigNumber) == .OrderedAscending {

previous = current

current = result

++position

}

else {

// This is just too much.... Start over.

resetCalculation()

}

let resultsMessage = "Position \(position) = \(current)"

switch UIApplication.sharedApplication().applicationState {

case .Active:

resultsLabel.text = resultsMessage

case .Background:

NSLog("App is backgrounded. Next number = %@", resultsMessage)

NSLog("Background time remaining = %.1f seconds", UIApplication.sharedApplication().backgroundTimeRemaining)

case .Inactive:

break

}

}

再一次玖翅,我們將展示另一個(gè)方法即使你的app在后臺(tái)運(yùn)行依舊能夠顯示結(jié)果.在這種情形下翼馆,還有一個(gè)有趣的信息: backgroundTimeRemaining的數(shù)值.只有當(dāng)ios調(diào)用添加調(diào)用beginBackgroundTaskWithExpirationHandler(_:)的時(shí)才會(huì)停止.

編譯并且運(yùn)行割以,然后切換到第三個(gè)選項(xiàng)卡.

點(diǎn)擊play并且你將會(huì)看到app計(jì)算出的值.現(xiàn)在點(diǎn)擊home鍵然后查看xcode控制臺(tái).你應(yīng)該會(huì)看到app依舊會(huì)更新數(shù)字,與此同時(shí)時(shí)間依舊在向前走.

在大多數(shù)情況下应媚,這個(gè)時(shí)間將從第180秒開始并且延續(xù)5秒鐘.如果你等待重新回到你的app严沥,定時(shí)器將重新開始啟動(dòng)并且所有的錯(cuò)誤行為將繼續(xù).

在代碼里只有一個(gè)bug,它給我機(jī)會(huì)來解釋關(guān)于后臺(tái)通知.假設(shè)你或太運(yùn)行app并且等待分配的時(shí)間到期.在這種情況下中姜,你app將調(diào)用消玄??并且調(diào)用endBackgroundTask()丢胚,也就是終結(jié)后臺(tái)運(yùn)行時(shí)間的需求.

如果你繼續(xù)返回你的app翩瓜,定時(shí)器將繼續(xù)激活.但是如果你離開app,你將不會(huì)得到或太運(yùn)行時(shí)間.Why携龟?因?yàn)樵诔瑫r(shí)和回到后臺(tái)期間app沒有間隙來調(diào)用beginBackgroundTaskWithExpirationHandler(_:).

你怎么解決這個(gè)問題呢兔跌?有許多方法能夠解決這個(gè)問題,并且其中一個(gè)是使用一種狀態(tài)來改變通知.

有兩種你可以得到通知并且你的app可以改變它的狀態(tài)的方法:第一種是通過你的主app委托方法峡蟋;第二種是通過監(jiān)聽ios發(fā)送給你的app的通知.

* 當(dāng)你的app將要進(jìn)入不活躍的狀態(tài)坟桅,UIApplicationWillResignActiveNotification和applicationWillResignActive(_:)將會(huì)被發(fā)送和調(diào)用.在這種情況下,你的app不是在后臺(tái)運(yùn)行蕊蝗,它依舊在前臺(tái)運(yùn)行仅乓,但是它將不會(huì)接收到任何UI事件.

* 當(dāng)app進(jìn)入到后臺(tái)狀態(tài),UIApplicationDidEnterBackgroundNotification 和applicationDidEnterBackground(:)將會(huì)被發(fā)送和調(diào)用.在這種情況下蓬戚,你的app將不會(huì)是在激活狀態(tài)夸楣,并且它是你最后的機(jī)會(huì)運(yùn)行你的代碼.如果你想得到更多的CPU時(shí)刻,這是一個(gè)調(diào)用beginBackgroundTaskWithExpirationHandler(:)非常完美的時(shí)機(jī).

* 當(dāng)app返回激活狀態(tài)子漩,UIApplicationWillEnterForegroundNotification 和applicationWillEnterForeground(:)將會(huì)被發(fā)送和調(diào)用.這是app依舊在后臺(tái)運(yùn)行豫喧,你已經(jīng)可以啟動(dòng)任何你想做的事.當(dāng)你真正進(jìn)入后臺(tái)運(yùn)行是如果你只調(diào)用了beginBackgroundTaskWithExpirationHandler(:),此時(shí)將是一個(gè)好的時(shí)機(jī)調(diào)用endBackgroundTask().

* 以防你的app從后臺(tái)運(yùn)行狀態(tài)返回幢泼,在前一個(gè)通知完成后UIApplicationDidBecomeActiveNotification和applicationDidBecomeActive(_:)將會(huì)被發(fā)送和調(diào)用.如果你的app只是臨時(shí)的中斷也會(huì)被調(diào)用-舉例—如果你的app沒有真正的進(jìn)入到后臺(tái)嘿棘,但是你依舊會(huì)收到UIApplicationWillResignActiveNotification.

你可以在Apple’s documentation for App States for Apps中看到所有的圖像化描述(文章—有著許多非常棒的圖表)

現(xiàn)在是解決這個(gè)bug的時(shí)間了.首先要重寫viewDidLoad()并且訂閱UIApplicationDidBecomeActiveNotification.

override func viewDidLoad() {

super.viewDidLoad()

NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("reinstateBackgroundTask"), name: UIApplicationDidBecomeActiveNotification, object: nil)

}

不管何時(shí)這app變成激活狀態(tài),指定的選擇器reinstateBackgroundTask將被調(diào)用.

不管何時(shí)你訂閱了一個(gè)通知你也應(yīng)該想到這個(gè)訂閱的通知哪里不應(yīng)該被訂閱.使用deinit來完成這個(gè)功能.按照下面的代碼加入到WhateverViewController.

deinit {

NSNotificationCenter.defaultCenter().removeObserver(self)

}

最后實(shí)現(xiàn)reinstateBackgroundTask().

func reinstateBackgroundTask() {

if updateTimer != nil && (backgroundTask == UIBackgroundTaskInvalid) {

registerBackgroundTask()

}

}

如果定時(shí)器依然運(yùn)行但是后臺(tái)任務(wù)沒有運(yùn)行旭绒,你只需要恢復(fù)就可以了.

把你的代碼分解成小的實(shí)用的代碼只需要做一件事就可以.當(dāng)一個(gè)后臺(tái)任務(wù)不是在當(dāng)前的定時(shí)器下你只需要調(diào)用registerBackgroundTask()即可.

然后你可以使用了.你可以下載這個(gè)程序.

這個(gè)課程的最后的一節(jié)是:Background Fetching.

后臺(tái)獲取

后臺(tái)獲取是iOS7中推出的讓你的APP在最大限度減少對電池?fù)p耗的時(shí)候總是展現(xiàn)最新的信息.舉個(gè)例子,假設(shè)你正在給你的APP填充信息.你可以通過viewWillAppear(_:).獲取最新數(shù)據(jù)來預(yù)先通知后臺(tái)模式.這個(gè)方案可以解決在新數(shù)據(jù)刷新過來之前你的用戶正在瀏覽前幾秒的數(shù)據(jù).當(dāng)用戶打開你APP的同時(shí),最新的數(shù)據(jù)同時(shí)被神奇的展現(xiàn)了,這種情況再好不過了.這是后臺(tái)模式能夠?yàn)槟銓?shí)現(xiàn)的操作.

當(dāng)APP被激活的時(shí)候,系統(tǒng)會(huì)使用慣用模式去決定什么時(shí)候執(zhí)行后臺(tái)獲取.比如,如果用戶每天都在早上9點(diǎn)打開改APP,后臺(tái)獲取在這個(gè)時(shí)間點(diǎn)之前預(yù)先執(zhí)行是很可能的.系統(tǒng)決定什么時(shí)候是安排后臺(tái)獲取的最好時(shí)間,因此你不應(yīng)該用它去做緊急的更新.

這里有你為了實(shí)現(xiàn)后臺(tái)獲取必須做的三件事情:

* 檢查你APPCapabilities選項(xiàng)中后臺(tái)模式的后臺(tái)獲取選項(xiàng)框是否被選中.

* 使用setMinimumBackgroundFetchInterval(_:) 為你的APP創(chuàng)建一個(gè)合適的時(shí)間間隔.

* 在你APP委托中實(shí)現(xiàn)application(_:performFetchWithCompletionHandler:)去管理后臺(tái)獲取.

后臺(tái)獲取就像他名字表示的一樣,他通常涉及到從外源,比如網(wǎng)絡(luò)服務(wù),中獲取信息.就這個(gè)教程的意圖,你將不會(huì)使用網(wǎng)絡(luò)而僅僅獲取現(xiàn)在的時(shí)間.這樣簡化講讓你理解在不同擔(dān)心外在的服務(wù)的時(shí)候操作并測試后臺(tái)模式所需要的每一樣?xùn)|西.

對于有限長度的任務(wù),你只有以按秒為單位的時(shí)間去執(zhí)行操作,公認(rèn)的時(shí)間是不超過30秒鸟妙,但越短越好.如果您需要下載大量資源最為獲取的部分,這就是你需要使用NSURLSession的背景傳輸服務(wù)的地方.

開始的時(shí)間到了.首先,打開FetchViewController.swift,并將下面的屬性和方法添加到FetchViewController中.

var time: NSDate?

func fetch(completion: () -> Void) {

time = NSDate()

completion()

}

這些代碼是代替你真正的從外源(json或XML RESTful 服務(wù))中獲取數(shù)據(jù)的一種簡化.因?yàn)樗赡苄枰獛酌腌妬慝@取和分析數(shù)據(jù)挥吵,你傳遞一個(gè)完成的handler,這個(gè)handler在進(jìn)程完成后被調(diào)用.你待會(huì)兒會(huì)看到為什么很很重要.

接下來,完成view controller的代碼.將下面的方法添加到FetchViewController中.

func updateUI() {

if let time = time {

let formatter = NSDateFormatter()

formatter.dateStyle = .ShortStyle

formatter.timeStyle = .LongStyle

updateLabel?.text = formatter.stringFromDate(time)

}

else {

updateLabel?.text = "Not yet updated"

}

}

override func viewDidLoad() {

super.viewDidLoad()

updateUI()

}

@IBAction func didTapUpdate(sender: UIButton) {

fetch { self.updateUI() }

}

updateUI()格式化這個(gè)時(shí)間并顯示它.它是一個(gè)可選的類型,所以如果它沒有被創(chuàng)建,他將展示至今沒有更新的信息.當(dāng)這個(gè)view初次被加載時(shí)(在 viewDidLoad()中)你不能獲取到,但是直接調(diào)用updateUI()函數(shù),將會(huì)有“Not yet updated”的字樣在開始時(shí)顯示.最后,當(dāng)更新按鈕被監(jiān)聽的時(shí)候,它運(yùn)行獲取的代碼并且會(huì)完成對UI的更新.

就這一點(diǎn)而言,該view controller正在工作.

然而,后臺(tái)獲取沒有起作用.

啟用后臺(tái)獲取的第一步是在Capabilities選項(xiàng)欄里選中Background fetch.到現(xiàn)在這個(gè)操作已經(jīng)是老一套的了,直接找到它并選中.

接下來,打開AppDelegate.swift,通過在 application(_:didFinishLaunchingWithOptions:)中設(shè)置最小的后臺(tái)獲取時(shí)間間隔來請求后臺(tái)獲取操作.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

UIApplication.sharedApplication().setMinimumBackgroundFetchInterval(

UIApplicationBackgroundFetchIntervalMinimum)

return true

}

默認(rèn)的時(shí)間間隔是你想切換回去的UIApplicationBackgroundFetchIntervalNever,比如,你的用戶日志和不需要更新的內(nèi)容.你也可以設(shè)置一個(gè)精確到秒的時(shí)間間隔.系統(tǒng)在開始執(zhí)行后臺(tái)獲取之前將等待一段時(shí)間.

要小心,,不要將時(shí)間間隔設(shè)置過短,因?yàn)樗鼤?huì)多余的消耗電池和損害服務(wù)器.結(jié)束獲取信息的確切時(shí)間是由系統(tǒng)決定的,但是在執(zhí)行它之前將會(huì)等待一段時(shí)間.通常,UIApplicationBackgroundFetchIntervalMinimum是很好用的默認(rèn)值.

最后,為了啟用后臺(tái)程序,你必須實(shí)現(xiàn)application(_:performFetchWithCompletionHandler:).將下列方法添加到AppDelegate.swift中.

// Support for background fetch

func application(application: UIApplication, performFetchWithCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {

if let tabBarController = window?.rootViewController as? UITabBarController,

viewControllers = tabBarController.viewControllers as? [UIViewController] {

for viewController in viewControllers {

if let fetchViewController = viewController as? FetchViewController {

fetchViewController.fetch {

fetchViewController.updateUI()

completionHandler(.NewData)

}

}

}

}

}

首先你需要獲取FetchViewContoller.然后,因?yàn)閞ootViewController在每個(gè)APP中不是必須的UITabBarController,所以它是可以選擇創(chuàng)建的,不過它在這個(gè)APP中,所以它絕不會(huì)出現(xiàn)問題.

接下來,你在選項(xiàng)卡控制器中循環(huán)添加所有的視圖控制器,并且將它們成功的放到FetchViewController中.在這個(gè)APP中,你知道它是最后的控制器,所以你不能對它進(jìn)行硬編碼,但是在你決定以后添加或刪除選項(xiàng)卡的時(shí)候循環(huán)創(chuàng)建會(huì)提高程序的健壯性.

最后,你可以調(diào)用fetch(_:).當(dāng)它執(zhí)行完后,你會(huì)更新UI,然后調(diào)用將completionHandler作為參數(shù)傳遞的函數(shù).你在這個(gè)操作的最后調(diào)用這個(gè)完成處理的程序是很重要的.你指定在獲取過程中獲取的結(jié)果作為以一個(gè)參數(shù).它的可能值為.NewData, .NoData或者.Failed.

為了簡單起見,該教程總是指定.NewData作為永遠(yuǎn)成功獲取時(shí)間的返回值,并且這個(gè)值和上一次的結(jié)果總是不同的.在這之后,iOS可以使用更好的時(shí)間間隔來執(zhí)行后臺(tái)獲取.該系統(tǒng)知道在這個(gè)時(shí)間點(diǎn)上的系統(tǒng)快照,所以它可以在應(yīng)用程序切換卡中顯示.以上是為了實(shí)現(xiàn)后臺(tái)獲取所需要的所有的操作.

提示:不是沿著信息傳遞完成對屬性的調(diào)用,而是保存一個(gè)屬性變量,并且在你獲取完成后調(diào)用它是很有誘惑力的.不這樣做的話,如果你多次調(diào)用 application(_:performFetchWithCompletionHandler:),先前的處理程序?qū)?huì)被覆蓋,永遠(yuǎn)不會(huì)被調(diào)用.最好通過傳遞處理程序,并且在它不會(huì)造成這種編程錯(cuò)誤的時(shí)候調(diào)用它.

測試后臺(tái)獲取

測試后臺(tái)獲取的一個(gè)方法是停下來等著系統(tǒng)決定去執(zhí)行它.這需要大量的等待.幸運(yùn)的是,Xcode體統(tǒng)了模擬后臺(tái)獲取的方法.有兩種你需要測試的情況,一種是當(dāng)你的APP在后臺(tái)中時(shí),另一種是你的APP處于從被掛起到繼續(xù)運(yùn)行的情況.第一種方法最簡單,僅僅是一個(gè)選擇菜單.

* 在真正的設(shè)備上運(yùn)行(不是模擬器);

* 在Xcode調(diào)試菜單中選擇模擬后臺(tái)獲取;

重新打開這個(gè)APP,注意被送到后臺(tái)的數(shù)據(jù).

切換到Fetch選項(xiàng)卡,(注意當(dāng)你模擬后臺(tái)獲取而且不是顯示“Not yet updated”的時(shí)候時(shí)間)

另一種方法是在從掛起狀態(tài)回復(fù)的時(shí)候測試后臺(tái)獲取.這里有一個(gè)啟動(dòng)項(xiàng)讓你APP一運(yùn)行就直接進(jìn)入掛起狀態(tài).因?yàn)槟憧赡芤獪y試這種臨界狀態(tài),用這個(gè)選項(xiàng)始終建立新的Scheme是最好的.Xcode使這種情況很容易實(shí)現(xiàn).

首先選擇Manage Schemes選項(xiàng).

接下來,選擇列表里僅有的方案,然后點(diǎn)擊齒輪圖標(biāo),選擇Duplicate Scheme.

最后,用合理的名字重命名你的方案,比如 “Background Fetch”,并選中 Launch due to background fetch event的復(fù)選框.

需要注意的是在Xcode6.1中,在模擬器上這并不能可靠的運(yùn)行.我自己測試的時(shí)候,我需要使用真正的設(shè)備正確的從啟動(dòng)進(jìn)去到掛起狀態(tài).

用這個(gè)方案運(yùn)行你的APP.你會(huì)發(fā)現(xiàn),該APP沒有真正的打開,而是直接運(yùn)行到了掛起狀態(tài).現(xiàn)在,手動(dòng)開啟它,并進(jìn)入Fetch選項(xiàng).你會(huì)看到,當(dāng)你運(yùn)行該APP時(shí),時(shí)間會(huì)更新,而不會(huì)顯示“Not yet updated”.

使用后臺(tái)獲取能夠有效地讓你的用戶們流暢的一直獲取最新的內(nèi)容.

何去何從?

你可以在這里下載完整的示例工程.

如果你想讀我們這里涉及到蘋果文檔里的內(nèi)容,最佳開始地點(diǎn)是Background Execution.該文檔介紹了每一個(gè)后臺(tái)模式,并為每個(gè)模式鏈接到相應(yīng)的位置.

該文檔有趣的部分談?wù)摿巳绾螛?gòu)建一個(gè)可靠的APP.你應(yīng)該知道釋放正在后臺(tái)運(yùn)行的APP中的一些細(xì)節(jié)或多或少會(huì)涉及到你到APP.

最后,如果你打算做大型網(wǎng)絡(luò)信息傳輸,確保檢查NSURLSession.

我們希望你能享受這個(gè)課程,如果你有任何疑問或意見, 請加入下面的論壇討論.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末重父,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子忽匈,更是在濱河造成了極大的恐慌房午,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丹允,死亡現(xiàn)場離奇詭異郭厌,居然都是意外死亡袋倔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門折柠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宾娜,“玉大人,你說我怎么就攤上這事扇售∏八” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵承冰,是天一觀的道長华弓。 經(jīng)常有香客問我,道長困乒,這世上最難降的妖魔是什么寂屏? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮娜搂,結(jié)果婚禮上凑保,老公的妹妹穿的比我還像新娘。我一直安慰自己涌攻,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布频伤。 她就那樣靜靜地躺著恳谎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪憋肖。 梳的紋絲不亂的頭發(fā)上因痛,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天,我揣著相機(jī)與錄音岸更,去河邊找鬼鸵膏。 笑死,一個(gè)胖子當(dāng)著我的面吹牛怎炊,可吹牛的內(nèi)容都是我干的谭企。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼评肆,長吁一口氣:“原來是場噩夢啊……” “哼债查!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起瓜挽,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤盹廷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后久橙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俄占,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡管怠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缸榄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渤弛。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖碰凶,靈堂內(nèi)的尸體忽然破棺而出暮芭,到底是詐尸還是另有隱情,我是刑警寧澤欲低,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布辕宏,位于F島的核電站,受9級特大地震影響砾莱,放射性物質(zhì)發(fā)生泄漏瑞筐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一腊瑟、第九天 我趴在偏房一處隱蔽的房頂上張望聚假。 院中可真熱鬧,春花似錦闰非、人聲如沸膘格。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瘪贱。三九已至,卻和暖如春辆毡,著一層夾襖步出監(jiān)牢的瞬間菜秦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工舶掖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留球昨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓眨攘,卻偏偏與公主長得像主慰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子鲫售,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容