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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章