Swift 調用 Shell - 優化

之前寫過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. 直接拿 1.0版本的 execShellScript ()方法在swift中調用shell命令,能夠成功獲取返回值。
  2. 運行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()
				}
			}
		})
    }
  1. 實際上線測試時,發現問題了。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 信號退出。

  1. 並且通過監控App運行狀態,發現出現內存泄露的現象。App的內存佔用隨時間緩慢線性增長。同時測試App不對execShellScript()調用,則內存佔用不會異常增長。
    在這裏插入圖片描述
  2. 檢查execShellScript() 部分的代碼發現開啓了管道對象,且沒有關閉這個管道對象對緩衝區數據的讀取。
  3. 增加如下代碼,用於在數據讀取到變量後,手動關閉管道。再次運行測試App,沒有發現內存泄露,並解決了crash的問題。
	pipe.fileHandleForReading.closeFile()
  1. iOS 的崩潰捕獲-堆棧符號化-崩潰分析 https://www.jianshu.com/p/302ed945e9cf
  2. Apple Development Document - Pipe https://developer.apple.com/documentation/foundation/pipe
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章