鍍金池/ 教程/ iOS/ XPC
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

XPC

關(guān)于 XPC

XPC 是 OS X 下的一種 IPC (進(jìn)程間通信) 技術(shù), 它實(shí)現(xiàn)了權(quán)限隔離, 使得 App Sandbox 更加完備.

首先,XPC 更多關(guān)注的是實(shí)現(xiàn)功能某種的方式,通常采用其他方式同樣能夠?qū)崿F(xiàn)。并沒有強(qiáng)調(diào)如果不使用 XPC,無法實(shí)現(xiàn)某些功能。

XPC 目的是提高 App 的安全性和穩(wěn)定性。XPC 讓進(jìn)程間通信變得更容易,讓我們能夠相對容易地將 App 拆分成多個(gè)進(jìn)程的模式。更進(jìn)一步的是,XPC 幫我管理了這些進(jìn)程的生命周期,當(dāng)我們需要與子進(jìn)程通信的時(shí)候,子進(jìn)程已經(jīng)被 XPC 給運(yùn)行起來了。

我們將使用在頭文件 NSXPCConnection.h 中聲明的 Foundation framework API,它是建立在頭文件 xpc/xpc.h 中聲明的原始 XPC API 之上的。XPC API 原本是純 C 實(shí)現(xiàn)的 API,很好地集成了 libdispatch(又名 GCD)。本文中我們將使用Foundation 中的類,它們可以讓我們使用 XPC 的幾乎全部功能(真實(shí)的表現(xiàn)了實(shí)際底層 C API 是如何工作的),同時(shí)與 C API 相比,F(xiàn)oundation API 使用起來會(huì)更加容易。

哪些地方用到了 XPC ?

Apple 在操作系統(tǒng)的各個(gè)部分廣泛使用了 XPC,很多系統(tǒng) Framework 也利用了 XPC 來實(shí)現(xiàn)其功能。你可以在命令行運(yùn)行如下搜索命令:

% find /System/Library/Frameworks -name \*.xpc

結(jié)果顯示 Frameworks 目錄下有 55 個(gè) XPC service(譯者注:在 Yosemite 下),范圍從 AddressBook 到 WebKit 等等。

如果在 /Applications 目錄下做同樣的搜索,我們會(huì)發(fā)現(xiàn)從 iWork 套件到 Xcode,甚至是一些第三方應(yīng)用程序都使用了 XPC。

Xcode 本身就是使用 XPC 的一個(gè)很好的例子:當(dāng)你在 Xcode 中編輯 Swift 代碼的時(shí)候,Xcode 就是通過 XPC 與 SourceKit 通信的(譯者注:實(shí)際進(jìn)程名應(yīng)該是SourceKitService)。SourceKit 是主要負(fù)責(zé)源代碼解析,語法高亮,排版,自動(dòng)完成等功能的 XPC service。更多詳情可以參考 JP Simard 的博客.

其實(shí) XPC 在 iOS 上應(yīng)用的很廣泛 - 但是目前只有 Apple 能夠使用,第三方開發(fā)者還不能使用。

一個(gè)示例 App

讓我們來看一個(gè)簡單的示例:一個(gè)在 table view 中顯示多張圖片的 App。圖片是以 JPEG 格式從網(wǎng)絡(luò)服務(wù)器上下載下來的。

App看起來是這樣:

http://wiki.jikexueyuan.com/project/objc/images/14-2.png" alt="" />

NSTableViewDataSource 會(huì)從 ImageSet 類加載圖片 ,像這樣:

func tableView(tableView: NSTableView!, viewForTableColumn tableColumn: NSTableColumn!, row: Int) -> NSView! {
    let cellView = tableView.makeViewWithIdentifier("Image", owner: self) as NSTableCellView
    var image: NSImage? = nil
    if let c = self.imageSet?.images.count {
        if row < c {
            image = self.imageSet?.images[row]
        }
    }
    cellView.imageView.image = image
    return cellView
}

ImageSet 類有一個(gè)簡單的屬性:

var images: NSImage![]

ImageLoader 類會(huì)異步的填充這個(gè)圖片數(shù)組。

不使用XPC

如果不使用XPC,我們可以這樣實(shí)現(xiàn) ImageLoader 類來下載并解壓圖片:

class ImageLoader: NSObject {
    let session: NSURLSession

    init()  {
        let config = NSURLSessionConfiguration.defaultSessionConfiguration()
        session = NSURLSession(configuration: config)
    }

    func retrieveImage(atURL url: NSURL, completionHandler: (NSImage?)->Void) {
        let task = session.dataTaskWithURL(url) {
            maybeData, response, error in
            if let data: NSData = maybeData {
                dispatch_async(dispatch_get_global_queue(0, 0)) {
                    let source = CGImageSourceCreateWithData(data, nil).takeRetainedValue()
                    let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil).takeRetainedValue()
                    var size = CGSize(
                        width: CGFloat(CGImageGetWidth(cgImage)),
                        height: CGFloat(CGImageGetHeight(cgImage)))
                    let image = NSImage(CGImage: cgImage, size: size)
                    completionHandler(image)
                }
            }
        }
        task.resume()
    }
}

明確而且工作得很好。

錯(cuò)誤隔離 (Fault Isolation) 和 權(quán)限隔離 (Split Privileges)

我們的 App 做了三件不同的事情:從互聯(lián)網(wǎng)上下載數(shù)據(jù),解碼為 JPEG,然后顯示。

如果把 App 拆分成三個(gè)獨(dú)立的進(jìn)程,我們就能給每個(gè)進(jìn)程單獨(dú)的權(quán)限了;UI 進(jìn)程并不需要訪問網(wǎng)絡(luò)的權(quán)限。圖片下載的進(jìn)程的確需要訪問網(wǎng)絡(luò),但它不需要訪問文件的權(quán)限(它只是轉(zhuǎn)發(fā)數(shù)據(jù),并不做保存)。而將 JPEG 圖片解碼為 RGB 數(shù)據(jù)的進(jìn)程既不需要訪問網(wǎng)絡(luò)的權(quán)限,也不需要訪問文件的權(quán)限。

通過這種方式,在我們的 App 中尋找安全漏洞的行為已經(jīng)變得很困難了。另一個(gè)好處是,我們的 App 會(huì)變得更穩(wěn)定;例如下載 service 因?yàn)?bug 導(dǎo)致的 crash 并不會(huì)影響 App 主進(jìn)程的運(yùn)行;而下載 service 會(huì)被重啟。

架構(gòu)圖如下:

http://wiki.jikexueyuan.com/project/objc/images/14-3.png" alt="" />

XPC 的使用十分靈活,我們還可以這樣設(shè)計(jì):讓 App 直接和兩個(gè) service 通信,由 App 來負(fù)責(zé) service 之間的數(shù)據(jù)交互。后面我們會(huì)看到 App 是如何找到 XPC services的。

迄今為止,大部分安全相關(guān)的 bug 都出現(xiàn)在解析不受信數(shù)據(jù)的過程當(dāng)中,例如數(shù)據(jù)是我們從互聯(lián)網(wǎng)上接收到的,不受我們控制的?,F(xiàn)實(shí)中 HTTP 協(xié)議和解析 JPEG 數(shù)據(jù)也需要處理這樣的問題,而通過這樣設(shè)計(jì),我們將解析不受信數(shù)據(jù)的過程挪進(jìn)了一個(gè)子進(jìn)程,即一個(gè) XPC service。

在 App 中使用 XPC Services

XPC service 由兩個(gè)部分組成:service 本身,以及與之通信的代碼。它們都很簡單而且相似,算是個(gè)好消息。

在 Xcode 中有模板可以添加新的 XPC service target。 每個(gè) service 都需要一個(gè) bundle id,一個(gè)好的實(shí)踐是將其設(shè)置為 App 的 bundle id 的 subdomain(子域)。

在我們的例子中,App 的 bundle id 是 io.objc.Superfamous-Images,我們可以把下載 service 的 bundle id 設(shè)為 io.objc.Superfamous-Images.ImageDownloader。

在 build 過程中,Xcode 會(huì)為 service target 創(chuàng)建一個(gè)獨(dú)立 bundle,這個(gè) bundle 會(huì)被復(fù)制到 XPCServices 目錄下,與 Resources 目錄平級(jí)。

當(dāng) App 將數(shù)據(jù)發(fā)給 io.objc.Superfamous-Images.ImageDownloader 這個(gè) service 時(shí),XPC 會(huì)自動(dòng)啟動(dòng)這個(gè) service。

基于 XPC 的通信基本都是異步的。我們通過一個(gè) App 和 service 都使用的 protocol 來進(jìn)行定義。在我們的例子中:

@objc(ImageDownloaderProtocol) protocol ImageDownloaderProtocol {
    func downloadImage(atURL: NSURL!, withReply: (NSData?)->Void)
}

請注意 withReply: 這部分。它表明了消息是如何通過異步的方式回給調(diào)用方的。若返回的消息帶有數(shù)據(jù),需要將函數(shù)簽名最后一部分寫成:withReply: 并接受一個(gè)閉包參數(shù)的形式。

在我們的例子中,service 只提供了一個(gè)方法;但是我們可以在 protocol 里定義多個(gè)方法。

App 到 service 的連接是通過創(chuàng)建 NSXPCConnection 對象來完成的,像這樣:

let connection = NSXPCConnection(serviceName: "io.objc.Superfamous-Images.ImageDownloader")
connection.remoteObjectInterface = NSXPCInterface(`protocol`: ImageDownloaderProtocol.self)
connection.resume()

我們可以把 connection 對象保存為 self.imageDownloadConnection,這樣之后就可以像這樣和 service 進(jìn)行通信了:

let downloader = self.imageDownloadConnection.remoteObject as ImageDownloaderProtocol
downloader.downloadImageAtURL(url) {
    (data) in
    println("Got \(data.length) bytes.")
}

我們還應(yīng)該給 connection 對象設(shè)置錯(cuò)誤處理函數(shù),像這樣:

let downloader = self.imageDownloadConnection.remoteObjectProxyWithErrorHandler {
        (error) in NSLog("remote proxy error: %@", error)
} as ImageDownloaderProtocol
downloader.downloadImageAtURL(url) {
    (data) in
    println("Got \(data.length) bytes.")
}

這就是 App 端的所有代碼了。

監(jiān)聽service請求

XPC service 通過 NSXPCListener 對象來監(jiān)聽從 App 傳入的請求(譯者注:這是 NSXPCListenerDelegate 中可選的方法)。listener 對象會(huì)給每個(gè)來自 App 的請求在 service 端創(chuàng)建對應(yīng)的 connection 對象。

main.swift 中,我們可以這樣寫:

class ServiceDelegate : NSObject, NSXPCListenerDelegate {
    func listener(listener: NSXPCListener!, shouldAcceptNewConnection newConnection: NSXPCConnection!) -> Bool {
        newConnection.exportedInterface = NSXPCInterface(`protocol`: ImageDownloaderProtocol.self)
        var exportedObject = ImageDownloader()
        newConnection.exportedObject = exportedObject
        newConnection.resume()
        return true
    }
}

// Create the listener and run it by resuming:
let delegate = ServiceDelegate()
let listener = NSXPCListener.serviceListener()
listener.delegate = delegate;
listener.resume()

我們創(chuàng)建了一個(gè)全局(相當(dāng)于 C 或 Objective-C 中的 main 函數(shù))的 NSXPCListener 對象,并設(shè)置了它的 delegate,這樣傳入連接就會(huì)調(diào)用我們的 delegate 方法了。我們需要給 connection 設(shè)置 App 端中同樣使用的 protocol。最后我們設(shè)置 ImageDownloader 實(shí)例,它實(shí)際上實(shí)現(xiàn)了接口:

class ImageDownloader : NSObject, ImageDownloaderProtocol {
    let session: NSURLSession

    init()  {
        let config = NSURLSessionConfiguration.defaultSessionConfiguration()
        session = NSURLSession(configuration: config)
    }

    func downloadImageAtURL(url: NSURL!, withReply: ((NSData!)->Void)!) {
        let task = session.dataTaskWithURL(url) {
            data, response, error in
            if let httpResponse = response as? NSHTTPURLResponse {
                switch (data, httpResponse) {
                case let (d, r) where (200 <= r.statusCode) && (r.statusCode <= 399):
                    withReply(d)
                default:
                    withReply(nil)
                }
            }
        }
        task.resume()
    }
}

值得注意的一個(gè)重要是,NSXPCListenerNSXPCConnection 默認(rèn)是掛起 (suspended) 的。我們設(shè)置好后需要調(diào)用它們的 resume 方法來啟動(dòng)。

GitHub上可以找到這個(gè)簡單的示例App。

監(jiān)聽者 (Listener),連接 (Connection) 和導(dǎo)出對象 (Exported Object)

在 App 端,我們有一個(gè) connection 對象。每次將數(shù)據(jù)發(fā)給 service 時(shí),我們需要調(diào)用 remoteObjectProxyWithErrorHandler 方法來創(chuàng)建一個(gè)遠(yuǎn)程對象代理 (remote object proxy)。

而在service端,則多了一層。首先需要一個(gè) listener,用來監(jiān)聽來自 App 的傳入 connection。App 可以創(chuàng)建多個(gè) connection,listener 會(huì)在 service 端建立相應(yīng)的 connection 對象。每個(gè) connection 對象都有唯一的 exported object,在 App 端,通過 remote object proxy 發(fā)送的消息就是給它的。

當(dāng) App 創(chuàng)建一個(gè)到 XPC service 的 connection 時(shí),是 XPC 在管理這個(gè) service 的生命周期,service 的啟動(dòng)與停止都由 XPC runtime 完成,這對 App 來說是透明的。而且如果 service 因?yàn)槟撤N原因 crash 了,也會(huì)透明地被重啟。

App 初始化 XPC connection 的時(shí)候,XPC service 并不會(huì)啟動(dòng),直到 App 實(shí)際發(fā)送的第一條消息到 remote object proxy 時(shí)才啟動(dòng)。如果當(dāng)前沒有未結(jié)束的響應(yīng),系統(tǒng)可能會(huì)因?yàn)閮?nèi)存壓力或者 XPC service 已經(jīng)閑置了一段時(shí)間而停止這個(gè) service。這種情況下,App 持有的 connection 對象任然有效,下次再使用這個(gè) connection 對象的時(shí)候,XPC 系統(tǒng)會(huì)自動(dòng)重啟對應(yīng)的 XPC service。

如果 XPC service crash 了,它也會(huì)被透明地重啟,并且其對應(yīng)的 connection 也會(huì)一直有效。但是如果 XPC service 是在接收消息時(shí) crash 了的話,App 需用重新發(fā)送該消息才能接受到對應(yīng)的響應(yīng)。這就是為什么要調(diào)用 remoteObjectProxyWithErrorHandler 方法來設(shè)置錯(cuò)誤處理函數(shù)了。

這個(gè)方法接受一個(gè)閉包作為參數(shù),在發(fā)生錯(cuò)誤的時(shí)候被執(zhí)行。XPC API 保證在錯(cuò)誤處理里的閉包或者是消息響應(yīng)里的閉包之中,只有一個(gè)會(huì)被執(zhí)行;如果消息消息響應(yīng)里的閉包被執(zhí)行了,那么錯(cuò)誤處理的就不會(huì)被執(zhí)行,反之亦然。這樣就使得資源清理變得容易了。

突然終止 (Sudden Termination)

XPC 是通過跟蹤那些是否有仍在處理請求來管理 service 的生命周期的,如果有請求正在運(yùn)行,對應(yīng)的 service 不會(huì)被停止。如果消息請求的響應(yīng)還沒有被發(fā)送,則這個(gè)請求會(huì)被認(rèn)為是正在運(yùn)行的。對于那些沒有 reply 的處理程序的請求,只要方法體還在運(yùn)行,這個(gè)請求就會(huì)被認(rèn)為是正在運(yùn)行的。

在某些情況下,我們可能想告訴 XPC 我們還有更多的工作要做,我們可以使用 NSProcessInfo 的 API 來實(shí)現(xiàn)這一點(diǎn):

func disableAutomaticTermination(reason: String!)
func enableAutomaticTermination(reason: String!)

如果 XPC service 接受傳入請求并需要在后臺(tái)執(zhí)行一些異步操作,這些 API 就能派上用場了(即告訴系統(tǒng)不希望被突然終止)。某些情況下我們還可能需要調(diào)整我們的 QoS (服務(wù)質(zhì)量)設(shè)置。

中斷 (Interruption) 和失效 (Invalidation)

XPC 的最常見的用法是 App 發(fā)消息給它的 XPC service。XPC 允許非常靈活的設(shè)置。我們通過下文會(huì)了解到,connection 是雙向的,它可以是匿名監(jiān)聽者 (anonymous listeners)。如果另一端消失了(因?yàn)?crash 或者是正常的進(jìn)程終止),這時(shí)連接將很有可能變得無效。我們可以給 connection 對象設(shè)置失效處理函數(shù),如果 XPC runtime 無法重新創(chuàng)建這個(gè) connection,我們的失效處理函數(shù)將會(huì)被執(zhí)行。

我們還可以給 connection 設(shè)置中斷處理程序,會(huì)在 connection 被中斷的時(shí)候會(huì)執(zhí)行,盡管此時(shí) connection 仍然是有效的。

NSXPCConnection中 對應(yīng)的兩個(gè)屬性是:

var interruptionHandler: (() -> Void)!
var invalidationHandler: (() -> Void)!

雙向連接 (Bidirectional Connections)

一個(gè)經(jīng)常被忽略而又有意思的事實(shí)是:connection 是雙向的。但是只能通過 App 創(chuàng)建到 service 的初始連接。service 不能主動(dòng)創(chuàng)建到 App 的連接(見下文的 service lookup)。一旦連接已經(jīng)建好了,兩端都可以發(fā)起請求。

正如 service 端給 connection 對象設(shè)置了 exportedObject,App 端也可以這么做。這樣可以讓 service 端通過 remoteObjectProxy 來和 App 的 exported object 進(jìn)行通信了。值得注意是,XPC service 由系統(tǒng)管理其生命周期,如果沒有未完成的請求,可能會(huì)被停止掉(參見上文的 Sudden Termination)。

服務(wù)查找 (Service Lookup)

當(dāng)我們連接到 XPC service 的時(shí)候,我們需要找到連接的另一端。對于使用私有 XPC service 的 App,XPC 會(huì)在 App 的 bundle 范圍內(nèi)通過名字查找。還有其他的方法來連接到 XPC,讓我們來看看所有的可能性。

XPC Service

假如 App 使用:

NSXPCConnection(serviceName: "io.objc.myapp.myservice")

XPC 會(huì)在 App 自己的命名空間 (namespace) 查找名為 io.objc.myapp.myservice 的service,這樣的 service 僅對當(dāng)前 App 有效,其他 App 無法連接。XPC service bundle 要么是位于 App 的 bundle 里,要么是在該 App 使用的 Framework 的 bundle 里。

Mach Service

另一個(gè)選擇是使用:

NSXPCConnection(machServiceName: "io.objc.mymachservice", options: NSXPCConnectionOptions(0))

這會(huì)在當(dāng)前用戶的登錄會(huì)話 (login session) 中查找名為 io.objc.mymachservice 的service。 我們可以在 /Library/LaunchAgents~/Library/LaunchAgents 目錄下安裝 launch agent,這些 launch agent 也以與 App 里的 XPC service 幾乎相同的方式來提供 service。由于 launch agent 會(huì)在 per-login session 中啟動(dòng)的,在同一個(gè)登錄會(huì)話中運(yùn)行的多個(gè) App 可以和同一個(gè) launch agent 進(jìn)行通信。

這種方法很有用,例如狀態(tài)欄 (Status Bar) 中的 menu extra 程序(即右上角只有菜單項(xiàng)的 App)需要和 UI App 進(jìn)行通信的時(shí)候。普通 App 和 menu extra 程序都可以和同一個(gè) launch agent 進(jìn)行通信并交互數(shù)據(jù)。當(dāng)你需要讓兩個(gè)以上的進(jìn)程需要相互通信,XPC 可以是一個(gè)非常優(yōu)雅的方案。

假設(shè)我們要寫一個(gè)天氣類的 App,我們可以把天氣數(shù)據(jù)的抓取和解析做成 launch agent 方式的 XPC service。我們可以分別創(chuàng)建 menu extra 程序,普通 App,以及通知中心的 Widget 來顯示同樣的天氣數(shù)據(jù)。它們都可以通過 NSXPCConnection 和同一個(gè) launch agent 進(jìn)行通信。

與 XPC service 相同,launch agent 的生命周期也可以完全由 XPC 掌控:按需啟動(dòng),閑置或者系統(tǒng)內(nèi)存不足的時(shí)候停止。

匿名監(jiān)聽者 (Anonymous Listeners) 和端點(diǎn) (Endpoints)

XPC 有通過 connection 來傳遞被稱為 listener endpoints 的能力。這個(gè)概念一開始會(huì)讓人非常費(fèi)解,但是它可以帶來更大的靈活性。

比如說我們有兩個(gè) App,我們希望它們能夠過 XPC 來互相通信,每個(gè) App 都不知道其他 App 的存在,但它們都知道相同的一個(gè)(共享)launch agent。

這兩個(gè) App 可以先連接到 launch agent。App A 創(chuàng)建一個(gè)被稱為 匿名監(jiān)聽者 (anonymous listener) 的對象,并通過 XPC 發(fā)送一個(gè)端點(diǎn) (endpoint),并由匿名監(jiān)聽者創(chuàng)建的對象給 launch agent。App B 可以通過 XPC 在同樣的 launch agent 中拿到這個(gè) endpoint。這時(shí),App B 就可以直接連接到這個(gè)匿名監(jiān)聽者,即 App A。

在 App A 創(chuàng)建一個(gè) anonymous listener:

let listener = NSXPCListener.anonymousListener()

類似于 XPC service 創(chuàng)建普通的 listener。然后從這個(gè) listener 創(chuàng)建一個(gè) endpoint:

let endpoint = listener.endpoint

這個(gè) endpoint 可以通過 XPC 來傳遞(實(shí)現(xiàn)了 NSSecureCoding 協(xié)議 )。一旦 App B 獲取到這個(gè) endpoint,它可以創(chuàng)建到 App A 的 listener 的一個(gè) connection:

let connection = NSXPCConnection(listenerEndpoint: endpoint)

Privileged Mach Service

最后一個(gè)選擇是使用:

NSXPCConnection(machServiceName: "io.objc.mymachservice", options: .Privileged)

這種方式和 launch agent 非常類似,不同的是創(chuàng)建了到 launch daemon 的 connection。launch agent 進(jìn)程是 per user 的,它們以用戶的身份運(yùn)行在用戶的登錄會(huì)話 (login session) 中。守護(hù)進(jìn)程 (Daemon) 則是 per machine 的,即使當(dāng)前多個(gè)用戶登錄,一個(gè) XPC daemon 也只有一個(gè)實(shí)例運(yùn)行。

如果要運(yùn)行 daemon 的話,有很多安全相關(guān)的問題需要考慮。雖然以 root 權(quán)限運(yùn)行 daemon 是可能的,但是最好是不要這么這么做。我們可能更希望它以一些獨(dú)特的用戶身份來運(yùn)行。具體可以參考 TN2083 - Designing Secure Helpers and Daemons。大多數(shù)情況,我們并不需要 root 權(quán)限。

文件訪問 (File Access)

假設(shè)我們要?jiǎng)?chuàng)建一個(gè) HTTP 文件下載的 service。我們需要允許 service 能發(fā)起對外的網(wǎng)絡(luò)連接請求。不太明顯的是,我們可以讓 service 下載寫入文件而不需要訪問任何文件。

它是如何做到的呢,首先我們在 App 中創(chuàng)建這個(gè)將被下載的文件,然后給這個(gè)文件創(chuàng)建一個(gè)文件句柄 (file handle):

let fileURL = NSURL.fileURLWithPath("/some/path/we/want/to/download/to")
if NSData().writeToURL(fileURL, options:0, error:&error) {
    let maybeHandle = NSFileHandle.fileHandleForWritingToURL(url:fileURL, error:&error)
    if let handle = maybeHandle {
        self.startDownload(fromURL:someURL, toFileHandle: handle) {
            self.downloadComplete(url: someURL)
        }
    }
}

func startDownload(fromURL: NSURL, toFileHandle: NSFileHandlehandle, completionHandler: (NSURL)->Void) -> Void

然后將這個(gè)文件句柄傳給 remote object proxy,實(shí)際上就是通過 XPC connection 傳給了 service,service 通過這個(gè)文件句柄寫入內(nèi)容,就可以保存到實(shí)際的文件中了。

同樣,我們可以在一個(gè)進(jìn)程中打開用于讀取數(shù)據(jù)的 NSFileHandle 對象,然后傳給另外一個(gè)進(jìn)程,這樣就可以做到那個(gè)進(jìn)程不需要直接訪問文件也能讀取其內(nèi)容了。

移動(dòng)數(shù)據(jù) (Moving Data)

雖然 XPC 非常高效,但是進(jìn)程間消息傳遞并不是免費(fèi)的。如果你需要通過 XPC 傳遞大量的二進(jìn)制數(shù)據(jù),你可以使用這些技巧。

正常情況下使用的 NSData 對象會(huì)在傳遞到另一端會(huì)被復(fù)制一份。對于較大的二進(jìn)制數(shù)據(jù),更有效的方法是使用 內(nèi)存映射 (memory-mapped) 數(shù)據(jù)。WWDC 2013 session 702 的slides 從 57 頁開始介紹了如何發(fā)送大量數(shù)據(jù)

XPC 有個(gè)技巧,能夠保證數(shù)據(jù)在進(jìn)程間傳遞不會(huì)被復(fù)制。訣竅就是利用 dispatch_data_tNSData 是 toll-free bridged 的。創(chuàng)建內(nèi)存映射的 dispatch_data_t 實(shí)例與之配合,就可以高效的通過 XPC 來傳遞了??瓷先ナ沁@樣:

let size: UInt = 8000
let buffer = mmap(nil, size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0)
let ddata = dispatch_data_create(buffer, size, DISPATCH_TARGET_QUEUE_DEFAULT, _dispatch_data_destructor_munmap)
let data = ddata as NSData

調(diào)試 (Debugging)

Xcode 支持通過 XPC 方式的進(jìn)程間通信的調(diào)試。如果你在內(nèi)嵌在 App 的私有 XPC service 里設(shè)置一個(gè)斷點(diǎn),調(diào)試器將以你期望的方式在斷點(diǎn)處停下來。

請務(wù)必看看 Activity Tracing。這組 API 定義在在頭文件 os/activity.h 中,提供了一種能夠跨越上下文執(zhí)行和進(jìn)程邊界的傳遞方式,來查看到底是什么引起了所需要執(zhí)行的行為。WWDC 2014 session 714, Fix Bugs Faster Using Activity Tracing,對此做了很好的介紹。

常見錯(cuò)誤 (Common Mistakes)

一個(gè)最常見的錯(cuò)誤是沒有調(diào)用 connection 或者 listener 的 resume 方法。記得它們創(chuàng)建后都是被掛起狀態(tài)。

如果 connection 無效,很大的可能是因?yàn)榕渲缅e(cuò)誤導(dǎo)致的。請檢查 bundle id 是不是和 service 名字相匹配,代碼中是否指定了正確的 service 名字。

調(diào)試守護(hù)進(jìn)程 (Debugging Daemons)

調(diào)試 daemon 會(huì)稍微復(fù)雜一些,但它仍然可以很好的工作。daemon會(huì)被 launchd 進(jìn)程啟動(dòng)。所以需要分兩部設(shè)置:在開發(fā)過程中,修改我們 daemon 的 launchd.plist,設(shè)置 WaitForDebugger 為true。然后在 Xcode 中,修改 daemon 的 scheme,在 scheme editor -> Run -> Info 頁下可以修改 Launch 方式,從 “Automatically” 改到 “Wait for executable to be launched.”

現(xiàn)在通過 Xcode 運(yùn)行 daemon,daemon 不會(huì)被啟動(dòng),但是調(diào)試器會(huì)一直等著直到它啟動(dòng)為止。一旦 launchd 啟動(dòng)了 daemon,調(diào)試器會(huì)自動(dòng)連接上,我們就可以開始干活了。

Connection 的安全屬性

每個(gè) NSXPCConnection 具有這些屬性

var auditSessionIdentifier: au_asid_t { get }
var processIdentifier: pid_t { get }
var effectiveUserIdentifier: uid_t { get }
var effectiveGroupIdentifier: gid_t { get }

來描述這個(gè) connection。在 listener 端,如在 agent 或者 daemon 中,可以利用這些屬性來查看誰在嘗試進(jìn)行連接,可以基于這些屬性來決定是否允許這些連接。對于在 App bundle 里的私有 XPC service,上面的屬性完全可以無視,因?yàn)橹挥挟?dāng)前 App 可以查找到這個(gè) service。

xpc_connection_create(3) 的 man page 中有一章 “Credentials”,介紹了一些使用這些 API 缺點(diǎn),在使用時(shí)需要多加小心。

QoS 和 Boosts

在 OS X 10.10 中,Apple 提出了 Quality of Service (QoS) 概念。可以用來輔助調(diào)解如給 UI 較高優(yōu)先級(jí),并降低后臺(tái)行為的優(yōu)先級(jí)。當(dāng) QoS 遇到 XPC service,事情就變得有趣了 - 想想 XPC service 一般是完成什么樣的工作?

QoS 會(huì)跨進(jìn)程傳送 (propagates),在大多數(shù)情況下我們都不需要擔(dān)心。當(dāng) UI 線程發(fā)起一個(gè) XPC 調(diào)用時(shí),service 會(huì)以 boosted QoS 來運(yùn)行;但是如果 App 中的后臺(tái)線程發(fā)起 XPC 調(diào)用,這也會(huì)影響到 service 的 QoS,它會(huì)以較低的 QoS 來運(yùn)行。

WWDC 2014 session 716, Power, Performance and Diagnostics ,介紹了很多關(guān)于 QoS 的內(nèi)容。其中它就提到了如何使用 DISPATCH_BLOCK_DETACHED 來分離當(dāng)前的QoS,即如何防止 QoS propagates。

所以當(dāng) XPC service 因?yàn)槟承┱埱蟮母弊饔枚_始一些不相關(guān)的工作時(shí),必須確保它從 QoS 中分離

低階API (Lower-Level API)

NSXPCConnection 所有的 API 都是建立 C API 之上,可以在 xpc(3) man page 和子頁面中找到它的文檔。

我們可以使用 C API 來為 App 創(chuàng)建 XPC service,只要兩端都使用 C API 就好。

在概念上 C API 和 Foundation 的 API 很相似(譯者注:實(shí)際上是 C API 在 10.7 中被率先引入),稍微令人困惑的一點(diǎn)是,C API 中一個(gè) connection 可以同時(shí)做為一個(gè)接受傳入連接請求的 listener ,或者是到另一個(gè)進(jìn)程的 connection。

事件流 (Event Streams)

目前只有 C API 提供的一個(gè)特性是,支持對于 IOKit events,BSD notifications,或者 Core Foundation 的distributed notifications 的 launch-on-demand(按需啟動(dòng))。這些在事件或者通知在 launch agent/daemons 也是可以使用的。

xpc_events(3) man page 中列出了這些事件流。通過 C API,可以相對簡單的實(shí)現(xiàn)一個(gè)當(dāng)特定的硬件連接后按需啟動(dòng)的一個(gè)后臺(tái)進(jìn)程 (launch agent)。