利用NSTask,我們可以在應用中調用外部程序或腳本并獲得它的<執行狀態和結果
NSTask最為常用的一個場景是為命令行操作提供圖形化的界面
1. NSTask 與NSThread的不同
- NSTask會創建隔離的可運行實體,但執行權限受App沙盒限制
- NSTask不與創建的它的進程共享內存空間
- NSTask實例在運行時,環境條件不能改變,需要在運行之前進行配置
- 一個NSTask實例只能運行一次,再次調用會報錯
- NSTask默認是異步執行,如果有同步需求,可調用waitUntilExit()方法
2. NSTask 在Swift 中與Objective-C中的不同
- Objective-C中, 是NSTask類
- Swift 中, 是Process類
3. NSTask 使用
我們通過創建一個簡單的克隆Git倉庫的工程來熟悉NSTask的使用
如果你比較捉急,可以提前從這里下載NSTaskDemo
-
3.1 創建工程(本示例使用Swift,并默認你已經熟悉基本的OSX UI開發),并設置好UI界面,效果如下:
UI界面 -
3.2 打開ViewController.swift,設置控件的連線屬性以及方法:
設置IBOutlet 和IBAction - 3.3 實現保存路徑選擇的方法selectPath
@IBAction func selectPath(_ sender: NSButton) {
// 1. 創建打開文檔面板對象
let openPanel = NSOpenPanel()
// 2. 設置確認按鈕文字
openPanel.prompt = "Select"
// 3. 設置禁止選擇文件
openPanel.canChooseFiles = true
// 4. 設置可以選擇目錄
openPanel.canChooseDirectories = true
// 5. 彈出面板框
openPanel.beginSheetModal(for: self.view.window!) { (result) in
// 6. 選擇確認按鈕
if result == NSModalResponseOK {
// 7. 獲取選擇的路徑
self.savePath.stringValue = (openPanel.directoryURL?.path)!
// 8. 保存用戶選擇路徑(為了獲取訪問權限)
UserDefaults.standard.setValue(openPanel.url?.path, forKey: kSelectedFilePath)
UserDefaults.standard.synchronize()
}
// 9. 恢復按鈕狀態
sender.state = NSOffState
}
}
- 3.4 使用NSTask 調用shell,執行git clone命令
@IBAction func startPull(_ sender: NSButton) {
guard let executePath = UserDefaults.standard.value(forKey: kSelectedFilePath) as? String else {
print("no selected path")
return
}
guard repoPath.stringValue != "" else {return}
if isLoadingRepo {return} // 如果正在執行,則返回
isLoadingRepo = true // 設置正在執行標記
task = Process() // 創建NSTask對象
// 設置task
task?.launchPath = "/bin/bash" // 執行路徑(這里是需要執行命令的絕對路徑)
// 設置執行的具體命令
task?.arguments = ["-c","cd \(executePath); git clone \(repoPath.stringValue)"]
task?.terminationHandler = { proce in // 執行結束的閉包(回調)
self.isLoadingRepo = false // 恢復執行標記
print("finished")
self.showFiles() // 顯示clone的倉庫文件列表
}
captureStandardOutputAndRouteToTextView(task!)
task?.launch() // 開啟執行
task?.waitUntilExit() // 阻塞直到執行完畢
}
// 顯示目錄文檔列表
fileprivate func showFiles() {
guard let executePath = UserDefaults.standard.value(forKey: kSelectedFilePath) as? String else {
print("no selected path")
return
}
let listTask = Process() // 創建NSTask對象
// 設置task
listTask.launchPath = "/bin/bash" // 執行路徑(這里是需要執行命令的絕對路徑)
// 設置執行的具體命令
listTask.arguments = ["-c","cd \(executePath + "/" + (repoPath.stringValue as NSString).lastPathComponent); ls "]
captureStandardOutputAndRouteToTextView(listTask)
listTask.launch() // 開啟執行
listTask.waitUntilExit()
}
- 3.5 使用NSPipe獲取NSTask 執行的結果信息
在Swift中,NSPipe 被改名為Pipe
extension ViewController{
fileprivate func captureStandardOutputAndRouteToTextView(_ task:Process) {
//1. 設置標準輸出管道
outputPipe = Pipe()
task.standardOutput = outputPipe
//2. 在后臺線程等待數據和通知
outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
//3. 接受到通知消息
NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading , queue: nil) { notification in
//4. 獲取管道數據 轉為字符串
let output = self.outputPipe.fileHandleForReading.availableData
let outputString = String(data: output, encoding: String.Encoding.utf8) ?? ""
if outputString != ""{
//5. 在主線程處理UI
DispatchQueue.main.async(execute: {
let previousOutput = self.showInfoTextView.string ?? ""
let nextOutput = previousOutput + "\n" + outputString
self.showInfoTextView.string = nextOutput
// 滾動到可視位置
let range = NSRange(location:nextOutput.characters.count,length:0)
self.showInfoTextView.scrollRangeToVisible(range)
})
}
//6. 繼續等待新數據和通知
self.outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}
}
}
4. NSTask 與 SandBox權限
在NSTaskDemo示例工程中,開啟了App 的沙盒權限,
- 開啟網絡訪問權限
-
開啟了用戶選擇文件的讀寫權限
沙盒權限
在osx 系統中 ,沙盒有個規則:在App運行期間通過NSOpenPanel用戶手動打開的任意位置的文件,把這個這個路徑保存下來,后面都是可以直接用這個路徑繼續訪問文件,但當App退出后再次運行,這個路徑默認是不可以訪問的
關于OSX的沙盒機制,推薦學習這篇文檔[Cocoa開發之沙盒機制及訪問Sandbox之外的文件
推薦文檔的補充說明: 永久訪問用戶授權的url,可以不必在.entitlements文件中填寫對應的key與value
(測試環境osx 10.12.5 ,Xcode 8.3.3)
5. 最終效果
6. 同步方式獲取NSTask的執行結果
func execCmd(cmd: String, arguments: [String]) -> String{
let task = Process() // 創建NSTask 實例
task.launchPath = cmd // 需要執行的命令
task.arguments = arguments // 命令參數
let output = Pipe() // 創建輸出實例
task.standardOutput = output // 設置輸出
task.launch() // 執行命令
task.waitUntilExit() // 等待退出
let data = output.fileHandleForReading.readDataToEndOfFile() // 獲取執行結果數據
return String(data: data, encoding: String.Encoding.utf8) ?? "" // 返回結果
}
7. 小結
NSTask為我們提供了可以在一個應用中,調用另一個應用<的可能.其中比較普遍的一個使用場景是我們可以在自己的App中,調用強大的Shell命令,或者執行自己寫的腳本來實現一些輔助功能
NSPipe用來輔助我們獲取NSTask的輸出結果,用來展示UI信息
8. 后語
關于NSTask的使用并不十分復雜,但如果想實現強大的需求,最好有一些必備的Shell編程知識,另外值得注意就是沙盒權限問題,文中的一下疑問或者意見,大家可以寫在評論區進行討論,最后希望大家周末愉快~