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

在沙盒中編寫腳本

Mac 應(yīng)用間的腳本在桌面生態(tài)系統(tǒng)中已經(jīng)存在很長時日了。它最早于 1993 年 10 月作為 System 7 的一部分,用來為像 QuarkXPress 這樣的出版相關(guān)應(yīng)用創(chuàng)建復(fù)雜的工作流,以方便使用。從那以后,很多應(yīng)用通過使用腳本字典來支持 AppleScript (Brent 的文章向你展示了如何實現(xiàn))。在這篇文章里,我會解釋如何使用腳本字典里的命令和對象來與其他的應(yīng)用進(jìn)行通訊。

但在我們開始之前,我們需要看看最近在 Mac 平臺發(fā)生的一些事情。在 2010 年底 Mac App Store 開張之后,Apple 宣布所有開發(fā)者所提交的應(yīng)用必須在 2011 年 11 月之前跑在沙盒里。這個最后期限被推遲了數(shù)次,最后他在 2012 年的六月一號成為了現(xiàn)實。

想讓 Mac 應(yīng)用運行在沙盒中實屬不易,相信這一點從不斷被推遲的最后期限中你就可以尋得蛛絲馬跡。與之相對,iOS 應(yīng)用一直就需要運行在沙盒里。一些資深的開發(fā)者會明白,一個安全的環(huán)境同時也意味著他們的應(yīng)用要做重大的改變。我從一個 Apple 的負(fù)責(zé)安全的工程師那里聽到過一句話:“我們正在試圖把打開的潘多拉盒子關(guān)上,這可不是一件容易事兒?!?/p>

其中一個主要的挑戰(zhàn)就是要處理那些使用了 AppleScript 的應(yīng)用。很多本來很簡單的功能突然就變得很困難了,有些事情甚至完全不可能完成。造成這么讓人沮喪的主要原因是應(yīng)用不再能任意地通過腳本來控制其他的應(yīng)用了。為了安全考慮,有非常多的理由可以證明允許這么做并不是什么好主意。但是從開發(fā)者或者顧客的視角來看的話,就是很多東西用不了了。

起初,Apple 通過在應(yīng)用的權(quán)限聲明里引入并許可'暫時例外'的方式來幫助過渡。這些例外可以允許應(yīng)用維持本來已經(jīng)應(yīng)該喪失了的功能。但是也正如名字所示,它們中的很多特例也正在消失,因為在最近版本的 OS X 中,一些作為替代手段的控制別的應(yīng)用的方法已經(jīng)可以使用了。

這個教程將向您展示現(xiàn)在使用 AppleScript 來控制別的應(yīng)用的最佳方式。我也會告訴您一些小技巧以幫助您和您的用戶用最小的努力就架設(shè)起 AppleScript。

第一步

你需要學(xué)習(xí)的第一件事是如何在你自己的應(yīng)用里跑 AppleScript。通常來說,最困難的部分是寫出 AppleScript 的代碼。來看看吧:

on chockify(inputString)
    set resultString to ""

    repeat with inputStringCharacter in inputString
        set asciiValue to (ASCII number inputStringCharacter)
        if (asciiValue > 96 and asciiValue < 123) then
            set resultString to resultString & (ASCII character (asciiValue - 32))
        else
            if ((asciiValue > 64 and asciiValue < 91) or (asciiValue = 32)) then
                set resultString to resultString & inputStringCharacter
            else
                if (asciiValue > 47 and asciiValue < 58) then
                    set numberStrings to {"ZERO", "ONE", "TWO", "THREE", "FOR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"}
                    set itemIndex to asciiValue - 47
                    set numberString to item itemIndex of numberStrings
                    set resultString to resultString & numberString & " "
                else
                    if (asciiValue = 33) then
                        set resultString to resultString & " DUH"
                    else
                        if (asciiValue = 63) then
                            set resultString to resultString & " IF YOU KNOW WHAT I MEAN"
                        end if
                    end if
                end if
            end if
        end if
    end repeat

    resultString
end chockify

在我看來,AppleScript 最大的優(yōu)點并不在于它的語法,也不是它在處理字符串上的能力,雖然這些都做得超級棒。

當(dāng)開發(fā)像這樣的腳本時,我通常會去查閱 AppleScript 腳本指南。好消息是與其他應(yīng)用進(jìn)行通訊的腳本一般來說都很短,也容易理解。AppleScript 可以被想做一種傳送的機(jī)制,而不是一種處理環(huán)境。上面展示的腳本就很典型。

一旦你寫好了腳本并且完成了測試,你就可以回到你的 Objective-C 的舒適的小窩了。你可能會寫下的第一行代碼會將你通過時光旅行帶回到 Carbon 年代:

#import <Carbon/Carbon.h> // for AppleScript definitions

放輕松,你不需要做一些比如將整個 Carbon 框架導(dǎo)入項目的瘋狂舉動。你所需要的只是 Carbon.h,因為它有關(guān)于所有的 AppleEvent 的定義。記住,這些代碼已經(jīng)在那兒超過 20 年了!

在有了定義之后,你就可以創(chuàng)建事件描述符 (event descriptor) 了。這是可以在你的腳本和應(yīng)用之間互相傳遞的一個數(shù)據(jù)塊。現(xiàn)在的話,你可以把它想象成一個封裝好的會去執(zhí)行某個事件的目標(biāo),一個將被調(diào)用的函數(shù),以及這個函數(shù)的參數(shù)。在這里我們?yōu)樯厦娴?"chockify" 創(chuàng)建了事件描述符,使用一個 NSString 作為參數(shù):

- (NSAppleEventDescriptor *)chockifyEventDescriptorWithString:(NSString *)inputString
{
    // parameter
    NSAppleEventDescriptor *parameter = [NSAppleEventDescriptor descriptorWithString:inputString];
    NSAppleEventDescriptor *parameters = [NSAppleEventDescriptor listDescriptor];
    [parameters insertDescriptor:parameter atIndex:1]; // you have to love a language with indices that start at 1 instead of 0

    // target
    ProcessSerialNumber psn = {0, kCurrentProcess};
    NSAppleEventDescriptor *target = [NSAppleEventDescriptor descriptorWithDescriptorType:typeProcessSerialNumber bytes:&psn length:sizeof(ProcessSerialNumber)];

    // function
    NSAppleEventDescriptor *function = [NSAppleEventDescriptor descriptorWithString:@"chockify"];

    // event
    NSAppleEventDescriptor *event = [NSAppleEventDescriptor appleEventWithEventClass:kASAppleScriptSuite eventID:kASSubroutineEvent targetDescriptor:target returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
    [event setParamDescriptor:function forKeyword:keyASSubroutineName];
    [event setParamDescriptor:parameters forKeyword:keyDirectObject];

    return event;
}

注意: 這些代碼放在了 GitHub 上。Automation.scpt 文件包含了 chockify 函數(shù)和本教程中用到的其他腳本。Objective-C 的代碼全都在 AppDelegate.m 中。

現(xiàn)在你有一個事件描述符了。它用來告訴 AppleScript 你想做的事情,你還得讓它有個什么去處吧。這意味著你需要從你的應(yīng)用包 (application bundle) 里加載 AppleScript。

NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Automation" withExtension:@"scpt"];
if (URL) {
    NSAppleScript *appleScript = [[NSAppleScript alloc] initWithContentsOfURL:URL error:NULL];

    NSAppleEventDescriptor *event = [self chockifyEventDescriptorWithString:[self.chockifyInputTextField stringValue]];
    NSDictionary *error = nil;
    NSAppleEventDescriptor *resultEventDescriptor = [appleScript executeAppleEvent:event error:&error];
    if (! resultEventDescriptor) {
        NSLog(@"%s AppleScript run error = %@", __PRETTY_FUNCTION__, error);
    }
    else {
        NSString *string = [self stringForResultEventDescriptor:resultEventDescriptor];
        [self updateChockifyTextFieldWithString:string];
    }
}

通過應(yīng)用包的一個 URL 可以創(chuàng)建 NSAppleScript 的實例。而反過來,腳本也要和上面創(chuàng)建的 chockify 事件描述符一起使用、如果一切正常的話,你會得到另一個事件描述符。如果出錯了,你會得到一個包含了描述錯誤信息的字典。雖說這個模式和很多其他 Foundation 類很相似,但是返回的錯誤并不是一個 NSError 的實例。

現(xiàn)在就剩從描述符中抽取出你想要的結(jié)果了:

- (NSString *)stringForResultEventDescriptor:(NSAppleEventDescriptor *)resultEventDescriptor
{
    NSString *result = nil;

    if (resultEventDescriptor) {
        if ([resultEventDescriptor descriptorType] != kAENullEvent) {
            if ([resultEventDescriptor descriptorType] == kTXNUnicodeTextData) {
                result = [resultEventDescriptor stringValue];
            }
        }
    }

    return result;
}

InputString 輸入可以被正確整形輸出,并且你現(xiàn)在也看到想在你的應(yīng)用里運行 AppleScripts 的方法了。

曾經(jīng)的方式

曾經(jīng)有一段時間你是可以將 AppleEvents 發(fā)送到任意應(yīng)用的,而不僅僅是當(dāng)前運行的應(yīng)用,就像我們上面的 chockify 里做的那樣。

比如你想知道 Safari 里現(xiàn)在最前面的窗口加載的 URL 地址是什么,你需要做的就是通過 tell application "Safari" 告訴 Safari 要做什么。

on safariURL()
    tell application "Safari" to return URL of front document
end safariURL

現(xiàn)在的話,可能得到的就只有 Debug Console 中的下面的輸出了:

AppleScript run error = {
    NSAppleScriptErrorAppName = Safari;
    NSAppleScriptErrorBriefMessage = "Application isn\U2019t running.";
    NSAppleScriptErrorMessage = "Safari got an error: Application isn\U2019t running.";
    NSAppleScriptErrorNumber = "-600";
    NSAppleScriptErrorRange = "NSRange: {0, 0}";
}

就算其實 Safari 是在運行的。買了個表..

沙盒限制

這是因為正嘗試在應(yīng)用的沙盒中運行腳本??紤]到在沙盒里,Safari 事實上確實沒有在運行。

問題在于沒有人授予你訪問 Safari 的權(quán)限。這其實和一個很大的安全漏洞相關(guān):一段腳本可以輕易地拿到瀏覽器當(dāng)前頁面上的內(nèi)容,甚至是在任意標(biāo)簽和窗口運行 JavaScript。想象一下如果這些頁面里有你的銀行賬號,或者包含你的信用卡信息什么的。好疼..

這也就是為什么從 Mac App Store 獲取的應(yīng)用的腳本不能隨便執(zhí)行的原因。

不過事情在最近的 OS X 版本中有所改善。在 10.8 山獅中, Apple 引入了一個新的抽象類 NSUserScriptTask。有三個具體的子類實現(xiàn)讓你分別可以運行 Unix shell 命令 (NSUserUnixTask),Automator 工作流 (NSUserAutomatorTask) 以及我們最喜愛的 AppleScript(NSUserAppleScriptTask)。教程的接下來的部分將會專注于最后一類,因為這也是最常用的類。

對于沙盒應(yīng)用,Apple 所提倡的是通過用戶的需要來驅(qū)動安全策略。實際操作上來說,這意味著用戶需要決定是否想要運行你的腳本。這些腳本可能是來自互聯(lián)網(wǎng),也可能是你的應(yīng)用的一部分,這并不關(guān)鍵。唯一相關(guān)的事情是你的用戶表示“好的,我想要運行這個腳本”。一旦得到了權(quán)限,腳本就可以以一種受限的方式與系統(tǒng)其他部分進(jìn)行交互了。NSUserScriptTask 類使這一切變得可能。

安裝腳本

那么,想要運行腳本的應(yīng)用要怎么向用戶請求許可呢?

機(jī)制超級簡單:你的應(yīng)用只能從用戶的賬戶里的一個特定的文件夾中運行腳本。而腳本想要進(jìn)入這個文件夾的唯一方式就是用用戶把它們復(fù)制到那里。本質(zhì)上來說,OS X 將這些腳本通過只讀的方式提供給你使用。

現(xiàn)在的挑戰(zhàn)是:這個特定的文件夾是 User > Library > Application Scripts 然后跟著是應(yīng)用的 bundle identifier。對于我們的 Scriptinator,文件夾的名字大概只有程序員會喜歡:com.iconfactory.Scriptinator。兩者對用戶都很不友好,特別是 Library 文件夾在 OS X 里默認(rèn)還是隱藏的。

解決這個問題的一個方法是實現(xiàn)一些代碼來為用戶打開這個隱藏文件夾,比如:

NSError *error;
NSURL *directoryURL = [[NSFileManager defaultManager] URLForDirectory:NSApplicationScriptsDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
[[NSWorkspace sharedWorkspace] openURL:directoryURL];

這對于用戶自己寫的腳本來說是個很好的解決方案。用戶可以通過你的應(yīng)用的某個控件打開這個文件夾,然后進(jìn)行編輯。

但是有時候你想要幫助最終用戶將你已經(jīng)寫好的腳本安裝到這個目錄里。很自然的,作為程序員,你的編程水平應(yīng)該比你的用戶的平均水平要強(qiáng)吧,你也更明白應(yīng)該如何寫代碼讓你的應(yīng)用與用戶的其他應(yīng)用更好地工作在一起。很自然地,存放這些你自己的腳本的理想的地方是應(yīng)用包里,但是要怎么樣才能把這些腳本放到用戶的腳本文件夾離去呢?

解決的方法是獲取對這個文件夾的寫入權(quán)限。在 Xcode 里,你需要更新你的應(yīng)用的 Capabilities,讓其包括 "User Selected File to Read/Write"。你可以在 App Sandbox > File Access 里找到相關(guān)選項。再一次,用戶的意愿是關(guān)鍵,因為你需要獲取權(quán)限以將腳本添加到文件夾:

NSError *error;
NSURL *directoryURL = [[NSFileManager defaultManager] URLForDirectory:NSApplicationScriptsDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
[openPanel setDirectoryURL:directoryURL];
[openPanel setCanChooseDirectories:YES];
[openPanel setCanChooseFiles:NO];
[openPanel setPrompt:@"Select Script Folder"];
[openPanel setMessage:@"Please select the User > Library > Application Scripts > com.iconfactory.Scriptinator folder"];
[openPanel beginWithCompletionHandler:^(NSInteger result) {
    if (result == NSFileHandlingPanelOKButton) {
        NSURL *selectedURL = [openPanel URL];
        if ([selectedURL isEqual:directoryURL]) {
            NSURL *destinationURL = [selectedURL URLByAppendingPathComponent:@"Automation.scpt"];
            NSFileManager *fileManager = [NSFileManager defaultManager];
            NSURL *sourceURL = [[NSBundle mainBundle] URLForResource:@"Automation" withExtension:@"scpt"];
            NSError *error;
            BOOL success = [fileManager copyItemAtURL:sourceURL toURL:destinationURL error:&error];
            if (success) {
                NSAlert *alert = [NSAlert alertWithMessageText:@"Script Installed" defaultButton:@"OK" alternateButton:nil otherButton:nil informativeTextWithFormat:@"The Automation script was installed succcessfully."];
                [alert runModal];
            }
            else {
                NSLog(@"%s error = %@", __PRETTY_FUNCTION__, error);
                if ([error code] == NSFileWriteFileExistsError) {
                    // this is where you could update the script, by removing the old one and copying in a new one
                }
                else {
                    // the item couldn't be copied, try again
                    [self performSelector:@selector(installAutomationScript:) withObject:self afterDelay:0.0];
                }
            }
        }
        else {
            // try again because the user changed the folder path
            [self performSelector:@selector(installAutomationScript:) withObject:self afterDelay:0.0];
        }
    }
}];

這么一來,應(yīng)用包中的 Automation.scpt 文件現(xiàn)在暴露在常規(guī)的文件系統(tǒng)中了。

在整個流程中讓你的用戶確實知道他們在做什么是很重要的。你必須記住,是你的用戶在控制你的腳本,而不是你。用戶可能會決定將所有腳本清理出腳本文件夾,而你必須做對應(yīng)的處理。你可能會需要禁用某些依賴于這個腳本的特性,或者解釋為什么腳本需要在再次安裝。

注意: Scriptinator 的示例代碼包括了上面兩種策略。如果你想看看現(xiàn)實的例子,不妨研究下 Overlay xScope 的免費試用版。應(yīng)用里有一個對用戶很友好的腳本設(shè)置步驟,以使應(yīng)用能夠呵用戶的網(wǎng)頁瀏覽器進(jìn)行通訊。還有一個好處是,你也許會發(fā)現(xiàn) xScope 真的是一個進(jìn)行開發(fā)的很棒的工具?。ㄗg者注:本文作者是 xScope 的合作開發(fā)者,同時也是 Twitterrific 的開發(fā)者,所以這里也算個小的軟廣告)

腳本任務(wù)

現(xiàn)在你的自動化腳本在正確的位置了,你可以開始使用它們了。

在下面的代碼中,我們在上面創(chuàng)建的事件描述符沒有改變,唯一不一樣的是它們是如何被運行的:你需要使用 NSUserAppleScriptTask 來替代 NSAppleScript。

你大概會經(jīng)常用到這些腳本任務(wù)。文檔警告說對于給定的類的某個實例, NSUserAppleScriptTask 不應(yīng)該被執(zhí)行多次。所以寫一個工廠函數(shù)來在需要的時候創(chuàng)建任務(wù)會是一個好主意:

- (NSUserAppleScriptTask *)automationScriptTask
{
    NSUserAppleScriptTask *result = nil;

    NSError *error;
    NSURL *directoryURL = [[NSFileManager defaultManager] URLForDirectory:NSApplicationScriptsDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
    if (directoryURL) {
        NSURL *scriptURL = [directoryURL URLByAppendingPathComponent:@"Automation.scpt"];
        result = [[NSUserAppleScriptTask alloc] initWithURL:scriptURL error:&error];
        if (! result) {
            NSLog(@"%s no AppleScript task error = %@", __PRETTY_FUNCTION__, error);
        }
    }
    else {
        // NOTE: if you're not running in a sandbox, the directory URL will always be nil
        NSLog(@"%s no Application Scripts folder error = %@", __PRETTY_FUNCTION__, error);
    }

    return result;
}

如果你正在寫一個同時適用于沙盒和非沙盒的 Mac 應(yīng)用的話,在獲取 directoryURL 時你需要特別小心。NSApplicationScriptsDirectory 只在沙盒中有效。

在創(chuàng)建腳本任務(wù)后,你需要使用 AppleEvent 并提供一個結(jié)束處理來執(zhí)行它:

NSUserAppleScriptTask *automationScriptTask = [self automationScriptTask];
if (automationScriptTask) {
    NSAppleEventDescriptor *event = [self safariURLEventDescriptor];
    [automationScriptTask executeWithAppleEvent:event completionHandler:^(NSAppleEventDescriptor *resultEventDescriptor, NSError *error) {
        if (! resultEventDescriptor) {
            NSLog(@"%s AppleScript task error = %@", __PRETTY_FUNCTION__, error);
        }
        else {
            NSURL *URL = [self URLForResultEventDescriptor:resultEventDescriptor];
            // NOTE: The completion handler for the script is not run on the main thread. Before you update any UI, you'll need to get
            // on that thread by using libdispatch or performing a selector.
            [self performSelectorOnMainThread:@selector(updateURLTextFieldWithURL:) withObject:URL waitUntilDone:NO];
        }
    }];
}

對于用戶寫的腳本,用戶可能期望你的應(yīng)用只是簡單地'運行'腳本 (而不去調(diào)用事件描述符中指定的函數(shù))。在這種情況下,你可以為 event 傳遞一個 nil,腳本就會像用戶在 Finder 中雙擊那樣的行為進(jìn)行執(zhí)行。

NSUserAppleScriptTask 中很好的一個東西就是結(jié)束時候的回調(diào)處理。腳本是異步執(zhí)行的,所以你的用戶界面并不會被一個 (比較長) 的腳本鎖住。要小心你在結(jié)束回調(diào)中做的事情,因為它并不是跑在主線程上的,所以你不能在那兒對你的用戶界面做更新。

幕后

背后發(fā)生了些什么事情呢?

也許你從腳本只能異步地運行一次這個事實中猜到了,這些代碼現(xiàn)在是通過 XPC 執(zhí)行的。就像 iOS 8 中使用 XPC 來確保擴(kuò)展不會影響調(diào)用應(yīng)用那樣,在 Mac 應(yīng)用中運行的腳本也無法訪問調(diào)用應(yīng)用的內(nèi)存地址空間。

如果你看看接收的事件描述符的 keySenderPIDAttr 屬性的話,你會發(fā)現(xiàn)進(jìn)程 ID 屬于 /usr/libexec/lsboxd,而不是你的應(yīng)用程序。這個迷之進(jìn)程大概是 Launch Services 的沙盒守護(hù)進(jìn)程。無論怎樣,你向其他進(jìn)程的請求肯定基本都是要被封送 (marshalling) 的。

如果想在更高層級理解關(guān)于應(yīng)用沙盒的安全目標(biāo)的內(nèi)容,我推薦看看 Ivan Krsti? 在 WWDC 2012"The OS X App Sandbox" 的演講、很出乎意料的是,這個演講非常有意思,在 36 分鐘的演講中,他介紹了在上面提到的關(guān)于自動化的改變。同一次大會中,Sal Soghoian 和 Chris Nebel 帶來的 "Secure Automation Techniques in OS X" 深入講解了自動化的改變。如果你只想學(xué)習(xí)關(guān)于應(yīng)用運行的用戶腳本方面的內(nèi)容,你可以跳過前 35 分鐘的內(nèi)容。

在這些演講中討論了另一個很重要的安全方面的內(nèi)容是訪問組 (access group),我們在這個教程中并沒有涉及這方面的內(nèi)容。如果你想要用腳本控制像郵件或者 iTunes 這樣的系統(tǒng)應(yīng)用,你絕對需要特別關(guān)注一下上面提到的視頻中有關(guān)這方面的內(nèi)容。

同步

正如我上面提到的,NSAppleScriptNSUserAppleScriptTask 有一個微妙的區(qū)別:新的機(jī)制是異步執(zhí)行的。對于大部分情況,使用一個結(jié)束回調(diào)來處理會是一個好得多的方式,因為這樣就不會因為執(zhí)行腳本而阻礙你的應(yīng)用。

然而有時候如果你想帶有依賴地來執(zhí)行任務(wù)的時候,事情就變得有些取巧了。比方說一個任務(wù)需要在另一個任務(wù)開始之前必須完成。這種情況下你就會想念 NSAppleScript 的同步特性了。

要獲得傳統(tǒng)方式的行為,一種簡單的方法是使用一個信號量 (semaphore) 來確保同時只有一個任務(wù)運行、在你的類或者應(yīng)用的初始化方法中,使用 libdispatch 創(chuàng)建一個信號量:

self.appleScriptTaskSemaphore = dispatch_semaphore_create(1);

接下來在初始化腳本任務(wù)之前,簡單地等待信號量。當(dāng)任務(wù)完成時,標(biāo)記相同的這個信號量:

// wait for any previous tasks to complete before starting a new one — remember that you're blocking the main thread here!
dispatch_semaphore_wait(self.appleScriptTaskSemaphore, DISPATCH_TIME_FOREVER);

// run the script task
NSAppleEventDescriptor *event = [self openNetworkPreferencesEventDescriptor];
[automationScriptTask executeWithAppleEvent:event completionHandler:^(NSAppleEventDescriptor *resultEventDescriptor, NSError *error) {
    if (! resultEventDescriptor) {
        NSLog(@"%s AppleScript task error = %@", __PRETTY_FUNCTION__, error);
    }
    else {
        [self performSelectorOnMainThread:@selector(showNetworkAlert) withObject:nil waitUntilDone:NO];
    }

    // the task has completed, so let any pending tasks proceed
    dispatch_semaphore_signal(self.appleScriptTaskSemaphore);
}];

再強(qiáng)調(diào)一下,除非確實有所需要,否則最好別這么做。

你能寫什么樣的腳本

在最后這個例子中,系統(tǒng)偏好設(shè)置面板中的網(wǎng)絡(luò)面板通過下面的 AppleScript 代碼可以被打開:

tell application "System Preferences"
    launch
    activate

    reveal pane id "com.apple.preference.network"
end tell

很棒,但是你是怎么知道這些各種面板的 ID 的?如果想打開的不是網(wǎng)絡(luò)面板,輔助功能或者是安全性與隱私面板的話,要怎么做呢?

正如你在 Brent 的文章中看到的那樣,每一個支持 AppleScript 的應(yīng)用都有一個腳本字典。這個字典描述了應(yīng)用數(shù)據(jù)模型的對象和屬性。所以我們只需要查看數(shù)據(jù)模型就可以找到你想要的東西了!

首先從你的 應(yīng)用 > 其他 文件夾中打開腳本編輯器。然后從文件菜單中,選取"打開字典..."。在這里,所有支持 AppleScript 的應(yīng)用都會被陳列出來 - 可能比你想象的要多!選擇系統(tǒng)偏好設(shè)置,并且點選"選取"。

在這里,你將看到一個標(biāo)準(zhǔn)套件 (Standard Suite) 和系統(tǒng)偏好設(shè)置 (System Preferences) 的樹形瀏覽器。標(biāo)準(zhǔn)套件里列出了像 "open" 這樣的命令,"window" 這樣的類,以及其他一些對大多數(shù)腳本字典來說都通用的東西。有意思的是另一個腳本套件:System Preferences。當(dāng)你選擇它后,你會看到一個叫做 "reveal" 的命令以及三個類 (對象類型),分別叫做 "application","pane" 和 "anchor"。

當(dāng)你查看 "application" 的話,你會看到兩個東西:元素 (elements) 和屬性 (properties)。元素是被所選對象管理的一組對象集合。屬性列出了備選對象所維護(hù)的數(shù)據(jù)。

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

applicaiton 里含有 panes,聽起來就是它。在一個新的腳本編輯器窗口中,創(chuàng)建一個簡單的腳本來顯示所有的面板對象:

tell application "System Preferences"
    panes
end tell

我們的目標(biāo)是打開輔助功能界面的安全面板,于是我們可以在輸出結(jié)果中查找,知道我們看到類似這樣的有用的東西:

pane id "com.apple.preference.security" of application "System Preferences"

查看它的 "localized name" 屬性:

tell application "System Preferences"
    localized name of pane id "com.apple.preference.security"
end tell

輸出為 Security & Privacy。就是它!現(xiàn)在我們嘗試使用之前見過的 "reveal" 命令和 "pane id" 另外寫一個腳本

tell application "System Preferences"
    reveal pane id "com.apple.preference.security"
end tell

系統(tǒng)偏好設(shè)置為我們打開了這個面板。現(xiàn)在讓我們來看看怎么打開特定的 tab 視圖。首先通過 pane 里包含的唯一的元素,anchor 對象,來查詢一下:

tell application "System Preferences"
    anchors of pane "com.apple.preference.security"
end tell

哈哈,我們看到:

anchor "Privacy_Accessibility" of pane id "com.apple.preference.security" of application "System Preferences"

這就是我們想要的。這里也顯示了系統(tǒng)偏好設(shè)置的結(jié)構(gòu):一個應(yīng)用含有 pane,而 pane 含有 anchors。我們調(diào)整一下我們的腳本:

tell application "System Preferences"
    reveal anchor "Privacy_Accessibility" of pane id "com.apple.preference.security"
end tell

完成!現(xiàn)在想象一下如果你的應(yīng)用需要用戶賦予控制電腦的權(quán)限的時候,比起告訴用戶如何在偏好設(shè)置面板里打開對應(yīng)面板,你現(xiàn)在直接為用戶打開了這個面板,干得漂亮。

總結(jié)

你學(xué)到了通過你自己的應(yīng)用去控制別的應(yīng)用的時候所需要知道的一切。不管你是想讓用戶可以創(chuàng)建他們自己的自動化工作流,還是只是想在你的應(yīng)用中啟用一些內(nèi)部功能,就算只能運行在沙盒里,AppleScript 依舊都是每個 Mac 應(yīng)用的強(qiáng)有力的部件。希望這篇教程能給你帶來新的工具和視野,并且在之后你自己的項目中使用這些特性時有所幫助。