之前寫過Swift調用Shell1.0 版本的代碼,在實際測試中發現1.0版本的調用會導致App內存泄露,並且App UI發生Crash。
Swift調用Shell(1.0 版本) https://blog.csdn.net/u011865919/article/details/81227507
最新代碼已維護至Gitlab https://gitlab.com/cyril_j/mutils/blob/master/Swift/Exec_shell.swift
先貼上Swift調用Shell優化後的代碼
/// 實現swift 對 shell 調用
/// - 不執行等待
/// - eg: status = Execution.execute(path: "/bin/ls")
class Exe_Shell{
/// 等待執行完畢
/// - parameter launchPath: 必須爲完整路徑
/// - parameter arguments (list): 參數列表
/// - Returns: (狀態碼,命令執行的標準輸出)
class func run_shell(launchPath:String,arguments:[String]? = nil) -> (Int, String) {
let task = Process();
task.launchPath = launchPath
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
task.environment = environment
if arguments != nil {
task.arguments = arguments!
}
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String(data: data, encoding: String.Encoding.utf8)!
task.waitUntilExit()
pipe.fileHandleForReading.closeFile() // 關閉pipe防止內存泄露
print("DEBUG 24: run_shell finish.")
return (Int(task.terminationStatus),output)
}
/// 不等待執行完畢,需使用try-catch
/// - parameter launchPath: 必須爲完整路徑
/// - parameter arguments (list): 參數列表
/// - Returns: (狀態碼,命令執行標準輸出)
class func run_shell2(launchPath: String, arguments:[String]? = nil) {
var environment = ProcessInfo.processInfo.environment
environment["PATH"] = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
let task = Process()
task.environment = environment
task.launchPath = launchPath
if arguments != nil {
task.arguments = arguments!
}
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
//let data = pipe.fileHandleForReading.readDataToEndOfFile()
pipe.fileHandleForReading.closeFile()
//let output: String = String(data: data, encoding: String.Encoding.utf8)!
print("DEBUG 23: run_shell2 finish.")
}
/// 命令後臺運行(非線程方式),超時自動銷燬退出 - 暫未寫好🚩
/// - parameter launchPath: 必須爲完整路徑
/// - parameter arguments (list): 參數列表
/// - Returns: (狀態碼,命令執行標準輸出)
class func run_shell3(launchPath: String, arguments:[String]? = nil, timeout:Int = 2) {
var args:[String]
if arguments == nil {
args = [launchPath]
}else {
args = arguments!
args.insert(launchPath, at: 0)
}
run_shell2(launchPath: Rep_runShell, arguments: args)
}
}
首先開發需求是這樣的:
有一個MacOS UI界面,界面中有ScrollView包含一個TextView,需要在這個TextView中實時刷新數據。原始數據是shell命令執行所產生的標準輸出。
需求挺簡單的,於是直接開幹:
- 直接拿 1.0版本的 execShellScript ()方法在swift中調用shell命令,能夠成功獲取返回值。
- 運行MacOS App,用一個死循環每隔1s執行一次TextView的刷新,,似乎一切正常。代碼如下:
while (true){
sleep(1)
refreshLongTextView()
// 自動滑動到底部(自定義NsTextView擴展方法)
self.longTextView.scrollToBottom()
}
/// 刷新主界面底部長文本框內容
func refreshLongTextView(){
let dataQueue = DispatchQueue(label: "data") // 開啓同步隊列
dataQueue.async(execute: DispatchWorkItem(flags: .barrier) {
let args = ["-n100", LOG_PATH]
//let (res, rev) = Exe_Shell.run_shell(launchPath: "/usr/bin/tail", arguments: args)
let (res, rev) = execShellScript("/usr/bin/tail", args)
if rev != "" {
sleep(1)
DispatchQueue.main.async { //通知ui刷新 async異步執行(實際是同步隊列異步執行)
//data = utils.readFileData(path: LOG_PATH) // 這樣讀取大文件容易爆炸💥
self.longTextView.string = rev
// 自動滑動到底部(自定義NsTextView擴展方法)
self.longTextView.scrollToBottom()
}
}
})
}
- 實際上線測試時,發現問題了。App固定在運行到約40分鐘時發生crash,然而調用的shell腳本還在後臺運行。
發生的Crash類型基本一致,截取部分Crash Log 如下:
(完整Log在:Crash_log.log)
Time Awake Since Boot: 280000 seconds
System Integrity Protection: disabled
Crashed Thread: 0 Dispatch queue: com.apple.main-thread
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Application Specific Information:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Failed to set posix_spawn_file_actions for fd -1 at index 0 with errno 9'
terminating with uncaught exception of type NSException
abort() called
由crash log可知,發生crash的原因是由於未捕獲的 Objective-C 異常(NSException),導致系統發送了 Abort 信號退出。
- 並且通過監控App運行狀態,發現出現內存泄露的現象。App的內存佔用隨時間緩慢線性增長。同時測試App不對execShellScript()調用,則內存佔用不會異常增長。
- 檢查execShellScript() 部分的代碼發現開啓了管道對象,且沒有關閉這個管道對象對緩衝區數據的讀取。
- 增加如下代碼,用於在數據讀取到變量後,手動關閉管道。再次運行測試App,沒有發現內存泄露,並解決了crash的問題。
pipe.fileHandleForReading.closeFile()
- iOS 的崩潰捕獲-堆棧符號化-崩潰分析 https://www.jianshu.com/p/302ed945e9cf
- Apple Development Document - Pipe https://developer.apple.com/documentation/foundation/pipe