iOS 7 的多任務

在 iOS 7 之前萤悴,當程序置于后臺之后開發(fā)者們對他們程序所能做的事情非常有限坟比。除了 VOIP 和基于地理位置特性以外荆姆,唯一能做的地方就是使用后臺任務(background tasks)讓代碼可以執(zhí)行幾分鐘。如果你想下載比較大的視頻文件以便離線瀏覽族奢,亦或者備份用戶的照片到你的服務器上,你都僅能完成一部分工作。

iOS 7 添加了兩個新的 API 以便你的程序可以在后臺更新界面以及內容。首先是后臺獲任酵蕖(Background Fetch),它允許你定期地從網絡獲取新的內容蜒滩。第二個 API 就是遠程通知(Remote Notifications)滨达,這是一個當事件發(fā)生時可以讓推送通知主動提醒應用的新特性,這兩者都為你的應用界面保持最新提供了極大的幫助俯艰。在新的后臺傳輸服務 (Background Transfer Service) 中執(zhí)行定期的任務捡遍,也允許你在進程之外可以執(zhí)行網絡傳輸(下載和上傳)工作。

后臺獲取 (Background Fetch) 和遠程通知 (Remote Notification) 基于簡單的 ApplicationDelegate 鉤子竹握,在應用程序掛起之前的 30 秒時鐘時間執(zhí)行工作画株。它們不是用于 CPU 頻繁工作或者長時間運行任務,而是用來處理長時間運行的網絡請求隊列啦辐,例如下載一部很大的電影谓传,或者執(zhí)行快速的內容更新。

對用戶來說芹关,多任務處理有一點顯而易見的改變就是新的應用切換程序 (the new app switcher)续挟,它用來呈現(xiàn)應用到后臺時的界面快照。這些快照的存在是有一定理由的--現(xiàn)在你可以在后臺完成工作后更新程序快照侥衬,以用來呈現(xiàn)新的內容诗祸。社交網絡跑芳、新聞或者天氣等應用現(xiàn)在都可以直接呈現(xiàn)最新的內容而不需要用戶重新打開應用。我們稍后會介紹如何更新屏幕快照直颅。

后臺獲取

后臺獲取是一種智能的輪詢機制博个,它很適合需要經常更新內容的程序,像社交網絡功偿,新聞或天氣的程序盆佣。為了在用戶啟動程序前提前觸發(fā)后臺獲取,系統(tǒng)會根據(jù)用戶行為喚醒應用程序脖含。舉個例子罪塔,如果用戶經常在下午 1 點使用某個應用程序,系統(tǒng)會學習养葵,適應并在使用周期前執(zhí)行后臺獲取。為了減少電池使用瘩缆,使用設備無線通信的所有應用的后臺獲取會被合并关拒,如果你向系統(tǒng)報告新數(shù)據(jù)無法獲取,iOS 會適應并使用此信息避免會繼續(xù)獲取庸娱。

開啟后臺獲取的第一步是在 info plist 文件中對 UIBackgroundModes 鍵指定特定的值着绊。最簡單的途徑是在 Xcode 5 的 project editor 中新的 Capabilities 標簽頁中設置,這個標簽頁包含了后臺模式部分熟尉,可以方便配置多任務選項归露。

capabilities-on-bgfetch.jpg

或者,你可以手動編輯這個值

<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
</array>  

接下來斤儿,告訴 iOS 多久進行一次數(shù)據(jù)獲取

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [application setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];

    return YES;
}

iOS 默認不進行后臺獲取剧包,所以你需要設置一個時間間隔,否則往果,你的應用程序永遠不能在后臺被喚醒疆液。UIApplicationBackgroundFetchIntervalMinimum 這個值要求系統(tǒng)盡可能頻繁地去管理你的程序到底什么時候應該被喚醒,但如果你不需要這樣的話陕贮,你也應該指定一個你想要的的時間間隔堕油。例如,一個天氣的應用程序肮之,可能只需要幾個小時才更新一次掉缺,iOS 將會在后臺獲取之間至少等待你指定的時間間隔。

如果你的應用允許用戶退出登錄戈擒,那么就沒有獲取新數(shù)據(jù)的需要了眶明,你應該把 minimumBackgroundFetchInterval 設置為 UIApplicationBackgroundFetchIntervalNever,這樣可以節(jié)省資源峦甩。

最后一步是在應用程序委托中實現(xiàn)下列方法:

- (void)                application:(UIApplication *)application 
  performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration];

    NSURL *url = [[NSURL alloc] initWithString:@"http://yourserver.com/data.json"];
    NSURLSessionDataTask *task = [session dataTaskWithURL:url 
                                        completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        if (error) {
            completionHandler(UIBackgroundFetchResultFailed);
            return;
        }

        // 解析響應/數(shù)據(jù)以決定新內容是否可用
        BOOL hasNewData = ...
        if (hasNewData) {
            completionHandler(UIBackgroundFetchResultNewData);
        } else {
            completionHandler(UIBackgroundFetchResultNoData);
        }
    }];

    // 開始任務
    [task resume];
}

系統(tǒng)喚醒應用程序后將會執(zhí)行這個委托方法赘来。需要注意的是现喳,你只有 30 秒的時間來確定獲取的新內容是否可用,然后處理新內容并更新界面犬辰。30 秒時間應該足夠去從網絡獲取數(shù)據(jù)和獲取界面的縮略圖嗦篱,但是最多只有 30 秒。當完成了網絡請求和更新界面后幌缝,你應該執(zhí)行完成的回調灸促。

完成回調的執(zhí)行有兩個目的。首先涵卵,系統(tǒng)會估量你的進程消耗的電量浴栽,并根據(jù)你傳遞的 UIBackgroundFetchResult 參數(shù)記錄新數(shù)據(jù)是否可用。其次轿偎,當你調用完成的處理代碼時典鸡,應用的界面縮略圖會被采用,并更新應用程序切換器坏晦。當用戶在應用間切換時萝玷,用戶將會看到新內容。這種通過 completion handler 來報告并且生成截圖的方法昆婿,在新的多任務處理 API 中是很常見的球碉。

在實際應用中,你應該將 completionHandler 傳遞到應用程序的子組件仓蛆,然后在處理完數(shù)據(jù)和更新界面后調用睁冬。

在這里,你可能想知道 iOS 是如何在應用程序后臺運行時獲得界面截圖的看疙,并且想知道應用程序的生命周期與后臺獲取之間有什么關系豆拨。如果應用程序處于掛起狀態(tài),系統(tǒng)會先喚醒應用狼荞,然后再調用 application: performFetchWithCompletionHandler:辽装。如果應用程序還沒有啟動,系統(tǒng)將會啟動它相味,然后調用常見的委托方法拾积,包括 application: didFinishLaunchingWithOptions:。你可以把這種應用程序運行的方式想像為用戶從 Springboard 啟動這個程序丰涉,區(qū)別僅僅在于界面是看不見的拓巧,在屏幕外渲染的。

大多數(shù)情況下一死,無論應用在后臺啟動或者在前臺肛度,你會執(zhí)行相同的工作,但你可以通過查看 UIApplication 的 applicationState 屬性來判斷應用是不是從后臺啟動投慈。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    NSLog(@"Launched in background %d", UIApplicationStateBackground == application.applicationState);

    return YES;
}

測試后臺數(shù)據(jù)獲取
有兩種可以模擬后臺獲取的途徑承耿。最簡單是從 Xcode 運行你的應用冠骄,當應用運行時,在 Xcode 的 Debug 菜單選擇 Simulate Background Fetch.

第二種方法加袋,使用 scheme 更改 Xcode 運行程序的方式凛辣。在 Xcode 菜單的 Product 選項,選擇 Scheme 然后選擇 Manage Schemes职烧。在這里扁誓,你可以編輯或者添加一個新的 scheme,然后選中 Launch due to a background fetch event 蚀之。如下圖:


edit-scheme-simulate-background-fetch.png

遠程通知

遠程通知允許你在重要事件發(fā)生時蝗敢,告知你的應用。你可能需要發(fā)送新的即時信息足删,突發(fā)新聞的提醒寿谴,或者用戶喜愛電視的最新劇集已經可以下載以便離線觀看的消息。遠程通知很適合用于那些偶爾出現(xiàn)失受,但卻很重要的內容拭卿,如果使用后臺獲取模式中在兩次獲取間需要等待的時間是不可接受的話,遠程通知會是一個不錯的選擇贱纠。遠程通知會比后臺獲取更有效率,因為應用程序只有在需要的時候才會啟動响蕴。

一條遠程通知實際上只是一條普通的帶有 content-available 標志的推送通知谆焊。你可以發(fā)送一條帶有提醒信息的推送去告訴用戶有事請發(fā)生了,同時在后臺對界面進行更新浦夷。但遠程通知也可以做到在安靜地辖试,沒有提醒消息或者任何聲音的情況下,只去更新應用界面或者觸發(fā)后臺工作劈狐。然后你可以在完成下載或者處理完新內容后罐孝,發(fā)送一條本地通知。

靜默的推送通知有速度限制肥缔,所以你可以大膽地根據(jù)應用程序的需要發(fā)送盡可能多的通知莲兢。iOS 和蘋果推送服務會控制推送通知多久被遞送,發(fā)送很多推送通知是沒有問題的续膳。如果你的推送通知達到了限制改艇,推送通知可能會被延遲,直到設備下次發(fā)送保持活動狀態(tài)的數(shù)據(jù)包坟岔,或者收到另外一個通知谒兄。

發(fā)送遠程通知
要發(fā)送一條遠程通知,需要在推送通知的有效負載(payload)設置 content-available 標志社付。content-available 標志和用來通知 報刊應用(Newsstand)的健值是一樣的承疲,因此邻耕,大多數(shù)推送腳本和庫都已經支持遠程通知。當你發(fā)送一條遠程通知時燕鸽,你可能還想要包含一些通知有效負載中的數(shù)據(jù)兄世,讓你應用程序可以引用事件。這可以為你節(jié)省一些網絡請求绵咱,并提高應用程序的響應度碘饼。

我建議在開發(fā)的時候,使用 Nomad CLI’s Houston 工具發(fā)送推送消息悲伶,當然你也可以使用你喜歡的庫或腳本艾恼。

你可以通過 nomad-cli ruby gem 來安裝 Houston

gem install nomad-cli

然后通過包含在 Nomad 的 apn 實用工具發(fā)送一條通知:

# Send a Push Notification to your Device
apn push <device token> -c /path/to/key-cert.pem -n -d content-id=42

在這里,-n 標志指定應該包含 content-available 健值麸锉,-d 標志允許添加我們自定義的數(shù)據(jù)健值到有效負荷钠绍。

通知的有效負荷(payload)結果和下面類似:

{
    "aps" : {
        "content-available" : 1
    },
    "content-id" : 42
}

iOS 7 添加了一個新的應用程序委托方法,當接收到一條帶有 content-available 的推送通知時花沉,下面的方法會被調用:

- (void)application:(UIApplication *)application 
  didReceiveRemoteNotification:(NSDictionary *)userInfo 
        fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@"Remote Notification userInfo is %@", userInfo);

    NSNumber *contentID = userInfo[@"content-id"];
    // 根據(jù) content ID 進行操作
    completionHandler(UIBackgroundFetchResultNewData);
}

和后臺抓取一樣柳爽,應用程序進入后臺啟動,也有 30 秒的時間去獲取新內容并更新界面碱屁,最后調用完成的處理代碼磷脯。我們可以像后臺獲取那樣,執(zhí)行快速的網絡請求娩脾,但我們可以使用新的強大的后臺傳輸服務赵誓,處理任務隊列,下面看看我們如何在任務完成后更新界面柿赊。

NSURLSession 和 后臺傳輸服務(Background Transfer Service)

NSURLSession 是 iOS 7 添加的一個新類俩功,它也是 Foundation networking 中的新技術。作為 NSURLConnection 的替代品碰声,一些熟悉的概念和類都保留下來了诡蜓,例如 NSURL,NSURLRequest 和 NSURLResponse胰挑。所以蔓罚,你可以使用 NSURLSessionTask 這一 NSURLConnection 的替代品,來處理網絡請求及響應洽腺。一共有 3 種會話任務:數(shù)據(jù)脚粟,下載和上傳。每一種都向 NSURLSessionTask 添加了語法糖蘸朋,根據(jù)你的需要核无,適當選擇一種。

一個 NSURLSession 對象協(xié)調一個或多個 NSURLSessionTask 對象藕坯,并根據(jù) NSURLSessionTask 創(chuàng)建的 NSURLSessionConfiguration 實現(xiàn)不同的功能团南。使用相同的配置噪沙,你也可以創(chuàng)建多組具有相關任務的 NSURLSession 對象。要利用后臺傳輸服務吐根,你將會使用 [NSURLSessionConfiguration backgroundSessionConfiguration] 來創(chuàng)建一個會話配置正歼。添加到后臺會話的任務在外部進程運行,即使應用程序被掛起拷橘,崩潰局义,或者被殺死,它依然會運行冗疮。

NSURLSessionConfiguration 允許你設置默認的 HTTP 頭萄唇,配置緩存策略,限制使用蜂窩數(shù)據(jù)等等术幔。其中一個選項是 discretionary 標志另萤,這個標志允許系統(tǒng)為分配任務進行性能優(yōu)化。這意味著只有當設備有足夠電量時诅挑,設備才通過 Wifi 進行數(shù)據(jù)傳輸四敞。如果電量低,或者只僅有一個蜂窩連接拔妥,傳輸任務是不會運行的忿危。后臺傳輸總是在 discretionary 模式下運行。

目前為止没龙,我們大概了解了 NSURLSession癌蚁,以及一個后臺會話如何進行,接下來兜畸,讓我們回到遠程通知的例子,添加一些代碼來處理后臺傳輸服務的下載隊列碘梢。當下載完成后咬摇,我們會通知用戶該文件已經可以使用了。

NSURLSessionDownloadTask
首先煞躬,我們先處理一條遠程通知肛鹏,并把一個 NSURLSessionDownloadTask 添加到后臺傳輸服務的隊列。在 backgroundURLSession 方法中恩沛,我們根據(jù)后臺會話配置在扰,創(chuàng)建一個 NSURLSession 對象,并把 application delegate 作為會話的委托對象雷客。文檔不建議對于相同的標識符 (identifier) 創(chuàng)建多個會話對象芒珠,所以我們使用 dispatch_once 來避免潛在的問題:

- (NSURLSession *)backgroundURLSession
{
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *identifier = @"io.objc.backgroundTransferExample";
        NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:identifier];
        session = [NSURLSession sessionWithConfiguration:sessionConfig 
                                                delegate:self 
                                           delegateQueue:[NSOperationQueue mainQueue]];
    });

    return session;
}

- (void)           application:(UIApplication *)application 
  didReceiveRemoteNotification:(NSDictionary *)userInfo 
        fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    NSLog(@"Received remote notification with userInfo %@", userInfo);

    NSNumber *contentID = userInfo[@"content-id"];
    NSString *downloadURLString = [NSString stringWithFormat:@"http://yourserver.com/downloads/%d.mp3", [contentID intValue]];
    NSURL* downloadURL = [NSURL URLWithString:downloadURLString];

    NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
    NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithRequest:request];
    task.taskDescription = [NSString stringWithFormat:@"Podcast Episode %d", [contentID intValue]];
    [task resume];

    completionHandler(UIBackgroundFetchResultNewData);
}

我們使用 NSURLSession 類方法創(chuàng)建一個下載任務,配置請求搅裙,并提供說明供以后使用皱卓。因為所有會話任務一開始處于掛起狀態(tài)裹芝,你必須謹記要調用 [task resume] 保證開始了任務。

現(xiàn)在娜汁,我們需要實現(xiàn) NSURLSessionDownloadDelegate 的委托方法嫂易,當下載完成時,調用回調函數(shù)掐禁。如果你需要處理認證或會話生命周期的其他事件怜械,你可能還需要實現(xiàn) NSURLSessionDelegate 或 NSURLSessionTaskDelegate 的方法。你應該閱讀 Apple 的 Life Cycle of a URL Session with Custom Delegates 文檔傅事,它講解了所有類型的會話任務的完整生命周期缕允。

NSURLSessionDownloadDelegate 中的委托方法全部是必須實現(xiàn)的,盡管在這個例子中我們只需要用到 [NSURLSession downloadTask:didFinishDownloadingToURL:]享完。任務完成下載時灼芭,你會得到一個磁盤上該文件的臨時 URL。你必須把這個文件移動或復制你的應用程序空間般又,因為當你從這個委托方法返回時彼绷,該文件將從臨時存儲中刪除。

#Pragma Mark - NSURLSessionDownloadDelegate

- (void)         URLSession:(NSURLSession *)session 
               downloadTask:(NSURLSessionDownloadTask *)downloadTask
  didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"downloadTask:%@ didFinishDownloadingToURL:%@", downloadTask.taskDescription, location);

    // 用 NSFileManager 將文件復制到應用的存儲中
    // ...

    // 通知 UI 刷新
}

- (void)  URLSession:(NSURLSession *)session 
        downloadTask:(NSURLSessionDownloadTask *)downloadTask 
   didResumeAtOffset:(int64_t)fileOffset 
  expectedTotalBytes:(int64_t)expectedTotalBytes
{
}

- (void)         URLSession:(NSURLSession *)session 
               downloadTask:(NSURLSessionDownloadTask *)downloadTask 
               didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten 
  totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
}

當后臺會話任務完成時茴迁,如果你的應用程序仍然在前臺運行寄悯,上面的代碼已經足夠了。然而堕义,在大多數(shù)情況下猜旬,你的應用程序可能是沒有運行的,或者在后臺被掛起倦卖。在這些情況下洒擦,你必須實現(xiàn)應用程序委托的兩個方法,這樣系統(tǒng)就可以喚醒你的應用程序怕膛。不同于以往的委托回調熟嫩,該應用程序委托會被調用兩次,因為您的會話和任務委托可能會收到一系列消息褐捻。app delegate 的:handleEventsForBackgroundURLSession: 方法會在這些 NSURLSession 委托的消息發(fā)送前被調用掸茅,然后,URLSessionDidFinishEventsForBackgroundURLSession 在隨后被調用柠逞。在前面的方法中昧狮,包含了一個后臺完成的回調(completionHandler),并在后面的方法中執(zhí)行回調以便更新界面:

- (void)                  application:(UIApplication *)application 
  handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
    // 你必須重新建立一個后臺 seesiong 的參照
    // 否則 NSURLSessionDownloadDelegate 和 NSURLSessionDelegate 方法會因為
    // 沒有 對 session 的 delegate 設定而不會被調用板壮。參見上面的 backgroundURLSession
    NSURLSession *backgroundSession = [self backgroundURLSession];

    NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);

    // 保存 completion handler 以在處理 session 事件后更新 UI
    [self addCompletionHandler:completionHandler forSession:identifier];
}

- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
    NSLog(@"Background URL session %@ finished events.\n", session);

    if (session.configuration.identifier) {
        // 調用在 -application:handleEventsForBackgroundURLSession: 中保存的 handler
        [self callCompletionHandlerForSession:session.configuration.identifier];
    }
}

- (void)addCompletionHandler:(CompletionHandlerType)handler forSession:(NSString *)identifier
{
    if ([self.completionHandlerDictionary objectForKey:identifier]) {
        NSLog(@"Error: Got multiple handlers for a single session identifier.  This should not happen.\n");
    }

    [self.completionHandlerDictionary setObject:handler forKey:identifier];
}

- (void)callCompletionHandlerForSession: (NSString *)identifier
{
    CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];

    if (handler) {
        [self.completionHandlerDictionary removeObjectForKey: identifier];
        NSLog(@"Calling completion handler for session %@", identifier);

        handler();
    }
}

如果當后臺傳輸完成時逗鸣,應用程序不再停留在前臺,那么,對于更新程序界面來說慕购,這個兩步處理過程是必要的聊疲。此外,如果當后臺傳輸完成時沪悲,應用程序根本沒有在運行获洲,iOS 將會在后臺啟動該應用程序,然后前面的應用程序和會話的委托方法會在 application:didFinishLaunchingWithOptions: 方法被調用之后被調用殿如。

配置和限制
我們簡單地體驗了后臺傳輸?shù)膹姶笾幑鄙海銘撋钊胛臋n,閱讀 NSURLSessionConfiguration 部分涉馁,以便最好地滿足你的使用場景门岔。例如,NSURLSessionTasks 通過 NSURLSessionConfiguration 的 timeoutIntervalForResource 屬性烤送,支持資源超時特性寒随。你可以使用這個特性指定你允許完成一個傳輸所需的最長時間。內容只在有限的時間可用帮坚,或者在用戶只有有限 Wifi 帶寬的時間內無法下載或上傳資源的情況下妻往,你也可以使用這個特性。

除了下載任務试和,NSURLSession 也全面支持上傳任務讯泣,因此,你可能會在后臺將視頻上傳到服務器阅悍,這保證用戶不需要再像 iOS 6 那樣保持應用程序前臺運行好渠。如果當傳輸完成時你的應用程序不需要在后臺運行,一個比較好的做法是节视,把 NSURLSessionConfiguration 的 sessionSendsLaunchEvents 屬性設置為 NO拳锚。高效利用系統(tǒng)資源,是一件讓 iOS 和用戶都高興的事寻行。

最后晌畅,我們來說一說使用后臺會話的幾個限制。作為一個必須實現(xiàn)的委托寡痰,您不能對 NSURLSession 使用簡單的基于 block 的回調方法。后臺啟動應用程序棋凳,是相對耗費較多資源的拦坠,所以總是采用 HTTP 重定向。后臺傳輸服務只支持 HTTP 和 HTTPS剩岳,你不能使用自定義的協(xié)議贞滨。系統(tǒng)會根據(jù)可用的資源進行優(yōu)化,在任何時候你都不能強制傳輸任務在后臺進行。

另外晓铆,要注意的是在后臺會話中勺良,NSURLSessionDataTasks 是完全不支持的,你應該只出于短期的骄噪,小請求為目的使用這些任務尚困,而不是用來下載或上傳。

總結

iOS 7 中強大的多任務和網絡 API 為現(xiàn)有應用和新應用開啟了一系列全新的可能性链蕊。如果你的應用程序可以從進程外的網絡傳輸和數(shù)據(jù)中獲益事甜,那么盡情地使用這些美妙的 API。一般情況下滔韵,你可以就像你的應用正在前臺運行那樣去實現(xiàn)后臺傳輸逻谦,并進行適當?shù)慕缑娓拢@里絕大多數(shù)的工作都已經為你完成了陪蜻。

使用適當?shù)男?API 來為你的應用程序提供內容邦马。
盡可能早地調用 completion handler 以提高效率。
讓 completion handler 為應用程序更新界面快照宴卖。
擴展閱讀

[WWDC 2013 session “What’s New with Multitasking”]
(https://developer.apple.com/videos/)
[WWDC 2013 session “What’s New in Foundation Networking”]
(https://developer.apple.com/videos/)
[URL Loading System Programming Guide]
(https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html#//apple_ref/doc/uid/10000165i)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末滋将,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子嘱腥,更是在濱河造成了極大的恐慌耕渴,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件齿兔,死亡現(xiàn)場離奇詭異橱脸,居然都是意外死亡,警方通過查閱死者的電腦和手機分苇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門添诉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人医寿,你說我怎么就攤上這事栏赴。” “怎么了靖秩?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵须眷,是天一觀的道長。 經常有香客問我沟突,道長花颗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任惠拭,我火速辦了婚禮扩劝,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己棒呛,他們只是感情好聂示,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著簇秒,像睡著了一般鱼喉。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上宰睡,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天蒲凶,我揣著相機與錄音,去河邊找鬼拆内。 笑死旋圆,一個胖子當著我的面吹牛,可吹牛的內容都是我干的麸恍。 我是一名探鬼主播灵巧,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抹沪!你這毒婦竟也來了刻肄?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤融欧,失蹤者是張志新(化名)和其女友劉穎敏弃,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體噪馏,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡麦到,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了欠肾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓶颠。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖刺桃,靈堂內的尸體忽然破棺而出粹淋,到底是詐尸還是另有隱情,我是刑警寧澤瑟慈,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布桃移,位于F島的核電站,受9級特大地震影響葛碧,放射性物質發(fā)生泄漏借杰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一吹埠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦缘琅、人聲如沸粘都。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽翩隧。三九已至,卻和暖如春呻纹,著一層夾襖步出監(jiān)牢的瞬間堆生,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工雷酪, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留淑仆,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓哥力,卻偏偏與公主長得像蔗怠,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吩跋,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容