原文地址 翻译:DeveloperLx
你的Mac是由一个成熟版本的UNIX作为操作系统的,这意味着它含有大量预装的命令行工具和脚本语言。Swift、Perl、Python、Bash、Ruby以及任何你可以安装的。使用这个来创建awesome的app,难道不是很棒么?
NSTask
让你可以在你的机器上执行另一个程序,作为一个子进程,并在你的主程序运行的时候,监视它的执行状态。例如,你可以运行
ls
命令来展示一个目录的列表 - 就是从你的app中!
一个
NSTask
的很好的类比是父母和孩子的关系。一个父母可以创建一个孩子,并告诉它去做确定的事情,(理论上)孩子必须服从。
NSTask
表现为类似的方式;你启动一个“孩子”程序,给它指示,并告诉它在哪里报告任何的输出或错误。但会更好 - 它会比你一般的学步小孩更顺从 :]
NSTask
的一个很棒的用途,是用来提供命令行程序的前端GUI。命令行的程序是很有用的,但它们要求你准确地记住。它们在你机器上的什么地方,怎么来调用它们,以及你可以为它们提供什么选项或参数。在前端添加一个GUI,可以提供给用户大量的控制能力 - 无需成为一个命令行的专家(gurn)!
本教程包含一个
NSTask
示例,它会展示给你如何执行一个简单的,带有参数的命名行程序,并在它允许时,展示它的标准输出到一个text field中。最后,你就可以在你的自己的app中使用NSTask了!
注意: 这个教程假定你对Mac OS X开发和终端已有一些基本的熟悉度。如果你对于Mac编程是纯小白,请查看我们的 初学者Mac OS X开发教程系列 。
为了把注意力直接集中在NSTask上,我已经为你创建了一个启动项目,它包含了基本的用户界面。 下载这个项目 ,并在Xcode中打开,运行项目。
这个启动的app有一个window,就像下面这样:
这个window有一个标题“TasksProject”。它包含一个简单的GUI,它将通过调用shell脚本,来让你构建一个iOS项目,创建一个
ipa
并观察正在发生什么。
NSTask
的示例,将通过
NSTask
在后台运行一些命令程序,来build并打包一个iOS app到
ipa
文件。大多数基本的UI功能都已到位 - 你的工作的重点都在
NSTask
上。
注意:
建议你要有一个苹果iOS开发者的账号,因为你需要合适的证书和provisioning profile来创建一个
ipa
文件,它可以安装到你的一个设备上。如果你没有也不要担心,你同样可以在没有这个账号的情况下,follow整个教程。
现在你将在嵌入的,标题为“Tasks View Controller”的View Controller中工作。在这个window中的第一部分,会请求用户选择一个Xcode项目的目录。为了节省时间,你将硬编码为一个你自己的Xcode项目目录,而不是在每次测试运行你的app时,手动选择一个目录。
要这么做,回到Xcode并打开 TasksViewController.swift 。看一下注释“Window Outlets“下的property和方法:
//View Controller Outlets @IBOutlet var outputText:NSTextView! @IBOutlet var spinner:NSProgressIndicator! @IBOutlet var projectPath:NSPathControl! @IBOutlet var repoPath:NSPathControl! @IBOutlet var buildButton:NSButton! @IBOutlet var targetName:NSTextField!
所有的这些property都对应于
Main.storyboard
中的
Tasks View Controller Scene
。注意
projectPath
property — 这是你想要改变的那一个。
打开
Main.storyboard
并单击
Project Location
这项。你会发现在对象的层级上位于第四层;它是
Stack View
的一个“孩子”。在
Attributes Inspector
中,
Path Control
下,找到
Path
元素:
设置
Path
为一个你的机器上包含iOS项目的目录。确保你使用的是项目的
parent
目录,而不是
.xcodeproj
文件它本身。
注意:
如果在你的你机器上没有任何的iOS项目,
就在这里下载一个简单的项目
,并将其解压到你机器上。然后使用前面的说明来设置你应用中的
Path
property。例如,如果你把这个包解压到了桌面上,你就应该设置
Path
为
。
/Users/YOUR_USERNAME_HERE/Desktop/SuperDuperApp
既然你在app中,有了一个默认的源项目路径来协助测试,你也需要使用一个默认的目的地路径。打开 Main.storyboard 并单击 Build Repository 项。
在Attributes Inspector,在 Path Control 下找到 Path 项:
将
Path
设置成一个容易在你的机器上被找到的目录,例如Desktop。它会是这个应用所创建的
.ipa
文件将要放置的地方。
这两个
Tasks View Controller Scene
中额外的地方,你需要了解:
Target Name
和一个关联的text field。
-
Target Name
为你想要build的
Target
的名称。 -
在Target Name下展示的文本域,将在你的项目运行时,展示
NSTask
对象的输出。
不知道你iOS项目target的名称?要找到它,在Xcode的project navigator中选择你的项目,并在 Info tab下查看 TARGETS 。下面的屏幕截图展示了,在哪里可以找到一个叫做“SuperDuperApp”的简单项目。
记住target的名称 - 你将在后面当app运行时输入它。
让我们来填充(flesh out)当按下“Build”按钮时,将要运行的代码。
打开
TasksViewController.swift
并添加下列的代码到
startTask(\_:)
中:
//1. outputText.string = "" if let projectURL = projectPath.url, let repositoryURL = repoPath.url { //2. let projectLocation = projectURL.path let finalLocation = repositoryURL.path //3. let projectName = projectURL.lastPathComponent let xcodeProjectFile = projectLocation + "/\(projectName).xcodeproj" //4. let buildLocation = projectLocation + "/build" //5. var arguments:[String] = [] arguments.append(xcodeProjectFile) arguments.append(targetName.stringValue) arguments.append(buildLocation) arguments.append(projectName) arguments.append(finalLocation) //6. buildButton.isEnabled = false spinner.startAnimation(self) }
以下是对上述代码一步一步的解释:
-
outputText
是window中的大文本框;它会包含所有来自你将要运行的脚本的输出。如果你运行这个脚本多次,你会想在每次运行前清空它,因此第一行会设置string
property(text框的内容)为一个空字符串。 -
projectURL
和repositoryURL
都是NSURL
的对象,为了将它们作为参数传你给你的NSTask
,代码获取了这些对象的字符串的表示。 -
一般地,目录的名称和项目文件的名称是相同的。在包含于
projectURL
的项目目录,获取lastPathComponent
property,并添加一个“.xcodeproj”的扩展,来获取项目文件的路径。 -
定义当创建
ipa
文件作为build
时,产生的中间的build文件,所将要保存在的子目录。 -
将参数保存到一个数组中。这个数组将传递给
NSTask
,用来在运行命令行工具时,构建.ipa
文件。 - 禁用“Build”按钮,并启动一个spinner动画。
为什么要禁用“Build”按钮?
NSTask
将在每次按下按钮时运行,因此app将在执行
NSTask
的时间内非常忙碌,用户会无耐心地多次点击它 - 每次都会产生一个新的build过程。这个动作避免了用户在app非常忙碌的时候创建按钮的点击事件。
运行项目,然后点击 Build 按钮。你应当看到“Build”按钮被禁用,并启动了spinner的动画:
你的app
看起来
非常得忙碌,但你知道现在实际上,它是没有做任何事的。是时候去添加
NSTask
的魔法了。
打开 TasksViewController.swift 并添加下面的方法:
func runScript(_ arguments:[String]) { //1. isRunning = true //2. let taskQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.background) //3. taskQueue.async { //TESTING CODE //4. Thread.sleep(forTimeInterval: 2.0) //5. DispatchQueue.main.async(execute: { self.buildButton.isEnabled = true self.spinner.stopAnimation(self) self.isRunning = false }) //TESTING CODE } }
如果你一步步地看方法,你会看到代码做了下列的事情:
-
设置
isRunning
为true
。这会允许Stop
按钮的使用,因为它通过 Cocoa Bindings 被绑定到了TasksViewController
的isRunning
property。你想要让这发生在主线程。 -
创建一个
DispatchQueue
来在后台的线程运行繁重的任务。 -
在
DispatchQueue
中使用async
。应用将会在主线程继续处理例如按钮点击的时间,但NSTask
将运行在后台线程直到它完成为止。 - 这是一行临时的代码,让当前的线程睡眠2秒钟,来模拟一个长时间运行的task。
-
当完工时,重新启用
Build
按钮,停止spinner动画,并设置isRunning
为false
,也就禁用了“Stop”按钮。这需要在主线程完成,因为你是在操纵UI元素。
既然你已有了一个,可以运行你的task在单独的线程中的方法,你需要在你的app 中的某个地方调用它。
仍然在
TasksViewController.swift
中,添加下列的代码到
startTask
的末尾,就在
spinner.startAnimation(self)
的后面:
runScript(arguments)
上面用你在
startTask
中构建的参数的数组,调用了
runScript
方法。
运行项目,点击
Build
按钮。你会注意到,
Build
按钮变成了不可用的状态,
Stop
按钮变成了可用的状态,spinner将开始动画:
当spinner的动画正在进行时,你仍然可以和应用进行交互。自己试一下 - 例如,你应当可以在spinner活动的时候,在 Target Name 框中进行输入。
过了两秒之后,spinner将会消失, Stop 按钮将会被禁用, Build 按钮则会变为可用。
注意:
如果你在应用完成睡眠之前,遇到了问题,请增加调用
sleep(forTimeInterval:)
时所传的秒数。
既然你已经解决了UI响应的问题,你就可以最终实现你对NSTask的调用了。
注意:
Swift使用
Process
这个名字来调用
NSTask
,因为
Foundation
的framework在Swift 3中去掉了
NS
的前缀。然而,你在本教程中读到的会是NSTask,因为如果你想学到更多的话,它会成为最有用的搜索术语。
在
TasksViewController.swift
中,找到
runScript
这行,被括号括起来的
//TESTING CODE
注释这里。用下面的代码替换
taskQueue.async
块中的全部内容:
//1. guard let path = Bundle.main.path(forResource: "BuildScript",ofType:"command") else { print("Unable to locate BuildScript.command") return } //2. self.buildTask = Process() self.buildTask.launchPath = path self.buildTask.arguments = arguments //3. self.buildTask.terminationHandler = { task in DispatchQueue.main.async(execute: { self.buildButton.isEnabled = true self.spinner.stopAnimation(self) self.isRunning = false }) } //TODO - Output Handling //4. self.buildTask.launch() //5. self.buildTask.waitUntilExit()
上面的代码:
-
获取了
BuildScript.command
脚本的路径,它包含在你的应用的bundle中。这个脚本现在还不存在 - 你将在稍后添加它。 -
创建一个新的
Process
对象,并把它赋给TasksViewController
的buildTask
property。launchPath
property是你想要运行的可执行文件的路径。将BuildScript.command
的path
赋值给Process
的launchPath
,然后将传递给runScript:
的参数赋给Process
的arguments
property。Process
将把参数传递给可执行文件,就像你在终端中输入它们一样。 -
Process
有一个terminationHandler
的property,它包含一个会在任务完成时被执行的block。它会更新UI来反映完成的状态,就像你之前做得一样。 -
为了运行task并执行脚本,调用
Process
对象的launch
方法。这里同样有终止,打断,挂起或继续一个Process
的方法。 -
调用
waitUntilExit
,它告诉Process
对象block住当前线程的其它活动,直到这个task完成为止。记住,这个代码运行在后台的线程。你的UI,则运行在主线程,将始终响用户的输入。
运行你的项目;你不会看到任何 看起来 不同的事,但点击 Build 按钮并查看输出控制台。你会看到像下面这样的错误:
Unable to locate BuildScript.command
这是你刚刚添加到代码开头的guard语句中的日志。由于你至今尚未添加任何的脚本,于是就触发了
guard
。
看起来到了写脚本的时候了!:]
在Xcode中,选择 File\New\File… ,并在 OS X 下选择 Other 的category。选择 Shell Script 并点击 Next :
将文件命名为 BuildScript.command 。在你点击 Create 之前,确保 Targets 下的 TasksProject 已被选中,就像下面这样:
打开 BuildScript.command ,并添加下列的命令到文件末尾:
echo "*********************************" echo "Build Started" echo "*********************************" echo "*********************************" echo "Beginning Build Process" echo "*********************************" xcodebuild -project "${1}" -target "${2}" -sdk iphoneos -verbose CONFIGURATION_BUILD_DIR="${3}" echo "*********************************" echo "Creating IPA" echo "*********************************" /usr/bin/xcrun -verbose -sdk iphoneos PackageApplication -v "${3}/${4}.app" -o "${5}/app.ipa"
这就是你的
NSTask
将会调用的全部的build脚本。
你所看到的贯穿你的脚本的
echo
命令,将把任何传给它的文本发送到
标准输出
中,你会捕获它作为你的
NSTask
对象的一部分的返回值,并展示到你的
outputText
框中。
echo
语句可以方便地让你知道你的脚本正在做什么,因为很多命令在命令行运行时,并不会提供很多的输出。
你会注意到,除了所有的
echo
命令外,还有两个命令:
xcodebuild
和
xcrun
。
xcodebuild
build你的应用,创建一个
.app
文件,并将其放到子目录
/build
中。回忆一下,你已在
startTask
中创建过一个引用这个目录的参数,因为你需要一个在build和打包的时候,存放中间build文件的地方。
xcrun
在命令行中运行开发者工具。这里你使用它来调用
PackageApplication
,它会把
.app
文件打包成一个
.ipa
文件。通过设置
verbose
标记,你将在标准输出中获取大量的细节信息,它们会展示在你的
outputText
框中。
在
xcodebuild
和
xcrun
命令中,你会注意到所有的参数被写作了
“${1}”
而不是
$1
。这是因为你的项目的路径可能包含空格。为了处理这种情况,得到正确的路径,你必须把你的文件路径用双引号引起来。通过双引号和花括号,脚本就会恰当地解析全路径,包含空格。
这个脚本的其它地方,就是Xcode自动为你添加的那些呢?它们是什么意思?、
脚本的第一行如下所示:
#!/bin/sh
尽管它看起来像一条评论,因为那个前缀
#
。实际上这行是告诉操作系统,当执行脚本剩余的部分时,使用一个指定的
shell
。它被称作
shebang
。这个shell就是运行你的命令的解释器,无论是在脚本文件或是从命令行的界面上。
有大量不同的shell可以使用,但是它们中的大多数依附在变化的Bourne shell或C shell语法上。你的脚本指明了应当使用 sh ,它是包含在OS X中的shell之一。
如果你想要指定另一个shell来执行你的脚本,就像
bash
,你就得将第一行改变为恰当的shell的可执行文件的全路径,就像:
#!/bin/bash
在脚本中,任何你传递的参数,都是通过一个
$
和一个数字来访问的。
$0
代表你调用的程序的名称,后面跟着的参数依次通过
$1
,
$2
等等来进行引用。
注意: Shell脚本存在的时间,就如同计算机一样地长,因此你可以在网上找到更多关于它的信息。例如一个简单(且相关)的开启的地方,可以访问苹果的 Shell脚本入门 。
现在你已经准备好从
NSTask
去调用你的脚本了,对么?
不完全是。此刻,你的脚本文件还没有 执行 的权限。你可以读和写文件,但你并不能执行它。
这意味着如果你现在运行app,当你点击Build按钮时,app就会crash。如果你想,就可以尝试一下。这在开发时并没什么大不了,你会在你的Xcode控制台中看到“launch path not accessible”的异常。
为了让它可执行,在终端中前往你的项目目录。终端默认位于你的家目录中,因此如果项目是在你的
Documents
目录下,你应该输入命令:
cd Documents/TasksProject
如果你的项目在除“Documents/TasksProject”目录的另一个目录下,你需要输入正确的项目目录的路径。为了快速地完成,从Finder中点击并拖拽项目目录到终端中。你项目的路径,将魔术般地出现在终端的窗口上!现在简单地将你的光标移动到路径的前边,输入
cd
,后跟一个空格,并点击enter。
为了确保你位于正确的地方,输入下列的命令到终端上:
ls
在生成的文件列表中,检查
BuildScript.command
。如果你并非位于正确的位置,检查你是否在终端中输入了正确的项目目录。
当确保你在正确的目录下之后,输入下列的命令到终端中:
chmod +x BuildScript.command
chmod
命令会改变脚本的权限,使它可以被你的
NSTask
对象执行。如果你尝试,在没有这些恰当权限的条件下,运行你的应用,你将看到“Launch path not accessible”的错误,就像之前一样。你只需要为每个你新添加到项目中的脚本执行一次该操作。
注意: 如果你是为自己开发,或在Mac App Store(MAS)外运送你的app,像这样地使用脚本是很简单的。然而在为MAS开发时,应用到你的app的沙盒规则将由你的脚本继承,你需要使用更复杂的技术来使用命令行程序。这些技术超出了本教程的范围。关于更多的详情,请参考末尾的链接。
清理并运行你的项目;清理的操作是必须的,因为Xcode不能获得文件权限的变化,因此也就不能将它拷贝到build仓库中。当应用打开时,输入你的测试app的target的名称,确保“Project Location”和“Build Repository”的值被正确地设置,最后点击 Build 。
当spinner消失的时候,你应当在期望的位置上得到了一个新的
.ipa
文件。成功!
OK,你已经相当地熟练向命令行程序传递参数了,但对于处理命令行程序的输出呢?
为了实际地查看这个,输入下列命令到终端中,并点击Enter:
date
你应当看到生成的信息,就像这样:
Fri 19 Feb 2016 17:48:15 GMT
date
告诉你当前的日期和时间。尽管不是很明显,它的结果是被发送到了一个名叫
standard output
的频道中。
进程通常有三个用来输入和输出的频道:
- standard input ,从调用者这里接受输入;
- standard output ,从进程中将输出发回到调用者这里;
- standard error ,从进程中将错误发回到调用者这里;
专业提示:你会看到这些通常被简写为 stdin , stdout ,和 stderr 。
还有一个
pipe
,可以让你重定向输出到另一个进程的输入上。你将创建一个pipe来让你的应用看到
NSTask
运行时,进程的标准输出。
为了看到pipe在运转,确保你的电脑已打开了音量,然后在终端中输入下列命令:
date | say
点击enter,你会听到你的电脑告诉了你时间。
注意:
你的键盘上的pipe字符“|”,通常位于
\
键这里,就在
enter/return
键的上面。
刚刚发生了:你创建了一个pipe,它将
date
的标准输出重定向到了
say
的标准输入中。你也可以向命令提供选项来和pipe交互,因此如果你想听到澳大利亚口音的日期,输入下面的命令来代替:
date | say -v karen
是的日期,宝贝!
你可以使用pipe构建一些相当长的命令链,重定向一条命令的标准输出到另一条命令的标准输入中。一旦你适应了stdin,stdout,以及管道重定向,你就可以在命令行中,使用pipe这样的工具,做一些相当复杂的事情。
到了在你的app中实现管道的时候了。
注意:
NSPipe
是一个
Foundation
中的类,它在Swift 3中被称为
Pipe
。大多数文档将引用
NSPipe
。
打开
TasksViewController.swift
并在
runScript(\_:)
中将评论
// TODO: Output Handling
替换为下面的代码:
self.captureStandardOutputAndRouteToTextView(self.buildTask)
现在,将这个方法添加到 TasksViewController.swift 中:
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) ?? "" //5. DispatchQueue.main.async(execute: { let previousOutput = self.outputText.string ?? "" let nextOutput = previousOutput + "\n" + outputString self.outputText.string = nextOutput let range = NSRange(location:nextOutput.characters.count,length:0) self.outputText.scrollRangeToVisible(range) }) //6. self.outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() } }
这个函数从外部的进程中采集了输出,并将它添加到了GUI的
outputText
框中。它就像下面这样地工作:
-
创建了一个
Pipe
并将它连接到buildTask
的标准输出上。Pipe
代表了你在终端
中创建的pipe的等效的类。任何写入到buildTask
的stdout
的内容,将被提供到Pipe
对象上。 -
Pipe
有两个property:fileHandleForReading
和fileHandleForWriting
,它们都是NSFileHandle
对象。NSFileHandle
的相关知识已经超出了这个教程的范围,但fileHandleForReading
是用来在pipe中读取数据的。你可以调用它的waitForDataInBackgroundAndNotify
方法,来使用一个单独的后台线程来获取有用的数据。 -
任何当数据可用的时候,
waitForDataInBackgroundAndNotify
就会通过调用你注册在NSNotificationCenter
中的代码块,来通知你处理NSFileHandleDataAvailableNotification
。 -
在你的通知处理中,获取
NSData
对象形式的数据,并将它转变为一个字符串。 -
在主线程中,添加从上一步获取的字符串到
outputText
文本的末尾,并将文本区域滚动到 用户可以看到刚刚给出的输出的地方。这个 must 必须 在主线程执行,就想所有的UI和用户交互一样。 - 最后,再次调用在后台等待数据的方法。这就创造了一个循环,它将持续等待有用的数据,处理数据,再次等待有用的数据,等等。
再次运行你的应用;确保
Project Location
和
Build Repository
是正确的,输入你的target的名称,并单击
Build
。
你应当在
outputText
中看到来自build进程的输出:
如果你开始了一个build,然后改变主意了,会发生什么?如果它花费了太长的时间,或一些其它的事看起来出错了,或只是在这儿挂起了,导致无法继续?这些都是你想要停下你的后台task的时刻。幸运的是,这些都很容易办到。
在
TasksViewController.swift
中,添加下列的代码到
stopTask(\_:)
中
if isRunning { buildTask.terminate() }
上面的代码检查了是否
NSTask
正在运行,如果是的话,调用
terminate
方法。这将停止
NSTask
在它的执行中。
运行你的app,确保全部的field配置正确,并单击
Build
按钮。然后在build完成前单击
Stop
按钮。你将看到每个事都停下了,且在你的输出目录下,没有创建新的
.ipa
文件。
这里是从上面的教程中完成的 NSTask示例项目 。
恭喜,你已经开始变成
NSTask
“忍者”(ninja)了!
在一篇简短的教程中,你学到了:
-
怎样创建带有参数和输出pipe的
NSTasks
;以及 - 怎样创建一个shell脚本,并从你的app中调用它!
要了解关于
NSTask
的更多信息,请访问苹果的官方文档
NSTask类参考文档
。
为了了解在沙盒app中,使用命令行程序的相关内容,请参考 守护进程和服务的编程向导 和 XPC服务API参考文档 。
这个教程仅处理了NSTask的stdout,但你也可以同样地使用 stdin和stderr !为了实践你的新技能,尝试使用它们。
我希望你可以喜欢这个
NSTask
教程,你会发现它在你将来的Mac OS X app中是非常有用的。如果你有任何的问题或评论,请参与我们下面的讨论!