之前写过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